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 후: 완료
  • awaitasync 함수 안에서만 사용 가능

  • 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/awaittry/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의 도움을 받아 작성되었습니다.

읽기 시리즈

현재 위치를 확인하고, 흐름을 따라 바로 다음 읽기로 이어갈 수 있습니다.

Reading Series

Dart
0편

현재 읽는 문서

Dart 비동기 프로그래밍

Dart 언어에 대해서 알아보기

이전 문서

시리즈의 시작 문서입니다.

다음 문서

시리즈의 마지막 문서입니다.