진행 하고 있는 프로젝트에는 API가 2가지가 들어갑니다. 처음 화면에 들어갔을때 추천 이미지를 불러오는 API와 이미지 검색을 하는 API가 사용됩니다. 기존 클린 아키텍쳐를 참고하여 프로젝트를 진행하고 있는데, 네트워크 레이어를 나눈 부분은 소스코드도 이해하기 어렵고, 왜 이런식으로 나누는지도 이해하기 어려웠습니다. 그래서 이런 어려움 없이 추상화를 제공해주는 Moya를 사용해보기로 했습니다.
Moya란?
Moya는 Alamofire를 이용할 때 추상화한 네트워크 계층을 작성할 필요없이 사용자가 매우 단순하게 캡슐화를 할 수 있고 쉽게 사용하게 해주는 라이브러리 입니다. 예를 하나 들어봅시다.
일반적으로 네트워크 계층을 작성할 때 아래와 같이 NetworkManager, APIManager등등 이라 부르는 네트워크 추상화 계층을 작성합니다.
class NetworkManager{
func fetchReviews(start: Int, completion : @escaping (Result<[ReviewModel], NetworkError>) -> Void){
let urlStr = "API_URL"
guard let url = URL(string: urlStr) else {
completion(.failure(.url))
return
}
let session = URLSession(configuration: .default)
session.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
completion(.failure(.network))
return
}
let decorder = JSONDecoder()
guard let data = try? decorder.decode(ReviewList.self, from: data) else {
completion(.failure(.decode))
return
}
completion(.success(data.data))
}.resume()
}
func uploadPost(with data : PostReviewModel, completion : @escaping (Bool)->Void){
guard let url = URL(string: "API_URL") else {
completion(false)
return
}
guard let data = try? JSONEncoder().encode(data) else {
completion(false)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = data
URLSession.shared.dataTask(with: request) { data, response, error in
guard let _ = data, error == nil else {
return
}
guard let response = response as? HTTPURLResponse, (200..<300) ~= response.statusCode else{
return
}
print(response.statusCode)
completion(true)
}.resume()
}
}
이런 방식은 몇가지 단점이 있습니다. API 작업이 많아지고 다양해질수록 코드가 점점 복잡해져서 알아보기 힘들어지고, 의존성도 여기저기 붙어있을 겁니다. Moya의 공식문서에서는 이러한 방식의 단점을 아래와 같이 나열합니다.
- 새 앱을 만들기 어렵습니다.
- 유지보수하기가 힙듭니다. (소스코드가 복잡해 뭐가 뭔지 알기 어렵습니다.)
- 단위테스트를 어렵게 만듭니다.
이런 단점을 해소하기 위해 Moya는 이를 단순화해 사용자가 보다 단순하게 작업을 할 수 있도록 도와줍니다.
Moya 사용해보기
Moya는 enum으로 작업을 정의하는것으로 시작합니다. 저의 경우에는 추천 이미지를 가져오는 작업, 이미지를 검색하는 작업입니다.
enum NetworkService {
case fetchRecommendImageList
case searchImageList(query: ImageQuery, page: Int)
}
그리고 해당 타입에 TargetType을 채택하여줍니다. 채택하고 난 후 baseURL을 처음 정의할텐데, 이때 자신의 값에 의존해서는 안됩니다. 저의 경우에는 BaseURL이 동일하여 분기처리를 해 줄 필요가 없습니다.
extension NetworkService: TargetType {
var baseURL: URL {
guard let base = Bundle.main.object(forInfoDictionaryKey: "APIBASEURL") as? String else { fatalError() }
guard let url = URL(string: base) else {fatalError()}
return url
}
}
그리고 각 작업에 따라 endpoint가 지정되도록 해줍니다.
var path: String {
switch self {
case .fetchRecommendImageList:
return "/photos"
case .searchImageList(_):
return "/search/photos"
}
}
method는 현재 GET 메소드 하나만 사용하기에 get을 리턴해줍니다. 이 역시 분기처리가 필요하다면 해주면 됩니다.
var method: Moya.Method {
return .get
}
task는 파라미터를 포함하는 작업을 정의하는 곳입니다. 검색하는 작업만 파라미터가 필요하기에 아래와 같이 정의해줍니다.
var task: Moya.Task {
switch self {
case .searchImageList(let query, let page):
return .requestParameters(parameters: ["query":query, "page":page], encoding: JSONEncoding.default)
case .fetchRecommendImageList:
return .requestPlain
}
}
task에는 위에 말고도 다양한 작업이 존재 합니다. 이에 관해서는 공식문서에서 살펴보셔서 필요한게 있으면 사용하시면 될 것 같습니다.
마지막으로 요청을 보내는데 필요한 headers를 정의합니다. API를 사용하고 있어서 저는 개인키를 헤더에 넣어서 보낼겁니다.
var headers: [String : String]? {
let nativeAppKey = Bundle.main.infoDictionary?["PICTEREST_NATIVE_APP_KEY"] as? String
return ["Authorization":"Client-ID \(nativeAppKey!)"]
}
이제 실제로 요청을 해봅시다. 요청을 Provider를 통해 요청합니다.
원하는 곳에서 Provider를 생성 한 후 request하면 됩니다.
let provider = MoyaProvider<NetworkService>()
provider.request(.fetchRecommendImageList) { result in
switch result {
case .success(let response):
//do something...
case .failure(let error):
print(error.localizedDescription)
}
}