Introducing Combine 정리
WWDC 2019
Cocoa SDK에는 다양한 비동기API가 있습니다. 이런 인터페이스들은 각각 중요합니다. 하지만 이것을 조합해서 사용할 때 어려움을 겪게 됩니다. Combine을 사용하면 이런 비동기 조합을 간편하게 할 수 있습니다.
Combine Features
Combine은 swift로, swift를 위해 만들었습니다. Generic같은 Swift feature을 사용할 수 있다는 뜻입니다.
- Generic: boilerplate 코드를 줄여줌.
- Type safe
- Composition first: 핵심 컨셉은 심플. 조합해서 사용할 때 효과적.
- Request driven: 앱의 메모리 사용, 퍼포먼스를 관리하기 좋음.
Key Concepts
- Publisher
- Subscriber
- Operators
Publisher
- 선언적인 부분(declarative part).
- 단지 ‘설명’(description)의 역할을 하기 때문에, value type.
- struct로 되어있음.
protocol Publisher {
associatedtype Output
associatedtype Failure: Error
func subscribe<S: Subscriber>(_ subscriber: S)
where S.Input == Output, S.Failure == Failure
}
Error가 발생할 일이 없다면, Never라는 타입을 지정해주면 됩니다. 다음은 Publisher의 한 가지 예입니다.
extension NotificationCenter {
struct Publisher: Combine.Publisher {
typealias Output = Notification
typealias Failure = Never
init(center: NotificationCenter, name: Notification.Name, object: Any? = nil)
}
}
기존의 API와 비슷한 init을 가지도록 만들었습니다.
Subscriber
- Subscriber는 Publisher의 Output을 구독함.
- Publisher가 끝나는 경우 completion을 받음.
- Subscriber는 전달받은 value을 기반으로 상태를 변경함.
- Subscriber는 class로 되어있음.
protocol Subscriber {
associatedtype Input
associatedtype Failure: Error
func receive(subscription: Subscription)
func receive(_ input: Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Failure>)
}
아래는 Subscriber의 한 가지 예입니다. 클래스로 되어있고, Failure가 발생하지 않아서 Never로 타입을 지정했습니다.
extension Subscribers {
class Assign<Root, Input>: Subscriber, Cancellable {
typealias Failure = Never
init(object: Root, keyPath: ReferenceWritableKeyPath<Root, Input>)
}
}
Publisher와 Subscriber는 이런 패턴을 가집니다.
- Subscriber가 Publisher를 구독한다.
- Subscriber는 value를 하나도 못받을 수도 있고,
- 여러번 value를 받을 수도 있다.
- 마지막에 completion을 받는다.
Operator
- Operator는 Publisher이다.
- 선언적이다. 그래서 value type.
- 값을 변경하기 위해 존재함.
- upstream을 구독하고, 결과값을 downstream으로 전달함.
// Using Operatorslet graduationPublisher =
NotificationCenter.Publisher(center: .default, name: .graduated, object: merlin)let gradeSubscriber = Subscribers.Assign(object: merlin, keyPath: \.grade)let converter = Publishers.Map(upstream: graduationPublisher) { note in
return note.userInfo?["NewGrade"] as? Int ?? 0
}converter.subscribe(gradeSubscriber)
Publisher.Output과 Subscriber.Input이 같아야하기 때문에, 위의 예제에서 가운데에 Map을 넣어줬습니다. (실제 syntax와 다름. 원리를 보여주기 위한 것으로 보입니다.)
실제로 사용할 때는 아래와 같이 사용됩니다.
// Chained Publishers
NotificationCenter.default.publisher(for: .graduated, object: merlin)
.map { note in
return note.userInfo?["NewGrade"] as? Int ?? 0
}
.assign(to: \.grade, on: merlin)
Declarative Operator API
- Functional transformations
- List operations
- Error handling
- Thread or queue movement
- Scheduling and time
Composition!
Combine은 작은 일을 하는 operator들을 많이 제공하고 있습니다. 이런 operator들을 조합해서 사용하는 것을 권장합니다.
NotificationCenter.default.publisher(for: .graduated, object: merlin)
.compactMap { note in
return note.userInfo?["NewGrade"] as? Int
}
.filter { $0 >= 5 }
.prefix(3)
.assign(to: \.grade, on: merlin)
Combining Publishers
영상에서 Zip, CombineLatest를 소개했는데 Rx에서 사용하는 개념과 동일해서 Rx의 자료를 가져왔습니다. (https://rxmarbles.com)
정리
RxSwift를 처음 사용할 때 바로 completion이 호출되는 경우에도 무리하게 Rx를 사용했던 경험이 있습니다. 무작정 사용하기 보다 정말 필요를 느낄 때 사용하면 좋을 것 같습니다. 여러 비동기 API 호출들을 제어 하거나, 스케줄링, 스레드 제어를 할 필요가 있을 때 사용하면 유용할 것 같습니다.