Dart의 비동기 프로그래밍에 대해 진행해볼 예정이다.
Future, async/await, Stream을 활용하면 네트워크 요청, 파일 읽기, 데이터베이스 처리 같은 시간이 오래 걸리는 작업을 효율적으로 처리할 수 있다.
1. Future ( 미래 값 )
Future는 비동기 작업의 결과를 담는 객체.
예를 들어, 데이터를 다운로드하는 작업이 끝나면 결과를 반환한다.
Future<Type>는 비동기적으로 Type 값을 반환 / Future<String>, Future<bool> 등 다양한 타입도 가능
await를 사용하면 Future<Type>의 결과를 기다릴 수 있음
✔️ 기본 Future 예제
Future<String> fetchData() {
return Future.delayed(Duration(seconds: 2), () => "📦 데이터 로드 완료!");
}
void main() {
print("⏳ 데이터 요청 중...");
fetchData().then((data) {
print(data); // 2초 후 "📦 데이터 로드 완료!" 출력
});
print("✅ 요청 완료 (비동기 실행 중)");
}
- Future.delayed(Duration(seconds: 2), () => "결과") → 2초 후 데이터를 반환
- then((data) => print(data)) → Future 완료 후 결과 처리
- 실행 순서: ⏳ 데이터 요청 중... → ✅ 요청 완료 → (2초 후) 📦 데이터 로드 완료!
2. async / await ( 비동기 작업을 동기 코드처럼 )
async/await를 사용하면 Future를 더 읽기 쉽게 작성할 수 있다.
✔️ async / await 사용 예제
async 함수 내부에서 await 사용 가능
await는 Future가 완료될 때까지 대기 (동기처럼 보이지만 비동기 처리)
실행 순서: ⏳ 데이터 요청 중... → (2초 후) 📦 데이터 로드 완료! → ✅ 요청 완료
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2)); // 2초 대기
return "📦 데이터 로드 완료!";
}
void main() async {
print("⏳ 데이터 요청 중...");
String data = await fetchData(); // Future 완료될 때까지 기다림
print(data);
print("✅ 요청 완료");
}
✔️ await 유무의 차이점
async 함수 내부에서 await 사용 가능
await는 Future가 완료될 때까지 대기 (동기처럼 보이지만 비동기 처리)
실행 순서: ⏳ 데이터 요청 중... → (2초 후) 📦 데이터 로드 완료! → ✅ 요청 완료
Future<int> fetchNumber() {
return Future.delayed(Duration(seconds: 2), () => 42); // 그냥 Future 반환
}
void main() async {
print("⏳ 숫자 가져오는 중...");
int result = await fetchNumber();
print("🎯 가져온 숫자: $result");
}
- fetchNumber()는 Future 객체를 바로 반환
- await fetchNumber();가 실행될 때 Future가 완료될 때까지 기다림
- 실행 순서: ⏳ 숫자 가져오는 중... → (2초 후) 🎯 가져온 숫자: 42
Future<int> fetchNumber() async {
await Future.delayed(Duration(seconds: 2)); // Future 실행을 여기서 기다림
return 42; // 실행 완료 후 값을 반환
}
void main() async {
print("⏳ 숫자 가져오는 중...");
int result = await fetchNumber();
print("🎯 가져온 숫자: $result");
}
- fetchNumber() 내부에서 await을 사용하여 Future가 완료될 때까지 기다림
- await fetchNumber();가 실행될 때 이미 내부에서 await 처리되었으므로 값만 반환
- 실행 순서: ⏳ 숫자 가져오는 중... → (2초 후) 🎯 가져온 숫자: 42
📌 차이점 정리
return Future.delayed(...) (상단 예제) | await Future.delayed(...) 후 return (하단 예제) | |
Future 실행 시점 | fetchNumber()를 호출하면 Future 객체만 반환 | fetchNumber() 내부에서 Future를 즉시 실행하고 기다림 |
호출한 곳의 동작 | await fetchNumber();에서 Future가 실행됨 | await fetchNumber();에서 이미 Future가 끝난 값 반환 |
코드 스타일 | 일반 Future를 직접 반환하는 방식 | async/await로 비동기 처리를 동기 코드처럼 작성 |
📌 결론: 언제 await을 쓰고 언제 안 써야 할까?
- Future 자체를 반환할 때 (Future<T> 반환) → return Future.delayed(...) 사용
- 내부에서 비동기 작업을 실행하고 값만 반환할 때 (async/await 활용) → await 후 return 사용
- 둘 다 같은 결과를 내지만, async/await를 쓰면 가독성이 더 좋아지고 동기 코드처럼 보이게 작성할 수 있다.
✔️ Future 매개변수 받아 return
비동기 함수에서 매개변수를 전달받아 작업을 수행하고, Future<Type>을 반환할 수 있다.
Future<String> fetchWeather(String city, int delaySeconds) async {
await Future.delayed(Duration(seconds: delaySeconds)); // 지정된 시간 대기
return "🌤️ $city의 날씨는 맑음!";
}
void main() async {
print("⏳ 날씨 정보를 가져오는 중...");
String weather = await fetchWeather("Seoul", 3); // 매개변수 2개 전달
print(weather);
}
- Future<T> 함수는 매개변수를 받을 수 있음
- 매개변수를 활용해 동적으로 비동기 작업을 수행 가능
- async/await와 함께 사용하면 코드 가독성이 좋아짐
3. Future의 예외 처리
비동기 작업에서 에러가 발생하면 try-catch로 예외를 처리할 수 있다.
✔️ Future 예외 처리
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
throw Exception("⚠️ 데이터 로드 실패!");
}
void main() async {
print("⏳ 데이터 요청 중...");
try { // try-catch를 사용하여 오류 처리 가능
String data = await fetchData();
print(data);
} catch (e) { //throw Exception("오류 메시지") → 오류 발생
print("🚨 오류 발생: $e");
}
print("✅ 요청 완료");
}
4. Stream ( 데이터 스트림 )
Stream은 연속적인 데이터를 비동기적으로 처리하는 객체.
예를 들어, 실시간 센서 데이터, 채팅 메시지, 파일 다운로드 진행률 등을 다룰 때 사용한다.
✔️ Stream 기본 예제
extends 를 사용하고, 부모 클래스의 기능을 자식 클래스에서 그대로 사용 가능하다.
Stream<int> countStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
print("⏳ 카운트 시작...");
await for (var value in countStream()) {
print("⏰ $value");
}
print("✅ 카운트 완료!");
}
- async*와 yield를 사용해 데이터를 순차적으로 반환
- await for (var value in stream) → 데이터를 하나씩 처리
- 실행 순서: ⏳ 카운트 시작... → ⏰ 1 → ⏰ 2 → ... → ✅ 카운트 완료!
✔️ Stream과 Future의 차이점
Future<Type>는 한 번만 값을 반환하지만, Stream<Type>는 여러 개의 값을 순차적으로 반환할 수 있다.
Future<Type> | Stream <Type> | |
반환값 | 단일 값 (type) | 여러 개의 값 ( type , type , ...) |
실행 방식 | 한 번만 실행되고 끝남 | 여러 번 데이터를 방출 가능 |
예제 | Future<int> fetchNumber() | Stream<int> countStream() |
Stream<int> countStream() {
return Stream<int>.periodic(Duration(seconds: 1), (count) => count + 1).take(5);
}
void main() async {
print("⏳ 카운트 시작...");
await for (var value in countStream()) {
print("⏰ $value");
}
print("✅ 카운트 완료!");
}
- Stream.periodic()을 사용하여 1초마다 값을 생성
- 하지만, 특정 조건(5까지 실행)을 설정하려면 take(5)를 사용해야 함
- 이 방식은 직접적으로 Stream을 조작할 수 없어 유연성이 떨어짐
Stream<int> countStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1)); // 1초 대기
yield i; // 값을 방출
}
}
void main() async {
print("⏳ 카운트 시작...");
await for (var value in countStream()) {
print("⏰ $value");
}
print("✅ 카운트 완료!");
}
- async*는 Stream을 반환하는 함수에서 사용됨
- yield를 사용하여 하나씩 값을 방출 가능
- 값을 방출할 때마다 await for에서 받을 수 있음
📌 return / yield 차이
return | yield | |
사용 가능 | Future 및 일반 함수 | fetchNumber() 내부에서 Future를 즉시 실행하고 기다림 |
반환 값 | 함수 종료 후 단일 값 반환 | 여러 개의 값을 순차적으로 방출 |
예제 | return 42; | yield 1; yield 2; yield 3; |
- return은 함수 실행을 종료하고 값을 반환하지만,
- yield는 Stream에서 값을 하나씩 방출하면서 계속 실행됨.
📌 정리
- async*는 Stream을 반환하는 함수에서 사용
- yield를 사용하여 데이터를 순차적으로 방출 가능
- Future<int>는 단일 값 반환, Stream<int>는 여러 개의 값을 반환 가능
- 비동기적으로 여러 개의 값을 처리할 때는 Stream과 async*가 더 적합
✔️ 테스트 예제 코드 1
위에서 진행했던 내용을 토대로 충분한 실습을 진행한 후, 테스트 예제를 진행해보도록 하자.
1. Future.delayed()를 사용하여 3초 후 "Hello, Dart!"를 출력하는 프로그램 만들기
2. async/await 활용하기
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 3)); // 3초 대기
return "Hello, Dart!";
}
void main() async {
print("⏳ 데이터 요청 중...");
String data = await fetchData(); // Future가 완료될 때까지 기다림
print(data);
print("✅ 요청 완료");
}
✔️ 테스트 예제 코드 2
1. async/await을 사용하여 비동기 API 요청을 시뮬레이션하는 프로그램 만들기
2. 비동기 API 요청을 시뮬레이션하여 데이터를 가져오는 함수 작성
3. 네트워크 지연을 Future.delayed로 구현
4. userData를 받아 2초 후에 랜덤 숫자를 표기되게 진행.
5. 이후 productData를 순차적으로 실행하여 2초 후 랜덤 숫자를 표기되게 진행.
import 'dart:math';
// 비동기 API 요청 시뮬레이션 함수
Future<String> fetchApiData(String endpoint) async {
print("🌐 [$endpoint] 데이터 요청 중...");
await Future.delayed(Duration(seconds: 2)); // 2초 지연 시뮬레이션
return "📦 [$endpoint] 데이터: ${Random().nextInt(100)}";
}
void main() async {
print("🚀 비동기 API 요청 시작");
try {
String userData = await fetchApiData("user"); // 사용자 데이터 요청
print(userData);
String productData = await fetchApiData("product"); // 상품 데이터 요청
print(productData);
print("✅ 모든 데이터 요청 완료");
} catch (e) {
print("🚨 오류 발생: $e");
}
}
- await을 사용하여 비동기 흐름을 동기 코드처럼 작성
- API 요청 중 에러 발생 시 try-catch로 안전하게 처리
- Future.delayed로 네트워크 지연 시뮬레이션
✔️ 테스트 예제 코드 3
1. Stream을 사용하여 사용자가 숫자를 입력하면 그 숫자만큼 카운트 다운
2. count를 동적으로 조절 가능하게 하기
3. countStrema(int maxCount) 처럼 매개 변수 받아 조절
4. 출력 메시지를 더 직관적으로 만들기
5. 입력된 숫자가 null 이거나 0 일경우 예외처리 추가
import 'dart:io';
Stream<int> countStream(int maxCount) async* {
for (int i = maxCount; i >= 1; i--) { // 입력한 숫자부터 1까지 감소
await Future.delayed(Duration(seconds: 1)); // 1초 대기
yield i; // 값을 방출
}
}
void main() async {
stdout.write("⏳ 카운트다운 시작! 숫자를 입력하세요: ");
String? input = stdin.readLineSync(); // 사용자 입력 받기
if (input == null || input.trim().isEmpty) {
print("⚠️ 숫자를 입력해야 합니다!");
return;
}
int? countLimit = int.tryParse(input); // 입력값을 숫자로 변환
if (countLimit == null || countLimit <= 0) {
print("⚠️ 올바른 양의 정수를 입력하세요.");
return;
}
print("⏳ $countLimit초 동안 카운트다운 시작...");
await for (var value in countStream(countLimit)) {
print("⏰ $value");
}
print("🎉 카운트다운 종료!");
}
📌 await for의 역할과 동작 방식
await for는 Stream을 처리할 때 사용되는 비동기 반복문이다.
즉, 테스트 예제 3번으로 설명을 하면 countStream(countLimit)이 연속적으로 데이터를 방출할 때마다 값을 하나씩 받아서 처리하는 역할을 한다.
그럼 await for 는 어떻게 동작할지 알아보자.
- countStream(countLimit) 실행
- await for 반복문 시작
- Stream이 완료될 때까지 반복
Stream<int> countStream(int maxCount) async* {
for (int i = maxCount; i >= 1; i--) {
await Future.delayed(Duration(seconds: 1)); // 1초 대기
yield i; // 값을 방출
}
}
void main() async {
print("⏳ 5초 동안 카운트다운 시작...");
await for (var value in countStream(5)) {
print("⏰ $value"); // 값이 하나씩 출력됨
}
print("🎉 카운트다운 종료!");
}
실행 순서
1. countStream(5) 실행 → Stream<int> 반환
2. await for가 countStream(5)의 첫 번째 값(5)이 방출될 때까지 대기
3. yield가 5를 방출 → print("⏰ 5") 실행
4. 다음 값(4)이 방출될 때까지 다시 대기
5. yield가 4를 방출 → print("⏰ 4") 실행
6. 이 과정이 1까지 반복됨
7. countStream()에서 더 이상 yield할 값이 없으면 await for 종료
8. print("🎉 카운트다운 종료!") 실행
📌 await for vs for 차이점
for | await for | |
사용 대상 | 일반 List, Iterable | Stream (비동기 데이터) |
동작 방식 | 모든 데이터를 미리 불러와 반복 | 하나씩 기다리면서 처리 (await) |
예제 | for (var item in list) {...} | await for (var item in stream) {...} |
- return은 함수 실행을 종료하고 값을 반환하지만,
- yield는 Stream에서 값을 하나씩 방출하면서 계속 실행됨.
📌 await for을 listen()으로 대체하는 방법
비슷한 기능을 listen()을 사용해서 구현할 수도 있다. listen()은 다음에 진행할 예정이지만 참고만 하시면 될 것 같다.
countStream(5).listen((value) {
print("⏰ $value");
}, onDone: () {
print("🎉 카운트다운 종료!");
});
- listen()을 사용하면 await 없이 Stream을 구독 가능
- onDone:을 사용해 Stream이 끝났을 때 실행할 코드 추가 가능
- 하지만 await for를 사용하면 동기 코드처럼 자연스럽게 작성 가능
📌 정리
- await for는 Stream 데이터를 순차적으로 하나씩 받아 처리하는 반복문
- yield로 값을 방출할 때마다 await for에서 값을 받아서 실행
- 비동기 데이터를 하나씩 기다리면서 반복 처리할 때 사용
- 동기 리스트(List<int>) 같은 경우는 for를 사용하지만, Stream<int>에서는 await for를 사용해야 함
이렇게 비동기 프로그래밍 (Future, async/await, Stream)에 대해서 알아보았다. 추가 적인 내용이 필요한 경우에는 댓글을 요청드리고, 틀린 부분이 있다면 이것 또한 댓글로 알려주시면 수정하도록 하겠습니다!
'IT > Dart' 카테고리의 다른 글
Dart_7일차 : 파일 입출력 & JSON 데이터 처리 (0) | 2025.03.05 |
---|---|
Dart_6일차 : 고급 Stream 활용 (listen(), StreamController, Broadcast Stream) (2) | 2025.03.04 |
Dart_4일차 : 클래스(Class)와 객체(Object) 기초 (0) | 2025.02.20 |
Dart_3일차 : 컬렉션(List, Set, Map), 예외처리 (0) | 2025.02.19 |
Dart_2일차 : 제어문과 함수 심화 학습 (1) | 2025.02.17 |