Swift Combine | 반응형 프로그래밍 완벽 가이드
이 글의 핵심
Swift Combine에 대한 실전 가이드입니다. 반응형 프로그래밍 완벽 가이드 등을 예제와 함께 설명합니다.
들어가며
Combine은 시간에 따라 들어오는 값을 Publisher–Subscriber로 연결합니다. UI 이벤트·네트워크 응답을 스트림처럼 합성할 때 씁니다.
1. Publisher와 Subscriber
기본 사용
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
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
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
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와 통합
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. 실전 예제
예제: 검색 기능
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를 시뮬레이션합니다.
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 시리즈를 완료했습니다! 다른 언어도 배워보세요:
- Kotlin 시작하기
- Rust 시작하기
- Java 시작하기
관련 글
- Swift 시작하기 | iOS 개발 공식 언어 완벽 입문
- Swift 변수와 타입 | var, let, 옵셔널
- Swift 함수와 클로저 | 함수 정의, 클로저, 고차 함수
- Swift 클래스와 구조체 | Class, Struct, Enum
- Swift 프로토콜과 확장 | Protocol, Extension