Dart 비동기 — Future, async await, Stream
Dart 비동기 — Future, async/await, Stream
네트워크 요청, 파일 읽기, DB 쿼리 등 시간이 걸리는 작업은 비동기로 처리해야 합니다. Dart는 단일 스레드 기반이지만, 이벤트 루프를 통해 비동기 작업을 효율적으로 처리합니다.
면접에서 "Dart는 단일 스레드인데 어떻게 비동기가 가능한가요?"라는 질문이 자주 나옵니다. ** 이벤트 루프 **가 답입니다.
Future
Future는 ** 미래에 완료될 값 **을 나타내는 객체입니다. JavaScript의 Promise와 같은 개념입니다.
// Future를 반환하는 함수
Future<String> fetchUserName() {
// 2초 후에 값을 반환하는 시뮬레이션
return Future.delayed(
const Duration(seconds: 2),
() => '심정훈',
);
}
// then 체이닝 (콜백 방식)
void loadUser() {
fetchUserName()
.then((name) => print('이름: $name'))
.catchError((error) => print('에러: $error'))
.whenComplete(() => print('완료'));
}
async/await
then 체이닝보다 async/await이 훨씬 읽기 좋습니다.
// async/await 방식 (권장)
Future<void> loadUser() async {
try {
final name = await fetchUserName();
print('이름: $name');
} catch (e) {
print('에러: $e');
} finally {
print('완료');
}
}
여러 Future를 동시에 실행
Future<void> loadDashboard() async {
// 순차 실행 (느림 - 총 4초)
final user = await fetchUser(); // 2초
final posts = await fetchPosts(); // 2초
// 병렬 실행 (빠름 - 총 2초)
final results = await Future.wait([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
// 구조 분해로 결과 받기
final (userData, postData) = await (
fetchUser(),
fetchPosts(),
).wait;
}
면접 포인트: Future.wait을 사용하면 독립적인 비동기 작업을 병렬로 실행할 수 있습니다. API 호출이 여러 개일 때 성능 개선에 효과적입니다.
Stream
Future는 ** 단일 값 **, Stream은 ** 연속적인 값 **을 비동기로 전달합니다.
// Stream 생성
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i; // 값을 하나씩 방출
}
}
// Stream 소비
void listenToCount() async {
// await for (권장)
await for (final count in countStream(5)) {
print('카운트: $count');
}
// listen (콜백 방식)
countStream(5).listen(
(count) => print('카운트: $count'),
onError: (error) => print('에러: $error'),
onDone: () => print('완료'),
);
}
Stream 종류
| 종류 | 특징 | 예시 |
|---|---|---|
| Single-subscription | 리스너 1개만 가능 | HTTP 응답, 파일 읽기 |
| Broadcast | 여러 리스너 가능 | 이벤트 버스, 소켓 |
// StreamController로 직접 Stream 만들기
final controller = StreamController<String>.broadcast();
// 값 추가
controller.sink.add('새 메시지');
// 구독
final subscription = controller.stream.listen((data) {
print('받은 데이터: $data');
});
// 정리 (메모리 누수 방지!)
subscription.cancel();
controller.close();
Stream 변환
final numbers = Stream.fromIterable([1, 2, 3, 4, 5]);
// map: 값 변환
final doubled = numbers.map((n) => n * 2);
// where: 필터링
final evens = numbers.where((n) => n % 2 == 0);
// expand: 1:N 변환
final expanded = numbers.expand((n) => [n, n * 10]);
// take: 앞에서 N개만
final firstThree = numbers.take(3);
// distinct: 중복 제거
final unique = numbers.distinct();
이벤트 루프
Dart의 이벤트 루프는 두 개의 큐를 관리합니다.
┌─────────────────────────────────┐
│ 이벤트 루프 │
│ │
│ 1. Microtask Queue (우선) │
│ - scheduleMicrotask() │
│ - Future.then() 콜백 │
│ │
│ 2. Event Queue │
│ - I/O 이벤트 │
│ - Timer │
│ - UI 이벤트 │
└─────────────────────────────────┘
void eventLoopDemo() {
print('1. 동기 코드');
Future(() => print('4. Event Queue'));
Future.microtask(() => print('3. Microtask Queue'));
print('2. 동기 코드');
}
// 출력 순서: 1 → 2 → 3 → 4
Microtask Queue가 Event Queue보다 우선순위가 높습니다. 동기 코드가 모두 실행된 후, Microtask → Event 순으로 처리됩니다.
Flutter에서의 활용
class DataScreen extends StatefulWidget {
const DataScreen({super.key});
@override
State<DataScreen> createState() => _DataScreenState();
}
class _DataScreenState extends State<DataScreen> {
late Future<List<String>> _dataFuture;
@override
void initState() {
super.initState();
_dataFuture = _loadData(); // 한 번만 호출
}
Future<List<String>> _loadData() async {
final response = await fetchFromApi();
return response;
}
@override
Widget build(BuildContext context) {
// FutureBuilder로 비동기 결과를 위젯에 반영
return FutureBuilder<List<String>>(
future: _dataFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('에러: ${snapshot.error}');
}
final data = snapshot.data!;
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) => Text(data[index]),
);
},
);
}
}
정리
- Future = 단일 비동기 값, Stream = 연속 비동기 값
async/await을 사용하면 비동기 코드를 동기처럼 읽기 좋게 작성할 수 있습니다- 독립적인 비동기 작업은
Future.wait으로 병렬 실행하세요 - StreamController 사용 후 반드시
close()해서 메모리 누수를 방지하세요 - 이벤트 루프와 Microtask/Event Queue의 우선순위는 면접 필수 포인트입니다
댓글 로딩 중...