Published on

🍎 Swift - Moya

Authors
  • avatar
    Name
    이창쀀
    Twitter

Moya

λͺ¨μ•Όκ°€ λŒ€μ²΄ λͺ¨μ•Ό?

(μ•„λ§ˆ ν•œκ΅­μΈμ΄ μž‘μ„±ν•œ λͺ¨μ•Ό κ²Œμ‹œλ¬Ό 80%λŠ” 이걸둜 μ‹œμž‘ν• λ“―β€¦)

κ·Έλž˜μ„œ λͺ¨μ•ΌλŠ” λ­˜κΉŒμš”?

MoyaλŠ” enum νƒ€μž…μ„ 적극적으둜 ν™œμš©ν•˜μ—¬ λ„€νŠΈμ›Œν¬ μš”μ²­ λ ˆμ΄μ–΄λ₯Ό type-safeν•˜κ²Œ λž˜ν•‘ν•œ λ„€νŠΈμ›Œν‚Ή λΌμ΄λΈŒλŸ¬λ¦¬μž…λ‹ˆλ‹€.

컴파일 νƒ€μž„μ— μ—”λ“œν¬μΈνŠΈ μ ‘κ·Ό κ΄€λ ¨ μ—λŸ¬λ₯Ό λ„μ›Œμ£ΌκΈ° λ•Œλ¬Έμ— λ”μš± μ•ˆμ „ν•˜κ³  κ°„νŽΈν•˜κ²Œ 앱을 λΉŒλ“œν•  수 μžˆλ‹€λŠ” μž₯점이 μžˆλ‹€κ³  ν•΄μš”!

Alamofireμ™€μ˜ 관계?

Swift μ–Έμ–΄λ₯Ό 처음 ν•™μŠ΅ν•  λ•Œ URLSession을 κ³΅λΆ€ν•œ 뒀에 λ„€νŠΈμ›Œν¬ κ΄€λ ¨λœ μœ μš©ν•œ λΌμ΄λΈŒλŸ¬λ¦¬κ°€ λ§Žλ‹€λŠ” 사싀을 μ•Œκ³  μ–΄λ–€ 것이 μžˆλ‚˜ μ‚΄νŽ΄λ³΄λ˜ λ•Œκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

λͺ¨λ‘κ°€ μ•Œκ³  μžˆλŠ” Alamofire, Kingfisher, Moya 등이 μžˆμ—ˆμ£ ..

κ·Έ μ€‘μ—μ„œ KingfisherλŠ” 이미지 κ΄€λ ¨ λΌμ΄λΈŒλŸ¬λ¦¬λ‹ˆ λ‚¨λŠ” 건 Alamofire와 Moyaμ˜€μŠ΅λ‹ˆλ‹€.

아무것도 λͺ¨λ₯΄λ˜ μ €λŠ” Moya의 κ·€μ—¬μš΄ 이름에 끌렀 λ¬΄μž‘μ • 곡뢀λ₯Ό μ‹œμž‘ν–ˆλ”λž©μ£ ..

무슨 말인지 ν•˜λ‚˜λ„ μ΄ν•΄ν•˜μ§€ λͺ»ν•˜κ³  Alamofire둜 ν›„ν‡΄ν–ˆμ—ˆλŠ”λ°μš”.. λ‹Ήμ—°ν•œκ±°μ˜€μŠ΅λ‹ˆλ‹€.

Moya의 λ„€νŠΈμ›Œν‚Ήμ€ 사싀 Alamofireλ₯Ό μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ΄μ£ β€¦!

Dependency에 λ–‘ν•˜λ‹ˆ λ°•ν˜€μžˆλŠ”κ²Œ λ³΄μ΄μ‹œμ£ ..

κ°œλ…

λͺ¨μ•Όλ₯Ό μ΄ν•΄ν•˜λ €λ©΄ μ„Έ 가지 핡심 μš”μ†Œλ₯Ό λ¨Όμ € μ•Œμ•„μ•Όν•©λ‹ˆλ‹€.

Provider

MoyaProvider 객체둜 μ ‘κ·Όν•  수 μžˆλŠ” 메인 κ°μ²΄μž…λ‹ˆλ‹€.

λ„€νŠΈμ›Œν¬ κ΄€λ ¨ κΈ°λŠ₯을 μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” 무쑰건 이 MoyaProvider 객체λ₯Ό μƒμ„±ν•˜κ±°λ‚˜ μ£Όμž…λ°›μ•„μ•Ό ν•©λ‹ˆλ‹€.

Target

APIλ₯Ό μ œκ³΅ν•˜λŠ” μ„œλΉ„μŠ€λ₯Ό λΆ€λ₯΄λŠ” λͺ…μΉ­μž…λ‹ˆλ‹€.

곡개된 μ˜€ν”ˆ API일 μˆ˜λ„ 있고, ν”„λ‘œμ νŠΈ λ‚΄λΆ€μ—μ„œ μ‚¬μš©λ˜λŠ” μ„œλ²„μ˜ API일 μˆ˜λ„ 있겠죠.

이 Target은 TargetType protocol을 μ‚¬μš©ν•΄μ„œ μ •μ˜ν•΄μ€„ 수 μžˆμŠ΅λ‹ˆλ‹€.

Endpoint

EndpointλŠ” λ„€νŠΈμ›Œν‚Ή μš”μ²­μ„ μœ„ν•΄ ν•„μš”ν•œ 정보듀을 λ‹΄λŠ” κ°μ²΄μž…λ‹ˆλ‹€.

HTTP λ©”μ„œλ“œ, request body / header λ“±μ˜ 정보듀을 λ‹΄κ³  있고, Target λ˜ν•œ MoyaProvider에 μ˜ν•΄ 이 Endpoint둜 λ³€ν™˜λ˜μ–΄ μ‚¬μš©λ©λ‹ˆλ‹€.

이 Endpointλ₯Ό μ»€μŠ€ν…€ν•˜λ©΄ λͺ¨λ“  μ’…λ₯˜μ˜ λ„€νŠΈμ›Œν‚Ήμ— ν•„μš”ν•œ 데이터 맡핑을 μˆ˜ν–‰ν•  수 μžˆλ‹€κ³  ν•˜λ„€μš”.

기초

곡식 λ¬Έμ„œλ³΄λ‹€λŠ” μ½”λ°μ½”μ˜ μ˜ˆμ œκ°€ μ‰¬μ›Œλ³΄μ΄λ‹ˆκΉŒ μ½”λ°μ½”μ˜ 예제둜 μ§„ν–‰ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

public enum Marvel {
  static private let publicKey = "PUBLIC_KEY"
  static private let privateKey = "PRIVATE_KEY"

  case comics
}

μ΄λ ‡κ²Œ enum νƒ€μž…μœΌλ‘œ API μ„œλΉ„μŠ€μ— λŒ€ν•œ 정보λ₯Ό 넣어쀄 수 μžˆμŠ΅λ‹ˆλ‹€.

caseμ—λŠ” ν•„μš”ν•œ API μ—”λ“œν¬μΈνŠΈλ§ˆλ‹€ μ •μ˜ν•΄μ£Όλ©΄ λ©λ‹ˆλ‹€.

enum MyService {
    case zen
    case showUser(id: Int)
    case createUser(firstName: String, lastName: String)
    case updateUser(id: Int, firstName: String, lastName: String)
    case showAccounts
}

μ΄λ ‡κ²Œμš”..!

μ—°κ΄€κ°’μœΌλ‘œ λ„£μ–΄μ€€ νŒŒλΌλ―Έν„°λ“€μ€ λ¦¬ν€˜μŠ€νŠΈμ‹œμ— νŒŒλΌλ―Έν„°κ°€ ν•„μš”ν•  경우 λ„£μ–΄μ£ΌλŠ” κ²ƒμœΌλ‘œ νŽΈν•˜κ²Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Target으둜 μ‚¬μš©λ  enum은 λ°˜λ“œμ‹œ TargetType ν”„λ‘œν† μ½œμ„ 채택해야 ν•œλ‹€κ³  ν–ˆμ—ˆμ£ ?

extension Marvel: TargetType {
  public var baseURL: URL {
	//
  }

  public var path: String {
	//
  }

  public var method: Moya.Method {
	//
  }

  public var sampleData: Data {
	//
  }

  public var task: Moya.Task {
	//
  }

  public var headers: [String : String]? {
	//
  }
}

ν”„λ‘œν† μ½œμ„ μ±„νƒν•˜κ³  ν•„μš”ν•œ ν”„λ‘œνΌν‹°λ“€μ„ μžλ™μœΌλ‘œ λ„£μ–΄μ£Όλ©΄ μœ„μ™€ 같은 ν˜•νƒœκ°€ λ©λ‹ˆλ‹€.

extension Marvel: TargetType {
  public var baseURL: URL {
    return URL(string: "https://gateway.marvel.com/v1/public")!
  }

  public var path: String {
    switch self {
    case .comics: return "/comics"
    }
  }

  public var method: Moya.Method {
    switch self {
    case .comics: return .get
    }
  }

  public var sampleData: Data {
    return Data()
  }

  public var task: Moya.Task { // TODO: μ•Œλ§žλŠ” μ˜΅μ…˜μœΌλ‘œ λ³€κ²½
    return .requestPlain
  }

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

  public var validationType: ValidationType {
    return .successCodes
  }
}

baseURL

API μ„œλΉ„μŠ€μ˜ baseURL을 μž…λ ₯ν•΄μ€λ‹ˆλ‹€.

path

baseURL 뒀에 λΆ™λŠ” μš”μ²­ API의 path μ£Όμ†Œλ₯Ό μž…λ ₯ν•΄μ€λ‹ˆλ‹€.

μ˜ˆμ‹œμ˜ 경우 전체 API μš”μ²­ μ£Όμ†ŒλŠ” https://gateway.marvel.com/v1/public/comics κ°€ 되겠죠!

method

path에 λ§žλŠ” HTTPS 톡신 λ©”μ„œλ“œλ₯Ό λ°˜ν™˜ν•΄μ€λ‹ˆλ‹€.

ex) .get, .delete, .patch λ“±

sampleData

μœ λ‹› ν…ŒμŠ€νŠΈλ₯Ό ν•  λ•Œλ‚˜ μ‹€μ œλ‘œ μ„œλ²„κ°€ 없을 경우, 이 sampleData둜 κ°€μƒμ˜ 데이터λ₯Ό λ‹΄μ•„μ£Όλ©΄ ν•΄λ‹Ή 데이터λ₯Ό λ°˜ν™˜κ°’μœΌλ‘œ λ°›μ•„μ˜΅λ‹ˆλ‹€.

ν•„μš”ν•˜μ§€ μ•Šμ„ 경우 Data()λ₯Ό λ°˜ν™˜ν•˜μ—¬ λΉ„μ–΄μžˆλŠ” 데이터λ₯Ό μ „λ‹¬ν•΄μ€λ‹ˆλ‹€!

task

μš”μ²­μ— νŒŒλΌλ―Έν„°λ₯Ό ν¬ν•¨ν•˜κ±°λ‚˜, 데이터λ₯Ό ν¬ν•¨ν•˜μ—¬ μš”μ²­μ„ ν•˜λŠ” λ“±μ˜ μ˜΅μ…˜λ“€μ„ μΆ”κ°€μ μœΌλ‘œ μ œκ³΅ν•˜μ—¬ HTTP μš”μ²­μ„ μ „μ†‘ν•©λ‹ˆλ‹€.

APIκ°€ μš”κ΅¬ν•˜λŠ” μ–‘μ‹μ΄λ‚˜ μ„œλ²„μ˜ μƒνƒœλ“±μ— 따라 λ„ˆλ¬΄λ‚˜ λ‹€μ–‘ν•œ κ²½μš°κ°€ μžˆμœΌλ―€λ‘œ μš°μ„  μ•„λ¬΄λŸ° μ˜΅μ…˜μ„ 넣지 μ•ŠλŠ” .requestPlain을 μ„ νƒν•΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

headers

HTTP 헀더λ₯Ό λ„£μ–΄μ€λ‹ˆλ‹€.

μ˜ˆμ‹œμ—μ„œλŠ” κ°€μž₯ 자주 μ“°μ΄λŠ” Content-Type: application/json을 λ„£μ–΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€. (JSON ν˜•μ‹μ˜ 컨텐츠)

validationType

ν•„μˆ˜μ μœΌλ‘œ ν•„μš”ν•œ ν•­λͺ©μ€ μ•„λ‹ˆμ§€λ§Œ, 자주 μ“°μ΄λŠ” ν•­λͺ©μž…λ‹ˆλ‹€.

.successCodesλŠ” 200..<299 μ‚¬μ΄μ˜ 응닡 μ½”λ“œλ₯Ό λ°›μœΌλ©΄ 톡신을 μ„±κ³΅ν–ˆλ‹€κ³  μ²˜λ¦¬ν•˜λŠ” caseμž…λ‹ˆλ‹€.

이 Target만 봐도 Moyaκ°€ λŒ€μΆ© μ–΄λ–€ λŠλ‚ŒμœΌλ‘œ μ“°μ΄λŠ” 지 μ•Œ 수 μžˆμ„ 것 κ°™λ„€μš”.

톡신에 ν•„μš”ν•œ 데이터듀을 ν•œ ꡰ데 λͺ¨μ•„두고 switch-caseλ₯Ό ν†΅ν•΄μ„œ μ‰½κ²Œ μš”μ²­ ν•­λͺ©μ„ 선택할 수 μžˆμ„ 것 κ°™μŠ΅λ‹ˆλ‹€.

https://developer.marvel.com/documentation/authorization

μ΄λ²ˆμ— μ‚¬μš©ν•˜λŠ” λ§ˆλΈ” API의 λ¬Έμ„œλ₯Ό μ‚΄νŽ΄λ³΄κ³ , task 뢀뢄을 μ±„μ›Œμ€μ‹œλ‹€.

Client-Side 앱은 사전에 인증이 λ˜μ–΄ μžˆμ–΄μ•Ό ν•œλ‹€λŠ”κ΅°μš”.

Server-Side λ°©μ‹μœΌλ‘œ μ§„ν–‰ν•΄μ€λ‹ˆλ‹€.

public var task: Task {
  let ts = "\(Date().timeIntervalSince1970)"
  let hash = (ts + Marvel.privateKey + Marvel.publicKey).md5
  let authParams = ["apikey": Marvel.publicKey, "ts": ts, "hash": hash]
  
  switch self {
  case .comics:
	return .requestParameters(
	  parameters: [
		"format": "comic",
		"formatType": "comic",
		"orderBy": "-onsaleDate",
		"dateDescriptor": "lastWeek",
		"limit": 50] + authParams,
	  encoding: URLEncoding.default)
  }
}

이제 μ‹€μ œλ‘œ API requestλ₯Ό λ³΄λ‚΄λ΄…μ‹œλ‹€.

let provider = MoyaProvider<Marvel>()

provider.request(.comics) { result in
  switch result {
  case .success(let response):
	do {
	  print(try response.mapJSON())
	} catch {
	  // error handling
	}
  case .failure:
	// error handling
  }
}

μœ„μ™€ 같이 짧고 κ°„κ²°ν•˜κ²Œ 톡신 μš”μ²­μ„ 보낼 수 μžˆμŠ΅λ‹ˆλ‹€.

μ—„μ²­λ‚˜κ²Œ λ§Žμ€ 데이터듀이 λ“€μ–΄μ˜€λŠ”κ΅°μš”..

이 데이터듀을 감싸고 View에 μ μš©ν•˜λŠ” λ‚΄μš©μ€ λ‹€λ₯Έ HTTP 톡신듀과 κ°™μŠ΅λ‹ˆλ‹€.

MoyaλŠ” RxSwift와도 μ•„μ£Ό 잘 μ–΄μšΈλ¦½λ‹ˆλ‹€.

provider.rx.requestWithProgress(.zen).subscribe { event in
    switch event {
    case .next(let progressResponse):
        if let response = progressResponse.response {
            // do something with response
        } else {
            print("Progress: \(progressResponse.progress)")
        }
    case .error(let error):
        // handle the error
    default:
        break
    }
}

μ΄λŸ°μ‹μœΌλ‘œ 기본적으둜 Rx화도 λ˜μ–΄μžˆκ±°λ“ μš”. (νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ν•  λ•Œ Moya/RxSwift도 ν•¨κ»˜ μ„€μΉ˜ν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.)

λŒ€λž΅μ μΈ μ‚¬μš© 방법을 μ•Œμ•˜μœΌλ‹ˆ 싀전에 μ μš©ν•΄λ³΄κΈ° μœ„ν•΄ λ– λ‚˜λ³΄λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€.

References

Kodeco - # Moya Tutorial for iOS: Getting Started

Moya - Basic Usage