[iOS] URLSession 덜 더럽게 사용해보기 (network 구조 설계)

Charlie·2022년 10월 1일
0

이전 글에서 URLSession의 boiler plate 코드들을 줄이고 조금 편하게 사용하는 방법을 알아보았었다.
조금 더 업그레이드를 시켜보자.

Config 파일

주로 static 변수들로 이루어진 baseURL등을 지정하는 파일

struct Config {
	static let baseURL = "https://velog.io/@tmdckd232"
}

Http Method

URLRequest에서 http method는 string으로 설정을 해야한다. string으로 계속 설정하는게 귀찮으니까 enum타입으로 하나 만들어주자.

enum HttpMethod<Body> {
	case get
    case post(Body)
    case put(Body)
    case patch(Body)
    case delete(Body)
}

extension HttpMethod {
    var method: String {
        switch self {
        case .get:
            return "GET"
            
        case .post:
            return "POST"
            
        case .put:
            return "PUT"
            
        case .patch:
            return "PATCH"
            
        case .delete:
            return "DELETE"
        }
    }
}

Resouce

기존에는 API별로 return type을 지정해야하는 문제가 있다. 따라서 각 request type에 따른 URLRequest를 설정하는 Resource를 생성하자. request에는 기본 GET 방식의 request, parameter들이 있는 GET request, body가 있는 POST, PUT, PATCH, DELETE request가 있으므로 따로 init을 만들어준다.

기본 GET request

URL을 전달해주고 결과로 나오는 원하는 타입으로 parsing만 하면 된다.

parameter와 함께하는 GET request

GET method에서 paramete와 함께 request를 보내는 것은 "https://baseURL.com/?name=value" 형태로 변환해서 요청을 하는 것이다.
parameter는 [String: String]의 dictionary 형태로 받고, 그 값은 URLQueryItem으로 추가한다.
그리고 그 결과로 나온 url을 URLComponent에서 꺼내 사용하고 parsing을 한다.

Body와 함께하는 request들

GET 방식 이외의 request들은 URLRequest에 별도로 method를 지정해주어야 한다.
또 body는 Encodable프로토콜을 채택하도록 하고 JSONEncoder를 사용한다.
이후 Content-Type, Accept를 application/json으로 설정해서 json data를 받을 수 있도록 한다.

struct Resource<T> {
	var urlRequest: URLRequest
    let parse: (Data) -> T?
}

extension Resouce where T: Decodable {
	// 기본 GET
	init(url: URL) {
    	self.urlRequest = URLRequest(url: url)
        self.parse = { data in
        	try? JSONDecoder().decode(T.self, from: data)
        }
	}
    
    // parameter와 함께하는 GET
    init(urlString: String, parameters) {
      var component = URLComponents(string: urlString)
        var parameters = [URLQueryItem]()
        
        for (name, value) in _parameters {
            if name.isEmpty { continue }
            parameters.append(URLQueryItem(name: name, value: value))
        }
        
        if !parameters.isEmpty {
            component?.queryItems = parameters
        }
        if let componentURL = component?.url {
            self.urlRequest = URLRequest(url: componentURL)
        } else {
            self.urlRequest = URLRequest(url: URL(string: urlString)!)
        }
        
        self.parse = { data in
            try? JSONDecoder().decode(T.self, from: data)
        }
    }
    
    // GET 이외의 method
    init<Body: Encodable>(url: URL, method: HttpMethod<Body>) {
        self.urlRequest = URLRequest(url: url)
        self.urlRequest.httpMethod = method.method
        
        switch method {
        case .post(let body), .delete(let body), .patch(let body), .put(let body):
            self.urlRequest.httpBody = try? JSONEncoder().encode(body)
            self.urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
            self.urlRequest.addValue("application/json", forHTTPHeaderField: "Accept")
            
        default:
            break
        }
        
        self.parse = { data in
            try? JSONDecoder().decode(T.self, from: data)
        }
    }
}

URLSession + Extension

URLSession의 request를 간편하게 사용하기 위해서 extension에서 request 함수를 만들어서 사용하자.

extension URLSession {
    func request<T>(_ resource: Resource<T>, completion: @escaping (T?, NetworkError?) -> Void) {
        dataTask(with: resource.urlRequest) { data, response, error in
            if let response = response as? HTTPURLResponse,
               (200..<300).contains(response.statusCode) {
                completion(data.flatMap(resource.parse), nil)
            } else {
                completion(nil, .responseError)
            }
        }
        .resume()
    }
}

API 만들기

만들어둔 resource와 request 함수를 통해 API를 만들어서 사용하면 된다.
링크 참고

지금은 articles를 가져오는 API 하나만 만들어보자.

final class APIService {
    static let shared: APIService = APIService()
    
    private init() {}
    
    func loadArticles(completion: @escaping (Result<[Article], NetworkError>) -> Void) {
        guard let url = URL(string: Config.baseURL) else {
            print("URL 생성 실패")
            return
        }
        let resource = Resource<ArticleDTO>(url: url)
        
        URLSession.shared.request(resource) { dto, error in
            guard let dto = dto else { return completion(.failure(error!)) }
            completion(.success(self.convertToArticles(dto: dto)))
        }
    }
}

// MARK: - Convert Method
extension APIService {
    private func convertToArticles(dto: ArticleDTO) -> [Article] {
        var articles: [Article] = []
        dto.articles?.forEach({ article in
            articles.append(Article(author: article.author ?? "nil", title: article.title, description: article.description ?? "nil", urlToImage: article.urlToImage ?? "nil"))
        })
        return articles
    }
}

Reference

URLSession Tutorial (iOS 기본 network 통신)

profile
Hello

0개의 댓글