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 | 레거시 코드베이스 |
추가 리소스
내부 동작과 핵심 메커니즘
이 글의 주제는 「Swift Combine | 반응형 프로그래밍 완벽 가이드」입니다. 여기서는 앞선 설명을 구현·런타임 관점에서 한 번 더 압축합니다. 구성 요소 간 책임 분리와 관측 가능한 지점을 기준으로 생각하면, “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)이 어디서 터지는가”가 한눈에 드러납니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
알고리즘·프로토콜 관점에서의 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(예: 버퍼 경계, 프로토콜 상태, 트랜잭션 격리)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수한 층과, 시간·네트워크에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합처럼 “한 번의 호출이 아니라 누적되는 비용”을 의심 목록에 넣습니다.
프로덕션 운영 패턴
실서비스에서는 기능 구현과 함께 관측·배포·보안·비용이 동시에 요구됩니다. 아래는 팀에서 자주 쓰는 최소 체크리스트입니다.
| 영역 | 운영 관점에서의 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수, 주요 의존성 타임아웃이 보이는가 |
| 안전성 | 입력 검증·권한·비밀 관리가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등한 연산에만 적용되는가, 서킷 브레이커·백오프가 있는가 |
| 성능 | 캐시 계층·배치 크기·풀링·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리, 마이그레이션 호환성이 문서화되어 있는가 |
운영 환경에서는 “개발자 PC에서는 재현되지 않던 문제”가 시간·부하·데이터 크기 때문에 드러납니다. 따라서 스테이징의 데이터 양·네트워크 지연을 가능한 한 현실에 가깝게 맞추는 것이 중요합니다.
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스 컨디션, 타임아웃, 외부 의존성 불안정 | 최소 재현 스크립트 작성, 분산 트레이스·로그 상관관계 확인 |
| 성능 저하 | N+1 쿼리, 동기 I/O, 잠금 경합, 과도한 직렬화 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 클로저/이벤트 구독 누수, 대용량 객체의 불필요한 복사 | 상한·TTL·스냅샷 비교(힙 덤프/트레이스) |
| 빌드·배포만 실패 | 환경 변수·권한·플랫폼 차이 | CI 로그와 로컬 diff, 컨테이너/런타임 버전 핀(pin) |
권장 디버깅 순서: (1) 최소 재현 만들기 (2) 최근 변경 범위 좁히기 (3) 의존성·환경 변수 차이 확인 (4) 관측 데이터로 가설 검증 (5) 수정 후 회귀·부하 테스트.
정리
핵심 요약
- 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, 반응형프로그래밍 등으로 검색하시면 이 글이 도움이 됩니다.