Alamofire + Combine로 API 깔끔하게 요청하기

peppermint100
PEPPERMINT100
8 min readMay 25, 2024

--

서론

클라이언트 사이드에서 API 호출은 수도 없이 많이 한다. 일반적으로 모든 앱은 API 호출을 쉽게 할 수 있도록 공통 모듈을 만들어둔다.

이전까지는 URLSession만을 이용했는데, 이번에 사이드 프로젝트에서 팀원과 Alamofire를 이용하기로 결정했다.

Alamofire를 이용하여 API 요청을 할 때 조금 더 깔끔하게 공통 모듈을 만들어보고 방법과 후기를 공유해본다.

Request 함수

Alamofire를 import하고 AF.request 함수의 파라미터를 보면 아래와 같다.

convertible 은 URLConvertible 형태이기 때문에 URL을 전달해주면 되고, method, parameter는 각각 HttpMethod와 HttpBody를 나타낸다. header도 보이고 encoder같은 경우는 데이터를 어떻게 인코딩할지에 대한 선택이다.

여러가지 옵션이 있는데, 기본적으로 default에 두고 가져온 데이터가 URL이어서 파싱을 해야한다면 queryString을 사용할수도 있을 것 같다.

interceptor 는 아마 로깅과 같이 API 요청 사이에 필요한 작업을 넣는 것 처럼 보인다.

나는 일단 이 모든 데이터를 하나의 Endpoint 라는 enum에서 관리하기로 했다.

Endpoint Enum

protocol Endpoint {
var baseURL: String { get }
var url: URL { get }
var path: String { get }
var headers: [String: String] { get }
var query: [String: String] { get }
var parameters: [String: Any] { get }
var method: HTTPMethod { get }
var encoding: URLEncoding { get }
}

extension Endpoint {
var url: URL {
var components = URLComponents()
components.scheme = "https"
components.host = self.baseURL
components.path = self.path
components.queryItems = self.query.map { URLQueryItem(name: $0, value: $1) }
return components.url!
}
}

이렇게 공통 Endpoint라는 프로토콜을 만들어주었다. 그리고 url은 모든 요청에서 공통적으로 들어갈 것 같아서 프로토콜 초기구현을 사용해주었다.

만약 프로퍼티도 모든 API에 공통적으로 작성될것으로 예상된다면 초기구현으로 미리 구현해주는 것이 깔끔해보인다.

위 Endpoint 안에 필요한 데이터를 넣어주고

Alamofire 네트워크 요청 함수

enum APIError: Error {
case networkingError(error: Error)
}

class AlamofireNetworkingManager {

static let shared = AlamofireNetworkingManager()
private init() {}

func run<T: Decodable>(_ endpoint: Endpoint, type: T.Type) -> AnyPublisher<T, APIError> {
let headersArray = endpoint.headers.map {
HTTPHeader(name: $0, value: $1)
}

let headers = HTTPHeaders(headersArray)

return AF.request(endpoint.url,
method: endpoint.method,
parameters: endpoint.parameters,
encoding: endpoint.encoding,
headers: headers)
.publishDecodable(type: T.self)
.value()
.mapError { error in
print(error.localizedDescription)
return APIError.networkingError(error: error)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}

func handleCompletion(completion: Subscribers.Completion<APIError>) {
switch completion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}
}

이제 이 EndPoint를 이용해서 Alamofire로 API를 요청하는 코드를 작성해주었다. 위에서 언급했듯 EndPoint는 이 AF.request 함수를 채우기 위해서 필요한 값들을 가지고 있다.

publishDecodable 을 통해 응답을 디코딩 하는 과정까지 있으므로 메인 스레드로 작업을 돌리는 코드까지 작성해주었다.

실제로 API를 요청할 Endpoint 만들기

enum CoinDataEndpoint {
case coins
}

extension CoinDataEndpoint: Endpoint {
var baseURL: String {
return "api.coingecko.com"
}

var path: String {
switch self {
case .coins:
return "/api/v3/coins/markets"
}
}

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

var parameters: [String : Any] {
[:]
}

var query: [String: String] {
return [
"vs_currency":"usd",
"order":"market_cap_desc",
"per_page":"250",
"page":"1",
"sparkline":"true",
"price_change_percentage":"24h",
"x_cg_demo_api_key": KeyConstant.apiKey
]
}

var method: HTTPMethod {
HTTPMethod.get
}

var encoding: URLEncoding {
URLEncoding.default
}
}

위 API는 coingecko의 API이다. API 스펙을 보고 맞는 값들을 넣어준다.

enum CoinDataEndpoint {
case coins
}

Endpoint의 case들이 각각 하나의 API 요청을 의미하고, Endpoint를 채택한 확장을 통해 API 스펙을 표현해준다.

그리고 Endpoint의 프로토콜 초기구현을 통해 만들어진 프로퍼티인 url 을 통해 AF.request 에 필요한 값들이 자동으로 세팅되도록 해주었다.

API 요청

func getCoins() {
coinSubscription = AlamofireNetworkingManager.shared.run(CoinDataEndpoint.coins, type: [CoinModel].self)
.sink(receiveCompletion: AlamofireNetworkingManager.shared.handleCompletion
,receiveValue: { [weak self] returnedCoins in
self?.allCoins = returnedCoins
self?.coinSubscription?.cancel()
})
}

이제 위 처럼 사용해주면 된다. run 함수에 Endpoint를 넣어주는 것으로 자동으로 내부의 값들이 세팅이되어 API를 요청하도록 사용한다.

이 방식은 이전 회사에 다닐 때 동료 iOS 개발자가 사용하는 방식을 눈팅하다가 이런 방식이 있구나 하고 공부하다가 알게 되었으며 튜토리얼 영상 중 URLSession으로 네트워킹을 하는 코드를 Alamofire로 스위칭해보며 작성했다. 이 글은 프로젝트를 진행하다가 더 좋은 방식이 있으면 수정될 수 있고, 이 방법이 가장 깔끔한지는 확실하지 않다!

--

--

PEPPERMINT100
PEPPERMINT100

Published in PEPPERMINT100

기억하기 위해 혹은 잊어버리기 위해 글을 씁니다.

peppermint100
peppermint100

Written by peppermint100

기억하기 위해 또는 잊어버리기 위해 작성하는 블로그입니다.