Wednesday, March 20, 2024

Flutter/Dart 비동기 프로그래밍: `async` vs `async*` 핵심 완벽 가이드

Flutter와 Dart로 반응성이 뛰어나고 빠른 애플리케이션을 만드는 데 있어 비동기 프로그래밍의 이해와 효과적인 사용은 필수적입니다. 비동기 처리를 통해 앱은 인터넷에서 데이터를 가져오거나 파일을 읽는 등 오래 걸리는 작업을 수행하는 동안에도 사용자 인터페이스(UI)가 멈추는 현상을 방지할 수 있습니다.

Dart 비동기 모델의 중심에는 FutureStream이라는 두 가지 강력한 개념이 있습니다. 그리고 이들을 다루기 위해, 우리는 asyncasync*라는 두 개의 필수 키워드를 마스터해야 합니다. 각 키워드가 어떤 역할을 하고 언제 사용해야 하는지 자세히 살펴보겠습니다.

Future와 Stream이란 무엇일까요?

키워드를 살펴보기 전에, 이들이 만들어내는 객체에 대해 간단히 알아보겠습니다.

  • Future: 미래의 특정 시점에 사용 가능해질 '단일' 값 또는 오류를 나타냅니다. 비동기 작업으로부터 반환될 '한 번의 결과'에 대한 약속이라고 생각할 수 있습니다. 예를 들어, HTTP 요청은 결국 단 하나의 응답으로 완료됩니다.
  • Stream: 비동기 이벤트의 연속적인 흐름입니다. 단일 결과 대신, Stream은 시간이 지남에 따라 여러 값을 전달할 수 있습니다. 웹소켓 연결을 통해 들어오는 데이터나 사용자 입력 이벤트의 흐름을 상상해 보세요.

`async` vs `async*`: 핵심 차이점

두 키워드 모두 비동기 함수를 정의하는 데 사용되지만, 근본적으로 다른 목적을 가집니다. 가장 큰 차이점은 반환하는 객체의 종류와 값을 생성하는 방식에 있습니다.

기능 async async*
반환 타입 Future Stream
값의 개수 하나 (또는 오류) 0개 이상
값 생성 방식 return 키워드 사용 yield 키워드 사용
주요 사용 사례 API 호출, 파일 입출력 등 일회성 비동기 작업 실시간 데이터 업데이트, 이벤트 리스너 등 연속적인 데이터 흐름 처리

`async`와 `Future` 심층 분석

async 키워드는 함수를 비동기로 표시하기 위해 사용합니다. 이 키워드를 붙이면 함수는 즉시 Future 객체를 반환합니다. 함수 본문은 나중에 실행되며, 실행이 완료되면 Future는 값 또는 오류로 완료됩니다.

실용 예제: 네트워크 데이터 가져오기

async의 대표적인 사용 사례는 웹 서버에서 데이터를 가져오는 것입니다. 이 작업은 시간이 걸리므로 메인 스레드를 차단해서는 안 됩니다.


import 'dart:convert';
import 'package:http/http.dart' as http;

// 이 함수는 최종적으로 문자열을 담게 될 Future를 반환합니다.
Future<String> fetchUserData() async {
  try {
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'));
    
    if (response.statusCode == 200) {
      // 서버가 200 OK 응답을 반환하면, JSON을 파싱합니다.
      return jsonDecode(response.body)['title'];
    } else {
      // 200 OK가 아니면 예외를 발생시킵니다.
      throw Exception('사용자 데이터를 불러오는 데 실패했습니다.');
    }
  } catch (e) {
    return '데이터 가져오기 오류: $e';
  }
}

async 함수 내에서는 await 키워드를 사용하여 다른 비동기 작업(다른 Future)이 완료될 때까지 함수 실행을 일시 중지할 수 있습니다.

Future 사용하기

fetchUserData가 반환한 Future에서 값을 얻으려면 주로 두 가지 방법이 있습니다.


void main() async {
  // 방법 1: await 사용 (다른 async 함수 내에서)
  print('사용자 데이터 가져오는 중...');
  String data = await fetchUserData();
  print('받은 데이터: $data');

  // 방법 2: .then() 사용
  fetchUserData().then((value) {
    print('.then()으로 받은 데이터: $value');
  }).catchError((error) {
    print('발생한 오류: $error');
  });
}

`async*`와 `Stream` 심층 분석

async*("에이싱크 스타"라고 발음) 키워드는 Stream을 반환하는 함수를 정의하는 데 사용됩니다. 이런 종류의 함수는 시간에 따라 값의 시퀀스를 생성할 수 있으므로 "제너레이터(generator) 함수"라고도 합니다.

실용 예제: 카운트다운 스트림 만들기

카운트다운 타이머처럼 매초 숫자를 방출하는 함수가 필요하다고 상상해 보세요. 이는 async*Stream에 완벽한 작업입니다.


// 이 함수는 매초 정수를 방출하는 Stream을 반환합니다.
Stream<int> countdown(int from) async* {
  for (int i = from; i >= 0; i--) {
    // 1초간 기다립니다.
    await Future.delayed(Duration(seconds: 1));
    // 'yield'는 스트림으로 값을 내보냅니다.
    yield i;
  }
}

async* 함수는 return 대신 yield 키워드를 사용하여 값을 방출합니다. 함수 실행은 각 yield에서 일시 중지되고, 스트림 소비자가 다음 값을 받을 준비가 되면 다시 시작됩니다.

Stream 사용하기

Stream이 방출하는 값은 await for 루프나 listen() 메서드를 사용하여 수신할 수 있습니다.


void main() async {
  print('카운트다운 시작...');
  Stream<int> numberStream = countdown(5);

  // 방법 1: await for 사용 (간결하여 권장됨)
  await for (int number in numberStream) {
    print(number);
  }
  print('카운트다운 종료!');

  // 방법 2: .listen() 사용
  // 참고: 스트림은 한 번만 수신할 수 있습니다.
  // 이 코드를 실행하려면 countdown(3)을 다시 호출해야 합니다.
  countdown(3).listen(
    (number) {
      print('Listen: $number');
    },
    onDone: () {
      print('Listen: 카운트다운 종료!');
    },
  );
}

`async`와 `async*` 함께 사용하기: 고급 시나리오

이러한 개념들을 결합하여 강력한 데이터 처리 파이프라인을 만들 수 있습니다. 예를 들어, ID의 스트림을 받아 각 ID에 대해 비동기적으로 데이터를 조회해야 하는 경우가 있습니다.

이 예제에서는 ID 스트림을 처리하고 각 ID에 대해 비동기 함수를 호출하여 데이터를 가져옵니다.


// 특정 ID로 Todo 항목을 가져오는 함수
Future<String> fetchTodoById(int id) async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/$id'));
  if (response.statusCode == 200) {
    return jsonDecode(response.body)['title'];
  } else {
    throw Exception('#$id Todo를 불러오는 데 실패했습니다.');
  }
}

// ID 스트림을 받아 각 ID에 대한 데이터를 가져와 스트림으로 반환하는 함수
Stream<String> fetchTodosFromStream(Stream<int> idStream) async* {
  await for (final id in idStream) {
    try {
      // 스트림의 각 ID에 대해 async 함수를 호출합니다.
      String todoTitle = await fetchTodoById(id);
      // 결과를 출력 스트림으로 yield합니다.
      yield 'Todo #$id: $todoTitle';
    } catch (e) {
      yield '#$id Todo 가져오기 오류: $e';
    }
  }
}

void main() async {
  // 1, 2, 3 숫자를 방출하는 스트림 생성
  Stream<int> idStream = Stream.fromIterable([1, 2, 3]);

  // ID 스트림 처리
  await for (String result in fetchTodosFromStream(idStream)) {
    print(result);
  }
}

여기서 fetchTodosFromStream 함수는 결과의 Stream을 생성하므로 async*로 표시됩니다. 그 안에서는 await for를 사용하여 들어오는 ID 스트림을 소비하고, 각 ID에 대해 async 함수인 fetchTodoByIdawait로 호출합니다.

결론: 핵심 요약

asyncasync*의 차이점을 이해하는 것은 효율적이고 깔끔한 비동기 Dart 코드를 작성하는 데 매우 중요합니다.

  • async / Future: API 호출과 같이 단일 결과를 생성하는 작업에 사용합니다. 함수는 한 번의 값으로 반환됩니다.
  • async* / Stream: 실시간 데이터 피드나 이벤트 처리와 같이 시간에 따라 값의 시퀀스를 생성하는 작업에 사용합니다. 함수는 여러 값을 yield할 수 있습니다.

이 도구들을 마스터함으로써 Flutter 애플리케이션의 모든 비동기 과제를 처리하고, 부드럽고 반응성 높은 사용자 경험을 보장할 수 있습니다.


0 개의 댓글:

Post a Comment