안녕하세요! 오늘은 Dart의 비동기 프로그래밍에서 한 단계 더 나아가 Stream 변환, Future 체이닝, Timer 활용, 오류 처리 패턴까지 심화 내용을 다뤄보겠습니다. 실무에서 자주 사용되는 패턴들을 실제 코드와 함께 살펴보겠습니다.
목차
- Stream 값 소비와 변환
- Future 체이닝 패턴
- Timer를 활용한 비동기 이벤트
- 비동기 오류 처리 전략
- 동기 vs 비동기 명확한 구분
- 실전 활용 팁
1. Stream 값 소비와 변환
Stream에서 데이터를 처리하는 다양한 방법을 살펴보겠습니다.
await for vs where vs listen
각 방법은 서로 다른 특성과 용도를 가지고 있습니다.
await for (블로킹 방식)
void filterEvenNumbers() async {
final stream = countStream(10);
// 직접 필터링
await for (final value in stream) {
if (value % 2 == 0) {
print("짝수: $value");
}
}
}
where 메서드 (함수형 접근)
void filterWithWhere() async {
final stream = countStream(10);
// where를 사용한 필터링 + await for
await for (var value in stream.where((n) => n % 2 == 0)) {
print("짝수: $value");
}
// 또는 toList()로 한번에 수집
var evenNumbers = await stream.where((n) => n % 2 == 0).toList();
print("모든 짝수: $evenNumbers");
}
listen (논블로킹 방식)
void filterWithListen() {
final stream = countStream(10);
// 논블로킹으로 필터링된 값 처리
stream.where((n) => n % 2 == 0).listen(
(value) => print("짝수: $value"),
onDone: () => print("스트림 완료!"),
onError: (error) => print("오류: $error"),
);
print("이 메시지는 즉시 출력됩니다!"); // 논블로킹이므로 즉시 실행
}
각 방식의 비교
방식실행 타입장점단점적합한 상황
await for | 블로킹 | 직관적, 예외 처리 쉬움 | 다른 작업 차단 | 순차적 처리 필요시 |
where + await for | 블로킹 | 함수형 스타일, 가독성 | 다른 작업 차단 | 필터링 후 순차 처리 |
listen | 논블로킹 | 다른 작업 방해하지 않음 | 콜백 기반, 복잡할 수 있음 | 실시간 이벤트 처리 |
2. Future 체이닝 패턴
비동기 작업을 연결하여 처리하는 다양한 방법을 알아보겠습니다.
then을 활용한 체이닝
Future<String> fetchUsername() async {
return Future.delayed(Duration(seconds: 1), () => "김철수");
}
Future<int> fetchUserAge(String name) async {
Map<String, int> userInfos = {"홍길동": 20, "김철수": 30};
return Future.delayed(
Duration(seconds: 1),
() => userInfos[name] ?? 0
);
}
void chainWithThen() {
fetchUsername().then((name) {
fetchUserAge(name).then((age) {
print("${name}님의 나이는 ${age}세 입니다!");
});
});
}
async/await를 활용한 체이닝
void chainWithAsync() async {
final name = await fetchUsername();
final age = await fetchUserAge(name);
print("$name님의 나이는 $age세 이네요!");
}
체이닝 방식 비교
방식장점단점언제 사용할까
then | 논블로킹, 함수형 스타일 | 중첩 시 콜백 지옥 | 간단한 체이닝, 논블로킹 필요시 |
async/await | 가독성 좋음, 디버깅 쉬움 | 블로킹 | 복잡한 로직, 순차 처리 필요시 |
유연한 반환 로직 구현
Future<int> fetchUserAge(String name) async {
Map<String, int> userInfos = {"홍길동": 20, "김철수": 30};
// ?? 연산자로 기본값 제공
return userInfos[name] ?? 0;
// 또는 삼항 연산자 활용
// return userInfos.containsKey(name) ? userInfos[name]! : 0;
}
3. Timer를 활용한 비동기 이벤트
Timer는 주기적인 작업이나 지연된 실행에 유용한 도구입니다.
Timer.periodic 활용
void simpleTimerEvents() {
int count = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
count++;
print("$count초 경과!");
if (count == 5) {
timer.cancel(); // 타이머 정지
print("타이머 종료!");
}
});
}
Timer의 실무 활용 예시
class AutoSaveManager {
Timer? _autoSaveTimer;
void startAutoSave() {
_autoSaveTimer = Timer.periodic(Duration(minutes: 5), (timer) {
saveData();
print("자동 저장 완료: ${DateTime.now()}");
});
}
void stopAutoSave() {
_autoSaveTimer?.cancel();
_autoSaveTimer = null;
}
void saveData() {
// 실제 저장 로직
}
}
StreamController와 Timer 조합
Stream<int> timerBasedStream() {
final controller = StreamController<int>();
int count = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
count++;
controller.add(count);
if (count == 10) {
timer.cancel();
controller.close();
}
});
return controller.stream;
}
4. 비동기 오류 처리 전략
비동기 작업에서 오류를 효과적으로 처리하는 다양한 패턴을 살펴보겠습니다.
동기 vs 비동기 오류 처리 패턴
동기: try-on-catch-finally
void synchronousErrorHandling() {
try {
// 위험한 동기 작업
int result = 10 ~/ 0; // 0으로 나누기
} on IntegerDivisionByZeroException {
print("0으로 나눌 수 없습니다!");
} catch (e) {
print("예상치 못한 오류: $e");
} finally {
print("정리 작업 수행");
}
}
비동기: then-catchError-whenComplete
void asynchronousErrorHandling() {
fetchDataWithPossibleError()
.then((data) => print("성공: $data"))
.catchError((error) => print("오류: $error"))
.whenComplete(() => print("작업 완료"));
}
랜덤 오류 시뮬레이션
import 'dart:math';
Future<String> fetchDataWithPossibleError() async {
final randomResult = Random.secure().nextBool();
if (randomResult) {
return "데이터 로드 성공!";
} else {
throw Exception("네트워크 오류 발생!");
}
}
void handleAsyncError() async {
try {
final result = await fetchDataWithPossibleError();
print("성공: $result");
} catch (e) {
print("실패했으니 다시 시도해보세요!");
print("오류 내용: $e");
} finally {
print("작업이 완료되었습니다.");
}
}
Future의 onError 파라미터 활용
void handleErrorWithOnError() {
fetchDataWithPossibleError().then(
(data) => print("성공: $data"),
onError: (error) => print("onError로 처리: $error")
);
}
5. 동기 vs 비동기 명확한 구분
개념 정리
동기(Synchronous)
- 특징: 작업이 완료될 때까지 기다림
- 예시: 커피 주문 후 카운터 앞에서 대기
- 코드: 일반적인 함수 호출, await 사용
비동기(Asynchronous)
- 특징: 작업을 시작하고 다른 일을 처리
- 예시: 세탁기를 돌리고 TV 시청
- 코드: then, listen, Timer 등
실제 코드로 비교
// 동기적 처리 (블로킹)
void synchronousExample() async {
print("1. 시작");
String result = await fetchUsername(); // 대기
print("2. 결과: $result");
print("3. 완료");
}
// 비동기적 처리 (논블로킹)
void asynchronousExample() {
print("1. 시작");
fetchUsername().then((result) {
print("2. 결과: $result");
});
print("3. 이 메시지가 먼저 출력될 수 있음");
}
6. 실전 활용 팁
StreamController 완전 활용
class EventManager {
final _controller = StreamController<String>.broadcast();
// 데이터 추가
void addEvent(String event) {
_controller.add(event);
}
// 오류 추가
void addError(Object error) {
_controller.addError(error);
}
// 스트림 노출
Stream<String> get events => _controller.stream;
// 리소스 정리
void dispose() {
_controller.close();
}
}
// 사용 예시
void useEventManager() {
final manager = EventManager();
// 이벤트 구독
manager.events.listen(
(event) => print("이벤트: $event"),
onError: (error) => print("오류: $error"),
onDone: () => print("스트림 종료"),
);
// 이벤트 발생
manager.addEvent("사용자 로그인");
manager.addEvent("데이터 업데이트");
// 정리
manager.dispose();
}
Sink.add vs add의 실무 활용
class DataProcessor {
final _controller = StreamController<int>();
// 외부에는 Sink만 노출 (캡슐화)
StreamSink<int> get inputSink => _controller.sink;
// 스트림은 읽기 전용으로 노출
Stream<int> get outputStream => _controller.stream;
// 내부에서는 직접 add 사용
void _processInternally(int data) {
_controller.add(data * 2); // 내부 처리
}
void dispose() {
_controller.close();
}
}
Future 체이닝 베스트 프랙티스
// 나쁜 예: 값을 직접 사용
void badExample() async {
print("${await fetchUsername()}님의 나이는 ${await fetchUserAge(await fetchUsername())}세");
// fetchUsername()이 두 번 호출됨!
}
// 좋은 예: 변수에 저장하여 재사용
void goodExample() async {
final name = await fetchUsername();
final age = await fetchUserAge(name);
print("${name}님의 나이는 ${age}세입니다");
}
결론
Dart의 비동기 프로그래밍은 다양한 패턴과 도구를 제공합니다:
- Stream 처리: await for (블로킹), listen (논블로킹), where (함수형) 적절히 선택
- Future 체이닝: 간단한 경우 then, 복잡한 로직은 async/await 사용
- Timer 활용: 주기적 작업이나 지연 실행에 Timer.periodic 활용
- 오류 처리: 동기는 try-catch, 비동기는 then-catchError 또는 async/await + try-catch
- 동기 vs 비동기: 대기 vs 병렬 처리의 차이 명확히 구분
비동기 프로그래밍은 처음에는 복잡해 보이지만, 실제로 코드를 작성하고 실행해보면서 각 패턴의 특성과 흐름을 이해하는 것이 중요합니다. 특히 언제 블로킹 방식을, 언제 논블로킹 방식을 사용할지 판단하는 능력이 실무에서 매우 중요합니다.
여러분은 어떤 비동기 패턴을 가장 자주 사용하시나요? 실제 프로젝트에서 비동기 처리할 때 겪었던 어려움이나 해결책이 있다면 댓글로 공유해주세요! 함께 배우고 성장하는 개발자 커뮤니티를 만들어가요! 😊
'📚 학습 기록 > Dart & Flutter 기초' 카테고리의 다른 글
🚀 다트(Dart) 언어 22일 완주 후기 - 비동기 마스터하고 플러터 준비 완료! (2) | 2025.05.29 |
---|---|
Dart 실전 프로젝트: 비동기 프로그래밍과 최신 문법 활용 가이드 (2) | 2025.05.28 |
Dart Stream 완벽 가이드: 기초부터 실전까지 (0) | 2025.05.21 |
Dart의 비동기 프로그래밍 완벽 가이드: Future, async/await, then (0) | 2025.05.20 |
Dart 기초 개념부터 실전 프로젝트까지: 완벽 정리 (0) | 2025.05.16 |