같은 출력값을 여러가지 방법을 통해 구현해보며 각각의 프레임워크에 대한 이해도를 높여보고자 한다. 이번 포스팅에서는 시리즈 01에서 completionHandler
와 DispatchQueue
를 통해 구현했던 API 통신을 Combine
을 사용하여 구현할 것이다. (통신한 URL과 데이터 모델, 커스텀 에러 코드는 이전 포스팅에 정리되어 있다.)
let todos: CurrentValueSubject<[Todo], CustomError> = .init([])
데이터와 에러를 업데이트 받고 값을 방출할 Publisher
를 view model
에 선언하고 controller
에서 구독하여 화면과 바인딩하도록 한다.
dataTaskPublisher
메소드는 네트워크 통신 결과에 따라 Output (data: Data, response: URLResponse)
또는 Failure (error: URLError)
타입의 퍼블리셔를 반환한다. 에러 발생 시 mapError
를 통해 에러 타입을 CustomError
로 변환하여 반환하고, 네트워크 통신 성공 시 tryMap
을 통해 응답 데이터를 반환한다. tryMap
과정에서 발생할 수 있는 에러도 커스텀으로 정의한 에러로 던져(throw
) 준다. 그 다음 data
를 디코딩하여 그 결과를 CurrentValueSubject
에 전달한다.
func fetchTodos() {
let urlString = "https://jsonplaceholder.typicode.com/todos"
guard let url = URL(string: urlString) else {
// 1. URL 유효성 체크
todos.send(completion: .failure(CustomError.invalidURL))
return
}
// 2. 네트워크 요청(Request)
URLSession.shared.dataTaskPublisher(for: url)
// 3. 요청 실패인 경우 error 반환
.mapError { error -> CustomError in
return CustomError.etc(error)
}
// 4. response를 HTTPURLResponse로 타입 캐스팅
.tryMap { (data: Data, response: URLResponse) -> Data in
guard let response = response as? HTTPURLResponse else {
throw CustomError.invalidResponse
}
// 5. 응답 코드 체크 (200번대여야 통신 성공)
guard (200..<300).contains(response.statusCode) else {
throw CustomError.badResponse(response.statusCode)
}
return data
}
// 6. 디코딩
.decode(type: [Todo].self, decoder: JSONDecoder())
.mapError { error in
if error is CustomError {
return error
}
return CustomError.decodingFailed
}
// 7. 결과값을 publisher인 todos에 전송
.sink(receiveCompletion: { [weak self] completion in
switch completion {
case .failure(let error):
self?.todos.send(completion: .failure(error as? CustomError ?? CustomError.etc(error)))
case .finished:
print("[URLSession] dataTask finished")
}
}, receiveValue: { [weak self] todos in
self?.todos.send(todos)
})
.store(in: &cancellables)
}
receive
메소드를 통해 결과 처리를 메인 스레드에서 이루어지도록 했다.
private func bind() {
viewModel.todos
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error):
print(error.debugMessage)
case .finished:
return
}
} receiveValue: { [weak self] todos in
self?.todoTableView.reloadData(todos: todos)
}
.store(in: &cancellables)
}
에러 발생 테스트는 시리즈 01에서 모두 다뤘으므로 생략한다. 다음에는 async await
를 사용하여 구현해보려 한다.
어 이거 뭔가 Rx에서 먹어본듯한 맛이에요