Combine은 Apple이 WWDC 2019에서 발표한 프레임워크로, 시간이 지남에 따라 값을 처리하는 반응형 프로그래밍(Reactive Programming)을 구현하기 위해 탄생했다.
쉽게 말해, 데이터의 흐름(스트림)을 선언적으로(Declaratively) 관리하고, 이 데이터가 도착하거나 변경될 때마다 정해진 로직을 실행하도록 하는 방식이다.
Combine의 핵심은 비동기 코드를 미래에 발생할 이벤트로 간주하고, 이 이벤트를 처리하는 파이프라인(Pipeline)을 구축하는 것이다.
이 파이프라인은 아래 세 가지 핵심 요소로 구성되어 있다.
| 요소 | 역할 | 비유 |
|---|---|---|
| Publisher | 데이터를 발행(Emit)하는 주체. 데이터가 언제, 어떻게 바뀔지 모르는 비동기 작업을 캡슐화. | 신문사 |
| Subscriber | 데이터를 구독(Subscribe)하고 최종적으로 소비(Receive)하는 주체. 데이터가 도착하면 특정 액션을 실행. | 구독자 |
| Operator | Publisher와 Subscriber 사이에 존재하며, 데이터의 흐름을 가공, 변형, 필터링하는 역할. | 신문 편집/검열 과정 |
Publisher는 Subscriber에게 다음 세 가지 종류의 이벤트를 순서대로, 그리고 한 번만 전달한다.
Publisher가 성공적으로 데이터를 생성했을 때 Subscriber에게 전달하는 데이터
.send(newValue) // 새로운 값을 전달
Publisher가 데이터를 발행하는 과정에서 에러가 발생하여 더 이상 값을 발행할 수 없을 때 전달
.send(completion: .failure(error)) // 에러 발생
참고: Combine의 모든 Publisher는 반드시
Output타입과Failure타입을 명시해야 한다.
만약Failure가 없는 경우Never타입을 사용한다.
Publisher가 모든 값을 성공적으로 발행했고, 더 이상 발행할 값이 없을 때 전달
.send(completion: .finished) // 성공적으로 완료
중요: Publisher는 Failure 또는 Finished 둘 중 하나를 보낸 후에는 더 이상 값을 보낼 수 없다. 이를 "스트림의 종료"라고 한다.
Combine에는 다양한 상황에서 데이터를 발행할 수 있는 여러 종류의 Publisher가 있다.
| Publisher 종류 | 특징 및 용도 | 예시 |
|---|---|---|
Just | 단 하나의 값만 발행하고 즉시 완료, 간단한 값을 스트림으로 바꾸어 파이프라인 시작 시 사용 | Just(5).sink { ... } |
Future | 비동기 작업의 결과를 단 한 번 발행, 클로저 기반의 비동기 코드를 Combine으로 변환할 때 유용 | Future<Data, Error> { promise in ... } |
PassthroughSubject | 직접 send() 메서드를 호출하여 값을 발행하는 Subject, 외부에서 이벤트를 주입해야 할 때 사용 | subject.send("새 데이터") |
CurrentValueSubject | PassthroughSubject와 유사하지만, 현재 값을 항상 저장하고 있다가 구독자가 생기면 즉시 마지막 값을 발행 | subject.value = 10 |
@Published | SwiftUI와 통합되어 클래스의 프로퍼티에 사용, 프로퍼티 값이 변경될 때마다 자동으로 값을 발행 | @Published var data: String |
| Foundation Publishers | 기존 Apple 프레임워크의 기능을 Publisher로 제공 | URLSession.shared.dataTaskPublisher, NotificationCenter.default.publisher |
Combine의 Subject는 Publisher이면서 동시에 Subscriber의 역할을 할 수 있는 특별한 타입이다.
Subject는 외부에서 임의로 데이터를 주입(Inject)하여 파이프라인을 시작하게 만들 때 유용하다.
| Subject 종류 | 특징 (Hot/Cold) | 초기 값 유무 |
|---|---|---|
PassthroughSubject | Hot Publisher | 없음 |
CurrentValueSubject | Hot Publisher | 초기 값 필수 |
일반적인 Publisher(Cold)는 Subscriber가 구독을 시작할 때마다 처음부터 데이터를 다시 발행한다.
하지만 Subject는 Hot Publisher로 분류되기 때문에, 구독이 시작되기 전부터 데이터를 발행할 수 있다.
Hot Publisher: 구독자가 생기기 이전부터 데이터를 발행 가능, 구독자가 스트림에 합류하면, 그 시점 이후에 발행되는 값만 받는다.CurrentValueSubject는 구독 시 현재 값을 즉시 발행함)이러한 특성 덕분에 ViewModel에서 사용자 액션이나 이벤트 스트림을 처리하고 싶을 때 PassthroughSubject를 사용하여 외부 이벤트(버튼 클릭 등)를 내부 파이프라인으로 주입하는 용도로 주로 사용된다.
Operator는 Publisher가 발행하는 데이터를 원하는 형태로 가공하는 핵심적인 도구이다. 데이터를 발행하는 시점과 구독하는 시점 사이에 수많은 가공 로직을 선언적으로 삽입할 수 있다.
| 연산자 그룹 | 목적 | 주요 Operator | 특징 |
|---|---|---|---|
| 변환 (Transform) | 발행된 값을 다른 타입이나 형태로 변경 | map, flatMap, tryMap | map은 값을 동기적으로 변환, flatMap은 값을 받아 새로운 Publisher를 리턴하여 비동기 체이닝에 사용됨. |
| 필터링 (Filter) | 특정 조건에 맞지 않는 값을 걸러내거나 중복 제거 | filter, removeDuplicates, compactMap | removeDuplicates는 이전 값과 동일한 값을 걸러내 불필요한 작업 방지. |
| 시간 조절 (Timing) | 이벤트 발행 시점 또는 빈도를 조절 | debounce, throttle | debounce: 일정 시간 동안 새 값이 없어야 값을 통과. (검색창에 주로 사용) |
| 결합 (Combine) | 여러 Publisher의 데이터를 하나로 합치거나 연결 | combineLatest, merge, zip | combineLatest: 모든 Publisher가 한 번씩 발행한 후, 어느 하나라도 값이 변하면 최신 값들을 튜플로 묶어 발행. |
| 오류 처리 (Error) | 에러 발생 시 처리 로직을 정의하거나 에러 타입 변경 | catch, replaceError, assertNoFailure | replaceError는 에러 발생 시 지정된 기본 값으로 대체하여 스트림을 Finished 상태로 유지. |
AnyCancellable을 통한 구독 취소Combine 파이프라인의 마지막 단계에서 sink나 assign으로 구독을 시작하면, 이 메서드들은 AnyCancellable 인스턴스를 반환한다. 이 인스턴스는 Combine의 메모리 관리에 있어 매우 중요한 인스턴스이다.
AnyCancellable의 역할AnyCancellable 인스턴스가 메모리에서 해제(deinit)되는 순간, 구독이 자동으로 취소되고 파이프라인이 종료된다. 이는 순환 참조를 방지하고 메모리 누수를 막는 핵심 메커니즘이다.이벤트 방출을 감지하고, 이를 통해 UI 업데이트 등을 지원하기 위해서는 Combine 파이프라인을 구축할 때마다 반환되는 AnyCancellable 객체를 저장해야 한다.
일반적으로 클래스 내부에 Set<AnyCancellable> 형태의 컬렉션을 선언하고 store(in:) 메서드를 사용하여 관리를 한다.
// 1. Set<AnyCancellable> 선언
private var cancellables = Set<AnyCancellable>()
// 2. 파이프라인 구축 및 저장
somePublisher
.sink { completion in
// ...
} receiveValue: { value in
// ...
}
.store(in: &cancellables) // 구독 객체를 Set에 저장
// 3. 뷰 모델(또는 객체)이 해제(deinit)될 때, cancellables Set의 모든 요소가 해제되며 구독 자동 취소
주의: 만약
AnyCancellable을 변수에 저장하지 않거나store(in:)을 호출하지 않으면, 해당 구독은 즉시 취소되어 값을 받지 못하는 경우가 발생할 수 있다.
사용자가 검색창에 텍스트를 입력할 때마다 실시간으로 API를 호출하는 상황을 가정해 보자.
만약 사용자가 빠르게 타이핑할 경우 불필요한 네트워크 요청이 너무 많이 발생할 수 있는데, Combine을 사용하면 debounce Operator를 활용해 이 문제를 해결할 수 있다.
import Combine
import Foundation
class SearchViewModel: ObservableObject {
// 1. Publisher 역할: @Published 프로퍼티 래퍼는 자체적으로 Publisher를 내장
@Published var searchText: String = ""
@Published var searchResults: [String] = []
private var cancellables = Set<AnyCancellable>() // 구독 취소를 위한 저장소
init() {
// 2. 파이프라인 구축
$searchText // $searchText는 String 타입의 Publisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) // 500ms 동안 새 입력이 없으면 다음으로 값 전달
.removeDuplicates() // 이전 값과 같으면 무시
.flatMap { query in // 네트워크 요청 Publisher로 변환
return self.performSearch(query: query)
}
.receive(on: DispatchQueue.main) // UI 업데이트를 위해 메인 스레드 지정
.assign(to: \.searchResults, on: self) // 3. Subscriber 역할: 최종 결과를 searchResults 프로퍼티에 할당
.store(in: &cancellables) // 구독(Subscriber)을 cancellables Set에 저장
}
// 더미 검색 Publisher (실제로는 URLSession.dataTaskPublisher 등을 사용)
private func performSearch(query: String) -> AnyPublisher<[String], Never> {
guard !query.isEmpty else {
return Just([]).eraseToAnyPublisher()
}
// 1초 후 더미 결과 발행
return Just(["Result for \(query) 1", "Result for \(query) 2"])
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
Combine을 이해하는 가장 좋은 방법은 오랫동안 iOS 개발 생태계를 지배했던 RxSwift와 비교하는 것이다. 두 프레임워크 모두 반응형 프로그래밍을 위한 도구이지만, Apple의 공식 지원 여부와 설계 철학에서 차이를 보인다.
| 구분 | Combine | RxSwift |
|---|---|---|
| 개발 주체 | Apple 공식 프레임워크 | 오픈소스 (ReactiveX 진영) |
| 프레임워크 이름 | Publisher | Observable |
| 데이터 발행 주체 | Subscriber | Observer |
| 구독 취소 객체 | AnyCancellable | Disposable |
| 에러 처리 | Output과 Failure 타입이 필수로 명시됨. Failure 타입이 없는 경우 Never 사용. | Element와 Error 타입으로 구성되며, 에러 타입 명시가 Combine만큼 엄격하지 않음. |
| 플랫폼 호환성 | iOS 13+, macOS 10.15+ (Apple 생태계 전용) | iOS, Android, Web 등 다양한 플랫폼 및 언어 지원 |
| 내장 통합 | SwiftUI, Foundation (URLSession, Timer 등)에 깊숙이 통합되어 있어 연동이 매우 쉽고 자연스러움. | 별도의 Bridge(예: RxCocoa)를 통해 UIKit/Foundation과 연동해야 함. |
| 결론 | Apple 생태계에서 새로운 프로젝트를 시작하거나 SwiftUI를 사용할 때 표준으로 권장됨. | 레거시 프로젝트나 크로스 플랫폼 지식이 필요할 때 유용함. |
Combine은 비동기 코드를 Callback Hell에서 구출하고, 데이터 흐름을 가독성 높은 선언적인 파이프라인으로 정의하게 해주는 강력한 도구이다.
RxSwift와 유사하지만 Apple 생태계에 표준으로 깊숙이 통합되어 있다는 강력한 이점을 가지고 있으며, Subject를 통한 유연한 이벤트 주입과 Cancellable을 통한 깔끔한 메모리 관리는 Combine 개발의 핵심 역량이라고 생각된다.