제가 이번에 진행했던 프로젝트의 비동기 이벤트 처리를 Combine을 이용해 리팩토링을 준비하고 있는데요.
지난 시간에는 Combine을 처음 소개한 영상을 정리했는데 오늘부터는 Combine 공식 문서를 정리해 보도록 하겠습니다.
이벤트를 처리하는 연산자를 조합하여 비동기 이벤트 처리를 사용자화 하는 프레임워크이다.
큰 틀은 Publisher가 시간에 따라 변하는 데이터를 보내면 Subscriber 해당 값을 받아 처리하는 것이다.
Publisher는 Subscriber가 요청할 때 데이터를 보내준다. 필요할 때만 수신하기 때문에 속도를 제어할 수 있다.
Publisher를 보면 upstream, downstream Publisher라는 표현이 등장하는 데 upstream은 자신 이전에 연산을 처리하는 Publisher를 의미하고 downstream은 이후에 처리하는 Publisher를 의미합니다.
여러 Publisher를 결합해 사용할 수 있다.
Combine은 선언적 방식을 통해 이벤트 처리에 접근한다. 이벤트를 처리하기 위해 각각의 Operator를 사용하게 되는데 연결 고리마다 배치해 고유한 작업을 수행합니다.
protocol Publisher<Output, Failure>
protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible
Publisher와 Subscriber 둘 다 associated type을 이용해 Output은 Input, Input은 Output과 같은 타입으로 매칭 시켜야 한다.
기본적인 방법은 Notification.default
인스턴스를 사용하는 방법입니다.
let pub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
위 방법을 이용해 이벤트를 보내는 Publisher를 알 수 있습니다. TextField처럼 User의 이벤트를 처리하는 객체에 사용할 수 있습니다.
또한 Combine은 Output과 Failure를 자동으로 매칭 시켜주는 방법을 2개 제공합니다.
sink(receiveCompletion:receiveValue:)
: 두 가지 클로저를 활용합니다. 첫 번째는 성공과 실패를 판단하는 Subscriber.Completion을 수신할 때 동작하고 두 번째는 Publisher로부터 element를 받을 때 실행됩니다.assign(to:on:)
: 정해진 경로의 즉시 할당합니다.let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.sink(receiveCompletion: { print ($0) },
receiveValue: { print ($0) })
Publisher는 체이닝 중간중간 연산자를 배치해 이벤트 전달을 사용자화 할 수 있습니다.
Publisher의 Output을 변경하는 가장 쉬운 방법은 map(_:)
을 사용하는 것입니다.
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.assign(to: \MyViewModel.filterString, on: myViewModel)
위 코드는 TextField를 Notificaion의 개체로 가져온 다음 필드의 문자열 값을 ViewModel에 할당하고 있습니다.
비동기 작업을 하다 보면 우리가 원하는 시간만큼 동작하거나 가져온 데이터를 가지고 View를 업데이트해야 하는 경우가 발생합니다.
debounce(for:scheduler:options:)
: 작업을 진행하는 최소 시간을 설정할 수 있습니다.
ex) 비용이 많이 발생하는 작업 같은 경우 사용자가 입력이 멈출 때 까지 기다리는 것이 효율적이다.
receive(on:options:)
: Scheduler의 RunLoop 인스턴스를 매개변수로 지정하면 main loop에서 호출할 수 있습니다.
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.filter( { $0.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } )
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.receive(on: RunLoop.main)
.assign(to:\MyViewModel.filterString, on: myViewModel)
사용자 정의는 결국 체이닝 사이에 얼마나 Operator를 활용하냐에 따라 달라집니다.
Publisher를 취소하지 않으면 계속해서 element를 보내기 때문에 필요하지 않다면 구독을 취소해야 합니다.
연결 부분에서 설명한 2가지 함수는 모두 Cancellable을 채택한 Subscirber 타입을 반환합니다. 필요에 따라 구독을 취소할 수 있습니다.
sub?.cancel() // 처음 생성되는 Subscriber를 변수나 상수에 저장해 필요에 따라 취소한다.