일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 2022 플러터 설치
- ANR이란
- 멤버변수
- 플러터 설치 2022
- android ar 개발
- 자바 다형성
- 스택 자바 코드
- 안드로이드 레트로핏 crud
- jvm이란
- ar vr 차이
- rxjava disposable
- 안드로이드 라이선스 종류
- 안드로이드 유닛 테스트
- 클래스
- android retrofit login
- 안드로이드 라이선스
- 서비스 쓰레드 차이
- 서비스 vs 쓰레드
- 스택 큐 차이
- 객체
- Rxjava Observable
- jvm 작동 원리
- 안드로이드 유닛테스트란
- rxjava cold observable
- 2022 플러터 안드로이드 스튜디오
- rxjava hot observable
- 큐 자바 코드
- 안드로이드 os 구조
- 안드로이드 유닛 테스트 예시
- 안드로이드 레트로핏 사용법
- Today
- Total
나만을 위한 블로그
[Flutter] Future, async / await를 통한 비동기 처리 방법 본문
안드로이드에선 주로 레트로핏을 사용해 통신하는 경우가 많은데, 플러터에선 http라는 패키지를 통해 네트워크 통신을 하는 경우도 있는 듯하다. 물론 dio, 플러터의 retrofit 등 여러가지를 쓰는 경우가 많겠으나 네트워크 통신을 하려면 이런 라이브러리보다 async, await, Future라는 키워드를 먼저 알아야 한다.
아래는 플러터 비동기 프로그래밍을 다루는 공식문서다.
https://dart.dev/libraries/async/async-await
(중략)...이 튜토리얼은 다음을 다룬다
- async, await를 쓰는 방법, 시기
- async, await 사용이 실행 순서에 주는 영향
- try-catch를 써서 비동기 호출 오류를 처리하는 법
비동기 프로그래밍을 구현하면 다른 연산이 완료되길 기다리는 동안 다른 작업을 할 수 있다.
플러터에서 비동기 계산은 일반적으로 Future 형태의 결과를 제공하고, 결과가 여러 파트로 구성됐다면 스트림으로 제공한다.
언제 도착할 지 모르는 비동기 계산의 결과를 받아서 처리하기 위해 필요한 키워드가 async, await다.
두 키워드를 보기 전에 먼저 Future가 뭔지 확인한다.
Future는 비동기 연산의 결과를 나타내고 uncompleted(값이 생성되기 전의 미래 상태), completed의 2가지 상태를 가진다.
- uncompleted : 비동기 함수를 호출하면 완료되지 않은 미래를 리턴한다. 이 Future는 비동기 연산이 끝나거나 오류 발생을 기다린다
- completed : 비동기 연산이 성공하면 Future가 value로 완료된다. 그렇지 않고 어떤 이유로든 실패하면 error와 함께 완료된다
Future의 뒤에는 제네릭 타입을 붙여서 Future<T> 형태로 쓸 수 있다. Future<String>은 문자열 값을 생성하는 것이다. 만약 값을 만들지 않는다면 Future<void>라고 쓸 수 있다.
아래는 Future를 사용하는 간단한 예시다.
Future<void> fetchUserOrder() {
// 이 함수가 다른 서비스, DB에서 유저 정보를 가져온다고 가정하라
return Future.delayed(const Duration(seconds: 2), () => print('Large Latte'));
}
void main() {
fetchUserOrder();
print('Fetching user order...');
}
위 코드를 실행하면 2초 대기하는 delayed()의 영향 때문에 메인 함수의 print()가 먼저 완료되고 2초 뒤에 "Large Latte"가 출력된다.
다음은 에러로 완료되는 Future의 예시 코드다.
Future<void> fetchUserOrder() {
return Future.delayed(
const Duration(seconds: 2),
() => throw Exception('Logout failed: user ID is invalid'),
);
}
void main() {
fetchUserOrder();
print('Fetching user order...');
}
일부러 유저 ID가 유효하지 않다는 예외를 일으킨다. 마찬가지로 메인 함수의 print()가 먼저 완료되고 이후 에러가 표시된다.
이제 async / await를 확인한다. 두 키워드는 비동기 함수를 정의하고 비동기 함수의 결과를 사용하기 위한 키워드다.
이걸 사용하려면 기억해야 할 2가지 기본 지침이 있다.
- 비동기 함수를 정의하려면 함수 본문 앞(중괄호 앞)에 async를 추가한다
- await는 비동기 함수에서만 쓸 수 있다
함수의 중괄호 부분을 함수 본문이라고 한다. 이 중괄호에서 여는 중괄호 앞에 async를 붙이면 그 함수는 비동기 함수가 된다.
void main() async { ··· }
함수가 어떤 값을 리턴해야 한다면 타입을 Future<T>로 설정한다. 값을 리턴하지 않으면 Future<void>로 설정한다.
Future<void> main() async { ··· }
이제 await를 써서 비동기 함수가 완료되기를 기다리면 된다.
print(await createOrderMessage());
await를 붙이지 않으면 비동기 함수가 비동기적으로 연산되지 않고 즉시 완료돼 버리니 주의한다.
대충 확인했으니 이제 실제론 어떻게 사용할 수 있는지 예시를 확인한다. 플러터 쿡북 중 인터넷에서 데이터 받아오기라는 이름의 문서가 있어서 이걸 기준으로 확인한다.
https://docs.flutter.dev/cookbook/networking/fetch-data
먼저 안드로이드, 아이폰 에뮬레이터에 네트워크 권한 처리를 해 둔다. 그리고 http 패키지를 pubspec에 명시하고 의존성을 적용한다.
그리고 jsonplaceholder라는 도메인에서 앨범 정보를 받아오는 url에 GET 요청을 날리는 함수를 정의한다.
import 'package:http/http.dart' as http;
Future<http.Response> fetchAlbum() {
return http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
}
http.dart 패키지에 http라는 별칭을 붙였기 때문에 get()을 호출할 때 앞에 http를 붙이는 걸 볼 수 있다.
앞서 Future가 어떤 값을 리턴한다면 <T>를 붙인다고 했기 때문에, 위 함수에서 Future는 http 패키지의 Response 클래스 형태와 같은 값을 리턴할 거라고 추측할 수 있다.
그리고 JSON 안의 데이터를 파싱하기 위한 모델을 만든다. 패턴 매칭을 써서 JSON에서 필요한 값들을 뽑아오는 팩토리 함수도 같이 만든다.
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 switch (json) {
{
'userId': int userId,
'id': int id,
'title': String title,
} =>
Album(
userId: userId,
id: id,
title: title,
),
_ => throw const FormatException('Failed to load album.'),
};
}
}
이제 네트워크 통신 결과가 담긴 http.response를 Album 형태로 변환해야 한다. fetchAlbum()을 수정한다
import 'dart:convert';
import 'album.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) {
// 200 응답을 받으면 JSON 파싱
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
// 200 응답이 아니면 예외를 던진다
throw Exception('Failed to load album');
}
}
여기서 Uri.parse()에 넣은 경로를 실행하면 어떤 값을 받는지 확인한다.
{
"userId": 1,
"id": 1,
"title": "quidem molestiae enim"
}
JSON은 key, value로 이뤄진 Map과 같은 형태를 갖고 있다. 그래서 Album에 만든 fromJson()은 switch문을 써서 JSON 안의 key 이름들을 나열하고 그 값을 앱에서 어떤 자료형의 값으로 쓸 것인지 정의한 후, Album의 생성자를 호출해 파싱한 값들이 담긴 인스턴스를 만들어낸다.
외부에서 데이터를 가져왔다면 화면에 뿌릴 일만 남았다. 공식문서에선 FutureBuilder 위젯을 사용하라고 설명하니 이것을 사용해서 가져온 데이터를 표시한다.
이 때 2가지 파라미터를 반드시 넘겨야 한다.
- fetchAlbum()에서 리턴된 Future
- 로딩, 성공, 에러 같은 Future 상태에 따라 렌더링할 내용을 알려주는 빌더 함수
fetchAlbum()은 널이 아닌 값만 리턴할 수 있어서 400, 500 대의 에러가 발생해도 예외를 던져야 한다. 그래야 왜 데이터가 표시되지 않는지 알 수 있고, 사용자에겐 적절한 오류 메시지의 팝업 등을 띄워서 안내할 수 있다.
또한 공식문서에 갑자기 snapshot이란 단어가 튀어나온다. 이 객체를 통해 hasData, hasError 함수를 호출하는데 이 스냅샷이란 게 무엇인가?
https://stackoverflow.com/a/67049934
스냅샷은 FutureBuilder에서 수신 중인 Future 또는 스트림의 결과다. 리턴되는 데이터와 상호작용해서 빌더에서 쓰기 전에 먼저 데이터에 접근해야 한다. FutureBuilder가 사용자에게 간접적으로 전달하는 이 데이터에 접근하려면 FutureBuilder에 요청해야 한다
스냅샷은 말하자면 내가 플러터에 사용하겠다고 말한 별명이기 때문에 먼저 스냅샷이라고 한다. 왜냐면 내 FutureBuilder는 이런 모양을 하고 있기 때문이다
FutureBuilder(
future: someFutureFunction(),
builder: (context, snapshot) { // 여기서 플러터에게 snapshot이란 단어를 쓰도록 지시했다
if (snapshot.connectionState == ConnectionState.waiting)
return Center(child: CircularProgressIndicator());
else
return Text(counter.toString());
}),
중간까지 읽었다면 내가 snapshot이란 단어를 쓴 적도 없는데 갑자기 이건 어디서 튀어나왔나 싶을 수 있다.
그러나 공식문서 맨 밑의 전체 코드를 보면 알 수 있다. 아래는 메인 함수와 Stateful 위젯을 정의한 코드들이다.
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Future<Album> futureAlbum;
@override
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: Scaffold(
appBar: AppBar(
title: const Text('인터넷에서 데이터 가져오기'),
),
body: Center(
child: FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text("snapshot - title : ${snapshot.data!.title}");
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
),
),
),
);
}
}
Center의 builder 부분을 보면 context, snapshot이란 단어가 쓰인 게 보인다.
builder에 마우스를 올리면(맥은 cmd + J를 누르면) 문서가 작게 표시된다.
AsyncSnapshot이란 타입을 넘기라고 하는데 이것은 가장 최근에 이뤄진 비동기 계산과의 상호작용을 불변으로 표시하기 위해 존재하는 클래스다.
https://api.flutter.dev/flutter/widgets/AsyncSnapshot-class.html
이 클래스에 정의된 함수들을 보면 스냅샷에 널이 아닌 값이 포함됐는지 확인해서 bool 값을 리턴하는 hasData, 널이 아닌 에러가 포함된지 확인하는 hasError 등 여러 함수들이 있다. 스냅샷은 왜 필요한 건가?
앞서 비동기 처리를 통해 인터넷에서 데이터를 가져오는 코드를 작성했다. 그 비동기 처리의 결과는 Future<Album> 형태로 받아오고 있다. 이 때 언제 끝날지 모르는 Future 데이터에 접근해서 비동기 작업의 상태를 확인하고 성공했으면 결과값(데이터), 실패했으면 에러 상태를 확인해서 다른 처리를 하기 위해 AsyncSnapshot(=스냅샷)이 필요하다.
예시에선 이 클래스에 접근하기 위해 buildContext(context)와 AsyncSnapshot(snapshot)을 각각 다른 이름으로 간략화해서 사용할 뿐이다. 이를 통해 AsyncSnapshot에 정의된 hasData, hasError를 통해 데이터가 있는 경우와 에러만 있는 경우에 대해 각각 다른 화면을 표시할 수 있게 된다. snapshot이 아닌 asyncSnapshot 같은 다른 이름으로 바꿔도 전혀 문제없다.
이제 앱에 빌드하면 아래와 같은 화면이 표시된다.
'Flutter' 카테고리의 다른 글
[Dart] 반복문 (0) | 2024.08.06 |
---|---|
[Flutter] Stateless, Stateful 위젯의 생명주기 (0) | 2024.08.05 |
[Dart] final vs const (0) | 2024.07.22 |
[Dart] 생성자(constructor) 알아보기 (0) | 2024.07.21 |
[Flutter] Scaffold란? (0) | 2024.07.17 |