dart:async
dart:async
Dart의 dart:async 라이브러리가 제공하는 주요 기능을 소개해 드릴게요. 비동기 프로그래밍의 핵심인 Future와 Stream을 이 라이브러리에서 다룰 수 있어요.
출처: dart:async
본문
비동기 프로그래밍은 흔히 콜백 함수(callback function)를 사용하지만, Dart에서는 대안이 있어요: Future와 Stream 객체예요. Future는 언젠가 결과가 제공되리라는 약속(promise)과 같아요. Stream은 이벤트처럼 값들의 연속을 얻는 방법이에요. Future, Stream 등은 dart:async 라이브러리(API 레퍼런스)에 있어요.
참고:
Future나StreamAPI를 항상 직접 사용해야 하는 건 아니에요. Dart 언어는async와await같은 키워드로 비동기 코딩을 지원해요. 자세한 내용은 비동기 프로그래밍 튜토리얼을 참고하세요.
dart:async 라이브러리는 웹 앱과 커맨드라인 앱 모두에서 동작해요. 사용하려면 dart:async를 import 하세요.
import 'dart:async';
팁:
Future와StreamAPI를 쓰기 위해dart:async를 import 할 필요는 없어요.dart:core가 그 클래스들을 내보내기(export) 하거든요.
Future
Future 객체는 Dart 라이브러리 곳곳에 나타나요. 흔히 비동기 메서드가 반환하는 객체지요. Future가 완료(complete)되면 그 값을 사용할 준비가 된 거예요.
await 사용하기 (Using await)
Future API를 직접 사용하기 전에 await를 써보는 걸 고려해 보세요. await 표현식을 사용하는 코드는 Future API를 쓰는 코드보다 이해하기 쉬울 때가 많아요.
다음 함수를 살펴볼게요. Future의 then() 메서드를 사용해서 비동기 함수 세 개를 차례로 실행하고, 다음 함수를 실행하기 전에 각각이 완료되기를 기다려요.
void runUsingFuture() {
// ...
findEntryPoint()
.then((entryPoint) {
return runExecutable(entryPoint, args);
})
.then(flushThenExit);
}
await 표현식을 쓰는 동일한 코드는 동기 코드처럼 보여요.
Future<void> runUsingAsyncAwait() async {
// ...
var entryPoint = await findEntryPoint();
var exitCode = await runExecutable(entryPoint, args);
await flushThenExit(exitCode);
}
async 함수는 Future에서 발생한 예외를 잡을 수 있어요. 예를 들어:
var entryPoint = await findEntryPoint();
try {
var exitCode = await runExecutable(entryPoint, args);
await flushThenExit(exitCode);
} catch (e) {
// Handle the error...
}
중요: async 함수는 Future를 반환해요. 함수가 future를 반환하고 싶지 않다면 다른 해결책을 쓰세요. 예를 들어 함수 안에서 async 함수를 호출할 수도 있어요.
await 사용 및 관련 Dart 언어 기능에 대한 자세한 내용은 비동기 프로그래밍 튜토리얼을 참고하세요.
기본 사용법 (Basic usage)
then()을 사용해서 future가 완료될 때 실행될 코드를 예약할 수 있어요. 예를 들어 Client.read()는 HTTP 요청에 시간이 걸릴 수 있으므로 Future를 반환해요. then()을 사용하면 그 Future가 완료되고 약속된 문자열 값이 준비되었을 때 어떤 코드를 실행할 수 있어요.
httpClient.read(url).then((String result) {
print(result);
});
catchError()를 사용해서 Future 객체가 던질 수 있는 오류나 예외를 처리할 수 있어요.
httpClient
.read(url)
.then((String result) {
print(result);
})
.catchError((e) {
// Handle or ignore the error.
});
then().catchError() 패턴은 try-catch의 비동기 버전이에요.
중요:
catchError()는 반드시then()의 결과에 대해 호출하세요. 원래Future의 결과에 호출하면 안 돼요. 그렇지 않으면catchError()가 원래 Future의 계산에서 나온 오류만 처리하고then()이 등록한 핸들러의 오류는 처리하지 못해요.
여러 비동기 메서드 체이닝하기 (Chaining multiple asynchronous methods)
then() 메서드는 Future를 반환해서, 여러 비동기 함수를 특정 순서로 실행하는 유용한 방법을 제공해요. then()에 등록한 콜백이 Future를 반환하면, then()은 콜백이 반환한 Future와 같은 결과로 완료되는 Future를 반환해요. 콜백이 다른 타입의 값을 반환하면, then()은 그 값으로 완료되는 새 Future를 만들어요.
Future result = costlyQuery(url);
result
.then((value) => expensiveWork(value))
.then((_) => lengthyComputation())
.then((_) => print('Done!'))
.catchError((exception) {
/* Handle exception... */
});
위 예시에서 메서드는 다음 순서로 실행돼요.
costlyQuery()expensiveWork()lengthyComputation()
같은 코드를 await로 쓰면 다음과 같아요.
try {
final value = await costlyQuery(url);
await expensiveWork(value);
await lengthyComputation();
print('Done!');
} catch (e) {
/* Handle exception... */
}
여러 Future 기다리기 (Waiting for multiple futures)
때로는 알고리즘이 여러 비동기 함수를 호출하고, 계속하기 전에 모두가 완료되기를 기다려야 할 때가 있어요. Future.wait() 정적 메서드를 사용해서 여러 Future를 관리하고 완료를 기다리세요.
Future<void> deleteLotsOfFiles() async => /* ... */
Future<void> copyLotsOfFiles() async => /* ... */
Future<void> checksumLotsOfOtherFiles() async => /* ... */
await Future.wait([
deleteLotsOfFiles(),
copyLotsOfFiles(),
checksumLotsOfOtherFiles(),
]);
print('Done with all the long steps!');
Future.wait()는 주어진 모든 future가 완료되면 완료되는 future를 반환해요. 주어진 future 중 하나라도 실패하면 오류와 함께 완료되고, 그렇지 않으면 그 결과들과 함께 완료돼요.
여러 Future의 오류 처리하기 (Handling errors for multiple futures)
병렬 연산을 기다릴 때도 다음에 대해 사용할 수 있어요.
- futures의 iterable(반복 가능 객체)
- positional 필드로 futures를 가진 record(레코드)
이 확장(extension)들은 주어진 모든 future의 결과값을 담은 Future를 반환해요. Future.wait와 달리 오류도 처리할 수 있게 해 주지요.
컬렉션의 어떤 future라도 오류로 완료되면, wait는 ParallelWaitError로 완료돼요. 이렇게 하면 호출자가 개별 오류를 처리하고 필요한 경우 성공한 결과를 정리(dispose)할 수 있어요.
각 future의 결과값이 필요 없을 때는 futures의 iterable에 wait를 사용하세요.
Future<int> delete() async => ...;
Future<String> copy() async => ...;
Future<bool> errorResult() async => ...;
void main() async {
try {
// Wait for each future in a list, returns a list of futures:
var results = await [delete(), copy(), errorResult()].wait;
} on ParallelWaitError<List<bool?>, List<AsyncError?>> catch (e) {
print(e.values[0]); // Prints successful future
print(e.values[1]); // Prints successful future
print(e.values[2]); // Prints null when the result is an error
print(e.errors[0]); // Prints null when the result is successful
print(e.errors[1]); // Prints null when the result is successful
print(e.errors[2]); // Prints error
}
}
각 future의 개별 결과값이 필요할 때는 futures의 record(레코드)에 wait를 사용하세요. 그러면 futures가 서로 다른 타입일 수 있다는 추가 장점도 있어요.
Future<int> delete() async => ...;
Future<String> copy() async => ...;
Future<bool> errorResult() async => ...;
void main() async {
try {
// Wait for each future in a record.
// Returns a record of futures that you can destructure.
final (deleteInt, copyString, errorBool) =
await (delete(), copy(), errorResult()).wait;
// Do something with the results...
} on ParallelWaitError<
(int?, String?, bool?),
(AsyncError?, AsyncError?, AsyncError?)
> catch (e) {
// ...
}
}
Stream
Stream 객체는 Dart API 곳곳에 나타나며 데이터의 연속(sequence)을 나타내요. 예를 들어 버튼 클릭 같은 HTML 이벤트는 스트림을 통해 전달돼요. 파일을 스트림으로 읽을 수도 있어요.
비동기 for 루프 사용하기 (Using an asynchronous for loop)
때로는 Stream API를 쓰는 대신 비동기 for 루프(await for)를 사용할 수 있어요.
다음 함수를 살펴볼게요. Stream의 listen() 메서드를 사용해서 파일 목록을 구독하고, 각 파일이나 디렉터리를 검색하는 함수 리터럴을 전달해요.
void main(List<String> arguments) {
// ...
FileSystemEntity.isDirectory(searchPath).then((isDir) {
if (isDir) {
final startingDir = Directory(searchPath);
startingDir.list().listen((entity) {
if (entity is File) {
searchFile(entity, searchTerms);
}
});
} else {
searchFile(File(searchPath), searchTerms);
}
});
}
비동기 for 루프(await for)를 포함한 await 표현식의 동일한 코드는 동기 코드처럼 보여요.
void main(List<String> arguments) async {
// ...
if (await FileSystemEntity.isDirectory(searchPath)) {
final startingDir = Directory(searchPath);
await for (final entity in startingDir.list()) {
if (entity is File) {
searchFile(entity, searchTerms);
}
}
} else {
searchFile(File(searchPath), searchTerms);
}
}
중요:
await for를 사용하기 전에, 그것이 코드를 더 명확하게 만드는지 그리고 정말로 스트림의 모든 결과를 기다리길 원하는지 확인하세요. 예를 들어 DOM 이벤트 리스너에는 보통await for를 쓰지 말아야 해요. DOM은 끝없는 이벤트 스트림을 보내거든요.await for로 DOM 이벤트 리스너 두 개를 연달아 등록하면, 두 번째 종류의 이벤트는 절대 처리되지 않아요.
await 사용 및 관련 Dart 언어 기능에 대한 자세한 내용은 비동기 프로그래밍 튜토리얼을 참고하세요.
스트림 데이터 듣기 (Listening for stream data)
값이 도착할 때마다 그 값을 얻으려면 await for를 쓰거나, listen() 메서드로 스트림을 구독하면 돼요.
// Add an event handler to a button.
submitButton.onClick.listen((e) {
// When the button is clicked, it runs this code.
submitData();
});
이 예시에서 onClick 프로퍼티는 제출 버튼이 제공하는 Stream 객체예요.
이벤트 하나에만 관심이 있다면, first, last, single 같은 프로퍼티로 그 이벤트를 얻을 수 있어요. 이벤트를 처리하기 전에 테스트하려면 firstWhere(), lastWhere(), singleWhere() 같은 메서드를 사용하세요.
이벤트의 하위 집합에 관심이 있다면 skip(), skipWhile(), take(), takeWhile(), where() 같은 메서드를 사용할 수 있어요.
스트림 데이터 변환하기 (Transforming stream data)
종종 스트림의 데이터를 사용하기 전에 형식을 바꿔야 할 때가 있어요. transform() 메서드를 사용해서 다른 타입의 데이터를 내보내는 스트림을 만들어 보세요.
var lines = inputStream
.transform(utf8.decoder)
.transform(const LineSplitter());
이 예시는 변환기(transformer) 두 개를 사용해요. 먼저 utf8.decoder로 정수 스트림을 문자열 스트림으로 변환해요. 그다음 LineSplitter로 문자열 스트림을 개별 줄로 된 스트림으로 변환해요. 이 변환기들은 dart:convert 라이브러리에서 온 거예요(dart:convert 섹션 참고).
오류와 완료 처리하기 (Handling errors and completion)
오류 및 완료 처리 코드를 지정하는 방법은 비동기 for 루프(await for)를 쓰는지 Stream API를 쓰는지에 따라 달라져요.
비동기 for 루프를 사용한다면 try-catch로 오류를 처리하세요. 스트림이 닫힌 후에 실행할 코드는 비동기 for 루프 뒤에 두면 돼요.
Future<void> readFileAwaitFor() async {
var config = File('config.txt');
Stream<List<int>> inputStream = config.openRead();
var lines = inputStream
.transform(utf8.decoder)
.transform(const LineSplitter());
try {
await for (final line in lines) {
print('Got ${line.length} characters from stream');
}
print('file is now closed');
} catch (e) {
print(e);
}
}
Stream API를 사용한다면 onError 리스너를 등록해서 오류를 처리하세요. 스트림이 닫힌 후 실행할 코드는 onDone 리스너를 등록해서 실행하세요.
var config = File('config.txt');
Stream<List<int>> inputStream = config.openRead();
inputStream
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
(String line) {
print('Got ${line.length} characters from stream');
},
onDone: () {
print('file is now closed');
},
onError: (e) {
print(e);
},
);
더 알아보기
커맨드라인 앱에서 Future와 Stream을 사용하는 몇 가지 예시는 dart:io 문서를 참고하세요. 다음 글과 튜토리얼도 함께 살펴보세요.