Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- Observable
- Protocol
- SWIFT
- clean architecture
- 클린 코드
- 리펙터링
- 리펙토링
- uiscrollview
- ios
- MVVM
- Refactoring
- rxswift
- 애니메이션
- collectionview
- RxCocoa
- Human interface guide
- Clean Code
- 스위프트
- Xcode
- uitableview
- ribs
- swift documentation
- HIG
- UITextView
- UICollectionView
- map
- 리팩토링
- swiftUI
- combine
- tableView
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] Moya를 사용한 network 구조 설계 (무료 API 테스트, REST) 본문
API 테스트 사이트 참고
- 무료 API 테스트 사이트: https://reqres.in/
- 위 링크 클릭, 복사: https://reqres.in/api/users?page=2
- 데이터 형식이 page, per_page, total, total_pages, data 형식인 경우 대응 - 두 데이터 사용 예정
Moya 프레임워크
- 네트워크 기본인 URL Session 개념 참고
- moya 프레임워크 개념 참고
- 다운 > swift Package Manager > https://github.com/Moya/Moya.git
Network 설계 주요 4가지 파일
- NetworkLoggerPlugin: 네트워크 통신 시 MoyaProvider라는 객체를 통해 접근하는데, MoyaProvider의 파라미터 값으로 plugin객체를 넣어주면 해당 plugin기능을 사용 가능
- authPlugin: bearer 토큰 세팅 전용의 플러그인
- LoggerPlugin: response, request 로그를 확인할 수 있는 플러그인
- Networkable: MoyaProvider 객체를 만들어서 리턴 (Target 타입, plugin 객체를 주입하여 생성)
- NetworkError: Error프로토콜을 준수하고 있는 에러 정의용도 (response에서 사용)
- ResponseData: 공통 response에 관하여 Codable로 정의하고 있고, success, failure에 관한 처리를 담당
- BaseTargetType: Moya에서 제공하는 endpoint에 관해 enum으로 정의하여 편리하게 api호출을 할수 있게 하는 targetType의 base
Moya의 PluginType프로토콜을 conform
- NetworkLoggerPlugin 설계: provider를 이용하여 네트워크 호출을 하는데, provider를 생성할 때 plugin을 넣어주면 willSend, didReceive, onSucced, onFail에 관한것을 정의하여 log 확인 용도
- Moya에서 정의한 protocol인 PluginType 프로토콜은 준수
import Moya
struct NetworkLoggerPlugin: PluginType {
func willSend(_ request: RequestType, target: TargetType) {
guard let httpRequest = request.request else {
print("[HTTP Request] invalid request")
return
}
let url = httpRequest.description
let method = httpRequest.httpMethod ?? "unknown method"
/// HTTP Request Summary
var httpLog = """
[HTTP Request]
URL: \(url)
TARGET: \(target)
METHOD: \(method)\n
"""
/// HTTP Request Header
httpLog.append("HEADER: [\n")
httpRequest.allHTTPHeaderFields?.forEach {
httpLog.append("\t\($0): \($1)\n")
}
httpLog.append("]\n")
/// HTTP Request Body
if let body = httpRequest.httpBody, let bodyString = String(bytes: body, encoding: String.Encoding.utf8) {
httpLog.append("BODY: \n\(bodyString)\n")
}
httpLog.append("[HTTP Request End]")
print(httpLog)
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case let .success(response):
onSuceed(response, target: target, isFromError: false)
case let .failure(error):
onFail(error, target: target)
}
}
func onSuceed(_ response: Response, target: TargetType, isFromError: Bool) {
let request = response.request
let url = request?.url?.absoluteString ?? "nil"
let statusCode = response.statusCode
/// HTTP Response Summary
var httpLog = """
[HTTP Response]
TARGET: \(target)
URL: \(url)
STATUS CODE: \(statusCode)\n
"""
/// HTTP Response Header
httpLog.append("HEADER: [\n")
response.response?.allHeaderFields.forEach {
httpLog.append("\t\($0): \($1)\n")
}
httpLog.append("]\n")
/// HTTP Response Data
httpLog.append("RESPONSE DATA: \n")
if let responseString = String(bytes: response.data, encoding: String.Encoding.utf8) {
httpLog.append("\(responseString)\n")
}
httpLog.append("[HTTP Response End]")
print(httpLog)
}
func onFail(_ error: MoyaError, target: TargetType) {
if let response = error.response {
onSuceed(response, target: target, isFromError: true)
return
}
/// HTTP Error Summary
var httpLog = """
[HTTP Error]
TARGET: \(target)
ERRORCODE: \(error.errorCode)\n
"""
httpLog.append("MESSAGE: \(error.failureReason ?? error.errorDescription ?? "unknown error")\n")
httpLog.append("[HTTP Error End]")
print(httpLog)
}
}
MoyaProvider를 생성하는 Networkable 프로토콜 정의
import Moya
protocol Networkable {
/// provider객체 생성 시 Moya에서 제공하는 TargetType을 명시해야 하므로 타입 필요
associatedtype Target: TargetType
/// DIP를 위해 protocol에 provider객체를 만드는 함수 정의
static func makeProvider() -> MoyaProvider<Target>
}
extension Networkable {
static func makeProvider() -> MoyaProvider<Target> {
/// access token 세팅
let authPlugin = AccessTokenPlugin { _ in
return "bear-access-token-sample"
}
/// 로그 세팅
let loggerPlugin = NetworkLoggerPlugin()
/// plugin객체를 주입하여 provider 객체 생성
return MoyaProvider<Target>(plugins: [authPlugin, loggerPlugin])
}
}
Network 에러 정의
- Error 프로토콜 준수한 에러타입 정의
import Foundation
import Moya
// back-end 팀과 정의한 에러 내용
enum ServiceError: Error {
case moyaError(MoyaError)
case invalidResponse(responseCode: Int, message: String)
case tokenExpired
case refreshTokenExpired
case duplicateLoggedIn(message: String)
}
extension ServiceError: LocalizedError {
var errorDescription: String? {
switch self {
case .moyaError(let moyaError):
return moyaError.localizedDescription
case let .invalidResponse(_, message):
return message
case .tokenExpired:
return "AccessToken Expired"
case .refreshTokenExpired:
return "RefreshToken Expired"
case let .duplicateLoggedIn(message):
return message
}
}
}
ResponseData 정의
- common response 정의: codable을 준수하고 Model을 generic으로 선언
struct ResponseData<Model: Codable> {
struct CommonResponse: Codable {
let page: Int
let perPage: String
let total: Int
let totalPages: Int
let data: Model
}
...
}
- response를 처리하는 processResponse 메소드 정의: 해당 메소드는 moya의 provider객체를 가지고 api 호출 후 응답을 받은 경우 그 응답을 해당 매소드에서 처리하기위한 용도
- 파라미터 Result<Response, MoyaError>로 놓고, 반환 타입은 Result<Model?, Error>로 정의
struct ResponseData<Model: Codable> {
struct CommonResponse: Codable {
let result: Model
}
static func processResponse(_ result: Result<Response, MoyaError>) -> Result<Model?, Error> {
switch result {
case .success(let response):
do {
// status code가 200...299인 경우만 success로 체크 (아니면 예외발생)
_ = try response.filterSuccessfulStatusCodes()
let commonResponse = try JSONDecoder().decode(CommonResponse.self, from: response.data)
return .success(commonResponse.result)
} catch {
return .failure(error)
}
case .failure(let error):
return .failure(error)
}
}
}
보통 Common response에 responseCode, message, result 형식으로 표현하지만, 예제데이터는 아래이므로 아래 데이터 통째 하나를 result로 보도록 처리
{
"page":2,
"per_page":6,
"total":12,
"total_pages":2,
"data":[
{
"id":7,
"email":"michael.lawson@reqres.in",
"first_name":"Michael",
"last_name":"Lawson",
"avatar":"https://reqres.in/img/faces/7-image.jpg"
},
{
"id":8,
"email":"lindsay.ferguson@reqres.in",
"first_name":"Lindsay",
"last_name":"Ferguson",
"avatar":"https://reqres.in/img/faces/8-image.jpg"
},
{
"id":9,
"email":"tobias.funke@reqres.in",
"first_name":"Tobias",
"last_name":"Funke",
"avatar":"https://reqres.in/img/faces/9-image.jpg"
},
{
"id":10,
"email":"byron.fields@reqres.in",
"first_name":"Byron",
"last_name":"Fields",
"avatar":"https://reqres.in/img/faces/10-image.jpg"
},
{
"id":11,
"email":"george.edwards@reqres.in",
"first_name":"George",
"last_name":"Edwards",
"avatar":"https://reqres.in/img/faces/11-image.jpg"
},
{
"id":12,
"email":"rachel.howell@reqres.in",
"first_name":"Rachel",
"last_name":"Howell",
"avatar":"https://reqres.in/img/faces/12-image.jpg"
}
],
"support":{
"url":"https://reqres.in/#support-heading",
"text":"To keep ReqRes free, contributions towards server costs are appreciated!"
}
}
- CommonResponse를 따르지 않는 resopnse모델 처리를 위한 함수도 추가
struct ResponseData<Model: Codable> {
struct CommonResponse: Codable {
let result: Model
}
static func processResponse(_ result: Result<Response, MoyaError>) -> Result<Model?, Error> {
switch result {
case .success(let response):
do {
// status code가 200...299인 경우만 success로 체크 (아니면 예외발생)
_ = try response.filterSuccessfulStatusCodes()
let commonResponse = try JSONDecoder().decode(CommonResponse.self, from: response.data)
return .success(commonResponse.result)
} catch {
return .failure(error)
}
case .failure(let error):
return .failure(error)
}
}
// CommonResponse 모델을 따르지 않는 모델을 처리하기 위한 함수
static func processJSONResponse(_ result: Result<Response, MoyaError>) -> Result<Model?, Error> {
switch result {
case .success(let response):
do {
let model = try JSONDecoder().decode(Model.self, from: response.data)
return .success(model)
} catch {
return .failure(error)
}
case .failure(let error):
return .failure(error)
}
}
}
BaseTargetType정의
- 개념: Moya에서 제공하는 endpoint에 관해 enum으로 정의하여 편리하게 api호출을 할수 있게 하는 targetType의 base
- configuration을 Phase별 api 설정 방법 개념 참고
- base url 확인 후 기입
protocol BaseTargetType: TargetType {
}
extension BaseTargetType {
var baseURL: URL {
// Configuration을 통해 phase별 baseURL 설정 방법: https://ios-development.tistory.com/660
// guard let urlString = Bundle.main.object(forInfoDictionaryKey: "API_URL") as? String else { fatalError("API URL not defined")}
// gaurd let apiURL = URL(string: urlString) else { fatalError("URL is invalid") }
return URL(string: "https://reqres.in")!
}
}
- 공통 Header 정의
import Moya
import UIKit
protocol BaseTargetType: TargetType {
}
extension BaseTargetType {
var baseURL: URL {
// Configuration을 통해 phase별 baseURL 설정 방법: https://ios-development.tistory.com/660
// guard let urlString = Bundle.main.object(forInfoDictionaryKey: "API_URL") as? String else { fatalError("API URL not defined")}
// gaurd let apiURL = URL(string: urlString) else { fatalError("URL is invalid") }
return URL(string: "https://reqres.in/api")!
}
var headers: [String: String]? {
var header = ["Content-Type": "application/json"]
let bundleVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
header["app-device-uuid"] = "uuid"
header["app-device-model-name"] = UIDevice.current.name
header["app-device-os-version"] = UIDevice.current.systemVersion
header["app-device-device-manufacturer"] = "apple"
header["app-version"] = bundleVersion
header["app-timezone-id"] = TimeZone.current.identifier
return header
}
// BaseTargetType을 상속받는 각 TargetType에서 sampleData를 필수로 구현하지 않아도 되도록 디폴트값 부여
var sampleData: Data {
return Data()
}
}
- encodable 정의: model을 json으로 serialization하는 함수
extension Encodable {
func toDictionary() -> [String: Any] {
do {
let data = try JSONEncoder().encode(self)
let dic = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
return dic ?? [:]
} catch {
return [:]
}
}
}
endpoint 정의
- request는 `?page=2` 이므로 codable을 준수하는 struct 정의
struct ListUserRequest: Codable {
let page: Int
}
- Target 정의
enum ListUserTarget {
case list(ListUserRequest)
}
extension ListUserTarget: BaseTargetType {
var path: String {
switch self {
case .list: return "/api/users"
}
}
var method: Method {
switch self {
case .list: return .get
}
}
var task: Task {
switch self {
case .list(let request): return .requestParameters(parameters: request.toDictionary(), encoding: URLEncoding.queryString)
}
}
}
- 주의: requestParameter가 아닌 body에 추가하는 data이면, .requestJSONEncodable 사용
var task: Task {
switch self {
// case .list(let request): return .requestParameters(parameters: request.toDictionary(), encoding: URLEncoding.queryString)
case .list(let request): return .requestJSONEncodable(request)
}
}
- 주의: bearer token이 필요한 경우 `AccessTokenAuthorizable` 프로토콜을 준수 후 authorizationType 프로퍼티에 .bearer 리턴
import Moya
enum ListUserTarget {
case list(ListUserRequest)
}
extension ListUserTarget: BaseTargetType, AccessTokenAuthorizable {
var path: String {
switch self {
case .list: return "/api/users"
}
}
var method: Method {
switch self {
case .list: return .get
}
}
var task: Task {
switch self {
case .list(let request): return .requestParameters(parameters: request.toDictionary(), encoding: URLEncoding.queryString)
}
}
var authorizationType: AuthorizationType? {
return .bearer
}
}
- Response 정의
- codable 형식의 struct 모델
- 직접적으로 domain에서 사용되는 모델이므로 domain디렉토리 하위에 위치 (이름도 -Response를 붙이지 않는것을 주의)
- json을 codable로 변환해주는 사이트는 이곳 참고
struct ListUser: Codable {
let page, perPage, total, totalPages: Int
let data: [Datum]
let support: Support
enum CodingKeys: String, CodingKey {
case page
case perPage = "per_page"
case total
case totalPages = "total_pages"
case data, support
}
struct Datum: Codable {
let id: Int
let email, firstName, lastName: String
let avatar: String
enum CodingKeys: String, CodingKey {
case id, email
case firstName = "first_name"
case lastName = "last_name"
case avatar
}
}
struct Support: Codable {
let url: String
let text: String
}
}
- API 정의
import Moya
struct ListUserAPI: Networkable {
typealias Target = ListUserTarget
/// page에 해당하는 User 정보 조회
static func getUserList(request: ListUserRequest, completion: @escaping (_ succeed: ListUser?, _ failed: Error?) -> Void) {
makeProvider().request(.list(request)) { result in
switch ResponseData<ListUser>.processJSONResponse(result) {
case .success(let model): return completion(model, nil)
case .failure(let error): return completion(nil, error)
}
}
}
}
- API사용
let request = ListUserRequest(page: 2)
ListUserAPI.getUserList(request: request) { response, error in
guard let response = response else {
print(error ?? #function)
return
}
let listUser = response
print(listUser)
}
* 전체 소스 코드: https://github.com/JK0369/NetworkExWithMoya
'iOS 응용 (swift)' 카테고리의 다른 글
Comments