이 글은 독자가 Generic Type, URLSession, 클로져와 Callback,
UIKit 혹은 SwiftUI 의 View LifeCycle 에 대해서
이미 알고 있음을 전제하고 있습니다.
Combine 은 앱 내에서 일어나는 이벤트들의 진행 경과
들을 선언적으로
코딩할 수 있게끔 도와주는 Apple 의 프레임워크입니다.
어떠한 이벤트를 추적할 때 Delegate 패턴
을 사용하거나, Completion 클로져
를 사용하는 대신 Combine 을 활용해볼 수 있습니다.
이 프레임워크는 Swift API 에 해당하기에
Swift 언어를 사용한다면 UIKit
이나 SwiftUI
둘 다 사용할 수 있습니다.
더 쉽게 말하자면
URLSession.shared.dataTask { }
쓰면completion()
써야하고
모시깽이 클래스
-모시깽이 Delegate
를 적용하는 상황들이직관적이지 않고, 조금만 복잡해져도 스파게티 코드가 되니
이걸 간단하게 해결해보자라는 취지
Combine 은 결국
Publisher
라는 이벤트 반응 전송기계
Subscriber
라는 이벤트 수집기계
이 두개를 연결해주는 프레임워크입니다.
Subscriber
는 Publisher
에게 데이터를 받기만 하는 일방향적 관계이며,
Subscriber
가 Publisher
에게 요청할 수 있는것은 데이터를 달라는 요청만 할 수 있습니다.
Publisher
는 Subscriber
에게 데이터를 전달할 때, 바로 전달할 수도 있지만
Operator
를 활용해서, 데이터를 가공해서 줄 수 있습니다.
Operator
는 Swift 에서 일상적으로 쓰는 메소드들인
map
, flatMap
, compactMap
, filter
등의 이름을 따서 만든 메소드들로, 이름과 유사한 기능들을 제공합니다.
더 쉽게 말하자면
Publisher
는 데이터를 전송만 담당,
Subscriber
는 데이터 수신만 담당
Operator
는Publisher
가 데이터 전송할 때, 중간에 수정하는 역할
Publisher
와 Subscriber
는
각각 실제 데이터에 해당하는 Output
과 Input
,
각각 오류에 해당하는 Failure
과 Failure
를 갖고 있습니다.
서로 연결되어있는 Publisher
와 Subscriber
는
OutPut
과 Input
이 같아야하며, 같은 Failure
를 갖고 있어야 합니다.
더 쉽게 말하자면
Publisher
랑Subscriber
는 같은 타입이여야해용
연결은 Publiser
에 Subscriber
를 붙이는 형태로 동작합니다.
SomePublisher
.sink(receiverCompletion:receiveValue:)`
.assign(to:on:)
/*
Publisher 에 sink() 혹은 assign() 을 사용하면
새로운 Subscriber 를 만들 수 있습니다.
*/
Subscriber
는 새로운 Subscriber
를 생성할 수 있습니다.
SomeSubscriber
.sink(receiverCompletion:receiveValue:)`
.assign(to:on:)
/*
Subscriber 에 또 sink, 혹은 assign 을 붙여서
새로운 subscriber 를 선언할 수 있다.
*/
더 쉽게 말하자면
Publisher
에sink()
혹은assign()
쓰면
새로운 Subscriber 를 선언할 수 있어요.
Class 에는 Observable 프로토콜 (옛날 이름 BindableObject) 을 사용할 수 있는데,
Class 내부 변수에 @Published 만 붙이면, 자동으로 Publisher 로 만들어요.
@Published var posts = [T]()
Operator
는 Publisher
에 붙여서 사용할 수 있는 메소드입니다.
Publisher 가 뿜어주는 데이터들을 원하는대로 가공할 때 사용합니다.
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.sink(receiveCompletion: { print ($0) },
receiveValue: { print ($0) })
/*
TextField 의 변화를 감지하는 NotificationCenter 를 만들어서,
해당 이벤트 (입력 감지) 기반으로 Publisher 를 만들고,
Publisher 가 뿜는 이벤트 기반 데이터 중, 실제 입력값인 String 값만 받도록 Map 해서
sink 를 통해 Subscriber 를 만든 후, 받은 이벤트 상태 (receiveCompletion) 과 값 (receiveValue) 를 출력하는 중 입니다.
*/
Operator 에는 다양한 종류가 있어, 필요에 따라 사용할 수 있습니다.
자주 사용될법한 Operator 들은 다음과 같이 있습니다.
.map { }
.flatMap { } // 이벤트가 1회 요청에 여러번 돌아올 때, 해당 값들을 모두 한번에 받습니다. (URLRequest 를 사용하는 시나리오가 대표적)
.compactMap { } // nil 값은 제외하고 이벤트 값들을 방출합니다.
.tryMap { } // 에러를 발생시키는 작업을 할 때, 에러가 발생하면 바로 Publisher 를 종료시킵니다.
.filter { } // 이벤트 값들을 특정 조건에 따라 거릅니다.
.tryFilter { } // 이벤트 값들을 특정 조건에 따라 거를 때, 에러가 발생하면 바로 Publisher 를 종료시킵니다.
.setFailureType(to: Error.self) // 해당 에러를 Publisher 가 방출하는 Error 로 바꿉니다.
.decode(type: T.self, decoder: JSONDecoder()) // JSON 등의 데이터를 T 자료형으로 디코딩합니다. 실패하면 Publisher 를 종료시킵니다.
.retry() // Publisher 의 흐름에 따라 내려올 때, 오류가 발생하면 retry의 횟수만큼 처음부터 다시 시도합니다. (URLRequest 를 실패했을 때, 1번 더 시도 하게 하는 등으로 사용할 수 있습니다.)
.debounce(for: .milliseconds(500), scheduler: RunLoop.main) // 얼마나 자주 이벤트를 요청할 건지 debounce 를 통해 지정할 수 있습니다.
.eraseToAnyPublisher() // Publisher 의 Operator 들을 거치며 생성된 중첩 상태의 OutPut 들 중 가장 마지막 타입을 기반으로 Publisher 로 1개의 타입으로 만듭니다.
Subscriber 를 한번 만들면, 특정 이유에 의해서 종료되지 않는 이상 메모리에 계속해서 남아서 이벤트를 요청하게 됩니다. 이러한 Publisher 들은 필요 없을 때 해제해주지 않으면 메모리 누수로 이어지기 때문에 적절한 시점에서 해제하는 것이 필요합니다.
해제를 하는 방법에는 두가지 방법이 있습니다.
Subscriber.cancel()
을 호출해서 직접 해제하는 방법Set<AnyCancellabel>()
에 Publisher 들을 모아서 해제하는 방법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)
// 어떤 Subscriber
sub?.cancel() // << 원하는 시점에서 Cancel()
Publisher
에서 sink()
혹은 assign()
을 통해 만들어진 Subscriber
들을, 원하는 시점에서 직접 Cancel() 을 호출해서 메모리에서 해제할 수 있습니다.
대표적인 시점은
UIKit
에서는 viewDidDisapeear()
SwiftUI
에서는 OnDisAppear()
가 될 수 있습니다.
Subscriber 들이 다수 존재하고, 특이점이 없는 LifeCycle 을 가진다면 (즉, View가 사라질 때 함께 사라지면 된다면) Set<AnyCancellabel>
를 사용할 수 있습니다.
private var cancellabels = Set<AnyCancellable>() // Subscriber 들을 저장해놓은 Set
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)
.store(in: &cancellabels) // << Subscriber 의 마지막에 store() 를 통해 해당 Subscriber 를 Set<Cancellabel> 에 저장한다.
// 이후 Cancellabels 가 사라질 때,
// Cancellabels 에 저장된 Subscriber 들도 모두 함께 메모리에서 해제됩니다.
더 쉽게 말하자면
Subscriber
는 한번 만들면 메모리 해제해줘야해요.
Subscriber.cancel()
쓰거나,
Set<AnyCancellabel>
-Subscriber.store()
를 통해
메모리에서 해제할 수 있어요.
Combine from Apple
Combine in Practice from Apple
URLSession dataTask Vs Combine dataTaskPublisher from Medium by PTeng