Combine 시작하기
WWDC
Introducing Combine Combine in Practice 해당 WWDC의 핵심 내용을 정리하면서, 내 프로젝트에 어떻게 적용할 수 있을지 고민해보았다.
Introduction
Reactice Programming?
Observable
- User Input, sensor data, API response
Observer
- An entity that listens to events emiited by observables.
- Application components, views
Operators
- Functions to transform and manupulate data
- map, filter, merge
Reactive Programming - Benefits
- Improved code readability
- Handling complex asynchronous scenarios
- Real-time and event-driven applications
비동기 프로그래밍의 필요성
회원가입 페이지를 만든다고 해보자. 이 때, 서버로부터 중복되지 않는 사용자 이름을 바로 확인하려고 하는 폼을 만들고자 한다.
이를 위해서는 여러 비동기 작업이 필요하다. 예를 들어 사용자가 타이핑을 멈추면 서버에 과도한 요청을 보내지 않도록 타이머를 사용하고, KVO(key value observing)을 사용해 비동기 작업의 진행 상황을 감시해야 한다.
이 때, combine을 사용할 수 있다.
Combine의 등장 배경
Cocoa SDK에는 다양한 비동기 인터페이스가 있다. Target/Action, NotificationCenter, ad-hoc callback 등 다양한 API가 존재한다. 그러나, 이들을 조합하여 사용하기는 어려울 수 있다. Combine은 이러한 비동기 인터페이스의 공통점을 찾아 통합 선언적 API를 제공함으로써 이를 해결할 수 있다.
Combine Key Concepts
- Publishers
- Subscribers
- Operators
Publishers: 값과 오류를 생성하는 방법을 선언적으로 표현한다. Swift 의 구조체로 구현되며, Subscriber를 등록하여 값을 전달할 수 있다.
Subscribers: Publisher로 부터 값을 받아 처리한다. 주로 상태를 변경하기 때문에 Swift class(reference type)로 구현된다.
Operators: Publishers를 변환하는 중간 단계 역할을 한다. 값 변환, 필터링, 오류 처리 등의 작업을 수행한다.
라고 하면 설명이 너무 추상적이고 와닿지 않을 수 있다.
풀어서 아래와 같이 이해할 수 있다.
Combine 쉽게 이해하기
퍼블리셔(Publisher)
퍼블리셔는 “나는 정보를 줄 거야”라고 말하는 객체예요. 시간이 지남에 따라 값을 여러 개 줄 수 있어요.
구독자(Subscriber)
구독자는 “나는 정보를 받을게”라고 말하는 객체예요. 퍼블리셔로부터 정보를 받습니다.
Cancellable (캔슬러블)
캔슬러블은 “구독을 취소할 수 있어”라는 의미예요. 구독을 취소하면 더 이상 정보를 받지 않게 됩니다.
이제 각각 좀 더 자세히 알아보자.
퍼블리셔와 구독자
import Combine
// 퍼블리셔 생성
let 퍼블리셔 = Just("안녕하세요, Combine!")
// 구독하고 값을 출력
let 구독 = 퍼블리셔.sink { 값 in
print(값) // "안녕하세요, Combine!" 출력
}
여기서 퍼블리셔는 “안녕하세요, Combine!”이라는 값을 줄 거예요. 구독자는 이 값을 받아서 출력합니다.
Cancellable 사용
// 구독 취소
구독.cancel()
이제 구독을 취소했기 때문에 퍼블리셔가 더 이상 값을 주더라도 구독자는 아무것도 받지 않아요.
ViewModel에서의 사용
이제 Combine을 사용하는 예제를 통해 어떻게 ViewModel에서 구독을 관리하는지 쉽게 설명해드릴게요.
예제 설명
searchText: 사용자가 입력하는 검색어 tips: 팁 리스트 filteredTips: 검색어에 따라 필터링된 팁 리스트 cancellables: 구독을 관리하는 용도
import Combine
class LibraryViewModel: ObservableObject {
@Published var searchText = ""
@Published var tips: [String] = ["Swift", "authentication", "authorization"]
@Published var filteredTips = [String]()
private var cancellables = Set<AnyCancellable>()
init() {
// searchText와 tips가 바뀔 때마다 최신 값을 가져옴
Publishers.CombineLatest($searchText, $tips)
.map { query, items in
// 검색어가 비어 있으면 모든 팁, 아니면 검색어가 포함된 팁만 필터링
items.filter { item in
query.isEmpty ? true : item.contains(query)
}
}
.sink { [weak self] filteredItems in
// 필터링된 결과를 filteredTips에 할당
self?.filteredTips = filteredItems
}
.store(in: &cancellables) // 구독을 cancellables에 저장
}
}
코드 해설
- 퍼블리셔 생성:
$searchText와 $tips가 퍼블리셔 역할을 합니다. 사용자가 searchText를 입력하거나 tips가 바뀔 때마다 값을 방출합니다.
- CombineLatest 사용:
CombineLatest는 두 퍼블리셔의 최신 값을 결합하여 새로운 값을 방출합니다.
- map 사용:
map 연산자를 통해 최신 값을 필터링합니다. 검색어(searchText)가 비어 있으면 모든 팁을 포함하고, 그렇지 않으면 검색어가 포함된 팁만 필터링합니다.
- sink 사용:
sink를 사용하여 필터링된 값을 filteredTips에 할당합니다. 클로저 안에서 filteredTips를 업데이트합니다.
- store(in:) 사용:
store(in:)를 사용하여 구독을 cancellables라는 세트에 저장합니다. 이렇게 하면 나중에 구독을 한 번에 취소할 수 있습니다.
요약
- 퍼블리셔: 정보를 방출하는 객체
- 구독자: 정보를 받는 객체
- Cancellable: 구독을 취소할 수 있는 객체
- CombineLatest: 두 퍼블리셔의 최신 값을 결합
- map: 값을 변환
- sink: 값을 처리
- store(in:): 구독을 저장하여 관리
그렇다면, sink 와 assign 의 차이점은 뭐지?
Combine 프레임워크에서 sink와 assign은 모두 퍼블리셔(Publisher)의 값을 처리하는 방법입니다. 둘 다 퍼블리셔의 값을 받아서 작업을 수행하지만, 용도와 사용 방식에 차이가 있습니다.
sink
sink는 퍼블리셔의 값을 받을 때마다 원하는 작업을 수행할 수 있는 매우 유연한 방법입니다. 클로저를 사용하여 값을 처리합니다.
특징: 퍼블리셔의 값을 받을 때마다 특정 작업을 수행할 수 있습니다. 값을 처리할 클로저를 직접 정의할 수 있습니다. 반환된 AnyCancellable 객체를 사용하여 구독을 관리하고 필요할 때 취소할 수 있습니다.
import Combine
// 퍼블리셔 생성
let publisher = Just("Hello, Combine!")
// sink를 사용하여 값을 처리
let cancellable = publisher.sink { value in
print(value) // "Hello, Combine!" 출력
}
// 구독 취소
cancellable.cancel()
위 코드에서 sink는 퍼블리셔로부터 값을 받을 때마다 클로저를 실행하여 값을 출력합니다.
assign
assign은 퍼블리셔의 값을 특정 객체의 프로퍼티에 할당하는 간단한 방법입니다. 주로 @Published로 선언된 프로퍼티에 사용됩니다.
특징: 퍼블리셔의 값을 특정 프로퍼티에 직접 할당합니다. 값 할당의 목적에 맞게 간결하고 명확한 사용이 가능합니다. 자동으로 메모리 관리가 되어 구독을 취소할 필요가 없습니다.
import Combine
class ExampleViewModel: ObservableObject {
@Published var text: String = ""
}
let viewModel = ExampleViewModel()
let publisher = Just("Hello, Combine!")
// assign을 사용하여 퍼블리셔의 값을 text 프로퍼티에 할당
publisher.assign(to: &viewModel.$text)
// viewModel.text의 값이 "Hello, Combine!"으로 설정됩니다.
print(viewModel.text) // "Hello, Combine!" 출력
assign은 퍼블리셔로부터 값을 받아 viewModel의 text 프로퍼티에 할당합니다.
차이점 요약
사용 목적
sink: 값을 받을 때마다 특정 작업을 수행해야 할 때 사용합니다. 예를 들어, 값을 받아서 출력하거나, 복잡한 로직을 처리해야 할 때 적합합니다. assign: 퍼블리셔의 값을 특정 프로퍼티에 직접 할당해야 할 때 사용합니다. 간단한 값 할당이 필요한 경우 적합합니다.
유연성
sink: 매우 유연하며, 클로저 안에서 값을 받아 임의의 작업을 수행할 수 있습니다. assign: 특정 프로퍼티에 값을 할당하는 단순한 작업에 적합합니다.
메모리 관리
sink: 반환된 AnyCancellable 객체를 사용하여 구독을 관리하고, 필요할 때 취소할 수 있습니다. assign: 자동으로 메모리 관리가 되어 별도의 구독 관리가 필요 없습니다.
Publishers
Publishers는 값과 오류를 생성하는 방법을 선언적으로 표현하는 역할을 한다.
Output, Failure 라는 두 가지 연관 타입을 가지며, subscribe 메서드를 통해 Subscriber를 등록한다.
protocol Publisher {
associatedtype Output
associatedtype Failure: Error
func subscribe<S: Subscriber>(_ subscriber: S) where S.Input == Output, S.Failure == Failure
}
Subscribers
Subscribers는 Publisher로부터 값을 받아 처리한다.
Subscriber 프로토콜은 Input과 Failure라는 두 가지 연관 타입을 가지며,
receive(subscription:), receive(_ input:), receive(completion:) 메서드를 구현한다.
protocol Subscriber: CustomCombineIdentifierConvertible {
associatedtype Input
associatedtype Failure: Error
func receive(subscription: Subscription)
func receive(_ input: Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Failure>)
}
대략적으로 이렇게 프로토콜로 이루어진 것들이 combine을 구성하고 있다라는 것을 파악은 했지만, 이것이 실제로 어떻게 쓰여지는지는 따로 봐야 한다. Combine in Practice 영상을 보자.
Practice
Just