Dart 비동기 — Future, async/await, Stream

네트워크 요청, 파일 읽기, DB 쿼리 등 시간이 걸리는 작업은 비동기로 처리해야 합니다. Dart는 단일 스레드 기반이지만, 이벤트 루프를 통해 비동기 작업을 효율적으로 처리합니다.

면접에서 "Dart는 단일 스레드인데 어떻게 비동기가 가능한가요?"라는 질문이 자주 나옵니다. ** 이벤트 루프 **가 답입니다.


Future

Future는 ** 미래에 완료될 값 **을 나타내는 객체입니다. JavaScript의 Promise와 같은 개념입니다.

DART
// 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이 훨씬 읽기 좋습니다.

DART
// async/await 방식 (권장)
Future<void> loadUser() async {
  try {
    final name = await fetchUserName();
    print('이름: $name');
  } catch (e) {
    print('에러: $e');
  } finally {
    print('완료');
  }
}

여러 Future를 동시에 실행

DART
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은 ** 연속적인 값 **을 비동기로 전달합니다.

DART
// 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여러 리스너 가능이벤트 버스, 소켓
DART
// StreamController로 직접 Stream 만들기
final controller = StreamController<String>.broadcast();

// 값 추가
controller.sink.add('새 메시지');

// 구독
final subscription = controller.stream.listen((data) {
  print('받은 데이터: $data');
});

// 정리 (메모리 누수 방지!)
subscription.cancel();
controller.close();

Stream 변환

DART
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의 이벤트 루프는 두 개의 큐를 관리합니다.

PLAINTEXT
┌─────────────────────────────────┐
│         이벤트 루프              │
│                                 │
│  1. Microtask Queue (우선)      │
│     - scheduleMicrotask()       │
│     - Future.then() 콜백        │
│                                 │
│  2. Event Queue                 │
│     - I/O 이벤트                │
│     - Timer                     │
│     - UI 이벤트                 │
└─────────────────────────────────┘
DART
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에서의 활용

DART
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의 우선순위는 면접 필수 포인트입니다
댓글 로딩 중...