Moya를 사용해보자

진행 하고 있는 프로젝트에는 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)
            }
        }