UIKit 환경에서 Combine 프레임워크를 활용해 상태를 자동으로 바인딩하는 방법을 배워보려 합니다.. 예제로는 내배캠 iOS 앱 개발 입문 1주차 강의의 Counter 앱을 활용했습니다. 기존에는 Delegate 패턴으로 사용자 이벤트를 처리했다면, 이번엔 Combine을 통해 모델의 상태가 바뀔 때 UI가 자동으로 업데이트되는 구조를 구현해보려 합니다.
Combine이란?
Combine은 Apple이 WWDC 2019에서 발표한 반응형(Reactive) 프로그래밍 프레임워크입니다. Swift 언어로 작성된 코드에서 다양한 이벤트 스트림(데이터, 네트워크 응답, 사용자 인터랙션 등)을 하나의 방식으로 처리할 수 있도록 만들어졌습니다.
Combine은 Swift 개발자가 다음과 같은 상황에서 강력하게 활용할 수 있습니다
- 네트워크 응답 처리 (URLSession과 함께 사용)
- 텍스트 필드 입력값 감지 및 처리
- 모델 값 변경에 따른 UI 반응 처리
- NotificationCenter, KVO, Timer 등을 대체
공식 문서 링크
Combine의 핵심 개념 정리
Publisher | 데이터를 발행하는 객체입니다. 새로운 값이 생성되거나 상태가 바뀌면 이를 구독자에게 전달합니다. 대표적으로 @Published 속성이 자동으로 Publisher를 생성합니다. |
Subscriber | Publisher로부터 데이터를 받아 처리하는 역할입니다. UIKit에서는 보통 .sink, .assign 같은 메서드로 처리합니다. |
@Published | Combine에서 가장 많이 사용되는 속성 래퍼입니다. 속성 값이 변경되면 자동으로 Publisher 이벤트를 발생시켜 UI 또는 다른 로직과 연결됩니다. |
ObservableObject | 모델 클래스가 이 프로토콜을 채택하면 SwiftUI나 Combine 구독자들이 해당 객체를 관찰할 수 있게 됩니다. 주로 @Published와 함께 사용됩니다. |
AnyCancellable | Combine의 구독자(subscriber)는 계속 살아있기 때문에, 구독을 명시적으로 저장하고 해제해야 합니다. 이를 위한 객체가 AnyCancellable입니다. 보통 Set<AnyCancellable>에 담아 생명 주기를 관리합니다. |
Subscriber | 데이터를 구독하고 반응하는 객체 (sink, assign) |
@Published | 값이 바뀔 때 Combine이 자동으로 알림 |
ObservableObject | Combine이 관찰할 수 있는 클래스 |
AnyCancellable | 메모리에서 구독을 해제할 수 있도록 저장하는 객체 |
왜 Combine을 사용하는가?
- 매번 수동으로 UI를 업데이트하지 않아도 됨
- 상태 관리가 명확해짐 (Model → View 단방향 흐름)
- MVVM 구조로 쉽게 확장 가능
CounterModel 정의하기
import Combine
class CounterModel: ObservableObject {
@Published var counter: Int = 0
func increment() {
counter += 1
}
func decrement() {
counter -= 1
}
func reset() {
counter = 0
}
}
- ObservableObject를 채택해 Combine이 이 객체를 관찰할 수 있도록 함
- @Published로 선언된 프로퍼티는 값이 바뀔 때마다 자동으로 이벤트 발생
ViewController에서 상태 바인딩
import UIKit
import Combine
class CounterViewController: UIViewController, CounterViewDelegate {
private let counterView = CounterView()
private let model = CounterModel()
private var cancellables = Set<AnyCancellable>()
override func loadView() {
self.view = counterView
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
counterView.delegate = self
model.$counter
.receive(on: RunLoop.main)
.sink { [weak self] newValue in
self?.counterView.updateCountLabel(with: newValue)
}
.store(in: &cancellables)
}
func counterViewDidTapIncrementButton(_ counterView: CounterView) {
model.increment()
}
func counterViewDidTapDecrementButton(_ counterView: CounterView) {
model.decrement()
}
func counterViewDidTapReSetButton(_ counterView: CounterView) {
model.reset()
}
}
코드 해설
- model.$counter는 @Published된 값을 관찰하는 Combine Publisher
- .sink는 값이 바뀔 때마다 실행될 클로저 정의
- .receive(on: RunLoop.main)은 UI 업데이트를 메인 스레드에서 안전하게 처리
- .store(in:)은 구독을 메모리에 유지
CounterView는 어떻게 바뀌나요?
학습 목표가 Delegate 패턴과 Combine을 사용해 보는 것이 목표인 만큼 뷰는 여전히 Delegate를 통해 이벤트를 전달합니다. Combine은 모델과 뷰컨트롤러 간의 상태 흐름 처리에만 사용되며, 사용자 입력은 그대로 delegate 패턴을 유지할 수 있습니다.
참고 문서
'iOS > UIKit' 카테고리의 다른 글
[UIKit] ViewController의 개념과 생명주기 정리 (0) | 2025.04.14 |
---|---|
[ UIKit ] SwiftUI만 공부하던 내가 UIKit MVVM에서 클로저를 처음 마주쳤을 때 (0) | 2025.04.09 |
[ UIKit ] Delegate 패턴 알아보기 (0) | 2025.04.06 |
[ UIKit ] UISlider (20) | 2024.02.03 |
[ UIKit ] UIButton (19) | 2024.01.28 |
UIKit 환경에서 Combine 프레임워크를 활용해 상태를 자동으로 바인딩하는 방법을 배워보려 합니다.. 예제로는 내배캠 iOS 앱 개발 입문 1주차 강의의 Counter 앱을 활용했습니다. 기존에는 Delegate 패턴으로 사용자 이벤트를 처리했다면, 이번엔 Combine을 통해 모델의 상태가 바뀔 때 UI가 자동으로 업데이트되는 구조를 구현해보려 합니다.
Combine이란?
Combine은 Apple이 WWDC 2019에서 발표한 반응형(Reactive) 프로그래밍 프레임워크입니다. Swift 언어로 작성된 코드에서 다양한 이벤트 스트림(데이터, 네트워크 응답, 사용자 인터랙션 등)을 하나의 방식으로 처리할 수 있도록 만들어졌습니다.
Combine은 Swift 개발자가 다음과 같은 상황에서 강력하게 활용할 수 있습니다
- 네트워크 응답 처리 (URLSession과 함께 사용)
- 텍스트 필드 입력값 감지 및 처리
- 모델 값 변경에 따른 UI 반응 처리
- NotificationCenter, KVO, Timer 등을 대체
공식 문서 링크
Combine의 핵심 개념 정리
Publisher | 데이터를 발행하는 객체입니다. 새로운 값이 생성되거나 상태가 바뀌면 이를 구독자에게 전달합니다. 대표적으로 @Published 속성이 자동으로 Publisher를 생성합니다. |
Subscriber | Publisher로부터 데이터를 받아 처리하는 역할입니다. UIKit에서는 보통 .sink, .assign 같은 메서드로 처리합니다. |
@Published | Combine에서 가장 많이 사용되는 속성 래퍼입니다. 속성 값이 변경되면 자동으로 Publisher 이벤트를 발생시켜 UI 또는 다른 로직과 연결됩니다. |
ObservableObject | 모델 클래스가 이 프로토콜을 채택하면 SwiftUI나 Combine 구독자들이 해당 객체를 관찰할 수 있게 됩니다. 주로 @Published와 함께 사용됩니다. |
AnyCancellable | Combine의 구독자(subscriber)는 계속 살아있기 때문에, 구독을 명시적으로 저장하고 해제해야 합니다. 이를 위한 객체가 AnyCancellable입니다. 보통 Set<AnyCancellable>에 담아 생명 주기를 관리합니다. |
Subscriber | 데이터를 구독하고 반응하는 객체 (sink, assign) |
@Published | 값이 바뀔 때 Combine이 자동으로 알림 |
ObservableObject | Combine이 관찰할 수 있는 클래스 |
AnyCancellable | 메모리에서 구독을 해제할 수 있도록 저장하는 객체 |
왜 Combine을 사용하는가?
- 매번 수동으로 UI를 업데이트하지 않아도 됨
- 상태 관리가 명확해짐 (Model → View 단방향 흐름)
- MVVM 구조로 쉽게 확장 가능
CounterModel 정의하기
import Combine
class CounterModel: ObservableObject {
@Published var counter: Int = 0
func increment() {
counter += 1
}
func decrement() {
counter -= 1
}
func reset() {
counter = 0
}
}
- ObservableObject를 채택해 Combine이 이 객체를 관찰할 수 있도록 함
- @Published로 선언된 프로퍼티는 값이 바뀔 때마다 자동으로 이벤트 발생
ViewController에서 상태 바인딩
import UIKit
import Combine
class CounterViewController: UIViewController, CounterViewDelegate {
private let counterView = CounterView()
private let model = CounterModel()
private var cancellables = Set<AnyCancellable>()
override func loadView() {
self.view = counterView
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
counterView.delegate = self
model.$counter
.receive(on: RunLoop.main)
.sink { [weak self] newValue in
self?.counterView.updateCountLabel(with: newValue)
}
.store(in: &cancellables)
}
func counterViewDidTapIncrementButton(_ counterView: CounterView) {
model.increment()
}
func counterViewDidTapDecrementButton(_ counterView: CounterView) {
model.decrement()
}
func counterViewDidTapReSetButton(_ counterView: CounterView) {
model.reset()
}
}
코드 해설
- model.$counter는 @Published된 값을 관찰하는 Combine Publisher
- .sink는 값이 바뀔 때마다 실행될 클로저 정의
- .receive(on: RunLoop.main)은 UI 업데이트를 메인 스레드에서 안전하게 처리
- .store(in:)은 구독을 메모리에 유지
CounterView는 어떻게 바뀌나요?
학습 목표가 Delegate 패턴과 Combine을 사용해 보는 것이 목표인 만큼 뷰는 여전히 Delegate를 통해 이벤트를 전달합니다. Combine은 모델과 뷰컨트롤러 간의 상태 흐름 처리에만 사용되며, 사용자 입력은 그대로 delegate 패턴을 유지할 수 있습니다.
참고 문서
'iOS > UIKit' 카테고리의 다른 글
[UIKit] ViewController의 개념과 생명주기 정리 (0) | 2025.04.14 |
---|---|
[ UIKit ] SwiftUI만 공부하던 내가 UIKit MVVM에서 클로저를 처음 마주쳤을 때 (0) | 2025.04.09 |
[ UIKit ] Delegate 패턴 알아보기 (0) | 2025.04.06 |
[ UIKit ] UISlider (20) | 2024.02.03 |
[ UIKit ] UIButton (19) | 2024.01.28 |