서버 통신을 위한 Moya 알아보기

Duna·2021년 7월 22일
3
post-thumbnail

모야쟁이로서 여러분께 모야에 대해 소개해드리려고 합니다. 서버 통신에서 모야를 한 번 써보십시오.


🎁 Moya가 뭐야?


  • MoyaAlamofire를 사용하면서 네트워크 계층을 구조화하는 다른 접근방법을 제공하는 라이브러리입니다.
  • Moya는 일반적으로 열거형(enum)을 사용해서 네트워크 요청을 타입 안전한 방식으로 캡슐화하는데 초점을 맞춘 네트워킹 라이브러리입니다.
  • Moya라이브러리는 urlSession, Alamofire를 한 번 더 감싼 통신 API입니다.
  • Moya는 추상화 네트워킹 라이브러리입니다.

→ 무슨 소리냐?

✨ 기분 좋아지는 깔끔한 네트워크 레이어 구성
Moya는 자체적으로 네트워킹을 수행하지 않아요!

MoyaAlamofire의 네트워킹 기능을 사용하고 Alamofire를 추상화하기 위한 추가적인 능력, 타입, 개념을 제공합니다. 즉, Alamofire 를 기반으로 하고 있는 MoyaAlamofire 를 사용하는 것!


🤔 그래서 어떻게 쓰는디?


Moya를 사용해서 서버 통신을 하게 된다면 제가 제시해드리는 방식으로 진행해보세요.
진행중에 문제가 생기거나 이상한 부분이 있다면 언제든 물어봐주십쇼. 제가 틀리게 설명한 부분이 있을수도 있으니 언제든 지적 부탁드릴게요!

1️⃣ : 서버 통신할 때 우리가 사용하는 baseURL은 항상 동일하기 때문에 GeneralAPI 구조체를 생성해서 baseURL를 저장해둡시다.

    import Foundation

    struct GeneralAPI {
        static let baseURL = "http://13.124.39.26"
        // static let token = "eyJhbGciOiJIUzI1NiJ9.NjA1MTgyZDUwNzgyMjYzZTllMjFmNTdj.z-6ZMM91gIaYW7C7fxPCcldsL2CE44Fwg9wdWu1re-E"
    }
    // baseURL 자리에 자신이 사용할 URL를 넣어주시면 됩니다. 저 서버는 닫았어요..ㅎ

baseURL말고도 token도 이런 식으로 저장해둘 수 있어요. 계속 반복해서 사용되는 것들을 이렇게 저장을 해둡시다.


2️⃣ : baseURL은 잡아뒀고 이제 원하는 Service를 구현할 수 있게 파일을 하나 생성해보겠습니다. 저는 Login 서비스를 관할하는 LoginServices라는 파일을 생성했습니다.

    import Foundation
    import Moya

    enum LoginServices {
        case signUp(param: SignupRequest) // 에러의 주 원인
        case signIn(param: SigninRequest) 
    }

    extension LoginServices: TargetType {
      public var baseURL: URL {
        return URL(string: GeneralAPI.baseURL)!
      }
      
      var path: String {
        switch self {
        case .signUp:
            return "/users/register"
        case .signIn:
            return "/users/login"
        }
      }
      
      var method: Moya.Method {
        switch self {
        case .signUp,
             .signIn:
          return .post
        }
      }
      
      var sampleData: Data {
        return "@@".data(using: .utf8)!
      }
      
      var task: Task {
        switch self {
        case .signUp(let param):
            return .requestJSONEncodable(param)
        case .signIn(let param):
            return .requestJSONEncodable(param)
        }
      }

      var headers: [String: String]? {
        switch self {
        default:
          return ["Content-Type": "application/json"]
        }
      }
    }

아마 해당 파일을 복붙해서 넣으면 Error가 무수히 많이 발생할 겁니다. 이유는 위에 있는 enum caseparam값에 해당하는 Request 파일들을 아직 생성하지 않아서 입니다. 에러가 너무 눈에 거슬리겠지만 좀만 참으십셔. 일단 파일을 하나하나 해석해볼게요~!

  • 제가 위에서 Moya는 일반적으로 열거형(enum)을 사용해서 네트워크 요청을 타입 안전한 방식으로 캡슐화하는데 초점을 맞춘 네트워킹 라이브러리입니다. 이렇게 말씀을 드렸을겁니다.
    ▶️ enum 형태로 우리가 가져올 URL별로 case를 나눠주세요.
    ▶️ param에 들어가는 것은 Request할 값이 들어갑니다. 즉, Request할 값이 있다면(Body, query) 무조건 저 곳에 param 으로 들어갈 Model을 제시해줘야해요.

  • LoginServicesextension해서 TargetType를 채택했네요.
    Q. TargetType는 누구냐?
    A. 네트워크에 필요한 속성들을 제공해줘요!

  • 필요한 속성들
    baseURL: 서버의 도메인
    - 모든 target의 baseURL을 명시.
    - Moya는 이를 통하여 endpoint객체 생성

    path: 서버의 도메인 뒤에 추가 될 Path (일반적으로 API)
    - Moya는 path를 통해 라우팅함.
    - baseURL 뒤에 들어갈 subPath를 정의하며 case에 따른 endPoint를 생성

    method: HTTP method (GET, POST, …)
    - Target의 모든 case를 위하여 정확한 HTTP 메소드를 제공
    - JSONEncoding, URLEncoding 두 가지를 대체로 사용
    - URL에 query 값이 들어가는 경우 → URLEncoding
    - 그 외의 경우 → JSONEncoding

    sampleData: 테스트용 Mock Data
    - 테스트를 위한 목업 데이터를 제공할 때 사용
    - 지금은 UnitTest를 진행하지 않기때문에 이상하게 생성한 상태("@@".data(using: .utf8)! )

    task: 리퀘스트에 사용되는 파라미터 설정
    - 제일 중요한 프로퍼티
    - 사용할 모든 endpoint마다 Task 열거형 케이스를 작성해야 함
    - plain request : A request with no additional data(param 필요없음)
    - parameter request : A requests body set with encoded parameters
    - JSONEncodable request : A request body set with Encodable type
    - data request upload request

    validationType: 허용할 response의 타입(위에 예시에는 없음)
    headers: HTTP header
    - 모든 Target의 endpoint를 위한 HTTP header를 반환.
    - 이번 예제에서 사용하는 모든 endpoint가 JSON데이터를 반환하기 때문에 "Content-Type": "application/json" 반환


3️⃣ : Service 구성을 완성했으니 param에 넣을 Request 모델 구조를 생성해봅시다. 만약, .get방식만 사용해서 Request할 필요가 없다면 Request모델을 생성해주지 않아도 됩니다.

하지만 모든 .get 방식이 아무것도 받지 않는 건 아니니..
⛔️ 꼭 요청받는 query가 없는지 항상 확인하십셔 ⛔️

    import Foundation

    struct SignupRequest: Codable {
        var name: String
        var email: String
        var id: String
        var password: String
        
        init(_ name: String, _ email: String, _ id: String, _ password: String) {
            self.name = name
            self.email = email
            self.id = id
            self.password = password
        }
    }

저는 Codable를 채택해서 Request모델을 만들어줬습니다.
init으로 name, email, id, password를 받아서 넣을 수 있게끔 해줬습니다.


4️⃣ : 데이터를 서버에 저장해달라 요청을 했으면 "잘 저장했습니다" 하고 답을 받아야할 거 아니에요? or 데이터를 서버에서 보내주길 부탁한다라고 했으면 "데이터 여기있다"하고 답이 와야할 거 아니에요?

🥳 그래서 우리는 Response를 받습니다.

    import Foundation

    // MARK: - SignupModel
    struct SignupModel: Codable {
        let status: Int
        let success: Bool
        let message: String
        let data: SignupResponse
    }

    // MARK: - SignupResponse
    struct SignupResponse: Codable {
        let name, email, id: String
    }

quicktype.io를 사용하면 쉽게 Response모델을 생성할 수 있습니다.
quicktype.io 최고..👍

우리는 이제 SignupModel를 통해서 우리가 Request시켰던 게 잘 들어갔는지 확인할 수 있어요.


5️⃣ : 이제 전체적인 세팅은 끝났으니 사용해봅시다.

    import UIKit
    import Moya

    class SignUpVC: UIViewController {
    	// MoyaTarget과 상호작용하는 MoyaProvider를 생성하기 위해 MoyaProvider인스턴스 생성
    	private let authProvider = MoyaProvider<LoginServices>()
    	// ResponseModel를 userData에 넣어주자!
        var userData: SignupModel?

     ... 

    	// signup버튼을 누르면 해당 액션이 일어날겁니다!!
    	// 그 때 signUp 함수로 데이터를 request시키고 가입축하 Alert가 뜹니다.
    	@objc 
    	func touchUpSignUp() {
          signUp()
          let alert = UIAlertController(title: "가입을 축하합니다", message: "Walkway의 회원이 되신걸 축하합니다.\nWalkway와 함께 걸어봐요", preferredStyle: UIAlertController.Style.alert)
          let okAction = UIAlertAction(title: "확인했어요", style: .default) { (Action) in
              self.navigationController?.popViewController(animated: true)
          }
          alert.addAction(okAction)
          present(alert, animated: true)
      }
    }

    // MARK: Network
    extension SignUpVC {
    		// signUp 함수
        func signUp() {
    	// signUp에서는 param값을 사용하기 때문에 signUpRequest 모델에 맞게
    	// 데이터들을 넣어줍니다.
    	// signUpModel에서 요청하는 데이터인 name, email, id, password를 넣어줬어요.
            let param = SignupRequest.init(self.nameTextField.text!, self.emailTextField.text!, self.idTextField.text!, self.passwordTextField.text!)
            print(param)
    	// LoginServices enum값 중에서 .signUp를 골라서 param과 함께 request시켜줍니다.
    	// 그에 대한 response가 돌아오면 해당 response가 .success이면 result값을
    	// SignupModel에 맞게끔 가공해주고나서 
    	// 위에 선언해뒀던 SignupModel모습을 갖춘 userData에 넣어줍니다. 
            authProvider.request(.signUp(param: param)) { response in
                switch response {
                    case .success(let result):
                        do {
                            self.userData = try result.map(SignupModel.self)
                        } catch(let err) {
                            print(err.localizedDescription)
                        }
                    case .failure(let err):
                        print(err.localizedDescription)
                }
            }
        }
    }

이런 식으로 사용해주면 됩니다..!! 원래 코드는 너무 길어서 필요한 부분들만 잘라서 구성했습니다. 상황에 맞게 응용해서 사용해주시면 되겠습니다~!


워낙 서버에서 보내주는 URL의 모습이 다양하고 Request시키는 방식도 다양해서 Moya를 쓸 때 호엑! 하는 부분이 있을 수 있어요🥲 서버 통신을 많이 해보는게 답인 거 같습니다..


💎 알아둘수록 좋은 서버 통신


위에 나오는 방식의 서버 통신은 빙산의 일각입니다..

enum FollowerServices {
    case followerDetail(String)
    case follow(String)
    case unfollow(String)
}

도대체 이건 어떤 형태로 들어가는걸까요? 안에 들어가는 param같은 게 있으니 .post형식일까요? 아닙니다.. 이건 .get 입니다..

var path: String {
    switch self {
    case .followerDetail(let followerID):
        return "/follower/\(followerID)"
    case .follow(let followerID):
        return "/follower/\(followerID)/follow"
    case .unfollow(let followerID):
        return "/follower/\(followerID)/unfollow"
    }
  }

URL에다가 넣어버리는 방식입니다..ㅎ..ㅎㅎㅎ

var parameterEncoding: ParameterEncoding {
        switch self {
        case .followerDetail,
             .follow,
             .unfollow:
            return JSONEncoding.default
        }
 }
  • URLEncoding 아니에요?? 아닙니다.. 저 안에 이미 집어넣어줬기 때문에 URL로 굳이 보낼게 없습니다.. 그냥 JSONEncoding 입니다.
  • URLEncoding/reports?start=start&end=end 이런식으로 endpoint를 생성하는 친구들을 생각해주시면 됩니다. 요청 쿼리가 있다? URLEncoding 이다!

Task 에다가 requestParameters 해야할까요!!?

var task: Task {
    switch self {
    case .followerDetail,
         .follow,
         .unfollow:
        return .requestPlain
    }
  }

아니요.. 아니야.. .post 가 아니야.. query가 아니야.. 이미 위에서 URL에 매다꽂아둔 아이들이기 때문에 더이상 아무것도 요청하라고 떼를 쓸 수 없습니다..

위에 있는 예시들은 기본적인 것들이기 때문에 상황에 맞춰서 잘 응용해서 사용해주시면 되겠습니다.👍

profile
더 멋진 iOS 개발자를 향해

0개의 댓글