다트 비동기 프로그래밍 Docs: https://dart.dev/codelabs/async-await
Stream Docs : https://dart.dev/tutorials/language/streams
플러터 공식유튜브 event-loop 설명 영상 : https://www.youtube.com/watch?v=vl_AaCgudcY
참고할만 한 비동기 강의 영상: https://www.youtube.com/watch?v=rk41rBXq3zQ
참고할만 한 비동기 강의 영상2 : https://www.youtube.com/watch?v=HjhPhAUPHos
목차
1. Event-loop
- a. isolate란?
- b. isolate 장점
- c. event-loop의 작동 방식
- d. background worker
2. Future
- a. Future란?
- b. Future 사용 예시
3. async, await
- a. 사용하기
- b. 값을 알고 있는 경우
4. then, catchError, whenComplete
- a. 사용법 및 예시
- b. 실행 순서 파악하기
5. Flutter에서 FutureBuilder 사용하기
6. Stream
1. Event-loop
a. Isolate란?
- Dart에서 비동기 프로그래밍을 가능하게 하는 요소
- 이 곳에서 Dart의 코드가 실행됨
- 단일 스레드가 메모리를 가진 채로 격리되어 있음
- 고유한 이벤트 루프를 가지고 이벤트를 처리함
- 여러 개의 Isolate를 만들 수 있지만 서로 메모리를 공유하지 않음
b. Isolate 장점
- 메모리할당, 가비지 콜렉션 시에 잠금을 필요로하지 않음(단일 스레드이기때문)
*멀티스레드 환경에서 가비지 콜렉션은 동기화 문제 때문에 락을 걸어줌
c. event-loop의 작동 방식
매우 단순하다. 이벤트 큐에 있는 이벤트들을 들어온 순서대로 하나씩 처리한다. 처리가 완료된 이벤트는 폐기한다. 손으로 모바일 화면을 탭하는 행동, Http 응답 데이터를 전달받는 것 등이 모두 이벤트가 될 수 있다.
d. background worker
Background worker : https://dart.dev/guides/language/concurrency#background-workers
용량이 큰 JSON 파일을 읽는 것과 같이 처리 시간이 긴 작업으로 인해 UI 반응이 느려질 수 있다. 그럴 때 background worker라 불리우는 worker isolate를 생성하는 것을 고려해볼 수 있다.
*대부분의 앱은 Main isolate로도 충분하다고 한다.
사용 예시
Isolate.spawn() : background work 생성 및 실행
Isolate.exit() : 작업 결과 데이터를 전송
Isolate library : https://api.dart.dev/stable/2.18.3/dart-isolate/Isolate-class.html
void main() async {
// Read some data.
final jsonData = await _parseInBackground();
// Use that data
print('Number of JSON keys: ${jsonData.length}');
}
// Spawns an isolate and waits for the first message
Future<Map<String, dynamic>> _parseInBackground() async {
final p = ReceivePort();
await Isolate.spawn(_readAndParseJson, p.sendPort);
return await p.first as Map<String, dynamic>;
}
Future<void> _readAndParseJson(SendPort p) async {
final fileData = await File(filename).readAsString();
final jsonData = jsonDecode(fileData);
Isolate.exit(p, jsonData);
}
2. Future
a. Future란?
비동기 함수를 실행하게 되면, 함수는 Future를 반환한다. 그때의 Future 상태는 uncompleted 상태다. 해당 함수가 종료되지 않았기 때문에 구체적인 값은 알 수 없는 상태를 의미한다.
이것을 마치 전달받은 택배에 비유할 수 있다.
아직 택배를 열 수는 없어서 안에 어떤 것이 들어있는 지는 모른다. - uncompleted
비동기 함수가 완료되어 값이 전달되면, 택배를 열 수 있다. 그럼 그 안에 값 또는 에러가 들어있다. - completed
Example)
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());
}
console
Your order is: Instance of '_Future<String>'
Future를 사용한 예시인데, 콘솔 결과를 보면 원하는 Large Latte가 출력되지 않았다. 그 이유는 변수 order에 할당된 Future값은 uncompleted 상태이기 때문이다. 비동기 함수가 완료되고 completed 상태의 Future를 할당받으려면 async, await 키워드를 추가적으로 사용해야 한다.(뒤에서 소개)
b. Future 사용 예시
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() {
fetchUserOrder();
print('Fetching user order...');
}
Console
Fetching user order...
Large Latte
에러 반환 예시)
Future<void> fetchUserOrder() {
// Imagine that this function is fetching user info but encounters a bug
return Future.delayed(const Duration(seconds: 2),
() => throw Exception('Logout failed: user ID is invalid'));
}
void main() {
fetchUserOrder();
print('Fetching user order...');
}
Console
Fetching user order...
Uncaught Error: Exception: Logout failed: user ID is invalid
3. async, await
a. 사용하기
비동기 함수를 정의하기 위해, body 앞에 async 키워드를 달아준다.
void main() async { ··· }
async 키워드가 있는 곳에 한하여 await 키워드를 비동기 함수 앞에 달아줄 수 있다.
print(await createOrderMessage());
async, await 키워드를 사용하여 비동기 함수를 올바르게 작성한 코드는 아래와 같다.
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());
}
Console
Fetching user order...
Your order is: Large Latte
createOrderMessage()에서는 fetchUserOrder()가 완료되기를 기다렸다가, 완료된 후 전달받은 ‘Large Latte’를 order 변수에 할당한다.
b. 값을 알고 있는 경우
값을 이미 알고 있는 경우 Future.value(), Future.error()를 사용해서 즉시 Future를 생성하도록 할 수 있다.
이 메서드도 여전히 비동기로 처리된다.
Future<int> getValue(){
return Future.value(3);
}
Future<int> getError(){
return Future.error(Exception());
}
4. then, catchError, whenComplete
a. 사용법 및 예시
then : 비동기 함수가 완료되고 나서 실행되는 메서드.
catchError : 에러가 있을 때 실행되는 메서드.
whenComplete : 값이 있든, 에러가 발생하든 실행되는 메서드. 자바의 finally와 유사하다.
위의 세가지 메서드 모두 반환값으로 Future를 반환하기 때문에 then을 여러 번 이어 붙이는 것도, error를 계속 이어 붙여 에러를 종류마다 체크하는 것도 전부 가능하다.
void main() {
Future.delayed(
Duration(seconds: 3),
() { return 100; },
).then((value){
print("value = $value");
}).catchError(
(err){
print('Caught $err');
},
test: (err) => err.runtimeType == String,
).whenComplete((){
print('Finished.');
});
print('waiting');
}
Console
waiting
value = 100
Finished.
b. 실행 순서 파악하기
then이 등장하면서 호출 순서에 대해 약간 헷갈려서 아래와 같은 코드를 작성하고 실행을 해보았다.
void main() async {
var result = await methodA();
print('result = $result');
}
Future<int> methodA(){
print('A');
return Future.delayed(
Duration(seconds: 1),
() {
print('B');
return 1;
},
).then((value){
print('C');
return 2;
});
}
console
A
//1초 후,
B
C
result = 2
과정은 다음과 같다.
- methodA가 실행됨 → print(’A’) 실행
- 비동기함수인 methodA가 실행되는 동안 await로 인해 main 함수는 잠시 대기
- 1초 딜레이 후 → print(’B’) 실행되고 1을 리턴 → Future 값은 1이 됨
- 뒤이어 바로 then이 실행되므로 1을 리턴하지 못하고 print(’C’) 실행 및 2를 리턴
- methodA()가 종료되고 반환값 2를 result 변수에 할당
- print('result = $result') 실행
5. Flutter에서 FutureBuilder 사용하기
플러터에서 Future를 사용하는 것은 매우 간단하다.
FutureBuilder를 사용하면 매우 편리하게 Future를 다룰 수 있다.
FutureBuilder의 future 파라미터에 원하는 비동기 함수를 등록하고, builder의 snapshot에는 현재의 데이터가 들어가 있기 때문에 이 snapshot을 가지고 원하는 동작을 구현해주면 된다.
snapshot의 메서드인 hasError, hasData를 통해 Future의 세 가지 상태(uncompleted, completed with value, completed with error)를 모두 구별해줄 수 있다.
6. Stream
- stream 특징
Stream은 async* 키워드를 사용한다.
yield를 통해 연속적으로 값을 리턴해줄 수 있다.
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
await for (final value in stream) {
print('sum');
sum += value;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
print('stream');
yield i;
}
}
void main() async {
var stream = countStream(5);
var sum = await sumStream(stream);
print(sum); // 15
}
Console
stream
sum
stream
sum
stream
sum
stream
sum
stream
sum
15
- listen() 메서드
listen() 메서드를 사용하면 yield를 통해 반환되는 값을 사용하는 함수를 작성할 수 있다.
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
yield i;
}
}
void main() async {
countStream(5).listen((val){
print(val);
});
}
- 스트림 처리 메서드
Future<T> get first;
Future<bool> get isEmpty;
Future<T> get last;
Future<int> get length;
Future<T> get single;
Future<bool> any(bool Function(T element) test);
Future<bool> contains(Object? needle);
Future<E> drain<E>([E? futureValue]);
Future<T> elementAt(int index);
Future<bool> every(bool Function(T element) test);
Future<T> firstWhere(bool Function(T element) test, {T Function()? orElse});
Future<S> fold<S>(S initialValue, S Function(S previous, T element) combine);
Future forEach(void Function(T element) action);
Future<String> join([String separator = '']);
Future<T> lastWhere(bool Function(T element) test, {T Function()? orElse});
Future pipe(StreamConsumer<T> streamConsumer);
Future<T> reduce(T Function(T previous, T element) combine);
Future<T> singleWhere(bool Function(T element) test, {T Function()? orElse});
Future<List<T>> toList();
Future<Set<T>> toSet();
- 스트림 변경 메서드
Stream<R> cast<R>();
Stream<S> expand<S>(Iterable<S> Function(T element) convert);
Stream<S> map<S>(S Function(T event) convert);
Stream<T> skip(int count);
Stream<T> skipWhile(bool Function(T element) test);
Stream<T> take(int count);
Stream<T> takeWhile(bool Function(T element) test);
Stream<T> where(bool Function(T event) test);
댓글