https://www.youtube.com/watch?v=r3WQTh1LB4k&list=PLw-3TTKkn1fM-5kugk9vyJTXZF8B0zHxC&index=8
위 영상을 보고 번역정리한 글, 자세한 내용은 링크를 눌러보시길바라바람
개요
먼저 컴플리션 핸들러와, 체인지 핸들러의 차이를 설명합니다
func loadData(_ completion: @escaping (Result<SomeModel, Error>) -> Void) {
let url = URL(string: "https://donnywals.com")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
// use data and decode model
completion(.success(decodeModel))
}.resume()
}
컴플리션 핸들러를 사용한 네트워크 데이터 요청 메소드입니다. 제일 자주 사용하고 자주 봤다고 함니당
// 데이터가 변함을 관찰하는 클로저를 두어서 사용하는 예시임니다
class TrackListViewModel {
private(set) var tracks: [Track] {
didSet {
onTracksChanged(tracks)
}
}
var onTracksChanged: ([Track]) -> Void = { _ in }
// ...
}
let viewModel = TrackListViewModel()
viewModel.onTracksChanged = { newTracks in
// ...
}
요런 식의 클로저를 체인지 핸들러(Change handler)라고 하나보네용.
이런식의 사용은, 언제든지 호출되는 일부 핸들러 또는 관찰자를 제공할 수 있습니다.
Combine은 functional reactive programming framework 라고 합니다. 기능적 반응형 프로그래밍 ?
예제 코드는 다음과 같습니다.
// 이 친구는 구독에 대한 수명 주기를 관리합니다.
var cancellables = Set<AnyCancellable>()
// 퍼블리셔를 만듭니다.
[1, 2, 3].publisher
.map({ integer in
return integer * 2
})
.sink(receiveValue: { integer in
print(integer)
})
.store(in: &cancellable)
할당 해제될 때마다 해당 구독을 취소가능한집합에 저장하므로 아무도 더 이상 구독에 대한 참조를 보유하지 않습니다.
이 두 가지를 살펴보면 위에서 설명한 핸들러들의 기능을 모두 수행할 수 있음을 알 수 있습니다.
따라서 Combine은 컴플리션 핸들러와, 체인지 핸들러를 대체할 수 있습니다.
func loadData() -> AnyPublisher<SomeModel, Error> {
let url = URL(string: "https://donnywals.com")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: SomeModel.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
하지만, 기존 코드를 바로 변경하고 싶지 않은 경우가 있을 수 있습니다. 그래서 아래 코드로 천천히 Combine을 적용하는 모습을 볼 수 있습니다.
아래 코드의 loadData는 기존 컴플리션핸들러를 사용하던 네트워크 call 메소드 임니다.
func loadData() -> AnyPublisher<SomeModel, Error> {
// Future도 퍼블리셔입니다.
return Future { promise in
loadData { result in
promise(result)
}
}.eraseToAnyPublisher()
}
위 코드를 통해, 콜백 기반 함수를 Combine World
로 연결할 수 있습니다.
Future
를 사용하여 천천히 Combine을 도입할 수 있었습니다.eraseToAnyPublisher
를 사용하여 Future
를 숨길 수 있습니다.Future
는 완료 핸들러에만 적합합니다. 한 번만 호출되기 때문임다.이제 체인지 핸들러를 Combine을 통해 연결해보려고 합니다.
class TrackListViewModel {
@Published private(set) var tracks: [Track]
}
// or
let viewModel = TrackListViewModel()
viewModel.$tracks
.sink { tracks in
// use tracks
}
.store(in: &cancellables)
이번에는 위치 공급자를 구축하여서 Combine을 빌드해보고자 합니다.
로케이션 매니저와 딜리게이트를 래핑하는 개체를 빌드하려고 합니다.
두 개의 퍼블리셔를 노출할 것입니다.
이런 식으로 구현하면, 구독자는 유저의 마지막 위치와 뒤에 받을 새로운 위치를 받게 되어 매우 유용합니다.
첫 번째 위치를 얻을 때까지 기다리지 않아도 즉시 얻을 수 있는 장점이 있습니다.
위치를 수동으로 쿼리하지 않고도 UI를 업데이트할 수 있습니다.
먼저 대리자 기반 솔루션입니다.
class LocationProvider: NSObject {
private let manager = CLLocationManger()
private(set) var currentLocation: CLLocation? = nil {
didSet {
if let currentLoation = currentLocation {
onLocationObtained(currentLocation)
}
}
}
var onLocationObtained: (CLLocation) -> Void = { _ in }
private(set) var locationPermissons: CLAuthorizationStatus? = nil {
didSet {
onLocationPermissonChanged(locationPermissons ?? .notDetermined)
}
}
var onLocationPermissonChanged: (CLAuthorizationStatus) -> Void = { _ in }
}
extension LocationProvider: CLLocationMangerDelegate {
func locationMangerDidChangeAuthorization(_ manager: CLLocationManger) {
locationPermissons = manager.authorizationStatus
}
func locationManger(_ manager: CLLocationManager, didUpadateLocations locations: [CLLocation]) {
currentLocation = locations.last
}
}
이제 위 코드를 리팩토링 하려고 합니다.
// delegate 메소드는 유지합니다.
// locationProvider
private let manager = CLLocationManager()
@Published private(set) var currentLocation: CLLocation? = nil
@Published private(set) var locationPermissons: CLAuthorizationStatus? = nil
우리는 @Published
를 사용하여 간단하게 프로퍼티를 게시자로 변환시켰습니다. 이는 쉽게 상태를 관찰할 수 있게 해줍니다.
// 사용하는 코드
var cancellables = Set<AnyCancellable>()
let provider = LocationProvider
provider.$currentLocation.sink(receiveValue: { location in
// use current location
}).store(in: &cancellables)
provider.$locationPermissons.sink(receiveValue: { location in
// use auth status
}).store(in: &cancellables)
이 매커니즘으로 Combine을 대체할 수도 있다고 합니다.
AsyncStream, AsyncSequence
로 퍼블리셔를 대체할수있습니다.async-algorithms
으로 우리는 대부분의 Combine 연산자를 대체 가능합니다.하지만 이 친구들이 실제로 잘하는 것이 무엇인지 살펴본다고 함니다.
func loadData() async throws -> SomeModel {
let url = URL(string: "~~~")
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
return try decoder.decode(SomeModel.self, from: data)
}
이전에 살펴본 Combine을 사용한 loadData
메소드보다 확실히 가독성이 좋습니다.
만약 기존 컴플리션 핸들러를 이용한 loadData
를 냅두고 사용한다면 다음과 같슴니다.
func loadData() async throws -> SomeModel {
return try await withUnsafeThrowingContinuation { continuation in
loadData { result in
continuation.resume(with: result)
}
}
}
self
가 강한 참조가 되지 않게 해야합니다self
가 할당 해제 되었을 때 반복을 반드시 종료해야합니다Task에서 weak self는 별 의미가 없고 취소를 이용하는 걸 권장하고 있었는데 이 분이 제시한 해결 방안도 그와 비슷하네여
이 분은 Combine을 좋아하기 때문에 아래 코드를 제안하였슴니다.
extension Task {
func store(in cancellables: inout Set<AnyCancellable>) {
cancellabels.insert(AnyCancellable {
self.cancel()
})
}
}
이번 세션에서는 Combine에 대해 좀 알게 되었단 부분이 좋았슴다. 기존에 async await는 나름 사용을 좀 해봤어서 익숙했는데 Combine은 아무래도 @Published
이 정도로 SwiftUI에서 사용해봤어서, 좀 더 알게되었네요
그리고 기존 코드를 마이그레이션 하지 않고 사용하는 부분도 나름 인상적이였습니다. 저 같으면 바로 코드를 엎을거같은데 일단 래핑해서 사용을 하고, 천천히 변경해나가는게 좀 더 맞는 판단인것 같습니다.
반응형 프로그래밍은 아직 경험이 부족한데, 좀 더 공부해서 슬슬 적용을 해봐야겠슴니다.