Dart 튜토리얼 15편: Stream 완전 가이드(async/await·using streams·creating streams)
요약
핵심 요지
- 문제 정의: 비동기 데이터가 “한 번만” 오는지(
Future1) “여러 번” 오는지(Stream2)를 구분하지 않으면, 코드가 복잡해지고 에러 처리도 빠지기 쉽다. - 핵심 주장:
Stream은 “시간에 따라 들어오는 값들의 흐름”이며,await for3 또는listen()4으로 소비하고, 필요하면async*5나StreamController6로 직접 만들 수 있다. - 주요 근거:
await for로 합계를 구하는 예시,try/catch로 에러 이벤트를 처리하는 예시,Stream.periodic()7,async*/yield8,StreamController예시가 제시된다. - 실무 기준: “소비(받기) → 에러 처리 → 필요한 경우 생성(만들기)” 순서로 익히면, 스트림이 등장하는 코드가 읽히기 시작한다.
문서가 설명하는 범위
Stream을await for/listen()으로 소비하는 기본 방법- 스트림의 에러 이벤트 처리(
try/catch)와 스트림 API 예시(lastWhere()9) - 스트림 생성 방법: 기존 스트림 변환,
async*생성기,StreamController
읽는 시간: 17분 | 난이도: 중급
참고 자료
- Asynchronous programming: futures, async, await - 비동기 코드 기본(미래값)
- Asynchronous programming: Streams - 스트림 소비(받기) 중심
- Creating streams in Dart - 스트림 생성(만들기) 중심
문제 상황
비동기 데이터는 두 종류로 나뉩니다.
한 번 결과가 오면 끝나는 작업(예: “요청 결과 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 for는async11 함수 안에서만 사용할 수 있습니다.
즉, 스트림을 반복문으로 받고 싶다면 “함수 자체가 비동기”여야 합니다.
결론: 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
- 기존 스트림 변환:
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);
async*생성기: 시간 간격으로 숫자를 내보내는 스트림을 만들 수 있습니다.Stream<int> timedCounter(Duration interval, [int? maxCount]) async* {int i = 0;while (true) {await Future.delayed(interval);yield i++;if (i == maxCount) break;}}
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
-
Future(퓨처): 나중에 완료되는 작업의 결과를 표현하는 타입이다. ↩
-
Stream(스트림): 시간이 지나면서 여러 이벤트/값이 들어오는 흐름을 표현하는 타입이다. ↩
-
listen()(리스েন): 스트림을 구독하고 이벤트 콜백을 등록하는 메서드다. ↩
-
*async(어싱크 스타)**:
yield8로 값을 내보내며Stream2을 만드는 비동기 생성기 문법이다. ↩ -
StreamController(스트림 컨트롤러): 스트림을 만들고, 임의의 시점에 이벤트/에러/종료를 추가하는 컨트롤러다. ↩
-
Stream.periodic()(주기 스트림): 일정 간격으로 이벤트를 내보내는 스트림을 만드는 생성자다. ↩
-
lastWhere()(라스트 웨어): 조건을 만족하는 마지막 요소를 찾아
Future로 돌려주는 스트림 메서드다. ↩ -
Iterable(이터러블): 순회 가능한 요소들의 모음을 표현하는 인터페이스다. ↩
-
async(어싱크): 함수가 비동기이며
await를 사용할 수 있음을 표시하는 키워드다. ↩ -
map()(맵): 각 이벤트를 변환해 새 스트림을 만드는 메서드다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!