안녕하세요! 오늘은 Dart에서 비동기 프로그래밍의 핵심 요소 중 하나인 Stream에 대해 자세히 알아보겠습니다. Flutter 개발에서 실시간 데이터 처리, 이벤트 핸들링, 상태 관리 등을 위해 Stream은 필수적인 개념입니다. 기초부터 실전까지 체계적으로 살펴봅시다.
목차
- Stream 개념 기초
- Stream 생성 방법
- yield와 yield* 이해하기
- Stream 구독과 값 인출
- 오류 처리 전략
- StreamController 활용
- 실전 예제 구현
1. Stream 개념 기초
Stream이란?
Stream은 비동기적으로 데이터의 시퀀스를 제공하는 방법입니다. Future가 단일 비동기 결과를 다룬다면, Stream은 시간에 따라 여러 비동기 이벤트를 처리합니다.
Stream vs Future
- Future: 단일 값을 비동기적으로 반환
- Stream: 시간에 따라 여러 값을 비동기적으로 반환
Stream의 주요 특징
- 데이터 이벤트 시퀀스 제공
- 오류 이벤트 전달 가능
- 완료 이벤트 전달 가능
- 비동기 처리 지원
2. Stream 생성 방법
Dart에서 Stream을 생성하는 방법은 크게 두 가지가 있습니다.
async* 함수 사용하기
Stream<int> countStream(int maxCount) async* {
for (int i = 1; i <= maxCount; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
여기서 async*와 yield는 Stream 생성의 핵심 키워드입니다:
- async*: 함수가 Stream을 반환함을 나타냄
- yield: 값을 Stream에 전달하되 함수 실행은 계속 진행
StreamController 사용하기
Stream<int> countStreamWithController(int maxCount) {
var streamController = StreamController<int>();
Future(() async {
try {
for (int i = 1; i <= maxCount; i++) {
await Future.delayed(Duration(seconds: 1));
streamController.add(i);
}
} finally {
streamController.close();
}
});
return streamController.stream;
}
StreamController는 Stream의 생명주기와 이벤트를 완전히 제어할 수 있게 해줍니다.
3. yield와 yield* 이해하기
Stream 생성에 핵심적인 두 키워드인 yield와 yield*의 차이점을 살펴보겠습니다.
yield
yield는 단일 값을 Stream에 전달합니다. 함수 실행을 종료하지 않고 계속 진행하면서 값을 Stream에 추가합니다.
Stream<int> singleValueStream() async* {
yield 1; // 값 1을 Stream에 추가
yield 2; // 값 2를 Stream에 추가
yield 3; // 값 3을 Stream에 추가
}
yield*
yield*는 다른 Stream의 모든 값을 현재 Stream에 연결합니다. 이는 여러 Stream을 조합할 때 유용합니다.
Stream<int> combinedStream() async* {
yield* Stream.fromIterable([1, 2, 3]); // 이 Stream의 모든 값을 현재 Stream에 추가
yield* anotherStream(); // 다른 Stream의 모든 값을 현재 Stream에 추가
}
오류 처리에서의 yield*
yield*는 오류 처리에도 중요한 역할을 합니다:
Stream<int> streamWithErrorHandling(int maxCount) async* {
for (int i = 1; i <= maxCount; i++) {
try {
if (i == 3) {
throw Exception("에러 발생!");
}
yield i;
} catch (e) {
yield* Stream.error(e); // 오류를 Stream으로 전달
continue; // 계속 진행
}
}
}
여기서 yield* Stream.error(e)는 오류를 Stream의 오류 이벤트로 전달하되, Stream 자체는 계속 실행될 수 있게 합니다.
4. Stream 구독과 값 인출
Stream에서 값을 가져오는 방법은 두 가지가 있습니다.
await for 사용
블로킹 방식으로 Stream의 모든 이벤트를 순차적으로 처리합니다.
void processStream() async {
print("스트림 처리 시작...");
await for (final value in countStream(5)) {
print("값: $value");
}
print("스트림 처리 완료!");
}
listen 메서드 사용
논블로킹 방식으로 Stream 이벤트를 처리합니다.
void processStreamWithListen() {
print("스트림 처리 시작...");
final subscription = countStream(5).listen(
(value) => print("값: $value"),
onError: (error) => print("오류: $error"),
onDone: () => print("스트림 처리 완료!"),
);
// 필요시 구독 취소 가능
// subscription.cancel();
}
두 방식의 차이점
특징await forlisten
실행 방식 | 블로킹 | 논블로킹 |
사용 환경 | async 함수 내부 | 어디서나 가능 |
구독 제어 | 자동 관리 | 수동 관리(취소 가능) |
코드 흐름 | 순차적 | 이벤트 기반 |
5. 오류 처리 전략
Stream에서 오류를 처리하는 방법은 여러 가지가 있습니다.
try-catch와 await for
void handleErrorWithTryCatch() async {
try {
await for (final value in streamWithError(5)) {
print("값: $value");
}
} catch (e) {
print("오류 발생: $e");
}
}
handleError 메서드
void handleErrorWithStreamMethod() async {
var handledStream = streamWithError(5).handleError(
(error) => print("오류 처리됨: $error"),
test: (error) => error is Exception // 특정 타입의 오류만 처리
);
await for (final value in handledStream) {
print("값: $value");
}
}
listen의 onError 콜백
void handleErrorWithListener() {
streamWithError(5).listen(
(value) => print("값: $value"),
onError: (error) => print("오류 처리됨: $error"),
onDone: () => print("스트림 처리 완료!"),
);
}
6. StreamController 활용
StreamController는 Stream의 생명주기를 완전히 제어할 수 있게 해주는 강력한 도구입니다.
기본 사용법
Stream<int> controlledStream() {
final controller = StreamController<int>();
// 데이터 추가
controller.add(1);
controller.add(2);
// 오류 추가
controller.addError(Exception("오류 발생!"));
// 스트림 종료
controller.close();
return controller.stream;
}
오류 처리가 있는 비동기 컨트롤러
Stream<int> countStreamWithErrorUseController(int maxCount) {
var streamController = StreamController<int>();
Future(() async {
try {
for (int i = 1; i <= maxCount; i++) {
await Future.delayed(Duration(seconds: 1));
try {
if (i == 3) {
throw Exception("에러 발생!");
}
streamController.add(i);
} catch (e) {
streamController.addError(e);
print("값 $i에서 에러! $e");
}
}
} finally {
streamController.close(); // 항상 닫아줘야 함
}
});
return streamController.stream;
}
StreamController vs async* 선택 기준
특징async* + yieldStreamController
순차적 데이터 생성 | 적합 | 덜 적합 |
외부에서 데이터 추가 | 불가능 | 가능 |
여러 소스에서 데이터 결합 | 어려움 | 쉬움 |
자원 관리 | 자동 | 수동(close 필요) |
가독성 | 높음 | 복잡한 경우 낮음 |
유연성 | 낮음 | 높음 |
7. 실전 예제 구현
지금까지 배운 개념을 활용한 실전 예제를 살펴보겠습니다.
오류 처리가 있는 카운터 스트림
Stream<int> countStreamWithError(int maxCount) async* {
for (int i = 1; i <= maxCount; i++) {
await Future.delayed(Duration(seconds: 1));
try {
if (i == 3) {
throw Exception("에러 발생!");
}
yield i;
} catch (e) {
yield* Stream.error(e);
continue;
}
}
}
void handleErrorWithStreamMethods() async {
// 오류 처리 후 대체 값 제공
var handleError = countStreamWithError(5).handleError(
(error) {
print("오류 발생 (처리됨): $error");
},
test: (error) => error is Exception
);
await for (final nums in handleError) {
print("카운트: $nums");
}
print("모든 처리 완료!");
}
StreamController를 사용한 카운터
void printCountStreamWithStreamController() async {
print("카운트 시작...");
try {
await for (final nums in countStreamWithErrorUseController(5)) {
print("카운트: $nums");
}
} catch (e) {
print("에러가 발생하여 처리됨. 발생한 에러: $e");
}
print("카운트 완료!");
}
결론: Stream 선택 가이드
- 간단한 데이터 시퀀스를 생성할 때는 async*와 yield를 사용
- 외부 이벤트를 처리하거나 여러 소스에서 데이터를 결합할 때는 StreamController 사용
- 예외 처리가 중요한 경우, yield* Stream.error()나 StreamController의 addError 메서드 활용
- 사용자 상호작용이나 실시간 데이터를 처리할 때는 listen의 논블로킹 방식 활용
- 순차적 처리가 필요한 경우 await for의 블로킹 방식 활용
Stream은 Flutter에서 상태 관리, 네트워크 요청, 사용자 입력 처리 등 다양한 영역에서 활용되는 강력한 개념입니다. 이 가이드를 통해 Stream의 기본부터 실전까지 이해하셨기를 바랍니다.
Stream은 처음에는 이해하기 어려울 수 있지만, 실제로 코드를 작성하고 흐름을 따라가보면 점차 개념이 명확해집니다. Future가 단일 값의 비동기 처리에 적합하다면, Stream은 여러 이벤트를 처리해야 하는 상황에서 더욱 빛을 발합니다.
여러분은 어떤 상황에서 Stream을 활용하시나요? 특별한 Stream 패턴이나 경험이 있으시다면 댓글로 공유해주세요! 함께 배우고 성장하는 개발자 커뮤니티를 만들어가요! 😊
'📚 학습 기록 > Dart & Flutter 기초' 카테고리의 다른 글
Dart 실전 프로젝트: 비동기 프로그래밍과 최신 문법 활용 가이드 (2) | 2025.05.28 |
---|---|
Dart 비동기 프로그래밍 심화: Stream, Future, Timer 완전정복 (1) | 2025.05.22 |
Dart의 비동기 프로그래밍 완벽 가이드: Future, async/await, then (0) | 2025.05.20 |
Dart 기초 개념부터 실전 프로젝트까지: 완벽 정리 (0) | 2025.05.16 |
Dart 3.0의 새로운 클래스 키워드와 패턴 매칭 완벽 가이드 (2) | 2025.05.06 |