제가 오늘 가지고 온 것은 Moya
모야쟁이로서 여러분께 모야에 대해 소개해드리려고 합니다. 이번 서버 통신 클론코딩에서 모야를 한 번 써보십시오. 모야 최고~!~!
모야가 뭐야? 대단한 라이브러리지~
•
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를 넣어주시면 됩니다. 저 서버는 닫았어요..ㅎ
Swift
복사
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"]
}
}
}
Swift
복사
아마 해당 파일을 복붙해서 넣으면 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
}
}
Swift
복사
저는 Codable를 채택해서 Request모델을 만들어줬습니다. init으로 name, email, id, password를 받아서 넣을 수 있게끔 해줬습니다.
4.
데이터를 서버에 저장해달라 요청을 했으면 "잘 저장했슴다" 하고 답을 받아야할 거 아니에요? 아니면 데이터를 서버에서 보내주길 부탁한다라고 했으면 "데이터 여기있다"하고 답이 와야할 거 아니에요? 그래서 우리는 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
}
Swift
복사
우리는 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)
}
}
}
}
Swift
복사
이런 식으로 사용해주면 됩니다..!! 원래 코드는 너무 길어서 필요한 부분들만 잘라서 구성했습니다. 상황에 맞게 응용해서 사용해주시면 되겠습니다~!
워낙 서버에서 보내주는 URL의 모습이 다양하고 Request시키는 방식도 다양해서 Moya를 쓸 때 호엑! 하는 부분이 있을 수 있어요 서버 통신을 많이 해보는게 답인 거 같습니다..
알아둘수록 좋은 서버 통신
위에 나오는 방식의 서버 통신은 빙산의 일각입니다..
enum FollowerServices {
case followerDetail(String)
case follow(String)
case unfollow(String)
}
Swift
복사
•
도대체 이건 어떤 형태로 들어가는걸까요? 안에 들어가는 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"
}
}
Swift
복사
•
URL에다가 넣어버리는 방식입니다..ㅎ..ㅎㅎㅎ
var parameterEncoding: ParameterEncoding {
switch self {
case .followerDetail,
.follow,
.unfollow:
return JSONEncoding.default
}
}
Swift
복사
•
URLEncoding 아니에요?? 아닙니다.. 저 안에 이미 집어넣어줬기 때문에 URL로 굳이 보낼게 없습니다.. 그냥 JSONEncoding 입니다.
•
URLEncoding 은 /reports?start=start&end=end 이런식으로 endpoint를 생성하는 친구들을 생각해주시면 됩니다. 요청 쿼리가 있다? URLEncoding 이다!
Task 에다가 requestParameters 해야할까요!!?
var task: Task {
switch self {
case .followerDetail,
.follow,
.unfollow:
return .requestPlain
}
}
Swift
복사
•
아니요.. 아니야.. .post 가 아니야.. query가 아니야.. 이미 위에서 URL에 매다꽂아둔 아이들이기 때문에 더이상 아무것도 요청하라고 떼쓸 수 없습니다.. 안돼 돌아가..
하는 도중에 도대체 뭐야!!! 하는 순간들이 많을텐데 그냥 우세요. 우는 게 차라리 나아요.
스몰 꾸르르를팁
여러분들께 NetworkLoggerPlugin 를 소개해드립니다~!~!
이 친구가 여러분들의 console창에 멋진 결과를 출력해줄겁니다. 그냥 쓰세요. 그냥 써..
원래는 이걸로 추정
하지만, 저희 앱잼 리드개발자 친구가 저에게 멋진걸 흘려두고 갔습니다.. 여러분들께만 공유
땡큐 유진, 알라뷰 쏘머치,
//
// NetworkLoggerPlugin.swift
// MyDaily_iOS
//
// Created by 이유진 on 2021/01/11.
//
import Foundation
import Moya
/// Logs network activity (outgoing requests and incoming responses).
public final class NetworkLoggerPlugin: PluginType {
fileprivate let loggerId = "Moya_Logger"
fileprivate let dateFormatString = "dd/MM/yyyy HH:mm:ss"
fileprivate let dateFormatter = DateFormatter()
fileprivate let separator = ", "
fileprivate let terminator = "\n"
fileprivate let cURLTerminator = "\\\n"
fileprivate let output: (_ separator: String, _ terminator: String, _ items: Any...) -> Void
fileprivate let requestDataFormatter: ((Data) -> (String))?
fileprivate let responseDataFormatter: ((Data) -> (Data))?
/// A Boolean value determing whether response body data should be logged.
public let isVerbose: Bool
public let cURL: Bool
/// Initializes a NetworkLoggerPlugin.
public init(verbose: Bool = true, cURL: Bool = false, output: ((_ separator: String, _ terminator: String, _ items: Any...) -> Void)? = nil, requestDataFormatter: ((Data) -> (String))? = nil, responseDataFormatter: ((Data) -> (Data))? = nil) {
self.cURL = cURL
self.isVerbose = verbose
self.output = output ?? NetworkLoggerPlugin.reversedPrint
self.requestDataFormatter = requestDataFormatter
self.responseDataFormatter = responseDataFormatter
}
public func willSend(_ request: RequestType, target: TargetType) {
if let request = request as? CustomDebugStringConvertible, cURL {
output(separator, terminator, request.debugDescription)
return
}
outputItems(logNetworkRequest(request.request as URLRequest?))
}
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
if case .success(let response) = result {
outputItems(logNetworkResponse(response.response, data: response.data, target: target))
} else {
outputItems(logNetworkResponse(nil, data: nil, target: target))
}
}
fileprivate func outputItems(_ items: [String]) {
if isVerbose {
items.forEach { output(separator, terminator, $0) }
} else {
output(separator, terminator, items)
}
}
}
private extension NetworkLoggerPlugin {
var date: String {
dateFormatter.dateFormat = dateFormatString
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
return dateFormatter.string(from: Date())
}
func format(_ loggerId: String, date: String, identifier: String, message: String) -> String {
return "\(loggerId): [\(date)] \(identifier): \(message)"
}
func logNetworkRequest(_ request: URLRequest?) -> [String] {
var output = [String]()
output += [format(loggerId, date: date, identifier: "Request", message: request?.description ?? "(invalid request)")]
if let headers = request?.allHTTPHeaderFields {
output += [format(loggerId, date: date, identifier: "Request Headers", message: headers.description)]
}
if let bodyStream = request?.httpBodyStream {
output += [format(loggerId, date: date, identifier: "Request Body Stream", message: bodyStream.description)]
}
if let httpMethod = request?.httpMethod {
output += [format(loggerId, date: date, identifier: "HTTP Request Method", message: httpMethod)]
}
if let body = request?.httpBody, let stringOutput = requestDataFormatter?(body) ?? String(data: body, encoding: .utf8), isVerbose {
output += [format(loggerId, date: date, identifier: "Request Body", message: stringOutput)]
}
return output
}
func logNetworkResponse(_ response: HTTPURLResponse?, data: Data?, target: TargetType) -> [String] {
guard let response = response else {
return [format(loggerId, date: date, identifier: "Response", message: "Received empty network response for \(target).")]
}
var output = [String]()
output += [format(loggerId, date: date, identifier: "Response", message: response.description)]
if let data = data, let stringData = String(data: responseDataFormatter?(data) ?? data, encoding: String.Encoding.utf8), isVerbose {
output += [stringData]
}
return output
}
}
fileprivate extension NetworkLoggerPlugin {
static func reversedPrint(_ separator: String, terminator: String, items: Any...) {
for item in items {
print(item, separator: separator, terminator: terminator)
}
}
}
Swift
복사
이걸 어떻게 사용하냐면,
그래서 어떻게 쓰는디? - 5번째에서 authProvider를 사용할 때 넣어주면 됩니다.
private let authProvider = MoyaProvider<LoginServices>(plugins: [NetworkLoggerPlugin(verbose: true)])
Swift
복사
이렇게 쓰시면 됩니다.