Future
단 하나의 값 또는 에러를 비동기적으로 방출하는 Publisher
주로 콜백 기반 비동기 API를 Combine 스트림으로 래핑할 때 사용한다.
개념
Future → 값 1개 방출 후 즉시 완료
또는 에러 방출 후 즉시 종료
| Publisher | 방출 횟수 |
| Just | 1번 (동기) |
| Future | 1번 (비동기) |
| PassthroughSubject | N번 |
| [1,2,3].publisher | N번 |
시그니처
final class Future<Output, Failure: Error>: Publisher {
init(_ attemptToFulfill: @escaping (@escaping Promise) -> Void)
}
// Promise의 정의
typealias Promise = (Result<Output, Failure>) -> Void
Promise는 단순히 Result를 받는 클로저 타입
이 클로저를 호출하는 순간 값 또는 에러가 방출된다.
기본 사용법
func fetchData() -> Future<String, Error> {
return Future { promise in
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
promise(.success("데이터 로드 완료"))
// 또는 실패: promise(.failure(SomeError.networkError))
}
}
}
fetchData()
.sink(
receiveCompletion: { print("완료: \($0)") },
receiveValue: { print("값: \($0)") }
)
.store(in: &cancellables)
// 출력 (1초 후):
// 값: 데이터 로드 완료
// 완료: finished
콜백 API를 Combine으로 래핑하기
Future의 가장 핵심적인 사용 목적
// ✅ 기존 콜백 기반 API
func legacyFetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
// 네트워크 요청...
}
// ✅ Future로 래핑
func fetchUser(id: Int) -> Future<User, Error> {
return Future { promise in
legacyFetchUser(id: id) { result in
promise(result) // Result를 그대로 전달
}
}
}
// ✅ Combine 체인에서 사용
fetchUser(id: 42)
.map { $0.name }
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { ... },
receiveValue: { name in self.userName = name }
)
.store(in: &cancellables)
즉시 실행 (Eager Execution)
Future는 구독 여부와 관계없이 생성 즉시 클로저를 실행한다.
// 구독하지 않아도 클로저 실행됨!
let future = Future<Int, Never> { promise in
print("클로저 실행됨") // Future 생성 시점에 즉시 출력
promise(.success(42))
}
// 아직 .sink 안 했는데도 이미 위 print가 실행된 상태
AnyPublisher나 일반 Publisher가 구독 시점에 실행되는 것과 반대
일반 Publisher: 생성 ── ... ── 구독 ── 실행
Future: 생성 = 실행
캐싱
즉시 실행의 연장선으로, Future는 Promise 결과를 내부에 캐싱한다.
나중에 구독하더라도 저장된 결과를 즉시 전달함
매번 새로 실행되길 원한다면 Deferred { Future { ... } } 패턴을 사용
let future = Future<Int, Never> { promise in
print("API 호출!")
promise(.success(42))
}
// 첫 번째 구독
future.sink { print("구독1: \($0)") }.store(in: &cancellables)
// 두 번째 구독 — API 재호출 없이 캐시된 42 전달
future.sink { print("구독2: \($0)") }.store(in: &cancellables)
// 출력:
// API 호출! ← 한 번만 실행
// 구독1: 42
// 구독2: 42
Deferred
Deferred는 구독이 발생하는 시점에 Publisher를 생성하는 래퍼
"Publisher를 만드는 Publisher"라고 볼 수 있음
왜 필요한가: Future의 문제
func makeRequest() -> Future<String, Error> {
return Future { promise in
print("요청 실행!")
networkCall { promise($0) }
}
}
let publisher = makeRequest() // ← 여기서 이미 네트워크 요청 발생
// ...
// 실제 구독은 훨씬 나중에
publisher.sink { ... }.store(in: &cancellables)
Future는 생성 즉시 실행되기 때문에, 구독 전에 요청이 나가버리는 문제가 생김
또한 재구독해도 캐시된 결과만 돌아옴
Deferred의 동작
let publisher = Deferred {
Future<String, Error> { promise in
print("요청 실행!")
networkCall { promise($0) }
}
}
// 아직 아무것도 실행 안 됨
publisher.sink { ... }.store(in: &cancellables)
// ← 구독 시점에 비로소 Future 생성 + 실행
Deferred 생성 → 아무것도 안 함
구독 발생 → 클로저 실행 → 내부 Publisher 생성 → 구독 연결
차이
// Future 단독 — 생성 즉시 실행, 결과 캐싱
let future = Future<Int, Never> { promise in
print("실행: \(Date())")
promise(.success(Int.random(in: 1...100)))
}
future.sink { print("구독1: \($0)") }.store(in: &cancellables)
future.sink { print("구독2: \($0)") }.store(in: &cancellables)
// 출력:
// 실행: 2024-01-01 00:00:00 ← 한 번만
// 구독1: 42
// 구독2: 42 ← 캐시된 값 재사용
// Deferred + Future — 구독마다 새로 실행
let deferred = Deferred {
Future<Int, Never> { promise in
print("실행: \(Date())")
promise(.success(Int.random(in: 1...100)))
}
}
deferred.sink { print("구독1: \($0)") }.store(in: &cancellables)
deferred.sink { print("구독2: \($0)") }.store(in: &cancellables)
// 출력:
// 실행: 2024-01-01 00:00:00 ← 구독1
// 구독1: 42
// 실행: 2024-01-01 00:00:01 ← 구독2 (새로 실행)
// 구독2: 87
시그니처
struct Deferred<DeferredPublisher: Publisher>: Publisher {
init(_ createPublisher: @escaping () -> DeferredPublisher)
}
클로저가 반환하는 타입이 곧 Publisher 타입이 된다. Future뿐 아니라 어떤 Publisher든 감쌀 수 있음
Future 외에도 활용 가능
// 구독 시점의 현재 값을 캡처하고 싶을 때
var currentUserId = 1
let publisher = Deferred {
Just(currentUserId) // 구독 시점의 currentUserId를 캡처
}
currentUserId = 99
publisher.sink { print($0) }
// 출력: 99 ← 구독 시점 값 반영
// 조건에 따라 다른 Publisher를 반환할 때
let publisher = Deferred { () -> AnyPublisher<String, Error> in
if isLoggedIn {
return fetchProfile().eraseToAnyPublisher()
} else {
return Fail(error: AuthError.notLoggedIn).eraseToAnyPublisher()
}
}
retry와 조합
retry는 실패 시 Publisher를 재구독한다.
Future 단독이면 캐시된 실패가 반복되지만, Deferred로 감싸면 매번 새 요청을 보낸다.
// ❌ retry가 의미 없음 — 캐시된 에러를 반복 전달
Future<Data, Error> { promise in
networkCall { promise($0) }
}
.retry(3)
// ✅ retry마다 실제로 새 요청 발생
Deferred {
Future<Data, Error> { promise in
networkCall { promise($0) }
}
}
.retry(3)
정리
| Future | Deferred { Future } | |
| 실행 시점 | 생성 즉시 | 구독 시점 |
| 재구독 | 캐시된 결과 반환 | 매번 새로 실행 |
| retry 호환 | ❌ | ✅ |
| 용도 | 단발성 비동기 작업 | 구독마다 독립 실행이 필요한 작업 |
Deferred는 "실행을 구독 시점까지 미루고 싶다"는 의도를 명확하게 표현하는 래퍼다.
Future와 함께 쓰는 경우가 많지만, 본질적으로는 어떤 Publisher의 생성 타이밍이든 지연시킬 수 있는 도구다.
Deferred + Future 패턴
구독할 때마다 새로 실행되어야 하는 경우 (예: 매번 새 API 요청)에 사용
// ❌ Future만 사용 — 항상 캐시된 결과 반환
let cachedFuture = Future<Int, Never> { promise in
promise(.success(Int.random(in: 1...100)))
}
// ✅ Deferred로 감싸기 — 구독마다 새로 실행
let freshFuture = Deferred {
Future<Int, Never> { promise in
promise(.success(Int.random(in: 1...100)))
}
}
flatMap과 함께 쓰는 패턴
이벤트 스트림에서 각 값마다 비동기 작업을 체이닝할 때 자주 사용
let userIds = [1, 2, 3].publisher
userIds
.flatMap { id in
Deferred { Future { promise in
fetchUser(id: id) { result in promise(result) }
}}
}
.sink(
receiveCompletion: { _ in },
receiveValue: { user in print(user.name) }
)
.store(in: &cancellables)
정리
| 특성 | 내용 |
| 방출 횟수 | 정확히 1번 |
| 실행 시점 | 생성 즉시 (Eager) |
| 결과 캐싱 | ✅ (재구독 시 재실행 없음) |
| 주요 용도 | 콜백 API → Combine 래핑 |
| 매번 새 실행 필요 시 | Deferred { Future { } } |
Future: 즉시 실행, 결과 캐싱
콜백 기반 비동기 코드를 Combine 스트림으로 연결하는 용도로 주로 쓰이며, 구독마다 새로 실행되어야 하는 경우엔 Deferred와 조합해야 한다.
'iOS > Combine' 카테고리의 다른 글
| [Combine] .handleEvents (0) | 2026.03.12 |
|---|---|
| [Combine] .removeDuplicates() (0) | 2026.03.11 |