[2026] Swift Combine | 반응형 프로그래밍 완벽 가이드
이 글의 핵심
Swift Combine: 반응형 프로그래밍 Publisher와 Subscriber·Operator.
들어가며
Combine은 시간에 따라 들어오는 값을 Publisher–Subscriber로 연결합니다. UI 이벤트·네트워크 응답을 스트림처럼 합성할 때 씁니다.
1. Publisher와 Subscriber
기본 사용
다음은 swift를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import Combine
// Just: 단일 값 발행
let publisher = Just("Hello")
let cancellable = publisher.sink { value in
print(value) // Hello
}
// PassthroughSubject: 수동 발행
let subject = PassthroughSubject<String, Never>()
let subscription = subject.sink { value in
print("받음: \(value)")
}
subject.send("첫 번째")
subject.send("두 번째")
subject.send(completion: .finished)
CurrentValueSubject
아래 코드는 swift를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
let subject = CurrentValueSubject<Int, Never>(0)
subject.sink { value in
print("값: \(value)")
}
subject.send(1)
subject.send(2)
print("현재 값: \(subject.value)") // 2
2. Operator
변환 Operator
다음은 swift를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
let numbers = [1, 2, 3, 4, 5].publisher
// map
numbers
.map { $0 * 2 }
.sink { print($0) }
// 2, 4, 6, 8, 10
// filter
numbers
.filter { $0 % 2 == 0 }
.sink { print($0) }
// 2, 4
// reduce
numbers
.reduce(0, +)
.sink { print("합계: \($0)") }
// 합계: 15
결합 Operator
다음은 swift를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
let pub1 = Just(1)
let pub2 = Just(2)
// zip: 쌍으로 결합
Publishers.Zip(pub1, pub2)
.sink { print("\($0), \($1)") }
// 1, 2
// combineLatest: 최신 값 결합
let subject1 = PassthroughSubject<Int, Never>()
let subject2 = PassthroughSubject<String, Never>()
subject1.combineLatest(subject2)
.sink { print("\($0), \($1)") }
subject1.send(1)
subject2.send("A") // 1, A
subject1.send(2) // 2, A
3. @Published
SwiftUI와 통합
다음은 swift를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import SwiftUI
import Combine
class ViewModel: ObservableObject {
@Published var count = 0
@Published var message = ""
func increment() {
count += 1
message = "Count: \(count)"
}
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
Text(viewModel.message)
Button("증가") {
viewModel.increment()
}
}
}
}
4. 실전 예제
예제: 검색 기능
다음은 swift를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import Combine
import SwiftUI
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] text in
self?.search(text)
}
.store(in: &cancellables)
}
func search(_ text: String) {
let allItems = ["사과", "바나나", "오렌지", "포도", "딸기"]
results = allItems.filter { $0.contains(text) }
}
}
struct SearchView: View {
@StateObject private var viewModel = SearchViewModel()
var body: some View {
VStack {
TextField("검색", text: $viewModel.searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
List(viewModel.results, id: \.self) { item in
Text(item)
}
}
}
}
실전 심화 보강
실전 예제: 페이지네이션 API를 flatMap으로 이어 붙이기
아래는 첫 응답의 nextPageURL이 있으면 다음 요청을 이어서 배출하는 패턴입니다. 실제 네트워크 대신 Future를 시뮬레이션합니다.
다음은 swift를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 필요한 모듈 import
import Combine
import Foundation
struct Page: Decodable {
let items: [String]
let next: URL?
}
func fetchPage(url: URL) -> AnyPublisher<Page, Error> {
// 실제로는 URLSession.shared.dataTaskPublisher
Future { promise in
promise(.success(Page(items: ["a", "b"], next: nil)))
}
.eraseToAnyPublisher()
}
enum PageLoader {
static func allItems(start: URL) -> AnyPublisher<[String], Error> {
func loop(_ url: URL) -> AnyPublisher<[String], Error> {
fetchPage(url: url)
.flatMap { page -> AnyPublisher<[String], Error> in
if let next = page.next {
return loop(next)
.map { page.items + $0 }
.eraseToAnyPublisher()
} else {
return Just(page.items)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
return loop(start)
}
}
실무에서는 flatMap(maxPublishers: .max(1))로 동시 요청 수를 제한하고, retry/retry(on:)에 백오프를 걸어 네트워크 안정성을 높입니다.
자주 하는 실수
sink반환값을 저장하지 않아 구독이 즉시 취소되는 경우.@Published를private로 두지 않고 내부 상태가 외부에 노출되는 경우.combineLatest의 초기 방출 조건을 몰라 첫 값이 안 나오는 문제를 겪는 경우.
주의사항
Scheduler선택(메인 큐 vs 백그라운드)에 따라 UI 업데이트 레이스가 생깁니다.- 메모리 순환 참조는
[weak self]와store(in:)패턴으로 끊습니다.
실무에서는 이렇게
- 입력 → 디바운스 →
switchToLatest로 검색·자동완성을 구현합니다. - 에러는
catch/replaceError로 UI에 친화적인 메시지로 매핑합니다. - 단위 테스트는
TestScheduler(iOS 15+) 또는 커스텀 스케줄러로 시간을 진행합니다.
비교 및 대안
| 방식 | 메모 |
|---|---|
| Combine | Apple 네이티브, SwiftUI와 궁합 |
| AsyncSequence + AsyncStream | Swift 5.5+ 단순 파이프라인 |
| RxSwift | 레거시 코드베이스 |
추가 리소스
정리
핵심 요약
- Publisher: 값 발행, Just, PassthroughSubject
- Subscriber: 값 구독, sink
- Operator: map, filter, combineLatest
- @Published: 자동 발행, SwiftUI 통합
- AnyCancellable: 구독 취소
다음 단계
Swift 시리즈를 완료했습니다! 다른 언어도 배워보세요: