Combine — 핵심 개념, Publisher 및 반응형 프로그래밍

저자: IT Sectr 게시일: 2026-05-03 읽는 시간: 11 분

Combine은 iOS 13, macOS Catalina, tvOS 13 및 watchOS 6에 도입된 Apple의 네이티브 반응형 프로그래밍 프레임워크입니다. Publisher와 Subscriber 패턴을 통해 비동기 이벤트를 처리하는 선언형 Swift API를 제공하며, 델리게이트, 클로저 및 NotificationCenter를 통일된 체인으로 대체합니다. Apple, 2025에 따르면 Combine은 SwiftUI 및 최신 iOS 아키텍처의 기반이며, async/await 및 Structured Concurrency와 긴밀하게 작동합니다. 이 프레임워크는 스레드 안전성을 보장하면서 비동기 작업의 구성을 위해 설계되었습니다.

핵심 요점

  • Combine은 Publisher, Subscriber, Subject 타입과 Failure를 통한 오류 타입을 가진 Apple의 네이티브 반응형 프레임워크
  • Publisher는 시간에 따라 이벤트를 방출: Output 값, 성공 또는 Failure 오류로 완료 가능
  • Subscriber는 Publisher에서 이벤트를 수신하고 Demand를 통해 특정 수의 항목 요청 가능
  • 연산자 map, flatMap, filter, combineLatest, zip, debounce가 경쟁 조건 없이 스트림 변환
  • SwiftUI는 @Published, ObservableObject 및 @StateObject를 통해 Combine을 사용하여 반응형 인터페이스 업데이트

Combine이란?

Combine은 Apple SDK에 내장된 선언형 반응형 프로그래밍 프레임워크입니다. 반응형 스트림 패턴을 구현합니다: Publisher가 값을 생성하고, Subscriber가 소비하며, 연산자가 그 사이에서 스트림을 변환합니다. Combine은 콜백과 델리게이트의 문제를 해결하여 네트워크 응답에서 UI 변경까지 모든 비동기 이벤트에 대한 통합된 구성 모델을 제공합니다.

Combine 이전에 iOS 개발자들은 주로 RxSwift와 같은 타사 라이브러리를 사용했습니다. Apple은 생태계와의 깊은 통합을 갖춘 네이티브 대안으로 Combine을 만들었습니다: 이 프레임워크는 @objc 브릿지를 통해 Objective-C를 지원하고, KVO(NSObject.keyValuePublisher를 통한 Key-Value Observing) 및 NotificationCenter와 작동하며, SwiftUI의 기초 역할도 합니다. SwiftUI에 게시된 모든 UIKit 컴포넌트는 뷰 업데이트를 위해 내부적으로 Combine을 사용합니다.

Combine은 Swift Concurrency를 염두에 두고 설계되었습니다: iOS 15부터 Publisher는 .values를 통해 AsyncSequence로 변환되어 for-await-in 루프에서 사용할 수 있습니다. async 함수에서 Publisher로의 역변환은 Future를 통해 수행됩니다. Apple WWDC 2024에 따르면, 단발성 비동기 호출을 위한 async/await 도입에도 불구하고 Combine은 UIKit 애플리케이션에서 스트리밍 데이터 처리를 위한 권장 프레임워크로 남아 있습니다.

Combine의 기본 개념

Combine은 세 가지 프로토콜을 기반으로 합니다: Publisher(Output 타입의 값을 방출, Failure 타입의 오류로 실패 가능), Subscriber(값을 수신, Demand 관리 — 요청된 요소 수), Subscription(취소 기능이 있는 Publisher-Subscriber 연결 표현). 데이터 채널은 subscribe 시 초기화되고 취소, 완료 또는 오류 시 종료됩니다. Demand는 Combine의 고유한 개념입니다: Subscriber가 Publisher에게 처리할 준비가 된 요소 수를 알려 프로토콜 수준에서 역압(backpressure)을 구현합니다.

Publisher와 Subscriber: 반응형 스트림 아키텍처

Publisher는 두 개의 연관 타입을 가진 프로토콜입니다: Output(방출되는 값의 타입)과 Failure(Error를 준수하는 오류 타입). 스트림이 실패할 수 없는 경우 Failure는 Never로 지정됩니다 — 이는 Subscriber에게 onReceive가 Output으로만 호출됨을 보장합니다. 내장 Publisher에는 Just(단일 값), Sequence(배열), URLSession.DataTaskPublisher(네트워크 요청), NotificationCenter.Publisher 및 @Published 프로퍼티 래퍼가 포함됩니다.

swift
import Combine

// 시퀀스에서 Publisher 생성
let publisher = [1, 2, 3, 4, 5].publisher

// 값 처리를 포함한 Subscriber 생성
class PrintSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never

    func receive(subscription: Subscription) {
        subscription.request(.unlimited)
    }

    func receive(_ input: Int) -> Subscribers.Demand {
        print("Received: \(input)")
        return .unlimited
    }

    func receive(completion: Subscribers.Completion<Never>) {
        print("완료")
    }
}

publisher.subscribe(PrintSubscriber())

Subscription과 Demand

Subscription은 Publisher와 Subscriber 간의 활성 연결을 나타내는 프로토콜입니다. Subscriber는 receive(subscription:) 메서드에서 Subscription을 수신하고 request(_:)를 호출하여 Demand를 지정합니다: .unlimited(모든 값), .max(N)(제한된 수) 또는 .none(일시 중지). Demand는 동적으로 변경될 수 있으며, Subscriber는 데이터 수신 중에 요청된 요소 수를 늘리거나 줄일 수 있습니다. 이는 Publisher 측의 버퍼링 없이 역압을 제공합니다.

Subject와 CurrentValueSubject

Subject는 Publisher와 Subscriber를 결합한 타입입니다. Subject는 Publisher(구독자가 구독함)로 동시에 Subscriber(값이 전송됨)로 사용될 수 있습니다. Combine은 두 가지 유형의 Subject를 제공합니다: PassthroughSubject(상태를 저장하지 않음, 새 값만 전달)와 CurrentValueSubject(현재 값을 저장하고 새 구독자에게 전달). Subject는 명령형 코드를 Combine 반응형 체인에 통합하는 데 필요합니다.

swift
let subject = PassthroughSubject<String, Never>()

// Publisher로 구독
let cancellable = subject
    .map { $0.uppercased() }
    .sink { print($0) }

// Subscriber로 값 전송
subject.send("hello")  // "HELLO" 출력
subject.send("world")  // "WORLD" 출력

CurrentValueSubject는 초기 값과 value 속성을 가진다는 점에서 PassthroughSubject와 다릅니다: 구독자는 구독 시 즉시 현재 값을 받은 다음 모든 후속 업데이트를 받습니다. CurrentValueSubject.value는 읽고 쓸 수 있으며, value를 변경하면 자동으로 모든 구독자에게 새 값이 전송됩니다. 이로 인해 CurrentValueSubject는 MVVM 아키텍처에서 상태를 표현하는 이상적인 선택이 됩니다: ViewModel이 CurrentValueSubject를 게시하고 View가 sink를 통해 변경 사항을 구독합니다.

두 Subject 모두 send(completion: .finished) 또는 send(completion: .failure(error))를 호출하여 스트림을 종료할 수 있습니다. 종료 후 Subject는 이벤트 수락 및 방출을 중단합니다. 종료되지 않아야 하는 장기 스트림(예: UI 이벤트)의 경우 실수로 send(completion:)을 호출하는 것을 방지하기 위해 Never Failure와 함께 PassthroughSubject를 사용하는 것이 좋습니다.

변환 및 결합 연산자

Combine 연산자는 새 Publisher를 반환하는 Publisher 메서드입니다. 각 연산자는 업스트림 Publisher를 구독하고 변환된 값을 다운스트림으로 방출하는 새 객체를 생성합니다. Publisher는 제네릭 타입이므로 연산자는 엄격한 타입 지정을 유지합니다: map은 Output<A>를 Output<B>로 변환하고, tryMap은 오류 가능성을 추가합니다. Combine에는 약 100개의 내장 연산자가 있습니다.

카테고리연산자목적
변환map / tryMap / flatMap값 또는 스트림 변환
필터링filter / compactMap / removeDuplicates값 선택 또는 정리
결합combineLatest / zip / merge여러 Publisher 결합
시간 제어debounce / throttle / delay이벤트 지연 및 간격 조정
오류 처리catch / retry / replaceErrorFailure 후 복구
Demand 관리buffer / collect그룹화 또는 버퍼링

flatMap은 Combine에서 RxSwift 버전과 중요한 차이점이 있습니다: 동일한 Failure 타입을 가진 Publisher를 반환하는 클로저를 받아 중첩된 Publisher를 기본 스트림으로 평탄화합니다. maxPublishers: .max(1)를 사용한 flatMap은 switchMap처럼 작동하여 새 값이 도착하면 이전 중첩 Publisher를 취소합니다. 이는 검색 시나리오에서 중요합니다: 새 문자가 입력되면 이전 HTTP 요청이 자동으로 취소됩니다.

swift
// 이전 요청을 취소하는 디바운스 검색
searchTextField.textPublisher
    .debounce(for: .seconds(0.3), scheduler: RunLoop.main)
    .removeDuplicates()
    .flatMap(maxPublishers: .max(1)) { query in
        apiService.searchPublisher(query)
            .catch { _ in Just([]) }
    }
    .receive(on: DispatchQueue.main)
    .sink { results in
        self.tableView.reloadData()
    }
    .store(in: &cancellables)

결합 연산자 — combineLatest와 zip — 은 RxSwift와 유사하게 작동합니다: combineLatest는 Publisher 중 하나가 변경될 때 모든 Publisher의 최신 값 튜플을 방출하고, zip은 인덱스별로 값을 쌍으로 만듭니다. merge는 동일한 타입의 Publisher를 하나의 스트림으로 결합하며 순서 보장은 없습니다. Combine에는 select(여러 중 처음 완료되는 Publisher를 선택하는 드문 연산자)와 share(재실행 없이 여러 구독자에게 스트림 멀티캐스트)도 있습니다.

Scheduler와 스레드 관리

Scheduler는 Combine에서 연산자의 실행 컨텍스트를 정의하는 프로토콜입니다. 5개 이상의 내장 Scheduler를 가진 RxSwift와 달리 Combine은 기존 Apple 메커니즘을 사용합니다: DispatchQueue, RunLoop 및 OperationQueue. 이러한 각 타입은 Scheduler 프로토콜을 준수하므로 추가 어댑터 없이 receive(on:) 및 subscribe(on:)에 직접 전달할 수 있습니다.

receive(on:)은 다운스트림을 지정된 Scheduler로 전환합니다 — RxSwift의 observeOn과 동일합니다. receive(on:) 이후의 모든 연산자는 지정된 Scheduler에서 실행됩니다. subscribe(on:)은 업스트림을 전환하여 Publisher 실행에 영향을 미칩니다. 일반적인 패턴: 백그라운드 작업에는 subscribe(on: DispatchQueue.global()), UI 업데이트에는 receive(on: DispatchQueue.main). SwiftUI에서 .onReceive를 사용할 때 메인 스레드에 대한 내장 바인딩은 필요하지 않지만, sink의 경우 명시적인 receive(on:).main이 권장됩니다.

swift
// 백그라운드 로딩 + 메인 스레드 UI
URLSession.shared.dataTaskPublisher(for: url)
    .subscribe(on: DispatchQueue.global(qos: .background))
    .tryMap { data, response -> Data in
        guard let http = response as? HTTPURLResponse,
              http.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        return data
    }
    .receive(on: DispatchQueue.main)
    .decode(type: User.self, decoder: JSONDecoder())
    .sink(receiveCompletion: { print($0) },
         receiveValue: { self.nameLabel.text = $0.name })
    .store(in: &cancellables)

RunLoop.main은 UI 작업을 위한 DispatchQueue.main의 대안입니다. 차이점은 RunLoop.main은 현재 애플리케이션 이벤트 루프에 바인딩되는 반면, DispatchQueue.main은 전역 메인 스레드 큐에 바인딩된다는 것입니다. UIKit에는 DispatchQueue.main이, SwiftUI에는 RunLoop.main이 권장됩니다. ImmediateWhenScheduler는 현재 스레드에서 동기적으로 작업을 실행하며, 테스트 및 단순 Publisher에 기본적으로 사용됩니다.

Combine과 SwiftUI: ObservableObject를 통한 통합

ObservableObject는 변경 사항을 게시하는 객체를 위한 SwiftUI 프로토콜입니다. ObservableObject를 구현하는 클래스는 @Published 프로퍼티 래퍼를 사용하여 속성의 변경이 SwiftUI에 다시 그리기 필요성을 자동으로 알리도록 할 수 있습니다. 내부적으로 @Published는 Publisher를 생성하여 wrappedValue가 변경될 때 objectWillChange Publisher에 알립니다. SwiftUI는 @StateObject, @ObservedObject 또는 @EnvironmentObject를 통해 objectWillChange를 구독합니다.

@Published와 @StateObject

@Published는 Combine을 SwiftUI에 통합하는 가장 일반적인 방법입니다. @Published 속성의 값이 변경되면 SwiftUI는 해당 객체를 사용하는 모든 View를 업데이트합니다. @StateObject는 ObservableObject 인스턴스를 생성하고 그 변경 사항을 구독합니다. @StateObject로 생성된 View는 @Published 속성이 변경될 때 자동으로 다시 그려집니다. 여러 View 간에 객체를 공유해야 하는 경우 @ObservedObject 또는 @EnvironmentObject가 사용됩니다.

swift
class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    private var cancellables = Set<AnyCancellable>()

    init() {
        $name
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .sink { [weak self] newName in
                AnalyticsService.logNameChange(newName)
            }
            .store(in: &cancellables)
    }
}

struct UserView: View {
    @StateObject var viewModel = UserViewModel()

    var body: some View {
        TextField("Name", text: $viewModel.name)
    }
}

AnyCancellable은 구독 취소 토큰을 저장하는 Cancellable의 타입 소거 래퍼입니다. Set<AnyCancellable>은 구독의 수명 주기를 관리합니다: 소유자가 초기화 해제되면 모든 Cancellable이 자동으로 취소됩니다. SwiftUI 프로젝트에서 Set<AnyCancellable>은 ObservableObject 클래스에 선언되고 구독은 .store(in: &cancellables)를 통해 추가됩니다. UIKit의 경우 UIViewController에서 &cancellables를 통한 저장이나 수동 cancel() 호출과 동일한 메커니즘이 사용됩니다.

Combine vs RxSwift

CombineRxSwift는 동일한 반응형 프로그래밍 작업을 해결하지만 근본적인 아키텍처 차이가 있습니다. Combine은 Apple SDK의 일부로 iOS 13까지 하위 호환성을 가지며, RxSwift는 iOS 8+를 지원하는 타사 라이브러리입니다. Combine은 Failure 제네릭을 통해 엄격한 오류 타입 지정을 사용하고, RxSwift는 단일 Error 타입을 사용합니다. Combine은 플랫폼 수준에서 SwiftUI와 통합되어 있으며, RxSwift는 UI 확장을 위해 RxCocoa가 필요합니다.

Combine과 RxSwift 사이의 선택은 프로젝트 요구 사항에 따라 다릅니다. 최소 iOS 버전이 13 이상이고 프로젝트가 SwiftUI를 사용하는 경우, Combine은 내장 통합과 추가 종속성 없음 덕분에 자연스러운 선택입니다. 프로젝트가 iOS 11-12를 지원하거나, 기존 RxSwift 코드베이스를 포함하거나, RxSwift에서만 사용 가능한 특정 연산자(예: Observable.from(path:))가 필요한 경우 RxSwift가 유효한 솔루션으로 남아 있습니다.

특성CombineRxSwift
개발자Apple (SDK에 내장)ReactiveX (커뮤니티)
iOS 버전iOS 13+iOS 8+
오류 타입제네릭 Failure (UI용 Never)Error (모든)
UI 통합@Published + SwiftUIRxCocoa + UIKit
연산자~100 내장400+ 연산자
Swift Concurrency.values를 통해 (비동기 시퀀스)브릿지 라이브러리를 통해

자주 묻는 질문

PassthroughSubject와 CurrentValueSubject의 차이점은?

PassthroughSubject는 상태를 저장하지 않습니다 — 구독자는 구독 후에 전송된 이벤트만 받습니다. CurrentValueSubject는 현재 값을 저장하고 구독 시 각 새 구독자에게 즉시 전달합니다. CurrentValueSubject는 상태(예: isLoggedIn) 표현에 적합합니다.

Combine에서 구독을 취소하는 방법은?

구독은 AnyCancellable을 반환하며, cancel() 호출 또는 초기화 해제 시 취소됩니다. 그룹 관리를 위해 Set<AnyCancellable>을 사용하세요 — 세트가 지워지면 모든 구독이 취소됩니다. 이는 RxSwift의 DisposeBag과 유사합니다.

async/await 이후에도 Combine을 배워야 하나요?

네, Combine은 스트리밍 데이터(UI 이벤트, debounce, combineLatest, WebSocket)에 여전히 중요합니다. async/await은 단발성 요청에 편리하고, Combine은 연속적 또는 다중 스트림에 적합합니다. 두 프레임워크는 상호 보완적이며 — Publisher는 AsyncSequence로 변환될 수 있습니다.

UIKit과 함께 Combine을 사용하는 방법은?

UIKit에는 내장 Publisher가 없지만 Apple은 확장 기능을 제공합니다: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. 사용자 정의 UI 이벤트에는 Future 또는 Subject를 통해 Publisher로 래핑된 PassthroughSubject 또는 @IBAction이 사용됩니다.

Combine에서 역압(backpressure)이란?

역압(backpressure)은 흐름 속도를 제어하는 메커니즘입니다: Subscriber가 Demand를 통해 Publisher에게 처리할 준비가 된 요소 수를 알립니다. Demand = .max(1)인 경우 Publisher는 다음 값을 보내기 전에 요청을 기다립니다. 이는 생산자와 소비자 속도가 일치하지 않을 때 버퍼 오버플로를 방지합니다.

요약

  • Combine은 iOS 13+용 Apple의 네이티브 반응형 프레임워크로 Publisher-Subscriber 패턴과 엄격한 오류 타입 지정 제공
  • PublisherSubscriber는 역압을 위한 Demand 관리와 함께 통신 채널 형성
  • Subject(Passthrough 및 CurrentValue)는 명령형 코드를 반응형 체인에 통합
  • 연산자 map, flatMap, combineLatest, debounce, catch가 선언적 스트림 처리 제공
  • Scheduler는 DispatchQueue와 RunLoop를 통해 UI를 차단하지 않고 실행 스레드 관리
  • SwiftUI는 @Published, ObservableObject 및 @StateObject를 통해 Combine을 사용하여 반응형 View 구현
  • RxSwift는 iOS 8+ 프로젝트의 대안이며, Combine은 iOS 13+ 신규 프로젝트의 선택

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기