💻 이 글은
Swift 5
,Xcode 13.4.1
,Moya 15.0.0
를 바탕으로 작성하였습니다.
🔗 Moya - Github
대부분 iOS 개발자분들은 Alamofire
를 사용하고 계시거나, 혹시 사용한 적이 없더라도 알고는 있을거에요. Alamofire
는 URLSession
을 추상화하여 보기 쉬운 형태로 네트워킹을 제공하는데요,
오늘 알아볼 Moya
는 Alamofire
를 한번 더 추상화해서,
네트워크 계층을 템플릿화하여 재사용성을 높이고 가독성을 높여 개발자는 오로지 Response
와 Request
에만 신경을 쓸 수 있도록 하여 생산성을 높일 수 있습니다!
📚 So the basic idea of Moya is that we want some network abstraction layer that sufficiently encapsulates actually calling Alamofire directly. - Moya Github
Moya Github 에서 설명하듯,
컴파일시 API 엔드 포인트가 올바른지 체크
Enum을 이용해서 언제, 어디에 사용될지 안전하게 (type-safe) 정의
유닛 테스트를 용이하게 만듦.
기존 구조의 문제점으로 크게 3가지를 제시하고 있는데,
어디서 부터 시작할지 시작의 어려움
유지보수의 어려움
유닛 테스트 작성의 어려움
Moya
를 사용해서 깔끔한 네트워크 계층 구조를 갖게 하고, Moya 계층을 템플릿화하여 재사용할 수 있게 합니다.
CocoaPods 를 이용한 Moya 설치
프로젝트 폴더에서
Pod init
,
Podfile
에 아래와 같이 필요한 라이브러리 추가 →Pod install
pod 'Moya' pod 'Moya/RxSwift' pod 'Moya/ReactiveSwift' pod 'Moya/Combine'
Swift Package Manager 를 이용한 설치
Xcode
에서File
→Add Packages
https://github.com/Moya/Moya.git
입력 후 설치마찬가지로 필요한 라이브러리를
✅ 체크
해서 선택할 수 있습니다!
이외에 Accio
, Carthage
를 이용한 설치는 Moya Github - Installation 을 참고해주세용
enum UserAPI {
case LogIn(oAuthProvider : OAuthProvider, accessToken : String)
case refreshToken
}
Enum 을 사용해서 사용할 API 목록을 작성해줍니다.
Enum 을 사용함으로써 보다 안전하게 사용할 수 있고,
유지보수의 관점볼 때 새로운 API를 추가할때 편리하겠죠?
방금 enum으로 작성한 API 목록을 extension
할건데, TargetType
프로토콜을 Conform
하여 API 별로 리퀘스트에 필요한 것들을 지정할 수 있습니다.!
📌 Property of TargetType
baseURL
: Server base URL 지정path
: API Path 지정method
: HTTP Method 지정sampleData
: Mock Data for Testtask
: Parameters for request 지정validationType
: 허용할 response 정의 - validationType 참고
기존Alamofire 의 .validate()
처럼리스폰스의 StatusCode
에 따라 성공 유무를 판단headers
: HTTP headers 적용
: 기존의인터셉터의 Adapter
역할을 할 수 있습니다. (ex. 헤더에 JWT 값 넣기)
Switch 문
을 활용해서 간편하게 여러 타입의 API 에 대한 값들을 지정할 수 있고,
필요없는 파라미터는 Wild Card 구문 (_)
을 사용해서 표현하면 됩니다!
잘 이해가 되지 않는다면, 제가 작성한 예시코드를 참고하시면 이해에 도움이 될 것 같습니다.
extension UserAPI : TargetType {
var baseURL: URL { URL(string: ServiceAPI.baseURL)! }
var path: String {
switch self {
case .LogIn(oAuthProvider: let type, accessToken: _) : return "/login/oauth/" + type.rawValue
case .refreshToken : return "/token"
}
}
var method: Moya.Method {
switch self {
case .LogIn(oAuthProvider: _, accessToken: _) : return .post
case .refreshToken : return .get
}
}
// var sampleData: Data { ... }
var task: Task {
switch self {
case .LogIn(oAuthProvider: _, accessToken: let accessToken) :
let params : [String: String] = [ "accessToken" : accessToken ]
return .requestParameters(parameters: params, encoding: URLEncoding.default )
case .refreshToken : return .requestPlain
}
}
var headers: [String : String]? {
switch self {
case .refreshToken :
guard let userInfo = UserService.shared.userInfo else { return [ "Content-type": "application/json" ] }
return [ "Content-type": "application/json", "X-AUTH-TOKEN" : userInfo.token.tokenType + " " + userInfo.token.accessToken ]
case .LogIn(oAuthProvider: _, accessToken: _) : return nil // [ "Content-type": "application/json" ]
}
}
var validationType: ValidationType { .successCodes }
}
받아온 Response를 저장할 구조체
를 선언해줍니다.
Alamofire 를 사용할때와 동일한 방식으로 생성하면 됩니다 (Codable 프로토콜
을 준수)
struct OAuthLoginResponse : Codable, Equatable {
var userId : Int
var name : String
var email : String
var imageUrl : String
var role : String
var token : Token
}
이제 준비는 끝났고, 네트워크 요청을 해봅시다!
private let provider = MoyaProvider<UserAPI>()
네트워크 요청을 수행할 Moya Provider 인스턴스를 생성해줍니다.
코드에서 보는 것처럼 Moya Provider
는 제네릭 타입
으로 TargetType 프로토콜을 준수하는 Enum
을 받고 있습니다.
이말은 즉, API 의 동작 범위에 따라 인스턴스를 구분 (ex. 유저 APIs, 게시물 APIs, ...) 하여 생성할 수 있고, 이 인스턴스들을 이용해 동일 동작을 하는 메소드를 사용할때 재사용할 수 있겠죠?!
Moya
provider.request(.LogIn(oAuthProvider: .Kakao, accessToken: accessToken)) { response in
switch response {
case .success(let result) :
guard let data = try? result.map(DataResponse<OAuthLoginResponse>.self) else { return }
print(data)
case .failure(let err):
print(err.localizedDescription)
}
}
생성한 Provider 를 통해 request 를 요청할 수 있습니다.
provider.request 의 파라미터로 아까 생성한 TargetType
을 전달해주세요!
이때 반환되는 response 는 Result<Response, MoyaError>
형태입니다.
CombineMoya
import CombineMoya
...
private var subscription = Set<AnyCancellable>()
...
provider.requestPublisher(.LogIn(oAuthProvider: .Kakao, accessToken: accessToken))
.sink { completion in
switch completion {
case let .failure(error) :
print("LogIn Fail : " + error.localizedDescription)
case .finished :
print("LogIn Finished")
}
} receiveValue: { recievedValue in
guard let responseData = try? recievedValue.map(DataResponse<OAuthLoginResponse>.self) else { return }
print(responseData)
}.store(in : &subscription)
마찬가지로 Combine
의 .sink()
을 이용하여 비동기적으로 Response 를 처리할 수 있습니다!
Alamofire 를 사용할때는 request 함수에 파라미터로 인터셉터를 전달할 수 있었는데,
Moya 에서는 어떻게 Interceptor 를 사용할까요?
class Interceptor : RequestInterceptor {
func adapt(...) { ... }
func retry(...) { ... }
}
...
private let provider = MoyaProvider<UserAPI>(session : Moya.Session(interceptor: Interceptor()))
Moya Provider 를 생성할때, Moya Session
에 interceptor
를 추가해서 전달해주면 됩니다.
오늘은 Alamofire 의 네트워크 요청을 추상화하는 Moya
를 사용하는 방법에 대해 알아보았습니다.
처음에는 굳이 왜 쓰지? 라는 생각이 들었는데, 사용하면 할수록 enum 을 활용해서 좀 더 안전한 방식으로 캡슐화된 요청을 보낼 수 있고, 코드를 직관적으로 작성할 수 있어 가독성이 높아지는 장점이 있었습니다. 또한 재사용이 용이한 점, API 추가/삭제가 편리한 점도 있었습니다!
틀린 정보 또는 궁금한 점이 있다면 댓글 부탁드립니다! 읽어주셔서 감사합니다‼️