Add a reusable Networking layer | SwiftUI Crypto App #7
CryptoApp: Network Service
구현 목표
- 네트워크 서비스를 담당하는 별도의 서비스 클래스
 
구현 태스크
- URL을 통해 다운로드, 데이터를 리턴하는 함수
 
- URLSession의 
Response 담당 함수 
- 섭스크라이버의 컴플리션을 담당하는 함수
 
- 커스텀 함수
 
핵심 코드
static func download(with url: URL) -> AnyPublisher<Data, Error> {
        return URLSession
            .shared
            .dataTaskPublisher(for: url)
            .subscribe(on: DispatchQueue.global(qos: .default))
            .tryMap({try handleURLResponse(output: $0, url: url)})
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
- URL을 통해 URLSessions의 데이터 퍼블리셔 함수를 사용
 
eraseToAnyPublisher를 통해 간단한 퍼블리셔 타입으로 축약 가능 
- 재사용 가능성이 높은 코드를 서비스 클래스의 
static func로 구현 
static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data {
        guard
            let response = output.response as? HTTPURLResponse,
            response.statusCode >= 200 && response.statusCode < 300 else
        { throw NetworkingError.badURLResponse(url: url) }
        return output.data
    }
- URLSessions의 데이터 퍼블리셔가 리턴하는 아웃풋 타입을 핸들링하는 함수
 
- 커스텀 에러를 
throw하거나 아웃풋의 해당 데이터를 리턴 
- 본래 코드에서 
tryMap 코드 내부에서 하던 일을 재활용하기 위해 네트워크 매니저의 static func로 구현 
static func handleCompletion(completion: Subscribers.Completion<Error>) {
        switch completion {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished: break
        }
    }
sink 단의 컴플리션 문제가 일어났을 때 재활용하기 위한 코드 
소스 코드
import Foundation
import Combine
class NetworkingManager {
    enum NetworkingError: LocalizedError {
        case badURLResponse(url: URL)
        case unknown
        var errorDescription: String? {
            switch self {
            case .badURLResponse(url: let url): return "[🔥] Bad Response from URL: \(url.absoluteString)"
            case .unknown: return "[⚠️] Unknown error occured"
            }
        }
    }
    
    static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data {
        guard
            let response = output.response as? HTTPURLResponse,
            response.statusCode >= 200 && response.statusCode < 300 else
        { throw NetworkingError.badURLResponse(url: url) }
        return output.data
    }
    
    static func download(with url: URL) -> AnyPublisher<Data, Error> {
        return URLSession
            .shared
            .dataTaskPublisher(for: url)
            .subscribe(on: DispatchQueue.global(qos: .default))
            .tryMap({try handleURLResponse(output: $0, url: url)})
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    static func handleCompletion(completion: Subscribers.Completion<Error>) {
        switch completion {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished: break
        }
    }
}
- URLSession 데이터 퍼블리셔, URLResponse 핸들러, 컴플리션 핸들러
 
- 커스텀 에러 구현
 
private func fetchCoins() {
        guard let url = URL(string: "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250&page=1&sparkline=true&price_change_percentage=24h") else { return }
        coinSubscription = NetworkingManager
            .download(with: url)
            .decode(type: [CoinModel].self, decoder: JSONDecoder())
            .sink(receiveCompletion: NetworkingManager.handleCompletion,
                  receiveValue: { [weak self] coinModels in
                self?.allCoins = coinModels
                self?.coinSubscription?.cancel()
            })
    }
download, handleCompletion 등 코드 재사용성 및 가독성을 높이기 위한 방법