이전 글에서 URLSession의 boiler plate 코드들을 줄이고 조금 편하게 사용하는 방법을 알아보았었다.
조금 더 업그레이드를 시켜보자.
주로 static 변수들로 이루어진 baseURL등을 지정하는 파일
struct Config {
static let baseURL = "https://velog.io/@tmdckd232"
}
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"
}
}
}
기존에는 API별로 return type을 지정해야하는 문제가 있다. 따라서 각 request type에 따른 URLRequest를 설정하는 Resource를 생성하자. request에는 기본 GET 방식의 request, parameter들이 있는 GET request, body가 있는 POST, PUT, PATCH, DELETE request가 있으므로 따로 init을 만들어준다.
URL을 전달해주고 결과로 나오는 원하는 타입으로 parsing만 하면 된다.
GET method에서 paramete와 함께 request를 보내는 것은 "https://baseURL.com/?name=value" 형태로 변환해서 요청을 하는 것이다.
parameter는 [String: String]
의 dictionary 형태로 받고, 그 값은 URLQueryItem으로 추가한다.
그리고 그 결과로 나온 url을 URLComponent에서 꺼내 사용하고 parsing을 한다.
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의 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()
}
}
만들어둔 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
}
}