Apple에서는 프로토콜로 미리 구현한 Publisher가 존재한다. 오늘은 해당 Publisher에 대해 살펴보고, 그 중 Future에 대해 자세히 알아보는 시간을 갖고자 한다.
Publisher를 간략하게 설명하면, Subscription을 만들고 Subscriber들에게 이벤트를 방출하는 타입을 위한 프로토콜이다.
그리고 Apple에서 이러한 Publisher를 프로토콜로 미리 구현했는데, 다음과 같다.
1. Just
2. Future
3. Deffered
4. Empty
5. Fail
6. Record
7. AnyPublisher
회사 코드에서 자주보이는 것들은 Future, AnyPublisher정도 였던 것 같은데 생각보다 많은 Publisher들이 존재했다.
오늘은 간단하게 Just를 살펴보고, Future에 대해 좀 자세히 알아보도록 하겠다.
1. Just
Just는 자신을 subscribe하는 Subscriber들에게 한번에 값을 내보낸 뒤 finish 이벤트를 보내는 Publisher이다.
Publisher 프로토콜은 Output, Failure 프로퍼티가 필요한데, Just는 구현부에 Failure 타입이 Never로 설정되어 있어서, 따로 Failure 타입을 지정해 주지 않아도 된다.
2. Future
Future는 하나의 결과를 비동기로 생성한 후에 completion event를 내보낸다.
Future는 생성할 때, 값을 내보낼 때 호출할 클로저를 매개변수로 받아 값을 한번 내보내면 해당 값을 계속 내보내는 Publisher이다.
그리고 오늘 소개한 7가지 Publisher들 중 유일하게 Class 타입이라는 특징을 가지고 있다. (나머지는 Struct)
기존에 completion handler를 이용해서 주로 비동기 작업을 진행했었는데, Future를 사용하면 completion handler의 역할을 대신할 수 있다고 한다.
Future는 말 그대로 일어나지 않은 미래를 의미하고,
Future 안에는 Promise 클로저가 존재한다.
아아.. 글로보니 하나도 이해가 되지 않는다..
예제를 살펴보도록 하겠다.
기본예제
// first
let future = Future<Int, Never> { promise in
promise(.success(1))
}
future.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// 출력
// 1
// finished
// second
let future = Future<Int, Never> { promise in
promise(.success(1))
promise(.success(2))
}
future.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// 출력
// 1
// finished
first 에서 Future<Int, Never> 라고 되어있으므로, value 타입이 Int이고, 오류 타입은 Never인 Publisher이다.
해당 퍼블리셔에 promise(.success(1))을 통해 1이라는 성공 값을 보내게 된다(subject send랑 비슷한 건가..?)
first 예제를 살펴보면 Future는 promise에게 value나 error를 한번 보내게 되면 바로 finished가 된다.
그렇기 때문에 second 예제에서 첫번째 promise(.success(1))을 보내고 두번째 promise(.success(2))를 보냈을 때 씹히는? 걸 볼 수가 있다.
- future는 Publish 타입 중 성공/실패가 확실한 결과값이 나오는 '하나'의 값을 리턴
- promise를 통해 값을 리턴
실제 예제
completionHandler를 Future로 변환하는 예제를 살펴보도록 하겠다.
1. completionHandler 활용
completionLocalItemsLoader.swift
enum CacheError: Error {
case connectivity
case invalidData
}
class LocalItemsLoader {
func load(completion: @escaping (Result<[Items], Error>) -> Void) {
cache.get { data, error in
if error != nil {
completion(.failure(CacheError.connectivity))
}
guard let items = try? JSONDecoder().decode([Item].self, data)
else { completion(.failure(CacheError.invalidData)) }
completion(.success(items))
}
}
}
내가 평소에 Service쪽에서 많이 보던 completion: @escaping 구문이 존재한다.
cache.get을 통해 데이터를 요청하고, 데이터를 받거나 데이터를 받지 못하는 경우 CompletionHandler 클로져를 호출하여 해당 클로져에 응답받은 데이터를 전달한다.
completionItemListViewModel.swift
class ItemsListViewModel {
@Published var items = [Item]()
private let itemsLoader: LocalItemsLoader
init(itemsLoader: LocalItemsLoader {
self.itemsLoader = itemsLoader
}
func getItems() {
itemsLoader.load { result in
switch result {
case let .success(items):
self.items = items
case .failure
// handle failure
}
}
}
}
그리고 @escaping을 통해 탈출된 클로저를 통해 응답받은 데이터를 result에 받아 .success, .failure에서 이를 처리하는 방식이다.
그렇다면 이러한 completion을 future에서는 어떻게 사용할 수 있을지 살펴보자
2. Future 활용
futureLocalItemsLoader.swift
class LocalItemsLoader {
func load() -> Future <[Items], Error> {
return Future() { promise in
cache.get { data, error in
if error != nil {
promise(Result.failure(CacheError.connectivity))
}
guard let items = try? JSONDecoder().decode([Item].self, data)
else { promise(Result.failure(CacheError.invalidData)) }
promise(Result.success(items))
}
}
}
}
마찬가지로 cache.get을 통해 데이터를 요청하는데 데이터를 받아오는데 실패하게 되면 promise(Result.failure()를 통해 error를 보내게 된다. 그리고 앞서 말했듯이 Future는 promise에게 value나 error를 한번 보내게 되면 바로 finished가 되므로 에러가 발생했을 경우 뒤에 있는 promise(Result.success[items])는 실행되지 않게 될 것이다.
만약 에러가 발생하지 않았다면 guard let items = try? JSONDeoder().decod([Item].self, data) 구문이 실행되고, 디코딩이 실패할 경우 promise(Result.failure(CacheError.invalidData))가 실행될 것이다.
최종적으로 디코딩도 성공했다면 마지막에 존재하는 promise(Result.success(items))가 실행될 것이다.
futureItemsListViewModel.swift
class ItemsListViewModel {
@Published var items = [Item]()
private let itemsLoader: LocalItemsLoader
private var subscriptions = Set<AnyCancellable>()
init(itemsLoader: LocalItemsLoader) {
self.itemsLoader = itemsLoader
}
func getItems() {
itemsLoader
.load()
.sink(receiveCompletion: { [weak self] completion in
if case .failure(let error) = completion {
// handle failure
}
}, receiveValue: { [weak self] value in
items = value
})
.store(in: &subscriptions)
}
}
ItemsListViewModel에서 getItems() 함수를 중점적으로 살펴보겠다.
itemsLoader.load()는 Future를 반환하기 때문에 Publisher이다.(Future가 Publisher이므로)
그리고 .sink를 통해 Subscriber가 생성되고, receiveCompletion에서는 failure가 된다면 이를 처리하는 로직을 구성하면 되고, receiveValue를 통해 값이 들어오게 된다면 미리 선언해둔 items Pulisher에 value값이 들어가게 될 것이다.
이렇게 Completion Handler를 활용한 콜백 기반 코드를 Future를 활용한 반응형 코드로 전환할 수 있다라는 것을 알아보는 시간을 가졌다.
completionHandler Future차이 참고 링크:
참고링크:
https://eunjin3786.tistory.com/228
'iOS 개발 > Combine' 카테고리의 다른 글
[Combine] combine 정리2 (sink, handleEvents, receive) (0) | 2023.06.20 |
---|---|
[Combine] Operater란? (1) (1) | 2023.05.25 |
[Combine] CurrentValueSubject & PassthroughSubject 예제(2 - 1) (0) | 2023.05.18 |
[Combine] Subject 정의 및 예제(2) (0) | 2023.05.18 |
[Combine] Publisher & Subscriber란?(1) (0) | 2023.05.16 |