Flutter 인터넷에서 데이터 가져오기

JJ·2023년 2월 19일
0

Flutter

목록 보기
1/3
post-thumbnail

부트캠프 강의를 들으며 비트코인 티커 앱을 만들던 중 데이터를 가져와 가공하는 과정에서 자꾸 문제가 생겼다.
그래서 다시 한번 개념을 제대로 정리하기위해 Flutter 공식문서를 읽어보며 공부하는게 나을 것 같았다.
개인적 공부를 위해 flutter 공식문서 내용을 대충 한국어로 번역해봤다.
(정확하지 않을 수 있음)

해당 공식문서의 주소는 아래와 같다.

fetch-data flutter cookbook

인터넷에서 데이터를 가져오는데에 네가지 단계가 있다고 한다.

  1. http 패키지 추가하기
  2. http 패키지를 통해 네트워크 request 하기
  3. response를 Dart object로 변환하기
  4. 플러터로 데이터를 fetch 하고 display 하기

1. http 패키지 추가하기

데이터를 가져올 수 있는 가장 간단한 방법은 http 패키지를 사용하는 것이라고 한다.
http 패키지를 설치하기 위해선, pubspec.yaml 파일의 dependencies 섹션에 http의 최신 버전을 추가하면 된다.
http의 최신 버전은 pub.dev에서도 찾아볼 수 있다.

dependencies:
  http: <latest_version>

그리고 사용하고자 하는 파일에서 패키지를 import 한다.

import 'package:http/http.dart' as http;

추가적으로, AndroidManifest.xml 파일에서 Internet permission을 추가한다.

<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />

2. 네트워크 request 하기

아래의 코드는 http.get() 메서드를 통해 JSONPlaceholder에서 샘플 앨범을 fetch 해오는 코드이다.

Future<http.Response> fetchAlbum() {
  return http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
}

http.get()메서드는 Response를 갖고있는 Future를 리턴한다.

  • Future는 Dart 의 async에 관한 중요한 class이다. Future object는 미래 한 때에 사용될 수 있는 잠재적 값이나 에러를 나타낸다.
  • http.Response class는 http 콜에서 성공적으로 받아진 데이터를 갖고있다.

3. responsecustom Dart object로 변환하기

네트워크 request를 하기는 쉽지만, 순수 Future<http.Response> 를 가지고 작업을 하는것은 간편하지 않다. 좀 더 쉽게 작업할 수 있도록 http.Response를 Dart object로 변환하자.

Album class 만들기

(Flutter 공식문서에서 예제로 사용하고 있는 api가 앨범 커버 api 이기 때문에 Album 클래스를 만드는 것이다.)

우선, network request로 받은 데이터를 갖고있는 Album class를 만들자. Album class는 JSON으로부터 Album을 만드는 factory 생성자 함수를 갖고있다.

class Album {
  final int userId;
  final int id;
  final String title;

  const Album({
    required this.userId,
    required this.id,
    required this.title,
  });

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}

http.ResponseAlbum으로 바꾸기

이제, fetchAlbum() 함수를 업데이트 해서 Future<Album>을 리턴 하도록 다음의 단계를 이용하자.

  1. dart:convert패키지를 통해 response body를 JSON Map으로 변환시키기.
  2. 서버가 200 코드와 함께 OK response를 리턴한다면, JSON Map을 fromJson() factory 메서드를 통해 Album으로 변환하기.
  3. 서버가 200 코드와 함께 OK response를 리턴하지 않는다면, exception 발생시키기. ("404 Not Found" server response이더라도 exception 발생시키기. null을 return하면 안된다. 이것은 아래와 같이 snapshot에서 데이터를 조사할때 중요하다.)
Future<Album> fetchAlbum() async {
  final response = await http
      .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

이제 당신은 인터넷에서 album을 가져오는 함수를 갖고있다!

4. 데이터 Fetch 하기

fetchAlbum() 메서드를 initState()didChangeDependencies() 메서드에서 call하자.

initState() 메서드는 정확히 한 번 실행된 후 다시는 실행되지 않는다. InheritedWidget의 변화에 따라 API가 다시 로드 되는 것을 원한다면, fetchAlbum() 메서드를 didChangeDependencies() 메서드 안에서 실행하자. State에 대해 더 많은 정보를 원한다면 State를 클릭하자.

class _MyAppState extends State<MyApp> {
  late Future<Album> futureAlbum;

  
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }
  // ···
}

이 Future는 다음 단계에서 사용될 것이다.

5. 데이터 나타내기

데이터를 스크린에 나타내기 위해서 FutureBuilder 위젯을 사용하자. FutureBuilder 위젯은 Flutter에 포함되어있으며 비동기식 데이터 소스를 대상으로 작업하는것을 편하게 해준다.

두 매개 변수를 제공해줘야 한다.

  1. 작업하고자 하는 Future. 지금의 경우, fetchAlbum()함수에서 리턴된 future.
  2. Future의 상태(loading, success 또는 error)에 따라서 Flutter에게 어떤 것을 만들어야하는지 알려주는 builder 함수.

snapshot.hasData 는 snapshot이 non-nullable data 값을 가졌을때만 true를 반환하는 것에 주의하자.

fetchAlbum은 non-nullable 값만을 리턴할 수 있기 때문에, "404 Not Found" server response 의 경우에도 exception을 발생시켜야한다. Exception 발생이 에러 메세지를 나타내는데 사용될 수 있는 snapshot.hasErrortrue로 만들어준다.

그 외에는 spinner가 나타날 것이다.

FutureBuilder<Album>(
  future: futureAlbum,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text(snapshot.data!.title);
    } else if (snapshot.hasError) {
      return Text('${snapshot.error}');
    }

    // By default, show a loading spinner.
    return const CircularProgressIndicator();
  },
)

fetchAlbum()initState() 안에서 실행됐는가?

편리하긴 해도, API call을 build() 메서드 안에 넣는것을 추천하지 않는다.

플러터는 무언가 바뀌어야 할 때마다 build() 메서드를 call 하는데, 이것은 생각보다 자주 일어나게 된다. 만약 fetchAlbum() 메서드가 build() 메서드 안에 위치했다면 매 rebuild 마다 반복적으로 실행되어 앱을 느려지게 만들었을것이다.

fetchAlbum() 의 결과를 상태 변수에 저장하는것은 Future가 한 번만 실행된 후 차후의 rebuild들을 위해 cache 된다.

테스팅

어떻게 이 기능들을 시험해볼 수 있는지를 위해서는 다음과 같은 recipe들을 보면 된다.

완성된 예시

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Album> fetchAlbum() async {
  final response = await http
      .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

class Album {
  final int userId;
  final int id;
  final String title;

  const Album({
    required this.userId,
    required this.id,
    required this.title,
  });

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late Future<Album> futureAlbum;

  
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Fetch Data Example'),
        ),
        body: Center(
          child: FutureBuilder<Album>(
            future: futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data!.title);
              } else if (snapshot.hasError) {
                return Text('${snapshot.error}');
              }

              // By default, show a loading spinner.
              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

번역해놓고 보니 차라리 구글 번역기로 한 번에 돌리는 게 더 빠르고 편했을 것 같기도 하다.

일단 번역만 해놓아서 한 번에 이해는 안 되지만, 내일 다시 한번 천천히 읽어보면서 직접 코드도 짜보고 하며 공부해 봐야겠다.

화이팅

profile
신규...개발자가...되자...

0개의 댓글