[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/Combine & Rx

[RxSwift] Rx로 네트워크 통신하기

inu 2022. 5. 13. 14:57
반응형

Rx로 네트워크 통신하기

  • RxSwift에서 Network를 처리하는 방법은 크게 3가지입니다.
    • Observable 직접 생성하기
    • RxCocoa가 제공하는 extension 사용하기
    • 외부 라이브러리 사용하기
  • 이들 중 외부 라이브러리를 제외한 두 방법에 대해 알아보겠습니다.

Observable 직접 생성하기

enum ApiError: Error {
    case badUrl
    case invalidResponse
    case failed(Int)
    case invalidData
}
  • 에러코드는 위와 같다고 가정합니다.
struct Result: Codable {
    let list: [Model]
    let code: Int
    let message: String?

    static func parse(data: Data) -> [Model] {
        var list = [Model]()

        do {
            let decoder = JSONDecoder()
            let result = try decoder.decode(Result.self, from: data)

            if result.code == 200 {
                list = result.list
            }
        } catch {
            print(error)
        }

        return list
    }
}

struct Model: Codable {
    let name: String
    let number: Int
}
  • 데이터 타입은 위와 같다고 가정합니다.
    func fetchModelList() {
        let resopnse = Observable<[Model]>.create { observer in
            guard let url = URL(string: urlStr) else {
                observer.onError(ApiError.badUrl)
                return Disposables.create()
            }

            let session = URLSession.shared

            let task = session.dataTask(with: url) { (data, response, error) in
                if let error = error {
                    observer.onError(error)
                    return
                }

                guard let httpResponse = response as? HTTPURLResponse else {
                    observer.onError(ApiError.invalidResponse)
                    return
                }

                guard (200...299).contains(httpResponse.statusCode) else {
                    observer.onError(ApiError.failed(httpResponse.statusCode))
                    return
                }

                guard let data = data else {
                    observer.onError(ApiError.invalidData)
                    return
                }

                do {
                    let decoder = JSONDecoder()
                    let result = try decoder.decode(Result.self, from: data)

                    if result.code == 200 {
                        observer.onNext(result.list)
                    } else {
                        observer.onNext([])
                    }

                    observer.onCompleted()
                } catch {
                    observer.onError(error)
                }
            }
            task.resume()

            return Disposables.create { task.cancel() }
        }
        .asDriver(onErrorJustReturn: [])

        resopnse
            .drive(list)
            .disposed(by: disposeBag)
    }
  • Model의 List를 불러오는 함수를 정의했습니다.
  • Observable.create를 통해 Observable 자체를 생성하여 하나씩 처리하고 있습니다.
  • 조금 복잡하고 귀찮습니다.
  • RxCocoa에서 제공하는 extension을 활용하면 이를 간단하게 처리할 수 있습니다.

RxCocoa가 제공하는 extension 사용하기

  • RxCocoa에 존재하는 URLSession+Rx 파일을 확인해봅시다.
  • 스크롤을 조금 내리다보면 아래와 같은 메서드를 확인할 수 있습니다.
// in [RxCocoa -> Foundation -> URLSession+Rx]
    public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
        return Observable.create { observer in

            // smart compiler should be able to optimize this out
            let d: Date?

            if URLSession.rx.shouldLogRequest(request) {
                d = Date()
            }
            else {
               d = nil
            }

            let task = self.base.dataTask(with: request) { data, response, error in

                if URLSession.rx.shouldLogRequest(request) {
                    let interval = Date().timeIntervalSince(d ?? Date())
                    print(convertURLRequestToCurlCommand(request))
                    #if os(Linux)
                        print(convertResponseToString(response, error.flatMap { $0 as NSError }, interval))
                    #else
                        print(convertResponseToString(response, error.map { $0 as NSError }, interval))
                    #endif
                }

                guard let response = response, let data = data else {
                    observer.on(.error(error ?? RxCocoaURLError.unknown))
                    return
                }

                guard let httpResponse = response as? HTTPURLResponse else {
                    observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
                    return
                }

                observer.on(.next((httpResponse, data)))
                observer.on(.completed)
            }

            task.resume()

            return Disposables.create(with: task.cancel)
        }
    }
  • URLRequest를 파라미터로 받아 Observable을 리턴하는 response 메서드가 존재하네요!
  • Observable은 response 객체와 data 객체를 tuple에 담아 방출하고 있습니다.
  • 내부를 살펴보면 앞서 Observable 직접 생성하여 구현한 Network 처리와 매우 유사함을 알 수 있습니다.
  • 기본적인 처리는 모두 이곳에서 수행해주고 있기 때문에 이를 활용하면 응답 확인 및 데이터 처리만 수행하면 됩니다.
    public func data(request: URLRequest) -> Observable<Data> {
        return self.response(request: request).map { pair -> Data in
            if 200 ..< 300 ~= pair.0.statusCode {
                return pair.1
            }
            else {
                throw RxCocoaURLError.httpRequestFailed(response: pair.0, data: pair.1)
            }
        }
    }
  • 좀 더 내려보면 상태코드를 확인하여 정상일 경우 Data만 방출해주는 메서드도 존재합니다.
  • 위 response 메서드를 활용해 statusCode까지 내부적으로 확인해서 정상일 경우 Data를 방출해줍니다.
  • 이를 활용하면 statusCode를 확인하는 작업조차 필요없어지겠네요!
    public func json(request: URLRequest, options: JSONSerialization.ReadingOptions = []) -> Observable<Any> {
        return self.data(request: request).map { data -> Any in
            do {
                return try JSONSerialization.jsonObject(with: data, options: options)
            } catch let error {
                throw RxCocoaURLError.deserializationError(error: error)
            }
        }
    }
  • 데이터를 JSONSerialization를 활용해 직렬화해주는 메서드도 존재합니다.
  • 반환값이 Any이지만 실질적으로는 Dictionary로 반환됩니다. (타입캐스팅 필요)
  • 데이터를 Dictionary 형태로 확인하고 싶을 때 사용합니다.
  • 이제 이들 중 data 메서드를 활용해 앞선 동작과 같은 역할의 로직을 구현해보겠습니다.
    func fetchModelList() {
        let response = Observable.just(urlStr)
            .map { URL(string: $0)! }
            .map { URLRequest(url: $0) }
            .flatMap { URLSession.shared.rx.data(request: $0) }
            .map { Result.parse(data: $0).list }
            .asDriver(onErrorJustReturn: [])

        response
            .drive(list)
            .disposed(by: disposeBag)
    }
  • 앞서 Observable을 직접 만드는 방법에서 살펴본 것과 같이 Model의 List를 불러오는 함수를 정의했습니다.
  • 앞선 코드보다 훨씬 간결해졌음을 알 수 있습니다.
  • response나 data 메서드를 활용하면 네트워크 서비스를 반응형으로 처리하기 훨씬 쉬워지네요!!
반응형