-
반응형
Swift Combine 기반으로 개발을 하다가 Alamofire & Combine Framework 기반의 괜찮은 네트워크 구조가있어서 벤치마킹 해 보았다.
아래 링크를 참고
www.vadimbulavin.com/modern-networking-in-swift-5-with-urlsession-combine-framework-and-codable/
Alamofire5 부터 Combine을 지원하기 시작했는데, Combine 관련 공식 문서 링크는 다음과 같다.
🏢 전체적인 구조
Agent의 run 함수
실제 Alamofire Combine을 이용해서 네트워크를 호출하는 부분으로 DataRequest는 Alamofire 프레임워크의 일부이고 리턴되는 Publisher는 Combine 프레임워크의 구조체이다. decoder는 응답을 원하는 데이터 타입 T로 디코드 할수 있는 디코더를 넣어주면 된다.
func run<T: Decodable>(_ request: DataRequest, _ decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<Response<T>, APIError>
Agent 의 전체 코드
// // Agent.swift // import Foundation import Combine import Alamofire // 1. Error 타입 정의 enum APIError: Error { case http(ErrorData) case unknown } // 2. ErrorData 안에 들어갈 정보 선언 struct ErrorData: Codable { var statusCode: Int var message: String var error: String? } struct Agent { // 4. Resonse 선언 struct Response<T> { let value: T let response: URLResponse } func run<T: Decodable>(_ request: DataRequest, _ decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<Response<T>, APIError> { return request.validate().publishData(emptyResponseCodes: [200, 204, 205]).tryMap { result -> Response<T> in if let error = result.error { if let errorData = result.data { let value = try decoder.decode(ErrorData.self, from: errorData) throw APIError.http(value) } else { throw error } } if let data = result.data { // 응답이 성공이고 result가 있을 때 let value = try decoder.decode(T.self, from: data) return Response(value: value, response: result.response!) } else { // 응답이 성공이고 result가 없을 때 Empty를 리턴 return Response(value: Empty.emptyValue() as! T, response: result.response!) } } .mapError({ (error) -> APIError in if let apiError = error as? APIError { return apiError } else { return .unknown } }) .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } }
이렇게 만들어놓은 기본적인 APIAgent는 APIClient 가 사용한다. APIClient는 baseURL 별로 따로 만드는 것이 일반적이고 함수 하나 당 API task 하나를 수행하게 한다.
아래 예시는
1) 로컬에 있는 file id를 ApiClient 의 urlForUpload GET 으로 호출해서 AWSUrl을 응답으로 받고
2) AWSClient를 이용해서 위에서 받은 id 로 AWS 서버에 파일을 업로드 하는 함수를 각각 구현한 것이다.
enum ApiClient { static let agent = Agent() static let base = "https://test-api.dundinstudio.com/" static func getUploadURL(caseId: String, uuid: String) -> AnyPublisher<GetUploadURLResponse, APIError> { var urlComps = URLComponents(string: base + "/urlForUpload")! urlComps.queryItems = [ URLQueryItem(name: "itemId", value: itemId), ] let request = AF.request(urlComps.url!) return agent.run(request) .map(\.value) .eraseToAnyPublisher() } } enum AWSClient { static let agent = Agent() static func uploadFile(url: String, fileURL: URL) -> AnyPublisher<Alamofire.Empty, APIError> { let request = AF.upload(fileURL, to: url, method: .put) return agent.run(request) .map(\.value) .eraseToAnyPublisher() } }
🏢 API Publisher 조합하기
Combine과 Alamofire 이 조합이 너무 깔끔한 이유는 다음 예제 코드에 있다.
file A, file B의 uploadURL을 각각 요청해서 AWS에 업로드 한 후 두 작업이 모두 끝났을 때 이를 조합해서 Api 서버에 싱크되었다는 상태를 저장해 보자. (요약된 코드)
// Viewmodel.swift let audioUpload = ApiClient.getUploadURL(uuid: item.id) .map { $0.signedUrl } .flatMap { signedUrl in AWSClient.uploadAudioFile(url: signedUrl, audioURL: item.audioURL) .map { empty -> String in return signedUrl } } let backgroundUpload = ApiClient.getUploadURL(uuid: item.id) .map { $0.signedUrl } .flatMap { signedUrl in AWSClient.uploadAudioFile(url: signedUrl, audioURL: item.backgroudURL) .map { empty -> String in return signedUrl } } Publishers.CombineLatest(audioUpload, backgroundUpload) .map { (audioURL, bgURL) -> AudioItem in return AudioItem(audioURL, bgURL) } .flatMap { item in ApiClient.syncToServer(item) } .sink(receiveCompletion: { (completion) in switch completion { case .finished: Toast.show("동기화 완료") case .failure(let error): switch error { case .http(let error): Toast.show(error.message) case .unknown: Toast.show("동기화 중 에러가 발생했습니다. 다시 시도해 주세요.") } } }, receiveValue: { log.debug("receive Value:\($0)") }) .store(in: &self.subscriptions)
여기서 가장 중요하게 봐야 할 것은 map 과 flatMap이 Swift 에서 기본적으로 제공하는 함수와는 다르다는 것이다. Combine에서의 map은 Publisher가 뱉은 값을 단순히 치환해 주는 것이라면 flatMap은 이전 publisher를 새로운 publisher로 바꿔주는 역할을 한다.
또한 audioUpload 와 backgroundUpload 두개의 publisher를 combineLatest로 묶은 부분도 나중에 다시 보기 좋을 것 같아서 정리했다.
반응형'개발' 카테고리의 다른 글
Mac 기본 캘린더와 구글캘린더 싱크 오류 해결방법 (0) 2021.02.25 CocoaPod 여러가지 (0) 2021.02.09 [iOS] Audio Sound Level 측정하기 (0) 2020.12.02 Ruby path 다시 설정하기 (0) 2020.11.30 [Xcode] Device Build, Archive 안되는 이슈 CodeSign error: unknown error -26276 해결 방법 (0) 2020.11.27 댓글