Dart 튜토리얼 15편: Stream 완전 가이드(async/await·using streams·creating streams)

요약#

핵심 요지#

  • 문제 정의: 비동기 데이터가 “한 번만” 오는지(Future1) “여러 번” 오는지(Stream2)를 구분하지 않으면, 코드가 복잡해지고 에러 처리도 빠지기 쉽다.
  • 핵심 주장: Stream은 “시간에 따라 들어오는 값들의 흐름”이며, await for3 또는 listen()4으로 소비하고, 필요하면 async*5StreamController6로 직접 만들 수 있다.
  • 주요 근거: await for로 합계를 구하는 예시, try/catch로 에러 이벤트를 처리하는 예시, Stream.periodic()7, async*/yield8, StreamController 예시가 제시된다.
  • 실무 기준: “소비(받기) → 에러 처리 → 필요한 경우 생성(만들기)” 순서로 익히면, 스트림이 등장하는 코드가 읽히기 시작한다.

문서가 설명하는 범위#

  • Streamawait for/listen()으로 소비하는 기본 방법
  • 스트림의 에러 이벤트 처리(try/catch)와 스트림 API 예시(lastWhere()9)
  • 스트림 생성 방법: 기존 스트림 변환, async* 생성기, StreamController

읽는 시간: 17분 | 난이도: 중급


참고 자료#


문제 상황#

비동기 데이터는 두 종류로 나뉩니다.
한 번 결과가 오면 끝나는 작업(예: “요청 결과 1개”)과, 시간이 지나며 여러 번 이벤트가 오는 작업(예: “파일을 조금씩 읽기”, “사용자 이벤트”)입니다.
둘을 같은 방식으로 처리하려고 하면, 코드가 길어지고 의도도 흐려집니다.
그래서 “Future vs Stream”을 먼저 구분한 뒤, 스트림은 스트림 방식으로 소비/생성하는 틀을 잡아야 합니다.


해결 방법#

단계 1: Stream은 “비동기 Iterable”이라는 관점을 먼저 잡기#

Why#

NOTE

Stream을 처음 보면 “for 문인데 await가 붙어 있네?”처럼 느껴질 수 있습니다.
관점을 단순하게 잡으면, 이후 문법과 API가 훨씬 빠르게 연결됩니다.

What#

NOTE

스트림은 “비동기 이벤트의 시퀀스”입니다.
동기 Iterable10이 “다음 값을 요청하면 받는” 구조라면, 스트림은 “다음 값이 준비되면 알려주는” 구조로 설명됩니다.

How#

TIP

스트림의 이벤트를 받는 기본 패턴은 await for입니다.

Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
await for (final value in stream) {
sum += value;
}
return sum;
}

이 함수는 “스트림에서 값이 하나씩 들어올 때마다” 누적하고, 스트림이 끝나면 합계를 반환합니다.

Watch out#

WARNING

await forasync11 함수 안에서만 사용할 수 있습니다.
즉, 스트림을 반복문으로 받고 싶다면 “함수 자체가 비동기”여야 합니다.

결론: Stream은 “비동기 Iterable”로 생각하고, 기본 소비는 await for로 시작합니다.


단계 2: 스트림의 에러 이벤트는 try/catch로 “루프 자체”를 감싸서 처리하기#

Why#

NOTE

스트림은 데이터 이벤트뿐 아니라 에러 이벤트도 전달할 수 있습니다.
이 에러를 놓치면 루프가 중단되고, 호출자는 “왜 끝났는지” 모르게 됩니다.

What#

NOTE

await for로 읽는 스트림에서 에러가 발생하면, 그 에러는 루프 문에서 던져집니다.
따라서 try/catch로 루프를 감싸서 처리하는 패턴이 제시됩니다.

How#

TIP

다음 예시는 반복 도중 에러를 던지고, try/catch에서 처리합니다.

Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
try {
await for (final value in stream) {
sum += value;
}
} catch (e) {
return -1;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
if (i == 4) {
throw Exception('Intentional exception');
} else {
yield i;
}
}
}
void main() async {
var stream = countStream(10);
var sum = await sumStream(stream);
print(sum); // 에러가 발생했으므로 -1을 출력한다.
}

Watch out#

WARNING

에러가 발생하면 스트림이 “끝나기 전이라도” 루프가 종료될 수 있습니다.
즉, 에러 처리 전략(예: 실패 시 기본값 반환)을 함수 수준에서 결정해 둬야 합니다.

결론: 스트림의 에러는 루프에서 던져지므로, try/catch로 루프를 감싸는 패턴을 기본으로 둡니다.


단계 3: 스트림 API의 “자주 쓰는 결과 메서드”를 익혀서 코드량을 줄이기#

Why#

NOTE

스트림을 직접 await for로 모두 구현하면 가능은 하지만, 흔한 작업은 이미 메서드로 제공됩니다.
기본 메서드를 아는 것만으로도 코드가 짧아집니다.

What#

NOTE

스트림에는 Future를 반환하는 여러 메서드가 있고, 예시로 lastWhere()가 제시됩니다.

How#

TIP

다음 코드는 “마지막 양수”를 구하는 예시입니다.

Future<int> lastPositive(Stream<int> stream) => stream.lastWhere((x) => x >= 0);

Watch out#

WARNING

이런 메서드는 내부적으로 스트림을 끝까지 소비할 수 있습니다.
따라서 “언제 끝나는 스트림인지”가 불분명한 경우에는, 먼저 스트림이 종료되는 조건을 확인해야 합니다.

결론: 기본 스트림 메서드를 익히면, 같은 작업을 더 짧고 읽기 쉽게 표현할 수 있습니다.


단계 4: 스트림을 “만드는 쪽”은 (1) 변환 (2) async* (3) StreamController 순서로 접근하기#

Why#

NOTE

스트림 소비는 빠르게 익힐 수 있지만, 스트림 생성은 선택지가 많아 막히기 쉽습니다.
그래서 생성 방식도 “자주 쓰는 순서”로 정리해두는 편이 좋습니다.

What#

NOTE

스트림은 다음 방식으로 만들 수 있다고 정리됩니다.
기존 스트림을 변환하거나, async* 생성기 함수를 사용하거나, StreamController를 사용하는 방식입니다.

How#

TIP
  1. 기존 스트림 변환: Stream.periodic()로 만든 스트림을 map()12으로 변환할 수 있습니다.
var counterStream = Stream<int>.periodic(
const Duration(seconds: 1),
(x) => x,
).take(15);
// 각 이벤트 값을 두 배로 만든 새 스트림
var doubleCounterStream = counterStream.map((int x) => x * 2);
doubleCounterStream.forEach(print);
  1. async* 생성기: 시간 간격으로 숫자를 내보내는 스트림을 만들 수 있습니다.
Stream<int> timedCounter(Duration interval, [int? maxCount]) async* {
int i = 0;
while (true) {
await Future.delayed(interval);
yield i++;
if (i == maxCount) break;
}
}
  1. StreamController: 여러 곳에서 이벤트를 넣어야 한다면 컨트롤러를 사용할 수 있습니다.
Stream<int> timedCounter(Duration interval, [int? maxCount]) {
var controller = StreamController<int>();
int counter = 0;
void tick(Timer timer) {
counter++;
controller.add(counter); // 스트림에 이벤트로 값을 보낸다.
if (maxCount != null && counter >= maxCount) {
timer.cancel();
controller.close(); // 스트림을 종료한다.
}
}
Timer.periodic(interval, tick);
return controller.stream;
}

Watch out#

WARNING

StreamController 예시에는 “구독자가 없는데도 시작하는 등 구현이 올바르지 않은(FLAWED) 예시”가 함께 제시됩니다.
즉, 컨트롤러 기반 스트림은 “언제 시작/중지할지”와 “일시정지(pause)” 같은 동작까지 고려해야 합니다.

결론: 스트림 생성은 “변환 → async*StreamController” 순서로 접근하고, 컨트롤러는 동작 규칙까지 포함해 설계합니다.

Footnotes#

  1. Future(퓨처): 나중에 완료되는 작업의 결과를 표현하는 타입이다.

  2. Stream(스트림): 시간이 지나면서 여러 이벤트/값이 들어오는 흐름을 표현하는 타입이다.

  3. await for(await for): Stream2 이벤트를 비동기 반복문으로 하나씩 받는 문법이다.

  4. listen()(리스েন): 스트림을 구독하고 이벤트 콜백을 등록하는 메서드다.

  5. *async(어싱크 스타)**: yield8로 값을 내보내며 Stream2을 만드는 비동기 생성기 문법이다.

  6. StreamController(스트림 컨트롤러): 스트림을 만들고, 임의의 시점에 이벤트/에러/종료를 추가하는 컨트롤러다.

  7. Stream.periodic()(주기 스트림): 일정 간격으로 이벤트를 내보내는 스트림을 만드는 생성자다.

  8. yield(일드): async*5에서 스트림 이벤트로 값을 내보내는 키워드다. 2

  9. lastWhere()(라스트 웨어): 조건을 만족하는 마지막 요소를 찾아 Future로 돌려주는 스트림 메서드다.

  10. Iterable(이터러블): 순회 가능한 요소들의 모음을 표현하는 인터페이스다.

  11. async(어싱크): 함수가 비동기이며 await를 사용할 수 있음을 표시하는 키워드다.

  12. map()(맵): 각 이벤트를 변환해 새 스트림을 만드는 메서드다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Dart 튜토리얼 15편: Stream 완전 가이드(async/await·using streams·creating streams)
https://moodturnpost.net/posts/dart/dart-stream-guide/
작성자
Moodturn
게시일
2026-01-04
Moodturn

목차