애플의 firstparty framework : combine 정복
이번글에서는 combine + URLSession을 사용하여 네트워킹을 하는 법을 정리해보려 한다. 특히 URLSession 과 combine + URLSession을 비교해보며 작성해보려한다.
우선, 시작에 앞서 간단히 URLSession에 대해 복기해보자.
URLSession은 iOS에서 HTTP 통신을 도와주는 API이다.
URLSession을 통해 서버와 통신을 할때, 우선 URLSessionConfiguration을 결정하고, Session을 생성한다.
enum NetworkError: Error {
case invalidRequest
case transportError(Error)
case responseError(statusCode: Int)
case noData
case decodingError(Error)
}
struct IdusInfo:Codable {
let resultCount: Int
let results: [IdusDetailInfo]
}
struct IdusDetailInfo: Codable {
let sellerName: String
let description: String
let screenshotUrls: [String]
}
final class NetworkService {
let session: URLSession
//네트워크 서비스가 생성될때 session 받도록 생성
init(configuration: URLSessionConfiguration) {
session = URLSession(configuration: configuration)
}
func fetchInfo(appID: Int, completion: @escaping (Result<IdusInfo, Error>) -> Void) {
let url = URL(string: "http://itunes.apple.com/lookup?id=\(appID)")!
let task = session.dataTask(with: url) { data, response, err in
if let err = err {
completion(.failure(NetworkError.transportError(err)))
return
}
if let httpResponse = response as? HTTPURLResponse,
!(200..<300).contains(httpResponse.statusCode) {
completion(.failure(NetworkError.responseError(statusCode: httpResponse.statusCode)))
return
}
guard let data = data else {
completion(.failure(NetworkError.noData))
return
}
//data -> 우리가 만든 모델
do {
let decoder = JSONDecoder()
//응 IdusInfo 형태로 디코딩 할거야 from : data 를
//실패할수 있으므로 try
let idus = try decoder.decode(IdusInfo.self, from: data)
completion(.success(idus))
} catch let error as NSError {
completion(.failure(NetworkError.decodingError(error)))
}
}
task.resume()
}
}
// -- network 담당하는 network service
// NetworkService 이용한 network작업
let networkService = NetworkService(configuration: .default)
networkService.fetchInfo(appID: 872469884) { result in
switch result {
case .success(let idus):
print("Idus: \(idus)")
case .failure(let error):
print("Error:\(error)")
}
}
URLSessionConfiguration에는 .default
,.ephemeral
,.background
가 있다.
.default
.ephemeral
.background
Combine은 URLSessioin을 통해 받아온 데이터를 Publish할 수 있게끔 Publihser인 URLSession.DataTaskPublisher
을 제공한다.
전달받은 데이터를 우리가 미리 선언해둔 type으로 변환할 수 있게끔 Operator를 제공한다.
URLSession.DataTaskPublisher
메서드는 task 성공시 결과값으로 data, response를 전달한다. 전달받은 값을 tryMap(_:)
연산자를 통해 에러를 throw하고 데이터를 뽑아낼 수 있다.
그리고 tryMap(_:)
을 거쳐 받은 데이터를 decode
를 통해 원하는 형태로 decode 할 수 있다.
import Combine
enum NetworkError: Error {
case invalidRequest
case responseError(statusCode: Int)
}
struct APIModel {
static let scheme = "http"
static let host = "itunes.apple.com"
static let path = "/lookup"
func searchApp(query: Int) -> URLComponents {
var components = URLComponents()
components.scheme = APIModel.scheme
components.host = APIModel.host
components.path = APIModel.path
components.queryItems = [
URLQueryItem(name: "id", value: "\(query)")
]
return components
}
}
struct IdusInfo:Codable {
let resultCount: Int
let results: [IdusDetailInfo]
}
struct IdusDetailInfo: Codable {
let sellerName: String
let description: String
let screenshotUrls: [String]
}
final class NetworkService {
let session: URLSession
let api = APIModel()
//네트워크 서비스가 생성될때 session 받도록 생성
init(configuration: URLSessionConfiguration) {
session = URLSession(configuration: configuration)
}
func getURL(appID: Int) throws -> URL {
guard let url = api.searchApp(query: appID).url else {
throw NetworkError.invalidRequest
}
return url
}
func fetchInfo(appID: Int) -> AnyPublisher<IdusInfo, Error> {
let url = URL(string: "http://itunes.apple.com/lookup?id=\(appID)")!
//dataTask Publisher
let publisher = session
.dataTaskPublisher(for: url)
// 서버에서 받은 response 확인
.tryMap { result -> Data in
guard let httpResponse = result.response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode) else {
let response = result.response as? HTTPURLResponse
let statusCode = response?.statusCode ?? -1
throw NetworkError.responseError(statusCode: statusCode)
}
return result.data
}
// 받은 data 디코딩 잘하기
.decode(type: IdusInfo.self, decoder: JSONDecoder())
.eraseToAnyPublisher() //type 지우는 역할
return publisher
}
}
// -- network 담당하는 network service
// NetworkService 이용한 network작업
let networkService = NetworkService(configuration: .default)
// 퍼블리셔 구독
let subscription = networkService
.fetchInfo(appID: 872469884) //퍼블리셔
.receive(on: RunLoop.main)
.sink { completion in
print("completion: \(completion)")
} receiveValue: { info in
print("Info :\(info)")
}
우선 제일 크게 느낀점은 Operator를 사용할 수 있다는 점이었다.
세세히 비교해보자면, dataTask(with:completionHandler:)
의 경우는 completion handler 클로저를 사용하여 추후 코드 작업을 진행했어야 했다.
하지만 combine을 사용하게 되면, Operator를 사용해 작업을 진행해주기 때문에 훨씬 직관적이라고 생각한다.
Refernce