Dart 비동기 프로그래밍

Future, async/await, Stream, FutureBuilder

Future는 단일 비동기 결과를, Stream은 연속적인 비동기 이벤트를 다룹니다. async/await로 비동기 코드를 동기식처럼 깔끔하게 작성하고, Future.wait()로 병렬 처리도 가능합니다. Flutter에서는 FutureBuilder와 StreamBuilder로 로딩/에러/성공 상태를 선언적으로 관리합니다.

Future — 나중에 완료되는 값

Future는 비동기 작업의 결과를 나타내는 객체입니다. then으로 성공, catchError로 실패, whenComplete로 완료를 처리합니다.

Future<String> fetchData() { return Future.delayed(Duration(seconds: 3), () { return '서버에서 받은 데이터'; }); } void main() { print('작업 시작'); fetchData().then((data) { print('데이터: $data'); }).catchError((error) { print('오류 발생: $error'); }).whenComplete(() { print('작업 완료'); }); print('다음 작업 진행'); }

Future 생성 방법

메서드설명
Future.value()즉시 완료되는 Future
Future.delayed()지정 시간 후 완료
Future.error()오류로 완료되는 Future
Completer복잡한 비동기 로직 직접 제어
// Future.value — 즉시 완료 Future<String> getFuture() { return Future.value('즉시 사용 가능한 값'); } // Future.delayed — 지정 시간 후 완료 Future<String> getDelayedFuture() { return Future.delayed(Duration(seconds: 2), () { return '2초 후 사용 가능한 값'; }); } // Future.error — 오류로 완료 Future<String> getErrorFuture() { return Future.error('오류 발생'); } // Completer — 수동 제어 import 'dart:async'; Future<String> complexOperation() { final completer = Completer<String>(); Timer(Duration(seconds: 2), () { if (DateTime.now().second % 2 == 0) { completer.complete('성공!'); } else { completer.completeError('실패!'); } }); return completer.future; }

Future 체이닝

여러 비동기 작업을 then으로 연결하여 순차적으로 실행할 수 있습니다.

void main() { fetchUserId() .then((id) => fetchUserData(id)) .then((userData) => saveUserData(userData)) .then((_) => print('모든 작업 완료')) .catchError((error) => print('오류 발생: $error')); } Future<String> fetchUserId() => Future.value('user123'); Future<Map<String, dynamic>> fetchUserData(String id) => Future.value({'id': id, 'name': '홍길동', 'email': 'hong@example.com'}); Future<void> saveUserData(Map<String, dynamic> userData) => Future.value(print('데이터 저장됨: $userData'));

async/await — 동기식처럼 읽히는 비동기 코드

async 함수는 항상 Future를 반환하며, await로 결과를 기다립니다. try-catch-finally로 에러 처리가 가능합니다.

Future<String> fetchData() async { await Future.delayed(Duration(seconds: 2)); return '서버에서 받은 데이터'; } void main() async { print('작업 시작'); try { String data = await fetchData(); print('데이터: $data'); } catch (e) { print('오류 발생: $e'); } finally { print('작업 완료'); } print('다음 작업 진행'); }

순차 처리 vs 병렬 처리

순차 처리는 await를 연속으로, 병렬 처리는 Future.wait()로 동시에 실행합니다.

// 순차 처리 — 총 6초 (2+3+1) Future<void> sequentialTasks() async { final startTime = DateTime.now(); final result1 = await task1(); // 2초 final result2 = await task2(); // 3초 final result3 = await task3(); // 1초 print('소요 시간: ${DateTime.now().difference(startTime).inSeconds}초'); } // 병렬 처리 — 총 3초 (가장 느린 작업 기준) Future<void> parallelTasks() async { final startTime = DateTime.now(); final results = await Future.wait([ task1(), // 2초 task2(), // 3초 task3(), // 1초 ]); print('소요 시간: ${DateTime.now().difference(startTime).inSeconds}초'); } Future<String> task1() => Future.delayed(Duration(seconds: 2), () => '작업1 결과'); Future<String> task2() => Future.delayed(Duration(seconds: 3), () => '작업2 결과'); Future<String> task3() => Future.delayed(Duration(seconds: 1), () => '작업3 결과');

Future API: wait, any, forEach

// Future.wait — 모두 완료될 때까지 대기 Future<void> waitExample() async { final results = await Future.wait([ Future.delayed(Duration(seconds: 1), () => '결과1'), Future.delayed(Duration(seconds: 2), () => '결과2'), Future.delayed(Duration(seconds: 3), () => '결과3'), ]); print(results); // [결과1, 결과2, 결과3] } // Future.any — 가장 먼저 완료된 하나만 Future<void> anyExample() async { final result = await Future.any([ Future.delayed(Duration(seconds: 3), () => '느린 작업'), Future.delayed(Duration(seconds: 1), () => '빠른 작업'), Future.delayed(Duration(seconds: 2), () => '중간 작업'), ]); print(result); // '빠른 작업' } // Future.forEach — 순차적으로 하나씩 처리 Future<void> forEachExample() async { final items = [1, 2, 3, 4, 5]; await Future.forEach(items, (int item) async { await Future.delayed(Duration(milliseconds: 500)); print('처리 중: $item'); }); print('모든 항목 처리 완료'); }

Stream — 연속적인 비동기 이벤트

Stream은 시간에 따라 여러 값을 비동기적으로 제공합니다. async*yield로 제너레이터를 만들 수 있습니다.

Stream<int> countStream(int max) async* { for (int i = 1; i <= max; i++) { await Future.delayed(Duration(seconds: 1)); yield i; } } void main() async { // await for 사용 await for (final count in countStream(5)) { print(count); } // listen 사용 countStream(5).listen( (data) => print(data), onError: (error) => print('오류: $error'), onDone: () => print('스트림 완료'), ); }

StreamController & Stream 생성 방법

import 'dart:async'; // StreamController — 세밀한 제어 Stream<int> getControllerStream() { final controller = StreamController<int>(); Timer.periodic(Duration(seconds: 1), (timer) { if (timer.tick <= 5) { controller.add(timer.tick); } else { controller.close(); timer.cancel(); } }); return controller.stream; } // Stream.fromIterable — 컬렉션에서 생성 Stream<int> getIterableStream() { return Stream.fromIterable([1, 2, 3, 4, 5]); } // Stream.periodic — 주기적 이벤트 Stream<int> getPeriodicStream() { return Stream.periodic(Duration(seconds: 1), (count) => count + 1) .take(5); }

Stream 변환 (map, where, take)

void streamTransformations() async { final stream = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); final doubled = stream.map((value) => value * 2); final evenOnly = doubled.where((value) => value % 2 == 0); final limited = evenOnly.take(3); await for (final value in limited) { print(value); // 2, 4, 6 } }

Broadcast Stream — 여러 구독자

일반 Stream은 한 번만 구독 가능하지만, StreamController.broadcast()로 여러 구독자를 지원합니다.

void broadcastStreamExample() { final controller = StreamController<int>.broadcast(); final subscription1 = controller.stream.listen( (data) => print('구독자 1: $data'), onDone: () => print('구독자 1: 완료'), ); final subscription2 = controller.stream.listen( (data) => print('구독자 2: $data'), onDone: () => print('구독자 2: 완료'), ); controller.add(1); controller.add(2); controller.add(3); subscription1.cancel(); // 구독자 1 해제 controller.add(4); // 구독자 2만 수신 controller.add(5); controller.close(); }

Stream 구독 관리 (메모리 누수 방지)

주의: Stream 구독 해제 필수!

구독을 해제하지 않으면 메모리 누수가 발생합니다. dispose()에서 반드시 cancel()을 호출하세요.

class DataService { StreamSubscription<int>? _subscription; void startListening() { _subscription?.cancel(); // 기존 구독 해제 _subscription = getPeriodicStream().listen( (data) => print('받은 데이터: $data'), onDone: () => print('스트림 완료'), ); } void stopListening() { _subscription?.cancel(); _subscription = null; } void dispose() { stopListening(); } }

FutureBuilder — 단일 비동기 결과를 위젯으로

connectionState로 로딩/에러/성공 상태를 자동 관리합니다.

FutureBuilder<String>( future: fetchData(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return CircularProgressIndicator(); } else if (snapshot.hasError) { return Text('오류 발생: ${snapshot.error}'); } else if (snapshot.hasData) { return Text('데이터: ${snapshot.data}'); } else { return Text('데이터 없음'); } }, )

StreamBuilder — 실시간 데이터를 위젯으로

StreamBuilder<int>( stream: countdownStream(10), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.active) { return Text('카운트다운: ${snapshot.data}'); } else if (snapshot.connectionState == ConnectionState.done) { return Text('카운트다운 완료!'); } else { return CircularProgressIndicator(); } }, )

실전 예제: FutureBuilder로 사용자 목록 표시

Future<List<User>> fetchUsers() async { final response = await http.get( Uri.parse('https://api.example.com/users'), ); if (response.statusCode == 200) { final List<dynamic> data = jsonDecode(response.body); return data.map((json) => User.fromJson(json)).toList(); } else { throw Exception('Failed to load users'); } } class UserListScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('사용자 목록')), body: FutureBuilder<List<User>>( future: fetchUsers(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { return Center(child: Text('오류: ${snapshot.error}')); } else if (snapshot.hasData) { final users = snapshot.data!; return ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ); } else { return Center(child: Text('사용자가 없습니다')); } }, ), ); } }

타임아웃 처리

Future<String> fetchWithTimeout() { return fetchData().timeout( Duration(seconds: 5), onTimeout: () => throw TimeoutException('요청 시간 초과'), ); }

compute() — CPU 집약 작업 격리

무거운 연산은 compute()로 별도 Isolate에서 실행하여 UI 스레드를 보호합니다.

Future<List<ComplexData>> processLargeDataSet( List<RawData> rawData, ) { return compute(processDataInBackground, rawData); } List<ComplexData> processDataInBackground( List<RawData> rawData, ) { return rawData.map((raw) => ComplexData.process(raw)).toList(); }

UI 피드백 패턴 (로딩/성공/실패)

Future<void> saveData() async { setState(() => _isLoading = true); try { await uploadData(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('데이터가 성공적으로 저장되었습니다')), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('저장 실패: $e')), ); } finally { setState(() => _isLoading = false); } }

모범 사례 요약

  • try-catch 필수 — 비동기 작업에는 항상 에러 처리를 추가하세요
  • Stream 구독 해제 — dispose()에서 반드시 cancel()을 호출하여 메모리 누수 방지
  • 병렬 처리 활용 — 독립적인 작업은 Future.wait()로 동시 실행하여 성능 향상
  • compute() 사용 — CPU 집약적 연산은 별도 Isolate에서 실행하여 UI 프레임 드롭 방지
  • 타임아웃 설정 — 네트워크 요청에는 .timeout()을 추가하여 무한 대기 방지

구현 순서

1

Future + async/await — async 함수 안에서 await로 결과를 기다리면 동기식처럼 읽히는 코드 작성 가능

2

Future.wait() — 여러 비동기 작업을 병렬로 동시 실행, 모두 완료될 때까지 대기

3

Stream + async*/yield — 연속적인 이벤트를 생성하는 제너레이터, 실시간 데이터에 적합

4

FutureBuilder/StreamBuilder — Flutter에서 비동기 데이터를 위젯으로 표시, 로딩/에러/성공 상태 자동 관리

장점

  • async/await로 콜백 지옥 없이 깔끔한 비동기 코드 작성
  • FutureBuilder/StreamBuilder로 비동기 상태를 선언적으로 관리

단점

  • Stream 구독을 해제하지 않으면 메모리 누수 발생
  • 에러 처리를 빠뜨리면 앱이 크래시할 수 있음

사용 사례

REST API 호출 후 결과를 화면에 표시 (FutureBuilder) 채팅, 주가, 위치 추적 등 실시간 데이터 (StreamBuilder)

참고 자료