아직 오지 않은 값
편의점에서 도시락을 데워달라고 맡겼습니다. 점원이 영수증을 건네줍니다. 도시락이 완성된 건 아니지만, 이 영수증은 "나중에 도시락을 받을 권리"를 나타냅니다. 영수증을 들고 기다리다가 점원이 "다 됐어요"라고 부르면 도시락을 받습니다.
Dart의 Future가 바로 이 영수증입니다.
Future<String>은 "언젠가 String 값이 완성될 것"을 나타내는 약속입니다. 지금 당장 값이 없어도 변수에 담아두고, 나중에 완성되면 처리할 수 있습니다.
Future의 3가지 상태
Future는 항상 세 가지 상태 중 하나에 있습니다.
미완료 (Uncompleted)
↓ 작업 진행 중
완료 - 값 (Completed with value)
완료 - 에러 (Completed with error)
도시락 비유로 설명하면, 영수증을 받은 직후가 "미완료"입니다. 도시락이 완성되면 "완료(값)"입니다. 만약 전자레인지가 고장났다면 "완료(에러)"입니다.
Future 생성하기
Future를 만드는 방법은 여러 가지입니다.
즉시 완료되는 Future. 이미 값이 있지만 Future 형태로 감싸야 할 때 사용합니다.
void main() async {
// 즉시 완료되는 Future
Future<int> immediate = Future.value(42);
int result = await immediate;
print(result); // 42
// 즉시 에러가 발생하는 Future
Future<int> failed = Future.error('오류 발생!');
}
지연 실행 Future. 일정 시간 후에 완료되는 Future를 만듭니다. 테스트나 데모에서 네트워크 지연을 흉내낼 때 자주 씁니다.
void main() async {
print('주문 접수');
// 2초 후에 완료
String meal = await Future.delayed(
Duration(seconds: 2),
() => '도시락 완성!',
);
print(meal); // 2초 후: 도시락 완성!
}
비동기 작업을 Future로 감싸기. 가장 일반적인 패턴입니다. 시간이 걸리는 작업을 Future 안에 담습니다.
Future<String> fetchWeather(String city) {
return Future(() {
// 실제로는 HTTP 요청을 하겠지만
// 여기서는 시뮬레이션
return '$city의 날씨: 맑음, 22°C';
});
}
void main() async {
var weather = await fetchWeather('서울');
print(weather);
}
then, catchError, whenComplete
Future의 결과를 처리하는 방법에는 두 가지가 있습니다. 하나는 then()을 체이닝하는 방식이고, 다른 하나는 다음 챕터에서 배울 async/await 방식입니다. 먼저 체이닝 방식을 이해해보겠습니다.
Future<String> fetchUserName(int id) {
return Future.delayed(
Duration(seconds: 1),
() {
if (id <= 0) throw ArgumentError('잘못된 ID');
return '사용자_$id';
},
);
}
void main() {
fetchUserName(1)
.then((name) {
// 성공 시 처리
print('이름: $name');
return name.toUpperCase(); // 다음 then으로 변환된 값 전달
})
.then((upperName) {
print('대문자: $upperName');
})
.catchError((error) {
// 에러 처리
print('오류: $error');
})
.whenComplete(() {
// 성공/실패 관계없이 항상 실행
print('작업 완료');
});
}
각 메서드의 역할을 정리하겠습니다.
then(): Future가 성공적으로 완료되면 실행됩니다. 반환값을 새로운 Future로 감싸서 다음then()으로 전달합니다.catchError(): Future가 에러와 함께 완료되면 실행됩니다.on키워드로 특정 예외 타입만 잡을 수 있습니다.whenComplete(): 성공이든 실패든 Future가 완료되면 반드시 실행됩니다. 리소스 해제나 로딩 표시 제거에 유용합니다.
특정 타입의 에러만 처리하고 싶다면 catchError()에 test 인자를 사용합니다.
fetchUserName(-1)
.catchError(
(error) => print('인자 오류: $error'),
test: (error) => error is ArgumentError,
)
.catchError(
(error) => print('기타 오류: $error'),
);
then 체이닝의 흐름
then()은 연속으로 연결할 수 있습니다. 각 then()의 반환값이 다음 then()의 입력이 됩니다.
Future<int> getScore(String username) {
return Future.delayed(Duration(milliseconds: 500), () => 85);
}
Future<String> getGrade(int score) {
return Future.delayed(Duration(milliseconds: 300), () {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
return 'F';
});
}
void main() {
getScore('홍길동')
.then((score) {
print('점수: $score');
return getGrade(score); // Future를 반환하면 자동으로 풀어줌
})
.then((grade) {
print('학점: $grade');
});
}
then() 안에서 Future를 반환하면, 그 Future가 완료될 때까지 기다렸다가 다음 then()을 실행합니다. Future를 중첩하지 않아도 자동으로 체이닝됩니다.
Future.wait — 여러 Future 동시 실행
여러 비동기 작업을 동시에 시작하고 모두 완료될 때까지 기다리려면 Future.wait()를 사용합니다.
Future<String> fetchProfile() async {
await Future.delayed(Duration(seconds: 2));
return '프로필 데이터';
}
Future<List<String>> fetchPosts() async {
await Future.delayed(Duration(seconds: 3));
return ['글 1', '글 2', '글 3'];
}
Future<int> fetchFollowers() async {
await Future.delayed(Duration(seconds: 1));
return 1024;
}
void main() async {
var stopwatch = Stopwatch()..start();
// 세 작업을 동시에 시작
var results = await Future.wait([
fetchProfile(),
fetchPosts(),
fetchFollowers(),
]);
print('소요 시간: ${stopwatch.elapsed.inSeconds}초'); // 약 3초
print('프로필: ${results[0]}');
print('게시글: ${results[1]}');
print('팔로워: ${results[2]}');
}
세 작업의 대기 시간을 합치면 6초지만, 동시에 실행되므로 가장 오래 걸리는 3초 만에 모두 완료됩니다. Future.wait()는 배열 순서 그대로 결과를 반환하므로 인덱스로 접근할 수 있습니다.
만약 하나라도 에러가 발생하면 Future.wait() 전체가 에러가 됩니다. 에러가 발생해도 나머지 결과를 받고 싶다면 eagerError: false를 사용합니다.
// 에러가 발생해도 모든 Future가 완료될 때까지 기다림
var results = await Future.wait(
[future1, future2, future3],
eagerError: false,
);
Future.any — 가장 빠른 것만
여러 Future 중 가장 먼저 완료되는 것만 필요하다면 Future.any()를 사용합니다.
Future<String> serverA() async {
await Future.delayed(Duration(seconds: 3));
return '서버 A 응답';
}
Future<String> serverB() async {
await Future.delayed(Duration(seconds: 1));
return '서버 B 응답';
}
Future<String> serverC() async {
await Future.delayed(Duration(seconds: 2));
return '서버 C 응답';
}
void main() async {
// 가장 먼저 응답한 서버의 결과만 사용
var fastest = await Future.any([serverA(), serverB(), serverC()]);
print(fastest); // 서버 B 응답 (1초 만에 완료)
}
실제 앱에서 여러 CDN 중 가장 빠른 서버를 선택할 때 이 패턴을 활용합니다.
Completer — Future를 직접 제어하기
Future.delayed()처럼 내부에서 자동으로 완료되는 게 아니라, 외부에서 원하는 시점에 완료시키고 싶을 때 Completer를 사용합니다.
import 'dart:async';
Future<String> waitForUserInput() {
var completer = Completer<String>();
// 5초 후 타임아웃
Future.delayed(Duration(seconds: 5), () {
if (!completer.isCompleted) {
completer.completeError('타임아웃!');
}
});
// 외부에서 completer.complete('입력값')을 호출하면 Future가 완료됨
// 여기서는 예시로 2초 후 완료
Future.delayed(Duration(seconds: 2), () {
if (!completer.isCompleted) {
completer.complete('사용자 입력: 안녕하세요');
}
});
return completer.future;
}
void main() async {
try {
var input = await waitForUserInput();
print(input);
} catch (e) {
print('오류: $e');
}
}
Completer는 비동기 콜백 기반 API를 Future로 변환할 때 특히 유용합니다.
정리
Future의 핵심을 정리하겠습니다.
Future는 아직 완료되지 않은 작업의 결과를 나타내는 약속입니다.- 미완료, 완료(값), 완료(에러)의 세 가지 상태를 가집니다.
Future.value(),Future.delayed(),Future(() => ...)등으로 생성합니다.then(),catchError(),whenComplete()으로 결과를 처리합니다.Future.wait()로 여러 Future를 동시에 실행하고 모두 기다립니다.
then() 체이닝은 강력하지만, 체인이 길어지면 읽기 어려워집니다. 다음 챕터에서는 async/await를 사용해 같은 비동기 코드를 마치 동기 코드처럼 깔끔하게 작성하는 방법을 배웁니다.