본문 바로가기
Xcode/IOS

IOS)Moya 간단 사용 정리하기

by 후르륵짭짭 2022. 6. 26.
728x90
반응형

서울 어딘가에서

안녕하세요. 후르륵짭짭입니다.

사내에서 Network 통신을 좀 더 수월하게 사용하기 위해서

Moya 라이브러리를 사용하고 있는데, 너무 유명한 라이브러리라 정리 할까 말까 고민하다가

제가 자주 사용법을 헷갈려서 작성하려고 합니다. ㅎㅎㅎㅎ

 

일단 Moya 라이브러리는 정말 유명한 알라모파이어 라이브러를 Enum 타입으로 더욱 사용하기 쉽게 만들었고

테스트 또한 쉽게 할 수 있도록 했습니다. (<- 이 부분은 나중에 정리하도록 하겠습니다. )

그래서 Moya를 안 사용해본 사람은 있어도 한번만 사용해본 사람은 없을것 같단 생각이 듭니다.

만약 사내에서 Custom으로 적용할 부분이 많다면,,, 사용 안하겠지만요

 

일단 사내에서는 Rx+Moya 방식으로 적용하고 있지만, 

여기서는 그냥 순수 Moya로 작성해보도록 하겠습니다.

 

** API Service 생성하기 **

Moya의 최대 장점은 이 서비스가 Enum으로 적용되어 있어 쉽게 타입을 지정 할 수 있습니다.

위에 처럼 필요한 부분에 채워넣어주기만 하면 됩니다. 

 - 케이스 선언 : 이 서비스가 제공하는 서비스들을 정의 해줍니다.

 - BaseUrl : 기본 도메인을 작성하는 겁니다.

위에 처럼 기본적인 URL을 선언해줍니다.
그러면 해당 Case에 기본 URL로 선언해준 것으로 Domain을 설정해줍니다.

- Path : 기본 Path를 정의 해줍니다.

Path도 마찮가지 입니다.

- Method : 어떤 방식으로 통신 할 것이냐

Method는 총 get post delete put 를 주로 사용하는데, 이 외에도 여러개가 있습니다.

- task : 어떻게 데이터를 전송 할 것이냐

1. requestPlain : 기본적으로 어떤 값을 넣지 않을 때 사용합니다. (주로 GET)
2. requestData : Body에 데이터를 담아서 보낼 때 사용합니다. (주로 POST)
3. requestParameters : 파일을 다운로드 할 때 다운로드 링크를 담습니다. (주로 GET)

이 외에도 다양한 Task를 수행하는 Method들이 존재합니다

- headers : 헤더에 어떤 값을 넣을 것이냐

Header에 추가적으로 담아야 할 것이 있다면 적어주면 됩니다.

 

** Provider Wrapper 생성 **

기본적으로 Moya는 Provider 객체와 TargetType(서비스)만 있으면 네트워크 통신을 할 수 있습니다.

하지만 이럴경우 매번 불편하게 Provider를 선언해주고 Custom을 할 수 없게 됩니다.

따라서 저는 이 Provider를 상속 받고 좀 더 Custom하게 사용할 수 있도록 하부 기능을 만들었습니다.

위에 처럼 사용하면 request나 다른 기능을 제 입맛 대로 변환해서 사용 할 수 있습니다.

- init 

endPointClosure : 통신 요청을 지정해서 할 수 있습니다. (이부분은 추후 다루겠습니다. 주로 테스트용)
stubClosure : 테스트를 할 때 사용합니다.
plugins : Custom 플러그인 입니다. 원하는 방식으로 Request와 Response를 변경 할 수 있습니다.

 

- requestSuccessRes

func requestSuccessRes<Model : Codable>(target : Provider, instance : Model.Type , completion : @escaping(Result<Model, MoyaError>) -> () )

위에 같이 제너릭을 사용해서 타입만 지정하면 값이 반환 될 수 있도록 하였고
상태코드가 200~300 일 때만 성공을 반환 할 수 있도록 변경 했습니다.

=> GET 호출 

=> POST 호출

=> Parameter 호출

 

- requestDownload

 case .download:
    return .downloadDestination { temporaryURL, response in
        print("temporaryURL : \(temporaryURL.absoluteString)")
        let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let desination = directory.appendingPathComponent("SwiftDocument.pdf")
        return (desination , [.removePreviousFile])
    }
}

위에 Task에서 Download에 다운로드할 Path를 지정해줍니다.
그리고 해당 다운로드가 완료 되면 결과 값을 반환하도록 했습니다

=> DownLoad 호출 

다음 처럼 Progress Block을 지정해주면 해당 부분에 

Progress : 0.01558497396955489
Complete : false
Progress : 0.03116994793910978
Complete : false
Progress : 0.04675492190866467
Complete : false
Progress : 0.29611450542154294
Complete : false
Progress : 0.5454740889344212
Complete : false
Progress : 0.7948336724472994
Complete : false
Progress : 1.0
Complete : false

위와 같이 결과가 출력이 됩니다.

그리고 지정한 FIlePath에 파일이 저장이 됩니다.

 

** Custom PlugIn **

원하는 방식 대로 Request와 Response가 내려온 시점에 특정 작업을 설정 할 수 있습니다.

예를들어 아래 처럼 로그를 찍을 수도 있겠죠??

URL Request - download : https://www.tutorialspoint.com/swift/swift_tutorial.pdf

URL Response - download : success(Status Code: 200, Data Length: 0)

그리고 이 PlugIn을 Provider Wrapper에 넣어줍니다.

 

지금 까지 Moya에 대한 사용법을 정리 해봤습니다.

여기에 작성한 것은 Moya가 어떤 것인지 정확하게 분석한 것이 아니라

사용법을 작성 한 겁니다. 

큰 도움이 됐음 좋겠습니다. 

 

** 전체 코드 **

    - ViewController 

더보기
import UIKit
import Moya
import RxSwift

struct VersionModel: Codable {
    let version: String
    let build: Int
    let requiredUpdate: Bool
    let comment: String
    let manager: String
}

struct PostResult : Codable {
    let id : Int
}

struct UserModel : Codable{
    let id : Int
    let name : String
    let username : String
    let email : String
}

struct CommentsModel : Codable {
    let id : Int
    let postId : Int
    let name : String
    let email : String
}

class ViewController: UIViewController {

    let apiManager = APIManager()
    let wrapper = NetworkWrapper<APIService>(plugins: [CustomPlugIn()])

    override func viewDidLoad() {
        super.viewDidLoad()

        print("URL : \(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteString)")
//        Wrapper를사용한방식()
    }


    private func Wrapper를사용하지않는방식(){
        apiManager.version(completion: {data in
            print("Real 데이터 : \(data)")
        }, failure: {error in

            print("Real 에러 : \(error)")
        })

        apiManager.testVersion { data in
            print("테스트 데이터 : \(data)")
        } failure: { error in
            print("테스트 에러 : \(error)")
        }

    }


    private func Wrapper를사용한방식(){
        wrapper.requestSuccessRes(target: .version, instance: [UserModel].self) { result in
            switch result {
            case .success(let 결과):
                print("Wrapper version 결과 : \(결과)")
            case .failure(let error):
                print("Wrapper error 실패 : \(error.localizedDescription)")
            }
        }
    }


    @IBAction func sendPost(_ sender: Any) {

        let userModel = UserModel(id: 12, name: "Hello", username: "World", email: "empty")
        guard let data = try? JSONEncoder().encode(userModel) else {return}

        apiManager.versionPost(data: data){ data in
            print("Real Post 데이터 : \(data)")
        } failure: { error in
            print("Real Post 실패 데이터 : \(error)")
        }

        
        apiManager.testVersionPost(data: data) { data in
            print("Post 테스트 데이터 : \(data)")
        } failure: { error in
            print("Post 테스트 실패 데이터 : \(error)")
        }

        wrapper.requestSuccessRes(target: .post(data), instance: PostResult.self) { result in
            switch result {
            case .success(let 결과):
                print("Wrapper Post 결과 : \(결과)")
            case .failure(let error):
                print("Wrapper error 실패 : \(error.localizedDescription)")
            }
        }

        
    }
    
    @IBAction func parameter(_ sender: Any) {

        wrapper.requestSuccessRes(target: .parameter(postId: 1), instance: [CommentsModel].self) { result in
            switch result {
            case .success(let 결과):
                print("Wrapper parameter version 결과 : \(결과)")
            case .failure(let error):
                print("Wrapper parameter error 실패 : \(error.localizedDescription)")
            }
        }

    }

    @IBAction func download(_ sender: Any) {

        wrapper.requestDownload(target: .download) { progress in
            print("Progress : \(progress.progress)")
            print("Complete : \(progress.completed)")

        } completion: { result in

            if result {
                print("Download 성공")
            }
            else{
                print("Download Fail")
            }

        }


    }
}

 

 - Moya Logic 

더보기
import Foundation
import Moya

enum APIService {
    case version
    case post(Data)
    case parameter(postId : Int)
    case download
}

extension APIService : TargetType {
    var baseURL: URL {
        switch self {
        case .version , .post , .parameter:
            return URL(string: "https://jsonplaceholder.typicode.com")!
        case .download :
            return URL(string: "https://www.tutorialspoint.com/swift/swift_tutorial.pdf")!
        }
    }
    
    var path: String {
        switch self{
        case .version :
            return "users"
        case .post :
            return "users"
        case .parameter:
            return "comments"
        case .download:
            return ""
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .version:
            return .get
        case .post :
            return .post
        case .parameter(_):
            return .get
        case .download:
            return .get
        }
    }
    
    var sampleData: Data {

        var data : Data?
        
        switch self {
        case .version:
            let version : VersionModel = VersionModel(version: "2.2.2",
                                       build: 22,
                                       requiredUpdate: false,
                                       comment: "테스트 데이터입니다.",
                                       manager: "테스트")

            data = try? JSONEncoder().encode(version)
            
        case .post(let input) :
            data = input

        case .parameter(postId:_):
            return Data()
        case .download:
            return Data()
        }

        if data == nil {
            return Data()
        }
        return data!

    }
    
    var task: Task {
        switch self {
        case .version:
            return .requestPlain
        case .post(let data):
            return .requestData(data)
        case .parameter(postId: let postId):
            return .requestParameters(parameters: ["postId" : postId, "hello":postId], encoding: URLEncoding.queryString)
        case .download:
            return .downloadDestination { temporaryURL, response in
                print("temporaryURL : \(temporaryURL.absoluteString)")
                let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
                let desination = directory.appendingPathComponent("SwiftDocument.pdf")
                return (desination , [.removePreviousFile])
            }
        }
    }
    
    var headers: [String : String]? {
        return nil
    }
    
    
}


class CustomPlugIn : PluginType {
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        print("URL Request - \(target) : \(request.url?.absoluteString ?? "없음")")
        return request
    }

    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>{
        print("URL Response - \(target) : \(result)")
        return result
    }

}


class NetworkWrapper<Provider : TargetType> : MoyaProvider<Provider> {

    init(endPointClosure : @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
         stubClosure : @escaping StubClosure = MoyaProvider.neverStub,
         plugins : [PluginType] ){

        let session = MoyaProvider<Provider>.defaultAlamofireSession()
        session.sessionConfiguration.timeoutIntervalForRequest = 20
        session.sessionConfiguration.timeoutIntervalForResource = 20

        super.init(endpointClosure: endPointClosure, stubClosure: stubClosure, session: session, plugins: plugins)
    }

    func requestSuccessRes<Model : Codable>(target : Provider, instance : Model.Type , completion : @escaping(Result<Model, MoyaError>) -> () ){
        self.request(target) { result in
            switch result {

            case .success(let response):

                if (200..<300).contains(response.statusCode) {
                    print("Status Code : \(response.statusCode)")
                    if let decodeData = try? JSONDecoder().decode(instance, from: response.data) {
                        completion(.success(decodeData))
                    }
                    else{
                        completion(.failure(.requestMapping("디코딩오류")))
                    }
                }
                else{
                    completion(.failure(.statusCode(response)))
                }

            case .failure(let error):
                completion(.failure(error))
            }
        }
    }

    func requestDownload(target : Provider, progress : @escaping ProgressBlock, completion : @escaping(Bool) -> ()){
        self.request(target, progress: progress) { result in
            switch result {

            case .success(let response):

                if (200..<300).contains(response.statusCode) {
                    print("Status Code : \(response.statusCode)")
                    completion(true)

                }
                else{
                    completion(false)
                }

            case .failure(_):
                completion(false)
            }
        }
    }

}







struct APIManager {
    
    private let provider = MoyaProvider<APIService>()
    
    let customEndpointClosure = { (target : APIService) -> Endpoint in
        
        return Endpoint(url: URL(target: target).absoluteString, sampleResponseClosure: {.networkResponse(400, target.sampleData)}, method: target.method, task: target.task, httpHeaderFields: target.headers)
        
    }
    
    private var testingProvider : MoyaProvider<APIService>?
    
    init(){
        
        testingProvider = .init(endpointClosure: customEndpointClosure,stubClosure: MoyaProvider.immediatelyStub)
        
    }
    
    func version(completion : @escaping ([UserModel]) -> () , failure : @escaping (String) -> ()){
        
        provider.request(.version, completion: { result in
            
            switch result {
            case let .success(response) :
                do {
                    let usermodel = try JSONDecoder().decode([UserModel].self, from: response.data)
                    
                    completion(usermodel)
                }
                catch let error{
                    failure(error.localizedDescription)
                }
                
            case let .failure(error):
                failure(error.localizedDescription)
            }
            
        })
    }

    func download(){
        provider.request(.download) { progress in
            print("Progress : \(progress.progress)")
            print("Complete : \(progress.completed)")
        } completion: { result in
            switch result {
            case let .success(response) :
                print("Download Success : \(response)")

            case let .failure(error):
                print("Download Fail : \(error)")
            }
        }

    }

    func versionPost(data : Data , completion : @escaping(String)->() , failure : @escaping(String)->()){
        provider.request(.post(data)) { result in
            switch result {
            case let .success(response) :
                let res = String(data: response.data, encoding: .utf8) ?? "NO Data"
                completion(res)

            case let .failure(error):
                failure(error.localizedDescription)
            }
        }
    }
    
    func testVersion(completion : @escaping (VersionModel) -> () , failure : @escaping (String) -> ()){
        
        testingProvider?.request(.version, completion: { result in
            
            switch result {
            case let .success(response) :
                do {

                    if (200..<300).contains(response.statusCode) {
                        print("Status Code : \(response.statusCode)")
                    }
                    else{
                        print("Error Status Code : \(response.statusCode)")
                    }

                    let version = try JSONDecoder().decode(VersionModel.self, from: response.data)
                    
                    completion(version)
                }
                catch let error{
                    failure(error.localizedDescription)
                }
                
            case let .failure(error):
                failure(error.localizedDescription)
            }
            
        })
        
    }
    
    func testVersionPost(data : Data ,completion : @escaping (UserModel) -> (), failure : @escaping (String) -> ()){
        
        testingProvider?.request(.post(data), completion: { result in
            
            switch result {
            case let .success(response) :
                do {
                    let userModel = try JSONDecoder().decode(UserModel.self, from: response.data)
                    
                    completion(userModel)
                }
                catch let error{
                    failure(error.localizedDescription)
                }
                
            case let .failure(error):
                failure(error.localizedDescription)
            }
            
            
        })
        
    }
    
}

 

참고 사이트 : 

Moya Provider 커스텀 

https://leejigun.github.io/Swift_Moya_Provider_custom

 

Swift Moya Provider 커스텀

Swift Moya Provider 커스텀 예전에 Moya + RxSwift에 대한 포스트를 올린적이 있었는데, 이번에는 제가 프로젝트에서 사용하고 있는 Moya Provider를 커스텀 한 내용을 공유하고자 합니다. Plugins moya plugun : ht

leejigun.github.io

Moya : 

https://github.com/Moya/Moya/blob/master/Readme.md

 

GitHub - Moya/Moya: Network abstraction layer written in Swift.

Network abstraction layer written in Swift. Contribute to Moya/Moya development by creating an account on GitHub.

github.com

Moya Download : 

https://gist.github.com/boboboa32/1f4305c6b5b1f660b5de7da0da473af9

 

Moya file download

Moya file download. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

Moya Test API 만들기 

https://swieeft.github.io/2020/11/10/MoyaTest.html

 

Moya에서 테스트 API 만들기 - 뀔뀔(swieeft)의 개발새발기

안녕하세요. 정말 오랜만에 블로그에 글을 작성하게 됬네요. 블로그를 시작할 때 한달에 한번 이상은 꼭 써야지 다짐 했었는데 막상 시작하고 나니 한달에 한번 글 쓰는 것도 어렵구나라고 많이

swieeft.github.io

 

기타 

- API 검증을 위한 사이트 

https://jsonplaceholder.typicode.com/

 

JSONPlaceholder - Free Fake REST API

{JSON} Placeholder Free fake API for testing and prototyping. Powered by JSON Server + LowDB. Tested with XV. As of Oct 2021, serving ~1.7 billion requests each month.

jsonplaceholder.typicode.com

- 쿼리 스트링이란 

https://velog.io/@pear/Query-String-%EC%BF%BC%EB%A6%AC%EC%8A%A4%ED%8A%B8%EB%A7%81%EC%9D%B4%EB%9E%80

 

Query String 쿼리스트링이란?

사용자가 입력 데이터를 전달하는 방법중의 하나로, url 주소에 미리 협의된 데이터를 파라미터를 통해 넘기는 것을 말한다.http://host:port/path?querystringquery parameters( 물음표 뒤에 = 로 연결된 key valu

velog.io

 

728x90
반응형

댓글