Dart 비동기 프로그래밍
이 문서 주변 탐색
주제 태그, 링크, 시리즈 흐름을 중심으로 옆으로 이동할 수 있습니다.
시리즈 흐름
이 문서는 아직 읽기 시리즈에 연결되지 않았습니다.
관련 문서
같이 읽을 만한 관련 문서가 아직 없습니다.
이 문서를 참조하는 문서
이 문서를 참조하는 다른 문서가 아직 없습니다.
1. 비동기 프로그래밍이란?
동기 vs 비동기
// 동기 (Synchronous) - 순차 실행, 기다림
void syncExample() {
print('1. 시작');
sleep(Duration(seconds: 2)); // 2초 동안 아무것도 못함
print('2. 끝');
}
// 비동기 (Asynchronous) - 기다리는 동안 다른 일 가능
Future<void> asyncExample() async {
print('1. 시작');
await Future.delayed(Duration(seconds: 2)); // 기다리는 동안 다른 작업 가능
print('2. 끝');
}왜 비동기가 필요할까?
// ❌ 동기 방식 - UI가 멈춤!
void fetchUserSync() {
var response = httpGetSync('https://api.example.com/user'); // 3초 대기
// 이 3초 동안 앱이 멈춤 (버튼 클릭, 스크롤 불가)
print(response);
}
// ✅ 비동기 방식 - UI 반응성 유지
Future<void> fetchUserAsync() async {
var response = await http.get('https://api.example.com/user'); // 3초 대기
// 기다리는 동안 다른 이벤트 처리 가능 (버튼, 스크롤 등)
print(response);
}Dart는 싱글 스레드임. 비동기는 멀티 스레드가 아님!
Event Loop가 비동기 작업을 관리함 (JavaScript랑 동일한 모델)
네트워크 요청, 파일 I/O, 타이머 등 시간이 걸리는 작업에 필수임
Flutter 앱이 크래시되면 사용자 경험이 크게 저하됨
Event Loop 이해하기
void main() {
print('1. main 시작');
Future(() => print('4. Future (Event Queue)'));
Future.microtask(() => print('3. Microtask (우선순위 높음)'));
print('2. main 끝');
}
// 출력 순서:
// 1. main 시작
// 2. main 끝
// 3. Microtask (우선순위 높음)
// 4. Future (Event Queue)
큐 | 우선순위 | 용도 |
|---|---|---|
Microtask Queue | 높음 | 짧은 내부 작업 |
Event Queue | 낮음 | I/O, 타이머, 사용자 이벤트 |
2. Future 기초
Future란?
Future는 **"미래에 완료될 값"**을 나타내는 객체임. 나중에 값이 올 거라는 약속 같은 거
void main() {
print('주문 시작');
// Future는 약속(Promise)과 같음
Future<String> orderFuture = orderCoffee();
print('다른 일 하는 중...');
// Future가 완료되면 then으로 결과 처리
orderFuture.then((coffee) {
print('받은 커피: $coffee');
});
print('주문 끝');
}
Future<String> orderCoffee() {
// 2초 후에 '아메리카노' 반환
return Future.delayed(
Duration(seconds: 2),
() => '아메리카노',
);
}
// 출력:
// 주문 시작
// 다른 일 하는 중...
// 주문 끝
// (2초 후)
// 받은 커피: 아메리카노Future 생성 방법
void main() async {
// 1. 즉시 완료되는 Future
var instant = Future.value(42);
print(await instant); // 42
// 2. 즉시 에러를 발생시키는 Future
var error = Future.error('에러 발생!');
// await error; // 에러 던짐
// 3. 지연된 Future
var delayed = Future.delayed(
Duration(seconds: 1),
() => '1초 후 완료',
);
print(await delayed);
// 4. 동기 코드를 Future로 감싸기
var computed = Future(() {
// 여기서 무거운 계산
return 100 * 100;
});
print(await computed); // 10000
}Future의 3가지 상태
Future<String> fetchData() async {
// 1. Uncompleted (대기중) - 작업 진행 중
await Future.delayed(Duration(seconds: 2));
if (Random().nextBool()) {
// 2. Completed with value (성공)
return '데이터 로드 완료';
} else {
// 3. Completed with error (실패)
throw Exception('네트워크 에러');
}
}Future는 JavaScript의 Promise랑 거의 동일함
then은 Promise의.then(),catchError는.catch()와 같음Future는 한 번만 완료됨. 여러 값이 필요하면 Stream 써야 함
3. async/await
기본 사용법
async/await는 비동기 코드를 동기 코드처럼 작성할 수 있게 해줌
// ❌ then 체이닝 - 콜백 지옥의 시작
void fetchUserThen() {
fetchUserId()
.then((userId) => fetchUserData(userId))
.then((userData) => fetchUserPosts(userData.id))
.then((posts) {
print('게시물: $posts');
})
.catchError((error) {
print('에러: $error');
});
}
// ✅ async/await - 읽기 쉬운 동기 스타일
Future<void> fetchUserAsync() async {
try {
var userId = await fetchUserId();
var userData = await fetchUserData(userId);
var posts = await fetchUserPosts(userData.id);
print('게시물: $posts');
} catch (error) {
print('에러: $error');
}
}async 함수의 특징
// async 함수는 항상 Future를 반환
Future<int> getNumber() async {
return 42; // 자동으로 Future<int>로 감싸짐
}
// void도 Future<void>가 됨
Future<void> doSomething() async {
print('작업 중...');
}
// await 없이 async만 써도 됨 (근데 의미 없음)
Future<String> unnecessary() async {
return '불필요한 async'; // 이건 그냥 Future.value와 같음
}await의 동작
Future<void> explainAwait() async {
print('1. await 전');
// await를 만나면 여기서 "일시 정지"
// 함수 실행이 멈추고 Event Loop로 제어권 반환
var result = await Future.delayed(
Duration(seconds: 1),
() => '완료',
);
// Future가 완료되면 여기서부터 재개
print('2. await 후: $result');
}
void main() {
print('A. main 시작');
explainAwait(); // await 없이 호출 - 기다리지 않음!
print('B. main 끝');
}
// 출력:
// A. main 시작
// 1. await 전
// B. main 끝
// (1초 후)
// 2. await 후: 완료await는 async 함수 안에서만 사용 가능await를 만나면 함수 실행이 "일시 정지"되고, Future가 완료되면 재개됨async 함수 호출할 때
await안 붙이면 기다리지 않고 바로 다음 줄 실행!모든
await가 순차 실행되니까, 독립적인 작업은Future.wait로 병렬화하면 됨
4. 에러 처리
try-catch-finally
Future<void> fetchWithErrorHandling() async {
try {
var data = await fetchData();
print('성공: $data');
} on FormatException catch (e) {
// 특정 예외 타입 처리
print('형식 에러: $e');
} on HttpException catch (e, stackTrace) {
// 스택 트레이스도 받을 수 있음
print('HTTP 에러: $e');
print('스택: $stackTrace');
} catch (e) {
// 나머지 모든 예외
print('알 수 없는 에러: $e');
} finally {
// 성공/실패 상관없이 항상 실행
print('정리 작업');
}
}then/catchError 방식
void fetchWithThen() {
fetchData()
.then((data) => print('성공: $data'))
.catchError(
(e) => print('형식 에러: $e'),
test: (e) => e is FormatException, // 조건부 처리
)
.catchError((e) => print('기타 에러: $e'))
.whenComplete(() => print('정리 작업')); // finally랑 같음
}에러 전파
Future<String> level1() async {
return await level2(); // 에러가 자동으로 전파됨
}
Future<String> level2() async {
return await level3();
}
Future<String> level3() async {
throw Exception('깊은 곳에서 발생한 에러');
}
void main() async {
try {
await level1(); // level3의 에러가 여기까지 전파
} catch (e) {
print('최상위에서 캐치: $e');
}
}실전: API 에러 처리 패턴
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException(this.statusCode, this.message);
@override
String toString() => 'ApiException($statusCode): $message';
}
Future<User> fetchUser(String id) async {
try {
final response = await http.get(Uri.parse('/users/$id'));
switch (response.statusCode) {
case 200:
return User.fromJson(jsonDecode(response.body));
case 404:
throw ApiException(404, '사용자를 찾을 수 없습니다');
case 401:
throw ApiException(401, '인증이 필요합니다');
default:
throw ApiException(response.statusCode, '서버 에러');
}
} on SocketException {
throw ApiException(0, '네트워크 연결을 확인하세요');
}
}
// 사용
void main() async {
try {
var user = await fetchUser('123');
print(user.name);
} on ApiException catch (e) {
if (e.statusCode == 401) {
// 로그인 화면으로 이동
} else {
// 에러 메시지 표시
showError(e.message);
}
}
}async/await랑try/catch조합이 가장 깔끔함에러 캐치 안 하면 호출자에게 전파됨
커스텀 Exception 클래스 만들어서 의미 있는 에러 정보 전달하면 좋음
finally/whenComplete는 리소스 정리(연결 닫기 등)에 씀
5. 여러 Future 처리
Future.wait - 모두 완료 대기
Future<void> fetchAllData() async {
// 🐌 순차 실행 - 총 6초
var user = await fetchUser(); // 2초
var posts = await fetchPosts(); // 2초
var comments = await fetchComments(); // 2초
// 🚀 병렬 실행 - 총 2초 (가장 느린 것 기준)
var results = await Future.wait([
fetchUser(), // 2초
fetchPosts(), // 2초
fetchComments(), // 2초
]);
var user = results[0];
var posts = results[1];
var comments = results[2];
}
Future.wait 타입 안전하게 쓰기
Future<void> fetchWithTypes() async {
// Record 타입으로 받기 (Dart 3.0+)
var (user, posts) = await (
fetchUser(),
fetchPosts(),
).wait;
print(user.name);
print(posts.length);
}
// 또는 구조 분해
Future<void> fetchDestructured() async {
final results = await Future.wait([
fetchUser(),
fetchPosts(),
]);
final user = results[0] as User;
final posts = results[1] as List<Post>;
}Future.wait 에러 처리
Future<void> fetchWithErrorHandling() async {
try {
// 하나라도 실패하면 전체 실패
var results = await Future.wait([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
} catch (e) {
print('하나 이상 실패: $e');
}
// eagerError: false면 모든 Future 완료 후 첫 번째 에러 던짐
try {
var results = await Future.wait(
[fetchUser(), fetchPosts()],
eagerError: false,
);
} catch (e) {
print('에러: $e');
}
}eagerError: false (기본값)
에러가 발생해도 나머지 Future가 모두 완료될 때까지 기다림
모든 Future가 끝난 후에야 에러를 던짐
여러 Future가 실패하면, 첫 번째 에러만 보고됨
eagerError: true
하나라도 에러가 발생하면 즉시 에러를 던짐
나머지 Future의 완료를 기다리지 않음
Future.any - 가장 빠른 것만
Future<String> fetchFromFastestServer() async {
// 가장 먼저 완료되는 것만 반환
var result = await Future.any([
fetchFromServer1(), // 3초
fetchFromServer2(), // 1초 ← 이것만 반환
fetchFromServer3(), // 2초
]);
return result;
}FutureOr
import 'dart:async';
// 동기/비동기 모두 반환 가능
FutureOr<int> getValue(bool useCache) {
if (useCache) {
return 42; // 동기 반환
} else {
return Future.delayed(Duration(seconds: 1), () => 42); // 비동기
}
}
void main() async {
var cached = await getValue(true); // 즉시 완료
var fetched = await getValue(false); // 1초 대기
}독립적인 작업은 반드시
Future.wait로 병렬화하면 됨. 성능 크게 향상됨Future.wait는 하나라도 실패하면 전체 실패임. 개별 에러 처리 필요하면 각 Future를 try-catch로 감싸면 됨Future.any는 CDN 선택, 타임아웃 구현 등에 유용함
6. Stream 기초
Stream이란?
Future가 하나의 미래 값이라면, Stream은 연속적인 미래 값들임
// Future: 한 번의 API 응답
Future<User> fetchUser() async { ... }
// Stream: 실시간 채팅 메시지들
Stream<Message> chatMessages() { ... }Stream 생성과 구독
void main() {
// Stream 생성
Stream<int> numberStream = Stream.periodic(
Duration(seconds: 1),
(count) => count,
).take(5); // 5개만
// Stream 구독 (listen)
numberStream.listen(
(number) => print('받음: $number'),
onError: (error) => print('에러: $error'),
onDone: () => print('스트림 종료'),
cancelOnError: false, // 에러 시 구독 취소 여부
);
}
// 출력:
// 받음: 0
// 받음: 1
// 받음: 2
// 받음: 3
// 받음: 4
// 스트림 종료Stream 생성 방법들
void main() async {
// 1. 값들로부터 생성
var stream1 = Stream.fromIterable([1, 2, 3, 4, 5]);
// 2. Future로부터 생성
var stream2 = Stream.fromFuture(fetchData());
// 3. 여러 Future로부터 생성
var stream3 = Stream.fromFutures([
Future.delayed(Duration(seconds: 1), () => 'A'),
Future.delayed(Duration(seconds: 2), () => 'B'),
]);
// 4. 주기적 스트림
var stream4 = Stream.periodic(
Duration(milliseconds: 500),
(count) => 'tick $count',
);
// 5. 빈 스트림
var stream5 = Stream<int>.empty();
// 6. 에러 스트림
var stream6 = Stream<int>.error('에러 발생!');
}await for - Stream을 동기처럼
Future<void> processStream() async {
var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
// await for로 각 이벤트 처리
await for (var number in stream) {
print('처리중: $number');
await Future.delayed(Duration(milliseconds: 500));
}
print('모든 처리 완료');
}Stream은 여러 번 이벤트 발생시킬 수 있음 (Future는 한 번만)
실시간 데이터에 적합: 웹소켓, 파일 읽기, 센서 데이터, 사용자 입력 등
listen()은 구독 시작하고,StreamSubscription반환함Single-subscription Stream: 한 번만 구독 가능 (기본)
Broadcast Stream: 여러 번 구독 가능 (
asBroadcastStream())
7. Stream 변환과 조작
기본 변환 메서드
void main() async {
var numbers = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
// map - 각 값 변환
var doubled = numbers.map((n) => n * 2);
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// where - 필터링
var evens = numbers.where((n) => n % 2 == 0);
// [2, 4, 6, 8, 10]
// take - 처음 N개만
var firstThree = numbers.take(3);
// [1, 2, 3]
// skip - 처음 N개 건너뛰기
var afterThree = numbers.skip(3);
// [4, 5, 6, 7, 8, 9, 10]
// distinct - 중복 제거
var unique = Stream.fromIterable([1, 1, 2, 2, 3]).distinct();
// [1, 2, 3]
// takeWhile - 조건 만족하는 동안만
var untilFive = numbers.takeWhile((n) => n < 5);
// [1, 2, 3, 4]
}체이닝
Future<void> streamChaining() async {
var result = await Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.where((n) => n % 2 == 0) // 짝수만: [2, 4, 6, 8, 10]
.map((n) => n * 10) // 10배: [20, 40, 60, 80, 100]
.take(3) // 3개만: [20, 40, 60]
.toList(); // List로 변환
print(result); // [20, 40, 60]
}집계 메서드
Future<void> streamAggregation() async {
var numbers = Stream.fromIterable([1, 2, 3, 4, 5]);
// 첫 번째, 마지막
print(await numbers.first); // 1
print(await Stream.fromIterable([1, 2, 3]).last); // 3
// 개수
print(await Stream.fromIterable([1, 2, 3]).length); // 3
// 조건 검사
print(await Stream.fromIterable([1, 2, 3]).any((n) => n > 2)); // true
print(await Stream.fromIterable([1, 2, 3]).every((n) => n > 0)); // true
print(await Stream.fromIterable([1, 2, 3]).contains(2)); // true
// 접기 (reduce/fold)
var sum = await Stream.fromIterable([1, 2, 3, 4, 5])
.reduce((a, b) => a + b);
print(sum); // 15
var product = await Stream.fromIterable([1, 2, 3, 4, 5])
.fold<int>(1, (prev, n) => prev * n);
print(product); // 120
}비동기 변환
Future<void> asyncTransform() async {
var userIds = Stream.fromIterable([1, 2, 3]);
// asyncMap - 비동기 변환
var users = userIds.asyncMap((id) async {
return await fetchUser(id); // 각 ID로 사용자 조회
});
await for (var user in users) {
print(user.name);
}
// asyncExpand - 비동기 확장 (1:N 변환)
var allPosts = userIds.asyncExpand((id) async* {
var posts = await fetchPostsByUser(id);
for (var post in posts) {
yield post;
}
});
}Stream 변환은 **게으른 평가(lazy evaluation)**임.
listen()이나await for로 구독해야 실행됨toList(),first,last등은 Stream 소비하고 Future 반환함asyncMap은 비동기 작업을 순차 처리함, 병렬 처리 필요하면 다른 방법 써야 함
8. StreamController
기본 사용법
StreamController는 직접 이벤트를 발생시키는 Stream 만들 때 씀
import 'dart:async';
void main() {
// StreamController 생성
final controller = StreamController<String>();
// Stream 구독
controller.stream.listen(
(data) => print('받음: $data'),
onDone: () => print('완료'),
);
// 이벤트 발생시키기
controller.sink.add('Hello');
controller.sink.add('World');
// 에러 발생시키기
controller.sink.addError('에러 발생!');
// 스트림 종료
controller.close();
}Broadcast StreamController
void main() {
// 여러 구독자 허용
final controller = StreamController<int>.broadcast();
// 첫 번째 구독자
controller.stream.listen((n) => print('구독자 A: $n'));
// 두 번째 구독자
controller.stream.listen((n) => print('구독자 B: $n'));
controller.add(1);
controller.add(2);
// 출력:
// 구독자 A: 1
// 구독자 B: 1
// 구독자 A: 2
// 구독자 B: 2
controller.close();
}실전: 이벤트 버스 패턴
// 앱 전체에서 이벤트 공유
class EventBus {
static final EventBus _instance = EventBus._internal();
factory EventBus() => _instance;
EventBus._internal();
final _controller = StreamController<AppEvent>.broadcast();
Stream<AppEvent> get stream => _controller.stream;
void fire(AppEvent event) {
_controller.add(event);
}
void dispose() {
_controller.close();
}
}
// 이벤트 정의
abstract class AppEvent {}
class UserLoggedIn extends AppEvent {
final String userId;
UserLoggedIn(this.userId);
}
class CartUpdated extends AppEvent {
final int itemCount;
CartUpdated(this.itemCount);
}
// 사용
void main() {
final bus = EventBus();
// 로그인 이벤트 구독
bus.stream
.where((e) => e is UserLoggedIn)
.cast<UserLoggedIn>()
.listen((e) => print('로그인: ${e.userId}'));
// 장바구니 이벤트 구독
bus.stream
.where((e) => e is CartUpdated)
.cast<CartUpdated>()
.listen((e) => print('장바구니: ${e.itemCount}개'));
// 이벤트 발생
bus.fire(UserLoggedIn('user123'));
bus.fire(CartUpdated(5));
}리소스 정리
class MyService {
final _controller = StreamController<String>();
StreamSubscription? _subscription;
Stream<String> get stream => _controller.stream;
void start() {
_subscription = someExternalStream.listen((data) {
_controller.add(data);
});
}
// 중요: 리소스 정리!
void dispose() {
_subscription?.cancel(); // 구독 취소
_controller.close(); // 컨트롤러 닫기
}
}
sink.add()로 데이터 추가,sink.addError()로 에러 추가,close()로 종료Single-subscription: 한 번만 구독 가능 (기본값)
Broadcast: 여러 구독자 가능, 구독 전 이벤트는 놓침
Flutter에서
dispose()에서 반드시close()랑cancel()호출해야 함!
9. async*/yield
Generator 함수로 Stream 만들기
// async*는 Stream을 반환하는 generator 함수
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
// yield로 하나씩 내보냄
yield i;
await Future.delayed(Duration(milliseconds: 500));
}
}
void main() async {
await for (var number in countStream(5)) {
print(number);
}
// 0, 1, 2, 3, 4 (각각 0.5초 간격)
}yield* - 다른 Stream 위임
Stream<int> outerStream() async* {
yield 1;
yield 2;
// yield*로 다른 Stream의 모든 이벤트 위임
yield* Stream.fromIterable([3, 4, 5]);
yield 6;
}
// 결과: 1, 2, 3, 4, 5, 6동기 Generator (sync*/yield)
// sync*는 Iterable을 반환
Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) {
yield k++;
}
}
void main() {
var numbers = naturalsTo(5);
print(numbers.toList()); // [0, 1, 2, 3, 4]
}실전: 페이지네이션 Stream
Stream<List<Post>> fetchAllPosts() async* {
int page = 1;
bool hasMore = true;
while (hasMore) {
final response = await fetchPosts(page: page);
yield response.posts;
hasMore = response.hasNextPage;
page++;
// 서버 부하 방지
await Future.delayed(Duration(milliseconds: 100));
}
}
void main() async {
await for (var posts in fetchAllPosts()) {
print('페이지 로드: ${posts.length}개');
// UI 업데이트...
}
print('모든 페이지 로드 완료');
}async*는 Stream 만드는 가장 직관적인 방법임yield는 값 하나 내보내고,yield*는 다른 Stream/Iterable 통째로 내보냄무한 스트림도 가능함 -
take()등으로 제한하면 됨sync*/yield는 동기 버전으로, 지연 평가되는 Iterable 만듦
10. Isolate
Isolate란?
Dart는 싱글 스레드지만, Isolate로 진정한 병렬 처리 가능함
import 'dart:isolate';
void main() async {
// 간단한 Isolate 실행
var result = await Isolate.run(() {
// 이 코드는 별도의 Isolate(스레드)에서 실행
return heavyComputation();
});
print('결과: $result');
}
int heavyComputation() {
int sum = 0;
for (int i = 0; i < 1000000000; i++) {
sum += i;
}
return sum;
}compute 함수 (Flutter)
import 'package:flutter/foundation.dart';
// Flutter에서 제공하는 간편한 Isolate 함수
Future<void> processImage() async {
final result = await compute(
processImageData, // 실행할 함수 (최상위 또는 static)
imageBytes, // 전달할 인자
);
}
// 최상위 함수여야 함 (클로저 불가)
Uint8List processImageData(Uint8List bytes) {
// 무거운 이미지 처리...
return processedBytes;
}Isolate간 통신
import 'dart:isolate';
Future<void> isolateCommunication() async {
// 메인 Isolate의 수신 포트
final receivePort = ReceivePort();
// 새 Isolate 생성
await Isolate.spawn(
workerIsolate,
receivePort.sendPort, // Worker에게 전달할 송신 포트
);
// Worker로부터 메시지 수신
await for (var message in receivePort) {
if (message is String) {
print('Worker가 보낸 메시지: $message');
} else if (message == 'done') {
break;
}
}
receivePort.close();
}
// Worker Isolate 함수
void workerIsolate(SendPort sendPort) {
sendPort.send('작업 시작');
// 무거운 작업...
for (int i = 0; i < 5; i++) {
sendPort.send('진행률: ${i * 20}%');
}
sendPort.send('작업 완료');
sendPort.send('done');
}언제 Isolate를 쓸까?
작업 유형 | Isolate 필요? | 이유 |
|---|---|---|
API 호출 | ❌ | I/O 작업은 비동기로 충분 |
JSON 파싱 (작은) | ❌ | 빠름 |
JSON 파싱 (큰) | ✅ | UI 블로킹 가능 |
이미지 처리 | ✅ | CPU 집약적 |
암호화 | ✅ | CPU 집약적 |
파일 읽기/쓰기 | ❌ | I/O 작업 |
복잡한 계산 | ✅ | CPU 집약적 |
Isolate는 메모리를 공유하지 않음. 모든 데이터는 복사되어 전달됨
Isolate는 진짜 병렬 처리임. 멀티코어 CPU 활용함
Isolate 생성 비용이 있어서, 가벼운 작업에는 과도함
Flutter의
compute()가 가장 간단한 방법임Isolate에서는 Flutter 위젯이나 플러그인 사용 못 함!
11. 실전 패턴
패턴 1: 타임아웃 처리
Future<T> withTimeout<T>(
Future<T> future,
Duration timeout, {
T Function()? onTimeout,
}) {
return future.timeout(
timeout,
onTimeout: onTimeout,
);
}
// 사용
void main() async {
try {
var result = await fetchData().timeout(
Duration(seconds: 10),
onTimeout: () => throw TimeoutException('요청 시간 초과'),
);
} on TimeoutException {
print('10초 초과!');
}
}패턴 2: 재시도 로직
Future<T> retry<T>(
Future<T> Function() fn, {
int maxAttempts = 3,
Duration delay = const Duration(seconds: 1),
}) async {
int attempts = 0;
while (true) {
try {
attempts++;
return await fn();
} catch (e) {
if (attempts >= maxAttempts) rethrow;
print('시도 $attempts 실패, ${delay.inSeconds}초 후 재시도...');
await Future.delayed(delay);
// 지수 백오프 (선택적)
delay *= 2;
}
}
}
// 사용
void main() async {
var data = await retry(
() => fetchData(),
maxAttempts: 5,
);
}패턴 3: 디바운스 (Debounce)
class Debouncer {
final Duration delay;
Timer? _timer;
Debouncer({this.delay = const Duration(milliseconds: 300)});
void run(void Function() action) {
_timer?.cancel();
_timer = Timer(delay, action);
}
void dispose() {
_timer?.cancel();
}
}
// 사용 - 검색 입력
class SearchWidget {
final _debouncer = Debouncer(delay: Duration(milliseconds: 500));
void onSearchChanged(String query) {
_debouncer.run(() {
// 500ms 동안 입력이 없으면 실행
performSearch(query);
});
}
}패턴 4: 쓰로틀 (Throttle)
class Throttler {
final Duration interval;
DateTime? _lastRun;
Throttler({this.interval = const Duration(milliseconds: 300)});
void run(void Function() action) {
final now = DateTime.now();
if (_lastRun == null ||
now.difference(_lastRun!) >= interval) {
_lastRun = now;
action();
}
}
}
// 사용 - 스크롤 이벤트
final throttler = Throttler(interval: Duration(milliseconds: 100));
void onScroll() {
throttler.run(() {
// 100ms에 한 번만 실행
updateScrollPosition();
});
}패턴 5: 캐싱
class AsyncCache<T> {
Future<T>? _cachedFuture;
T? _cachedValue;
DateTime? _cachedAt;
final Duration ttl;
AsyncCache({this.ttl = const Duration(minutes: 5)});
Future<T> get(Future<T> Function() fetch) async {
// 캐시가 유효하면 반환
if (_cachedValue != null &&
_cachedAt != null &&
DateTime.now().difference(_cachedAt!) < ttl) {
return _cachedValue!;
}
// 이미 요청 중이면 같은 Future 반환 (중복 요청 방지)
_cachedFuture ??= fetch().then((value) {
_cachedValue = value;
_cachedAt = DateTime.now();
_cachedFuture = null;
return value;
});
return _cachedFuture!;
}
void invalidate() {
_cachedValue = null;
_cachedAt = null;
}
}
// 사용
final userCache = AsyncCache<User>(ttl: Duration(minutes: 10));
Future<User> getCurrentUser() {
return userCache.get(() => api.fetchCurrentUser());
}패턴 6: Completer로 Future 수동 제어
import 'dart:async';
class DialogController<T> {
final _completer = Completer<T>();
Future<T> get result => _completer.future;
void complete(T value) {
if (!_completer.isCompleted) {
_completer.complete(value);
}
}
void error(Object error) {
if (!_completer.isCompleted) {
_completer.completeError(error);
}
}
}
// 사용 - 다이얼로그 결과 대기
Future<bool> showConfirmDialog() async {
final controller = DialogController<bool>();
showDialog(
builder: (context) => AlertDialog(
title: Text('확인'),
actions: [
TextButton(
onPressed: () => controller.complete(false),
child: Text('취소'),
),
TextButton(
onPressed: () => controller.complete(true),
child: Text('확인'),
),
],
),
);
return controller.result;
}본 글은 AI의 도움을 받아 작성되었습니다.
읽기 시리즈
현재 위치를 확인하고, 흐름을 따라 바로 다음 읽기로 이어갈 수 있습니다.