모야쟁이로서 여러분께 모야에 대해 소개해드리려고 합니다. 서버 통신에서 모야를 한 번 써보십시오.
Moya
는 Alamofire
를 사용하면서 네트워크 계층을 구조화하는 다른 접근방법을 제공하는 라이브러리입니다.Moya
는 일반적으로 열거형(enum
)을 사용해서 네트워크 요청을 타입 안전한 방식으로 캡슐화하는데 초점을 맞춘 네트워킹 라이브러리입니다.Moya
라이브러리는 urlSession
, Alamofire
를 한 번 더 감싼 통신 API입니다.Moya
는 추상화 네트워킹 라이브러리입니다.→ 무슨 소리냐?
✨ 기분 좋아지는 깔끔한 네트워크 레이어 구성
Moya
는 자체적으로 네트워킹을 수행하지 않아요!
Moya
는 Alamofire
의 네트워킹 기능을 사용하고 Alamofire
를 추상화하기 위한 추가적인 능력, 타입, 개념을 제공합니다. 즉, Alamofire
를 기반으로 하고 있는 Moya
로 Alamofire
를 사용하는 것!
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 case
에 param
값에 해당하는 Request 파일
들을 아직 생성하지 않아서 입니다. 에러가 너무 눈에 거슬리겠지만 좀만 참으십셔. 일단 파일을 하나하나 해석해볼게요~!
제가 위에서 Moya
는 일반적으로 열거형(enum
)을 사용해서 네트워크 요청을 타입 안전한 방식으로 캡슐화하는데 초점을 맞춘 네트워킹 라이브러리입니다. 이렇게 말씀을 드렸을겁니다.
▶️ enum
형태로 우리가 가져올 URL별로 case
를 나눠주세요.
▶️ param에 들어가는 것은 Request할 값이 들어갑니다. 즉, Request할 값이 있다면(Body, query) 무조건 저 곳에 param
으로 들어갈 Model
을 제시해줘야해요.
LoginServices
를 extension
해서 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에 매다꽂아둔 아이들이기 때문에 더이상 아무것도 요청하라고 떼를 쓸 수 없습니다..
위에 있는 예시들은 기본적인 것들이기 때문에 상황에 맞춰서 잘 응용해서 사용해주시면 되겠습니다.👍