일반적으로 프로그래밍에서 태스크가 실행되면 결과값이 바로 반환된다. 하지만 통신과 같은 태스크는 통신을 하고 응답을 받아오기까지 시간이 걸린다.
이렇게 결과값이 바로 반환되지않고 이후에 반환되는 시점을 인식해서 후처리를 하는 것을 비동기 태스크라고한다.
iOS에서 비동기 태스크를 처리하는 방식을 다양하게 있는데 최신에는 async await를 지원하고 있다. (escaping closure, Notification Center 등이 있었다) 이런 비동기 처리 방식을 통일시킨 프레임워크가 combine이다.
여기서는 기존의 async await 통신 코드를 combine으로 리팩토링 해보는 과정을 소개한다
import SwiftUI
import Foundation
final class FoodCategoryFilter {
static let instance = FoodCategoryFilter()
@MainActor
func queryFoods(category: String) async -> [Properties] {
var foodsData: [Properties] = []
let filterParameter = """
{
"filter": {
"property": "category",
"select": {
"equals": \(category)
}
}
}
""".data(using: .utf8) ?? Data()
let request = NSMutableURLRequest(url: NSURL(string: DataBaseInfo.readURL)! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = DataBaseInfo.headers
request.httpBody = filterParameter
do {
let (data, response) = try await URLSession.shared.data(for: request as URLRequest)
let httpResponse = response as! HTTPURLResponse
if !(200...299).contains(httpResponse.statusCode) {
throw NetworkError.responseError
}
print(httpResponse.statusCode)
print("data \(data)")
guard let decodedData = try? JSONDecoder().decode(Test.self, from: data) else { throw
NetworkError.decoidngError }
for data in decodedData.results {
foodsData.append(data.properties)
}
} catch NetworkError.decoidngError {
print(NetworkError.decoidngError.localizedDescription)
} catch NetworkError.responseError {
print(NetworkError.responseError.localizedDescription)
} catch {
print(error)
}
return foodsData
}
}
URLSession.DataTaskPublisher라는 Combine publisher을 제공하고 있다.
import SwiftUI
import Combine
final class FoodListVM: ObservableObject {
@Published var foods: [Properties] = []
@Published var favorites: [Properties] = []
@Published var isLoading: Bool = false
var cancellables = Set<AnyCancellable>()
func queryFoods(category: String) {
let parameters = "{\n\"filter\": {\n\"property\": \"category\",\n\"select\": {\n\"equals\": \"\(category)\"\n}\n}\n}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://api.notion.com/v1/databases/\(DataBaseInfo.databaseID)/query")!,timeoutInterval: Double.infinity)
request.addValue("2022-06-28", forHTTPHeaderField: "Notion-Version")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(DataBaseInfo.token, forHTTPHeaderField: "Authorization")
request.httpMethod = "POST"
request.httpBody = postData
self.isLoading = true
URLSession.shared.dataTaskPublisher(for: request)
.tryMap(handleOutput)
.decode(type: NotionDTO.self, decoder: JSONDecoder())
.map { decodedData in
decodedData.results.map { $0.properties }
}
.receive(on: DispatchQueue.main)
.sink { completion in
self.isLoading = false
} receiveValue: { [weak self] result in
print("receive value work")
self?.foods = result
}
.store(in: &cancellables)
}
func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data {
guard let response = output.response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return output.data
}
}
훨씬 코드 가독성이 증가하였다.
특히나 에러처리 방법은 tryMap 오퍼레이트안에 함수로 전달함으로서 에러처리에 대한 코드를 따로 관리할 수 있다.