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()을 추가하여 무한 대기 방지
実装ステップ
Future + async/await — async関数内でawaitで結果を待てば同期風に読めるコード
Future.wait() — 複数非同期タスクを並列実行、全完了まで待機
Stream + async*/yield — 連続イベントを生成するジェネレータ、リアルタイムデータに最適
FutureBuilder/StreamBuilder — Flutterで非同期データをウィジェット表示、ローディング/エラー/成功状態自動管理
メリット
- ✓ async/awaitでコールバック地獄なしにきれいな非同期コード
- ✓ FutureBuilder/StreamBuilderで非同期状態を宣言的に管理
デメリット
- ✗ Stream購読を解除しないとメモリリーク発生
- ✗ エラー処理を漏らすとアプリがクラッシュする可能性