Dart에서 비동기 처리를 어떻게 하는지 설명한다. (비동기가 뭔지에 대해선 다른 글로)
Dart 공식 비동기 튜토리얼(https://dart.dev/codelabs/async-await)을 보고 한글로 요약했습니다.
Dart에서도 다른 언어와 마찬가지로 언제 끝날지 모르는 연산들을 비동기로 처리한다. 예시는 다음과 같다.
위와 같은 비동기적 처리를 진행하는 함수에서 Dart는 return 값으로 Future와 Stream이라는 클래스 자료형을 가진다.
// This example shows how *not* to write asynchronous Dart code.
String createOrderMessage() {
var order = fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() =>
// Imagine that this function is more complex and slow.
Future.delayed(
const Duration(seconds: 2),
() => 'Large Latte',
);
void main() {
print(createOrderMessage());
}
출력
Your order is: Instance of '_Future<String>'
다음과 같이 비동기 처리를 위해서 2초간 텀을 두고 Large Latte를 반환하도록 의도한 코드에서도 그저 비동기 처리가 완료되지 않은 future 인스턴스를 반환한다는 것을 알 수 있다. 결과값은 2초 텀 없이 바로 나오는데, 이는 createOrderMessage가 비동기 처리를 기다려주지 않는 일반 함수이기 때문이다.
따라서 우리는 Future와 그 값을 어떻게 기다릴 수 있는지 알아야 할 필요가 있다. Dart에서 future 인스턴스는 비동기 연산의 결과로, 완료되지 않은(uncompleted) 상태와 완료된(completed) 상태, 두 가지 상태를 가진다. 여기서 완료했을 때 T라는 자료형을 갖는 비동기 함수는 Future<T>를 반환하며 비동기 처리가 정상적으로 진행되지 않았을 때 error 값을 가질 수 있다.
Future<void> fetchUserOrder() {
// Imagine that this function is fetching user info from another service or database.
return Future.delayed(const Duration(seconds: 2), () => print('Large Latte'));
}
void main() {
print(fetchUserOrder());
print('Fetching user order...');
}
출력
Instance of '_Future<void>'
Fetching user order...
(약 2초 후)
Large Latte
main 함수를 보면 fetchUserOrder 함수를 실행한 직후엔 아직 완료되지 않은 future 인스턴스가 출력됐다. 그리고 2초 딜레이 이후 원하는 대로 Large Latte가 출력되는 걸 알 수 있다.
물론 Dart에서도 delay말고 비동기 처리가 안에 하나라도 존재한다는 의미의 async와 비동기 처리를 기다려줄 수 있는 수단인 await 키워드가 존재한다. 단순하게 await을 사용하려는 함수 몸통 앞에 async를 선언하면 된다.
// 정상 완료시 String을 반환하는 비동기 함수
Future<String> func() async { ··· }
// func이 반환하는 future 인스턴스가 완료된 후 출력하는 함수
print(await func());
아까 코드 예제를 깜찍하게 바꾸면 다음과 같다.
Future<String> createOrderMessage() async {
var order = await fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() =>
// Imagine that this function is
// more complex and slow.
Future.delayed(
const Duration(seconds: 2),
() => 'Large Latte',
);
Future<void> main() async {
print('Fetching user order...');
print(await createOrderMessage());
}
출력
Fetching user order...
(약 2초 후)
Your order is: Large Latte
Dart스럽게 사용하기 위해서 async 함수들은 Future<T> 클래스를 반환하도록 설계하는 것 같다. (error를 처리하기 위함이라고 생각한다.)
밑의 예제의 결과가 어떻게 나올지 예상해보자.
Future<void> printOrderMessage() async {
print('Awaiting user order...');
var order = await fetchUserOrder();
print('Your order is: $order');
}
Future<String> fetchUserOrder() {
// Imagine that this function is more complex and slow.
return Future.delayed(const Duration(seconds: 4), () => 'Large Latte');
}
void main() async {
countSeconds(4);
await printOrderMessage();
}
// You can ignore this function - it's here to visualize delay time in this example.
void countSeconds(int s) {
for (var i = 1; i <= s; i++) {
Future.delayed(Duration(seconds: i), () => print(i));
}
}
main 함수에서 countSeconds 함수를 실행하고 printOrderMessage 함수의 결과를 기다리게 했다. 비동기 함수에서 첫 번째 await이 나오는 이전 부분들은 모두 동기로 실행된다는 것을 알면 결과를 쉽게 이해할 수 있다.
countSeconds 함수는 i초 후 i를 출력하는 future 인스턴스를 만들고 printOrderMessage 함수는 4초 후 Large 머시깽이가 나오므로 출력은 다음과 같다
출력
Awaiting user order...
(1초 후)
1
(1초 후)
2
(1초 후)
3
(1초 후)
4
Your order is: Large Latte
async함수의 에러 처리는 try-catch를 사용한다.
try {
print('Awaiting user order...');
var order = await fetchUserOrder();
} catch (err) {
print('Caught error: $err');
}
위와 같은 방법으로 사용할 수 있다. 하지만 future 값을 가져와서 widget의 변경에 사용할 땐 FutureBuilder 위젯을 사용할 수 있다.
이 외에도 공식 홈페이지에는 큐트한 예제들과 codelab이 있으니 가서 해보면 좋을 것 같다!!