Unsplash API 3개를 이용하는 네트워크 통신 코드를 추상화한다 (Alamofire 활용)
싱글톤 패턴을 이용한다
API마다 함수를 각각 구현한다
통신을 위해 필요한 값들은 함수 내에서 상수로 선언한다
final class NetworkBasic {
// 싱글톤 패턴
static let shared = NetworkBasic()
private init() { }
// 1. 사진 검색
func request(query: String, completionHandler: @escaping (Photo?, Error?) -> Void) {
let key = "tHisIsunsPlaShKey"
let url = "https://api.unsplash.com/search/photos"
let headers: HTTPHeaders = ["Authorization" : "Client-ID \(key)"]
let query = ["query": query]
AF.request(url, method: .get, parameters: query, encoding: URLEncoding(destination: .queryString), headers: headers)
.responseDecodable(of: Photo.self) { response in
switch response.result {
case .success(let data):
completionHandler(data, nil)
case .failure(let error):
completionHandler(nil, error)
}
}
}
// 2. 랜덤 사진
func random(completionHandler: @escaping (PhotoResult?, Error?) -> Void) {
let key = "tHisIsunsPlaShKey"
let url = "https://api.unsplash.com/photos/random"
let headers: HTTPHeaders = ["Authorization" : "Client-ID \(key)"]
AF.request(url, method: .get, headers: headers)
.responseDecodable(of: PhotoResult.self) { response in
switch response.result {
case .success(let data):
completionHandler(data, nil)
case .failure(let error):
completionHandler(nil, error)
}
}
}
// 3. 사진 상세
func detailPhoto(id: String, completionHandler: @escaping (PhotoResult?, Error?) -> Void) {
let key = "tHisIsunsPlaShKey"
let url = "https://api.unsplash.com/photos/\(id)"
let headers: HTTPHeaders = ["Authorization" : "Client-ID \(key)"]
AF.request(url, method: .get, headers: headers)
.responseDecodable(of: PhotoResult.self) { response in
switch response.result {
case .success(let data):
completionHandler(data, nil)
case .failure(let error):
completionHandler(nil, error)
}
}
}
}
1 코드는 모든 함수에 같은 key
, headers
, method
값이 중복되어 있다
url
도 앞부분은 거의 비슷하고, / 뒷부분만 차이가 있다
따라서 각 api를 열거형 case로 나누고, 해당 값들은 연산 프로퍼티로 가져온다
enum SeSACAPI {
// key는 항상 동일하다
private static let key = "tHisIsunsPlaShKey"
// 1, 2, 3 케이스를 나눈다
// 필요한 추가 파라미터는 열거형의 연관값을 이용한다
case search(query: String)
case random
case photo(id: String)
// url
var baseURL: String {
return "https://api.unsplash.com/"
}
var endpoint: URL {
switch self {
case .search:
return URL(string: baseURL + "search/photos")!
case .random:
return URL(string: baseURL + "photos/random")!
case .photo(let id):
return URL(string: baseURL + "photos/\(id)")!
}
}
// header
var header: HTTPHeaders {
return ["Authorization" : "Client-ID \(SeSACAPI.key)"]
}
// method
var method: HTTPMethod {
return .get
}
// query
var query: [String: String] {
switch self {
case .search(let query):
return ["query": query]
case .random, .photo:
return ["": ""]
}
}
}
결과적으로 함수 내부를 간소화할 수 있다 (사진 검색)
func request(query: String, completionHandler: @escaping (Photo?, Error?) -> Void) {
let api = SeSACAPI.search(query: query)
AF.request(api.endpoint, method: api.method, parameters: api.query, encoding: URLEncoding(destination: .queryString), headers: api.header)
.responseDecodable(of: Photo.self) { response in
switch response.result {
case .success(let data):
completionHandler(data, nil)
case .failure(let error):
completionHandler(nil, error)
}
}
}
completionHandler
의 매개변수는 (Photo?, Error?)
이다Photo?
에 값이 들어가고 Error?
는 nil,Error?
에 값이 들어가고 Photo?
는 nil이다불필요한 경우의 수를 줄이고 성공 or 실패 두 가지로만 나눠지도록 Result
열거형을 사용한다
추가적인 장점은, 매개변수에 대한 옵셔널 바인딩을 할 필요가 없어진다
함수 선언 (랜덤 사진)
func random(completionHandler: @escaping (Result<PhotoResult, Error>) -> Void) {
let api = SeSACAPI.random
AF.request(api.endpoint, method: api.method, headers: api.header)
.responseDecodable(of: PhotoResult.self) { response in
switch response.result {
case .success(let data):
completionHandler(.success(data))
case .failure(let error):
completionHandler(.failure(error))
}
}
}
(Photo?, Error?)
NetworkBasic.shared.random { photo, error in
if let photo { print("성공 : ", photo)}
if let error { print("실패 : ", error)}
}
Result<Photo, Error>
NetworkBasic.shared.random { response in
switch response {
case .success(let success):
dump(success)
case .failure(let failure):
print(failure)
}
}
네트워크 통신에 실패했을 때, 상태 코드(statusCode)로 뭐가 문제인지 파악할 수 있다
대표적인 상태 코드(401, 403, ...)를 rawValue로 갖는 열거형을 만든다
각 케이스별로 내가 원하는 description을 연산 프로퍼티로 받는다
enum SeSACError: Int, /*Error,*/ LocalizedError {
case unauthorized = 401
case permissionDenied = 403
case invalidServer = 500
case missingParameter = 400
var errorDescription: String {
switch self {
case .unauthorized:
return "인증 정보가 없어유"
case .permissionDenied:
return "권한이 없어요"
case .invalidServer:
return "서버에 문제있어요"
case .missingParameter:
return "파라미터가 없어요"
}
}
}
errorDescription
을 선언한 것처럼 연산 프로퍼티 형식으로 활용할 수 있을 것 같다Error
프로토콜을 채택하고 있기 때문에 SeSACError
에서는 얘만 채택해도 괜찮다함수 선언
func random(completionHandler: @escaping (Result<PhotoResult, SeSACError>) -> Void) {
let api = SeSACAPI.random
AF.request(api.endpoint, method: api.method, headers: api.header)
.responseDecodable(of: PhotoResult.self) { response in
switch response.result {
case .success(let data):
completionHandler(.success(data))
case .failure(_):
let statusCode = response.response?.statusCode ?? 500
guard let error = SeSACError(rawValue: statusCode) else { return }
completionHandler(.failure(error))
}
}
}
Result
에 들어가는 제네릭 타입도 SeSACError
로 바꿔준다failure(let error)
에서 error
값을 사용하지 않는다.NetworkBasic.shared.random { response in
switch response {
case .success(let success):
dump(success)
case .failure(let failure):
print(failure.errorDescription) // 연산 프로퍼티
print(failure.localizedDescription) // Error 프로토콜의 프로퍼티
print(failure.failureReason) // LocalizedError 프로토콜의 프로퍼티
}
}
여태까지 정리한 코드를 보면,
func request(query: String, completionHandler: @escaping (Result<Photo, SeSACError>) -> Void) {
let api = SeSACAPI.search(query: query)
AF.request(api.endpoint, method: api.method, parameters: api.query, encoding: URLEncoding(destination: .queryString), headers: api.header)
.responseDecodable(of: Photo.self) { response in
switch response.result {
case .success(let data):
completionHandler(.success(data))
case .failure(_):
let statusCode = response.response?.statusCode ?? 500
guard let error = SeSACError(rawValue: statusCode) else { return }
completionHandler(.failure(error))
}
}
}
func random(completionHandler: @escaping (Result<PhotoResult, SeSACError>) -> Void) {
let api = SeSACAPI.random
AF.request(api.endpoint, method: api.method, headers: api.header)
.responseDecodable(of: PhotoResult.self) { response in
switch response.result {
case .success(let data):
completionHandler(.success(data))
case .failure(_):
let statusCode = response.response?.statusCode ?? 500
guard let error = SeSACError(rawValue: statusCode) else { return }
completionHandler(.failure(error))
}
}
}
func detailPhoto(id: String, completionHandler: @escaping (Result<PhotoResult, SeSACError>) -> Void) {
let api = SeSACAPI.photo(id: id)
AF.request(api.endpoint, method: api.method, headers: api.header)
.responseDecodable(of: PhotoResult.self) { response in
switch response.result {
case .success(let data):
completionHandler(.success(data))
case .failure(_):
let statusCode = response.response?.statusCode ?? 500
guard let error = SeSACError(rawValue: statusCode) else { return }
completionHandler(.failure(error))
}
}
}
셋 다 거의 비슷하게 생겼다.
SeSACAPI
열거형을 받아서 어떤 요청인지 구분하고,Photo
, PhotoResult
)도 매개변수로 받아준다
func request<T: Decodable>(type: T.Type, api: SeSACAPI, completionHandler: @escaping (Result<T, SeSACError>) -> Void) {
AF.request(api.endpoint, method: api.method, parameters: api.query, encoding: URLEncoding(destination: .queryString), headers: api.header)
.responseDecodable(of: T.self) { response in
switch response.result {
case .success(let data):
completionHandler(.success(data))
case .failure(_):
let statusCode = response.response?.statusCode ?? 500
guard let error = SeSACError(rawValue: statusCode) else { return }
completionHandler(.failure(error))
}
}
}
URLRequestConvertible
프로토콜을 채택해서 Router 패턴을 구현한다SeSACAPI
열거형 대신 사용한다)method
, headers
, parameter
와 같은 초기 매개변수를 URLRequestConvertible로 캡슐화해서 값을 전달한다.Router
열거형
enum Router: URLRequestConvertible {
private static let key = "tHisIsunsPlaShKey"
case search(query: String)
case random
case photo(id: String)
// url
private var baseURL: URL {
return URL(string: "https://api.unsplash.com/")!
}
private var path: String {
switch self {
case .search:
return "search/photos"
case .random:
return "photos/random"
case .photo(let id):
return "photos/\(id)"
}
}
// header
private var header: HTTPHeaders {
return ["Authorization" : "Client-ID \(Router.key)"]
}
// method
var method: HTTPMethod {
return .get
}
// query
var query: [String: String] {
switch self {
case .search(let query):
return ["query": query]
case .random, .photo:
return ["": ""]
}
}
func asURLRequest() throws -> URLRequest {
let url = baseURL.appendingPathComponent(path)
var request = URLRequest(url: url)
request.headers = header
request.method = method
request = try URLEncodedFormParameterEncoder(destination: .methodDependent).encode(query, into: request)
return request
}
}
asURLRequest
안에서 헤더와 메서드를 request에 추가해준다함수 선언
func requestConvertible<T: Decodable>(type: T.Type, api: Router, completionHandler: @escaping (Result<T, SeSACError>) -> Void ) {
AF.request(api)
.responseDecodable(of: T.self) { response in
switch response.result {
case .success(let data):
completionHandler(.success(data))
case .failure(_):
let statusCode = response.response?.statusCode ?? 500
guard let error = SeSACError(rawValue: statusCode) else { return }
completionHandler(.failure(error))
}
}
}
api
매개변수의 타입이 Router
로 바뀌었다request
함수 내에 들어가는 매개변수가 URL
타입이 아니고, Router
타입으로 바로 들어간다Q. request의 매개변수 타입이 뭐길래
URL
타입도 들어가고Router
타입이 들어갈까
URLRequestConvertible
Router
를 선언할 때 채택한 프로토콜이다. 따라서Router
타입이 들어갈 수 있다
URLConvertible
- 찾아보니까
URL
에서 채택하고 있다. 따라서URL
타입이 들어갈 수 있다
class NetworkViewModel {
func requestRandom(completionHandler: @escaping (URL) -> Void) {
Network.shared.requestConvertible(type: PhotoResult.self, api: .random) { response in
switch response {
case .success(let success):
dump(success)
completionHandler(URL(string: success.urls.thumb)!)
case .failure(let failure):
print(failure.errorDescription)
}
}
}
}
viewModel.requestRandom { url in
self.imageView.kf.setImage(with: url)
}
completionHandler
에 원하는 작업(이미지 로딩)을 넣어서 함수를 실행해야 한다listener
로 넣어주면,class NetworkViewModel {
// Observable 타입의 url 프로퍼티
var url = Observable(URL(string: ""))
func requestRandom() {
Network.shared.requestConvertible(type: PhotoResult.self, api: .random) { response in
switch response {
case .success(let success):
dump(success)
// 새로운 값으로 업데이트
self.url.value = URL(string: success.urls.thumb)!
case .failure(let failure):
print(failure.errorDescription)
}
}
}
}
// listener에 원하는 작업 바인딩
viewModel.url.bind { url in
self.imageView.kf.setImage(with: url)
}
// 값이 바뀔 때마다 imageView에 사진이 로딩된다
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.viewModel.requestRandom()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.viewModel.requestRandom()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.viewModel.requestRandom()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.viewModel.requestRandom()
}
extension NetworkViewModel {
enum Action {
case requestRandom
}
func doAction(_ type: Action) {
switch type {
case .requestRandom:
requestRandom();
}
}
}
viewModel.url.bind { url in
self.imageView.kf.setImage(with: url)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.viewModel.doAction(.requestRandom)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.viewModel.doAction(.requestRandom)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.viewModel.doAction(.requestRandom)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.viewModel.doAction(.requestRandom)
}