반응형
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 메서드를 활용하면 네트워크 서비스를 반응형으로 처리하기 훨씬 쉬워지네요!!
반응형
'🍎 Apple > Combine & Rx' 카테고리의 다른 글
[Combine] Publishing 타이밍 조절하기 (Connectable Publishers) (0) | 2023.10.27 |
---|---|
[RxSwift] RxCocoa로 TableView 구현하기 (4) | 2022.05.02 |
[RxSwift] Relay (PublishRelay, BehaviorRelay, ReplayRelay) (0) | 2022.02.25 |
[RxSwift] Driver, Signal (0) | 2022.02.24 |
[RxSwift] ControlProperty, ControlEvent (0) | 2022.02.24 |