- 시작하며 -
오랜 시간이 걸리는 작업이나 외부 리소스와의 상호작용을 효율적으로 처리하는 데에는 비동기 작업이 필수적입니다. 이번 포스팅에서는 다트에서 비동기 작업을 어떻게 처리하는지, 그리고 어떤 기능들을 사용하는지 알아보도록 하겠습니다.
- 본문 -
다트에서 비동기 작업을 처리하기 위해 async와 await 키워드를 사용합니다. async 키워드는 함수 선언 앞에 붙여 비동기 함수임을 나타내고, await 키워드는 비동기 작업의 완료를 기다릴 때 사용됩니다. 이를 통해 코드를 보다 직관적이고 동기적으로 작성할 수 있습니다.
다트에서 비동기 작업은 Future와 Stream을 사용하여 처리됩니다. Future는 비동기 작업의 결과를 나타내는 객체이며, await 키워드와 함께 사용하여 비동기 작업의 완료를 기다릴 수 있습니다. 또한, Stream은 비동기적인 데이터의 흐름을 나타내는 객체로 여러 개의 값을 비동기적으로 제공하고 받을 수 있습니다.
Future의 팩토리 메서드로는
Future(FutureOr<T> computation())
- Timer.run을 사용하여 비동기적으로 computation 함수를 호출한 결과를 가지고 있는 Future 객체를 생성합니다.
- computation 함수는 FutureOr<T>를 반환해야 합니다.
- 특정 코드 블록이 이벤트 루프에 의해 더 이상 블로킹되지 않고 비동기적으로 실행되어야 할 때 사용됩니다
Future.delayed(Duration duration, [FutureOr<T> computation()?])
- 지정된 시간(duration) 후에 computation 함수를 실행하여 그 결과를 가지고 있는 Future 객체를 생성합니다.
- computation 함수는 선택적 매개변수이며, 기본값은 null입니다. FutureOr<T>를 반환해야 합니다.
- 특정 시간이 경과한 후에 작업을 비동기적으로 수행해야 할 때 사용됩니다.
Future.error(Object error, [StackTrace? stackTrace])
- 에러를 가지고 있는 Future 객체를 생성합니다.
- error 매개변수에는 발생한 에러를 전달하고, stackTrace 매개변수는 선택적으로 사용될 수 있는 에러의 스택 트레이스 정보를 전달합니다.
- 에러 처리가 필요한 상황에서 사용됩니다.
Future.microtask(FutureOr<T> computation())
- scheduleMicrotask를 사용하여 비동기적으로 computation 함수를 호출한 결과를 가지고 있는 Future 객체를 생성합니다.
- computation 함수는 FutureOr<T>를 반환해야 합니다.
- 마이크로태스크 큐에 넣어야 하는 작업을 비동기적으로 실행해야 할 때 사용됩니다. 마이크로태스크는 다른 이벤트 루프 이벤트들보다 우선순위가 높습니다.
Future.sync(FutureOr<T> computation())
- 즉시 호출된 computation 함수의 결과를 가지고 있는 Future 객체를 생성합니다.
- computation 함수는 FutureOr<T>를 반환해야 합니다.
- 동기적인 작업의 결과를 비동기적인 작업의 형태로 처리하고자 할 때 사용됩니다.
Future.value([FutureOr<T>? value]):
- 주어진 값(value)으로 완료된 Future 객체를 생성합니다.
- value는 선택적 매개변수이며, FutureOr<T> 타입의 값을 전달할 수 있습니다.
- 이미 완료된 값을 가지고 있는 Future를 생성하고자 할 때 사용됩니다.
여기서 FutureOr<T>들은 다트(Dart)에서 사용되는 제네릭 타입으로, T 타입 또는 Future<T> 타입을 나타냅니다. 즉, FutureOr<T>는 T 타입의 값 또는 Future<T> 객체를 가질 수 있는 유연한 타입을 의미합니다.
FutureOr<T>의 사용은 비동기 작업을 처리할 때 유용합니다. 보통 비동기 작업의 결과는 Future<T> 객체로 반환되지만, 때로는 동기적인 작업의 결과를 처리하기 위해 T 타입의 값을 직접 사용하고 싶을 수 있습니다. FutureOr<T>는 이러한 상황에서 T 타입의 값을 처리하거나 Future<T> 객체를 대신 사용할 수 있도록 해줍니다.
이 정도의 기본 지식만 가지고 Dart 공식문서의 코드들을 풀이해 봅시다
예: 비동기 함수를 잘못 사용하는 경우
// 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>'
해당코드는 이럼 흐름으로 흘러갑니다
1. main함수에서 createOrderMessage()라는 문자열을 함수를 호출합니다.
2. 호출된 createOrderMessage()에서는 order의 값을 가져오는 Future<String>을 반환하는 fetchUserOrder()라는 함수를 호출합니다.
3. fetchUserOrder()는 딜레이 때문에 2초 뒤 'Large Latte'라는 값을 반환한다고 하지만 해당 함수는 비동기함수이기 때문에 await 가 없는 이상 createOrderMessage()에서는 해당값을 기다리지 않고 Your order is: Instance of '_Future<String>'를 반환해 버립니다
4. print 함수는 createOrderMessage()가 일반적인 동기함수이기 때문에 Your order is: Instance of '_Future<String>'를 출력합니다.
이 코드에서 알 수 있는 점은 아래와 같습니다.
1. Futture라는 제네릭 형태의 클래스가 있으며 해당함수는 비동기 함수를 구현할 때 사용한다
2. 따로 처리를 하지 않는 이상 비동기 함수를 기다리지 않는다.
3. Future<String>의 문자열 표현법은 Instance of 'Future<String>'
그렇다면 이제 이 코드를 조금 수정하여 Your order is: Large Latte가 출력되게 바꿔봅시다.
Future<String> createOrderMessage() async {
var order = await fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() =>
Future.delayed(
const Duration(seconds: 2),
() => 'Large Latte',
);
void main() async {
print(await createOrderMessage());
}
//출력
//Your order is: Large Latte
createOrderMessage 함수에 async 키워드를 추가하고, fetchUserOrder 함수를 await 키워드와 함께 호출하여 비동기 작업이 완료될 때까지 기다리도록 수정했습니다. 이를 통해 비동기 작업의 결과를 올바르게 처리할 수 있게 되었습니다.
예: 비동기 함수 내에서 실행
Future<void> printOrderMessage() async {
print('Awaiting user order...');
var order = await fetchUserOrder();
print('finish await');
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();
print('finish');
}
// 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));
}
}
//출력
//Awaiting user order...
//1
//2
//3
//4
//finish await
//Your order is: Large Latte
//finish
1. main함수에서 countSeconds(4)를 호출합니다.
2. countSeconds(4)는 1초 후 1을, 이후 2초 후 2를 출력하며 이런 방식으로 4까지 출력할 것을 for문으로 Future.delayed() 함수를 호출하여 예약합니다. 여기서 main 함수로 넘어와서 main 함수에서는 printOrderMessage()를 await 하게 호출합니다.
3. await 하게 호출한 printOrderMessage()에서는 'Awaiting user order...'를 호출하고 fetchUserOrder()의 값을 await 하게 호출합니다.
4. await 하게 호출한 fetchUserOrder() 4초 후 'Large Latte'를 반환할 것을 예약합니다. 그렇지만 await 하게 불러왔으므로 4초를 기다리고 order에 'Large Latte'라는 값을 넣어줍니다. 그사이 countSeconds로 예약한 출력들은 4초가 지났기에 모두 출력됩니다.
5. 이후 printOrderMessage()에서는 finish await을 출력 이후 'Your order is: $order'이라는 값을 출력합니다. 여기서 order의 값은 await으로 Large Latte라는 값을 넘겨받았기 때문에 Your order is: Instance of '_Future <String>'가 출력되는 것이 아니라 Your order is: Large Latte를 출력합니다.
6. main함수에서는 printOrderMessage()의 작업이 모두 끝났기 때문에 finish를 추력하고 종료됩니다.
이예시는 await이 어떻게 동작하는지 보여줍니다. await이 해당 값을 사용할 때만 기다리는 것이 아닌 await이 호출된 즉시 기다린다는 것을 보여줍니다. 또한 await을 한상태에서도 이미 예약한 작업들을 무시하고 기다리는 것이 아닌 따로 돌아간다는 것을 보여줍니다.
예: try-catch를 사용한 비동기 및 대기
Future<void> printOrderMessage() async {
try {
print('Awaiting user order...');
var order = await fetchUserOrder();
print('test');
print(order);
} catch (err) {
print('Caught error: $err');
}
}
Future<String> fetchUserOrder() {
// Imagine that this function is more complex.
var str = Future.delayed(
const Duration(seconds: 4),
() => throw 'Cannot locate user order');
return str;
}
void main() async {
await printOrderMessage();
}
//출력
//Awaiting user order...
//Caught error: Cannot locate user order
1. main함수에서 printOrderMessage()를 await 하게 호출합니다.
2. printOrderMessage()에서는 try catch 문을 이용하여 에러가 생기경우 Caught error: $err형태로 출력합니다.
3. try catch 문안에서는 Awaiting user order... 를 출력 후 fetchUserOrder()를 await 하게 호출합니다.
4. await하게 호출한 fetchUserOrder()에서는 4초 후 'Cannot locate user order'라는 에러를 throw 해줍니다. 이후 printOrderMessage() 에서는 이 throw를 catch 하여 Caught error: Cannot locate user order를 출력하고 try catch 문은 닫히고 printOrderMessage() 함수는 종료됩니다.
이 코드에서 비동기작업을 try-catch 하여 비동기에서도 해당 작업의 에러들을 catch 할 수 있다는 것을 보여줍니다.
- 마치며 -
비동기 작업 기능은 개발자에게 편의성과 성능을 제공하여 생산성을 높이는데 큰 도움을 줄 수 있는 기능입니다. 이번에배운 Future클래스를 잘 사용한다면 비동기처리구현을 더욱 쉽게 구현할 수 있을 것입니다. 포스팅은 여기까지 마치며 다음에는 다트의 Stream으로 찾아오겠습니다.
참고 :
https://api.dart.dev/stable/3.0.4/dart-async/Future-class.html
https://api.dart.dev/stable/3.0.4/dart-async/FutureOr-class.html
https://dart.dev/codelabs/async-await
https://brunch.co.kr/brunchbook/dartforflutter
'Flutter > Dart' 카테고리의 다른 글
[Dart] Cascade Operator 와 Spread Operator (0) | 2023.06.10 |
---|---|
[Dart] Collection과 Iterable 데이터들을 다루기 (0) | 2023.06.09 |
[Dart] Dart를 알아봅시다 (0) | 2023.06.08 |