Swift / Combine

iOS 앱개발 공부

목록 보기
20/30

🧠 핵심 요약

Combine이란 무엇인가?

Combine은 Apple이 WWDC 2019에서 발표한 프레임워크로, 시간이 지남에 따라 값을 처리하는 반응형 프로그래밍(Reactive Programming)을 구현하기 위해 탄생했다.

쉽게 말해, 데이터의 흐름(스트림)을 선언적으로(Declaratively) 관리하고, 이 데이터가 도착하거나 변경될 때마다 정해진 로직을 실행하도록 하는 방식이다.

Combine의 핵심은 비동기 코드를 미래에 발생할 이벤트로 간주하고, 이 이벤트를 처리하는 파이프라인(Pipeline)을 구축하는 것이다.
이 파이프라인은 아래 세 가지 핵심 요소로 구성되어 있다.

요소역할비유
Publisher데이터를 발행(Emit)하는 주체. 데이터가 언제, 어떻게 바뀔지 모르는 비동기 작업을 캡슐화.신문사
Subscriber데이터를 구독(Subscribe)하고 최종적으로 소비(Receive)하는 주체. 데이터가 도착하면 특정 액션을 실행.구독자
OperatorPublisher와 Subscriber 사이에 존재하며, 데이터의 흐름을 가공, 변형, 필터링하는 역할.신문 편집/검열 과정

💡 Combine의 주요 기능

Publisher는 Subscriber에게 다음 세 가지 종류의 이벤트를 순서대로, 그리고 한 번만 전달한다.

1) 📬 Value (값 전달)

Publisher가 성공적으로 데이터를 생성했을 때 Subscriber에게 전달하는 데이터

.send(newValue) // 새로운 값을 전달

2) ⚡️ Failure (실패 전달)

Publisher가 데이터를 발행하는 과정에서 에러가 발생하여 더 이상 값을 발행할 수 없을 때 전달

.send(completion: .failure(error)) // 에러 발생

참고: Combine의 모든 Publisher는 반드시 Output 타입과 Failure 타입을 명시해야 한다.
만약 Failure가 없는 경우 Never 타입을 사용한다.

3) ✅ Finished (완료 전달)

Publisher가 모든 값을 성공적으로 발행했고, 더 이상 발행할 값이 없을 때 전달

.send(completion: .finished) // 성공적으로 완료

중요: Publisher는 Failure 또는 Finished 둘 중 하나를 보낸 후에는 더 이상 값을 보낼 수 없다. 이를 "스트림의 종료"라고 한다.


🌟 Combine Publisher의 종류 및 특징

Combine에는 다양한 상황에서 데이터를 발행할 수 있는 여러 종류의 Publisher가 있다.

Publisher 종류특징 및 용도예시
Just단 하나의 값만 발행하고 즉시 완료, 간단한 값을 스트림으로 바꾸어 파이프라인 시작 시 사용Just(5).sink { ... }
Future비동기 작업의 결과를 단 한 번 발행, 클로저 기반의 비동기 코드를 Combine으로 변환할 때 유용Future<Data, Error> { promise in ... }
PassthroughSubject직접 send() 메서드를 호출하여 값을 발행하는 Subject, 외부에서 이벤트를 주입해야 할 때 사용subject.send("새 데이터")
CurrentValueSubjectPassthroughSubject와 유사하지만, 현재 값을 항상 저장하고 있다가 구독자가 생기면 즉시 마지막 값을 발행subject.value = 10
@PublishedSwiftUI와 통합되어 클래스의 프로퍼티에 사용, 프로퍼티 값이 변경될 때마다 자동으로 값을 발행@Published var data: String
Foundation Publishers기존 Apple 프레임워크의 기능을 Publisher로 제공URLSession.shared.dataTaskPublisher, NotificationCenter.default.publisher

💡 Subject의 동작 원리 (Hot vs Cold)

Combine의 Subject는 Publisher이면서 동시에 Subscriber의 역할을 할 수 있는 특별한 타입이다.
Subject는 외부에서 임의로 데이터를 주입(Inject)하여 파이프라인을 시작하게 만들 때 유용하다.

Subject 종류특징 (Hot/Cold)초기 값 유무
PassthroughSubjectHot Publisher없음
CurrentValueSubjectHot Publisher초기 값 필수

Hot Publisher의 특징

일반적인 Publisher(Cold)는 Subscriber가 구독을 시작할 때마다 처음부터 데이터를 다시 발행한다.
하지만 Subject는 Hot Publisher로 분류되기 때문에, 구독이 시작되기 전부터 데이터를 발행할 수 있다.

  • Hot Publisher: 구독자가 생기기 이전부터 데이터를 발행 가능, 구독자가 스트림에 합류하면, 그 시점 이후에 발행되는 값만 받는다.
    (단, CurrentValueSubject는 구독 시 현재 값을 즉시 발행함)

이러한 특성 덕분에 ViewModel에서 사용자 액션이나 이벤트 스트림을 처리하고 싶을 때 PassthroughSubject를 사용하여 외부 이벤트(버튼 클릭 등)를 내부 파이프라인으로 주입하는 용도로 주로 사용된다.


⚙️ Combine 연산자(Operator)의 종류 및 특징

Operator는 Publisher가 발행하는 데이터를 원하는 형태로 가공하는 핵심적인 도구이다. 데이터를 발행하는 시점과 구독하는 시점 사이에 수많은 가공 로직을 선언적으로 삽입할 수 있다.

연산자 그룹목적주요 Operator특징
변환 (Transform)발행된 값을 다른 타입이나 형태로 변경map, flatMap, tryMapmap은 값을 동기적으로 변환, flatMap은 값을 받아 새로운 Publisher를 리턴하여 비동기 체이닝에 사용됨.
필터링 (Filter)특정 조건에 맞지 않는 값을 걸러내거나 중복 제거filter, removeDuplicates, compactMapremoveDuplicates는 이전 값과 동일한 값을 걸러내 불필요한 작업 방지.
시간 조절 (Timing)이벤트 발행 시점 또는 빈도를 조절debounce, throttledebounce: 일정 시간 동안 새 값이 없어야 값을 통과. (검색창에 주로 사용)
결합 (Combine)여러 Publisher의 데이터를 하나로 합치거나 연결combineLatest, merge, zipcombineLatest: 모든 Publisher가 한 번씩 발행한 후, 어느 하나라도 값이 변하면 최신 값들을 튜플로 묶어 발행.
오류 처리 (Error)에러 발생 시 처리 로직을 정의하거나 에러 타입 변경catch, replaceError, assertNoFailurereplaceError는 에러 발생 시 지정된 기본 값으로 대체하여 스트림을 Finished 상태로 유지.

🔒 필수 관리 항목: AnyCancellable을 통한 구독 취소

Combine 파이프라인의 마지막 단계에서 sinkassign으로 구독을 시작하면, 이 메서드들은 AnyCancellable 인스턴스를 반환한다. 이 인스턴스는 Combine의 메모리 관리에 있어 매우 중요한 인스턴스이다.

AnyCancellable의 역할

  • 구독 유지: Subscriber가 Publisher를 구독하여 연결된 상태를 유지하도록 한다.
  • 구독 취소: AnyCancellable 인스턴스가 메모리에서 해제(deinit)되는 순간, 구독이 자동으로 취소되고 파이프라인이 종료된다. 이는 순환 참조를 방지하고 메모리 누수를 막는 핵심 메커니즘이다.

🛠️ Cancellable 관리 방법

이벤트 방출을 감지하고, 이를 통해 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:)을 호출하지 않으면, 해당 구독은 즉시 취소되어 값을 받지 못하는 경우가 발생할 수 있다.


📝 Combine 활용 예시: 실시간 검색 결과

사용자가 검색창에 텍스트를 입력할 때마다 실시간으로 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과 RxSwift 비교

Combine을 이해하는 가장 좋은 방법은 오랫동안 iOS 개발 생태계를 지배했던 RxSwift와 비교하는 것이다. 두 프레임워크 모두 반응형 프로그래밍을 위한 도구이지만, Apple의 공식 지원 여부와 설계 철학에서 차이를 보인다.

구분CombineRxSwift
개발 주체Apple 공식 프레임워크오픈소스 (ReactiveX 진영)
프레임워크 이름PublisherObservable
데이터 발행 주체SubscriberObserver
구독 취소 객체AnyCancellableDisposable
에러 처리OutputFailure 타입이 필수로 명시됨. Failure 타입이 없는 경우 Never 사용.ElementError 타입으로 구성되며, 에러 타입 명시가 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 개발의 핵심 역량이라고 생각된다.

profile
이유있는 코드를 쓰자!!

0개의 댓글