Swift Combine | 반응형 프로그래밍 완벽 가이드
이 글의 핵심
Swift Combine: 반응형 프로그래밍 Publisher와 Subscriber·Operator.
들어가며
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
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 시리즈를 완료했습니다! 다른 언어도 배워보세요:
관련 글
- Swift 시작하기 | iOS 개발 공식 언어 완벽 입문
- Swift 변수와 타입 | var, let, 옵셔널
- Swift 함수와 클로저 | 함수 정의, 클로저, 고차 함수
- Swift 클래스와 구조체 | Class, Struct, Enum
- Swift 프로토콜과 확장 | Protocol, Extension
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Swift Combine: 반응형 프로그래밍 완벽 가이드. Publisher와 Subscriber·Operator로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Swift·Combine·반응형프로그래밍 중… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Swift 에러 처리 | do-catch, throw, Result
- Swift 제네릭 | Generic 함수, 타입, 제약
- Matplotlib 기초 | Python 데이터 시각화 완벽 정리
이 글에서 다루는 키워드 (관련 검색어)
Swift, Combine, 반응형프로그래밍 등으로 검색하시면 이 글이 도움이 됩니다.