You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
RxSwift, RxCocoa를 소개하고 테스트를 만드는 방법을 배운 후, Apple이나 서드파티 프레임워크 위에 RxSwift를 사용하여 확장을 만드는 방법만 맛만 보았다. Apple 또는 타사 프레임워크의 구성 요소를 래핑하는 것은 RxCocoa에 관한 장에 소개되었으므로 프로젝트를 진행하면서 학습을 확장할 수 있다.
이번에는 엔드포인트와의 통신을 관리하고 일반적으로 일반 애플리케이션의 일부인 캐시 및 기타 것들을 관리하기 위해 URLSession 확장을 구성할 예정이다. 네트워킹과 함께 RxSwift를 사용하고 싶다면, 뒷부분에서도 다루는 RxAlamofire를 포함하여 이를 수행할 수 있는 여러 라이브러리의 도움도 받을 수 있다.
Getting started
시작하려면 웹에서 가장 인기 있는 GIF 서비스 중 하나인 Giphy의 API 키가 필요하다. API 키를 얻으려면 공식 문서로 가서 키를 구성하자, 해당 페이지에서 앱을 만들 때("Create an App" 버튼을 통해), Giphy "API"를 선택하면 작업하기에 충분한 개발 키를 받게 된다. 튜토리얼 프로젝트 내 ApiController.swift를 열고 키를 매핑하면, 실습할 준비는 끝난다.
How to create extensions
Cocoa 클래스나 프레임워크를 통해 확장을 만드는 것은 어려운 작업처럼 보일 수 있다; 프로세스가 까다로울 수 있으며 솔루션을 계속하기 전에 사전에 설계가 필요할 수 있다.
여기서 목표는 rx 네임스페이스로 URLSession을 확장하고, RxSwift 확장을 격리하고, 클래스를 더 확장해야 하는 경우 충돌이 일어나지 않는지 확인하는 거다.
How to extend URLSession with .rx
extensionReactivewhere Base:URLSession{
// ...
}
Reactive extension은 간단명료한 프로토콜 확장을 통해 URLSession을 통해 .rx 네임스페이스를 노출시킨다. 이것은 RxSwift로 URLSession을 확장하는 첫 번째 단계가 끝난다. 이제 진짜 포장지를 만들 시간이다.
How to create wrapper methods
URLSession을 통해 .rx 네임스페이스를 노출했으므로 이제 노출하려는 데이터 유형의 Observable을 반환하기 위해 래퍼 함수를 만들 수 있다. API는 다양한 유형의 데이터를 반환할 수 있으므로 앱이 기대하는 데이터 유형을 먼저 확인하는 것이 좋다. 다음 유형의 데이터를 처리하기 위한 래퍼를 만들어보자
• Data: 그냥 평범한 데이터.
• String: 텍스트로서의 데이터.
• JSON: JSON 객체의 인스턴스.
• Decodable: 디코딩 가능한 호환 객체로 디코딩.
• Image: 이미지의 인스턴스.
래퍼 함수는 필요한 정확한 타입을 얻을 수 있도록 할 거다. 그렇지 않으면 오류가 전송되고 응용 프로그램이 충돌하지 않고 오류가 발생하게 된다. 모든 래퍼를 만드는 데 사용될 래퍼는 HTTPURLResponse와 결과 데이터를 반환할 거다. 래퍼 구성의 목표는 나머지 세 개의 연산자를 만드는 데 사용될 Observable<Data>를 반환하는 거다:
먼저 주요 response 함수의 골격을 만들어 무엇을 반환해야 할지 정한다. 방금 만든 확장 프로그램 안에 추가해보자:
extensionReactivewhere Base:URLSession{func response(request:URLRequest)->Observable<(HTTPURLResponse,Data)>{returnObservable.create{ observer in
// content goes here
returnDisposables.create()}}}
이미 이 함수가 무엇을 할 것인지 추측하고 있을 수도 있다. HTTPURLResponse는 요청이 성공적으로 처리되었는지 확인하는 부분이며, Data는 반환된 실제 데이터다.
URLSession은 콜백과 tasks을 기반으로 한다 예를 들어, 요청을 보내고 서버 응답을 다시 수신하는 내장 메서드는 dataTask(with:completionHandler:)다. 이 방법은 콜백을 사용하여 결과를 관리하므로, 관찰 가능한 논리는 필요한 클로저 내에서 관리되어야 한다. 그렇게 하려면, Observable.create 안에 다음을 추가해보자:
두 guard 문 모두 모든 구독에 알리기 전에 요청이 성공적으로 수행되었음을 확인한다. 요청이 올바르게 완료되었는지 확인한 후, 이 Observable에게 필요로 하는 데이터를 전달하자. 이렇게 하면 모든 구독자에게 이벤트를 보낸 다음 즉시 onCompleted()을 통해 Observable을 완료한다. socket 통신과 같이 Observable을 유지하는 경우에는 완료하지 않고 다른 방식으로 구성하면 된다.
URLSession을 래핑하는 가장 기본적인 연산자이다. 애플리케이션이 올바른 타입의 데이터를 다루고 있는지 확인하기 위해 몇 가지를 더 래핑해야 한다. 좋은 건 이 래핑 함수를 재사용하여 나머지 편의 방법을 구축할 수 있다는 거다.
extensionReactivewhere Base:URLSession{func response(request:URLRequest)->Observable<(HTTPURLResponse,Data)>{returnObservable.create{ observer inlettask=self.base.dataTask(with: request){ data, response, error inguardlet response = response,let data = data else{
observer.onError(error ??RxURLSessionError.unknown)return}guardlet httpResponse = response as?HTTPURLResponseelse{
observer.onError(RxURLSessionError.invalidResponse(response: response))return}
observer.onNext((httpResponse, data))
observer.onCompleted()}
task.resume()returnDisposables.create{ task.cancel()}}}func data(request:URLRequest)->Observable<Data>{returnresponse(request: request).map{ response, data ->Datainguard200..<300~= response.statusCode else{throwRxURLSessionError.requestFailed(response: response, data: data)}return data
}}func string(request:URLRequest)->Observable<String>{returndata(request: request).map{ data inreturnString(data: data, encoding:.utf8)??""}}func json(request:URLRequest)->Observable<Any>{returndata(request: request).map{ data inreturntryJSONSerialization.jsonObject(with: data)}}func decodable<D:Decodable>(
request:URLRequest,
type:D.Type)->Observable<D>{returndata(request: request).map{ data inletdecoder=JSONDecoder()returntry decoder.decode(type, from: data)}}func image(request:URLRequest)->Observable<UIImage>{returndata(request: request).map{ data inreturnUIImage(data: data)??UIImage()}}}
방금 했던 것처럼 확장을 모듈화할 때, 더 나은 composability를 허용합니다. 예를 들어, 마지막 observable인 Image는 다음과 같은 방식으로 시각화될 수 있다:
map()와 같은 RxSwift의 일부 연산자는 오버헤드가 되지 않도록 조립할 수 있으므로, 여러 map() 체인을 단일 호출하면서 과정을 최적화한다. 체인으로 계속 연결시키거나, 클로저 블록에 너무 많은 코드가 포함하는 것에 대해 걱정할 필요가 없다. 1개의 함수는 1개의 역할만 하도록 구성하면 된다.
How to create custom operators
RxCocoa에 관한 챕터에서 데이터를 캐시하는 메서드를 만들었었다. 이것은 일부 GIF 파일의 크기를 고려할 때 좋은 접근 방식처럼 보인다. 또한, 좋은 응용 프로그램은 가능한 한 로딩 시간을 최소화해야 한다.
이 경우 좋은 접근 방식은 튜플 타입 (HTTPURLResponse, Data)에서만 사용할 수 있는 데이터를 캐싱하는 특수 연산자를 만드는 거다. 목표는 가능한 한 데이터를 많이 캐시하는 것이므로, Observable<(HTTPURLResponse, Data)>에만 해당 연산자를 만들고 응답 객체를 사용하여 요청의 절대 URL을 검색하고 딕셔너리의 키로 사용하는 것이 합리적으로 보인다.
캐싱 전략은 간단한 딕셔너리로 구성된다; 나중에 이 기본 동작을 확장하여 캐시를 지속하고 앱을 다시 열 때 다시 로드할 수 있지만, 이것은 현재 프로젝트의 범위를 넘어서기에 다음에 알아보자. 캐싱 관련 확장은 아래와 같이 간단하게 구성된다.
privatevarinternalCache=[String: Data]()extensionObservableTypewhere Element ==(HTTPURLResponse,Data){func cache()->Observable<Element>{returnself.do(onNext:{ response, data inguardlet url = response.url?.absoluteString,200..<300~= response.statusCode else{return}internalCache[url]= data
})}}
그리고 해당 캐싱 기능을 위에서 구성한 함수에다 추가해보자. 래핑 함수들에서 선행적으로 실행되는 data(request: URLRequest) -> Observable 메서드에서 구성한 딕셔너리에 등록하는 로직을 추가하자.
func data(request:URLRequest)->Observable<Data>{
// 데이터가 이미 사용 가능한지 확인하기 위한 딕셔너리 확인
iflet url = request.url?.absoluteString,let data =internalCache[url]{returnObservable.just(data)}
// cache() 메서드를 통한 데이터 딕셔너리 등록
returnresponse(request: request).cache().map{ response, data ->Datainguard200..<300~= response.statusCode else{throwRxURLSessionError.requestFailed(response: response, data: data)}return data
}}
이제 특정 타입의 Observable만 확장된 매우 기본적인 캐싱 시스템이 만들어졌다. 이것이 일반적인 솔루션인 점을 고려하면 동일한 방식을 재사용하여 다른 종류의 데이터를 캐시할 수 있습니다.
Using custom wrappers
URLSession을 둘러싼 일부 래퍼와 특정 타입의 Observable을 대상으로 하는 일부 커스텀 연산자를 만들었다. 이제 몇 가지 결과를 가져오고 재미있는 고양이 GIF를 표시해보자.
현재 프로젝트에는 이미 배터리가 포함되어 있으므로, 외부에서 받아와야 할 유일한 것은 Giphy API에서 오는 GiphyGif 구조 목록이다. ApiController.swift를 열고 search() 메서드를 살펴보자.
내부의 코드는 Giphy API에 대한 적절한 요청을 준비하지만, 맨 아래에서는 네트워크 호출을 하지 않는다. 이것은 placeholder 코드이기 때문에 대신 단순히 빈 Observable을 반환한다. 이제 URLSession 리액티브 확장을 완료했으므로, 맞춤형 메서드로 네트워크에서 데이터를 가져오고 적절한 모델로 디코딩할 수 있다.
주어진 쿼리 문자열에 대한 요청을 처리하지만, 데이터는 여전히 표시되지 않는다. GIF가 실제로 화면에 나타나기 전에 수행해야 할 마지막 단계가 하나 있다. Cell View로 이동해보자.
SingleAssignmentDisposable()의 사용은 일을 잘 유지하기 위해 필수적이다. GIF 다운로드가 시작되면, 사용자가 이미지 렌더링을 기다리지 않고 스크롤할 경우, 다운로드가 중단되었는지 확인해야 한다. 이것의 균형을 올바르게 맞추기 위해, prepareForReuse()의 스타터 코드에 이미 포함된 다음 두 줄이 있다: SingleAssignmentDisposable()은 모든 단일 셀에 대해 주어진 시간에 하나의 구독만 사용할 수 있도록 보장하므로 리소스를 낭비하지 않는다.
특히 다른 프레임워크를 래핑하거나 커스텀 모델에 대한 응답을 디코딩할 때 일부 테스트를 만들고 모든 것이 제대로 작동하는지 확인하는 것이 좋다. 테스트 제품군은 구현 상태를 양호하게 유지하며, 변경 사항이나 버그로 인해 코드가 실패한 위치를 찾는 데 도움이 된다.
How to write tests for custom wrappers
이전 장에서 테스트에 대해 알 수 있었다. 이 장에서는 Nimble이라는 Swift에 대한 테스트를 작성하는 데 사용되는 공통 라이브러리를 래핑한 RxNimble과 함께 사용하여 테스트를 진행해보자. RxNimble을 사용하면 테스트를 더 쉽게 작성할 수 있으며 코드를 보다 간결하게 만들 수 있다.
import RxNimble
// 기존 test 코드 작성
letresult=try! observabe.toBlocking().first()expect(result).first !=0
// RxNimble을 활용한 코드
expect(observable)!=0
테스트 파일 iGifTests.swift를 열고 import 부분을 확인하면
사용되는 Nimble, RxNimble, OHTTPStub(네트워크 요청을 stub하는 데 사용), RxBlocking(비동기 작업을 차단 작업으로 변환)을 볼 수 있다.
파일 끝에는 다음과 같은 단일 기능으로 Blocking Observable에 대한 짧은 extension도 구성되어 있다.
이렇게 하면 테스트 파일 전체에서 try? 방법을 남용하는 것을 방지할 수 있다.
import XCTest
import RxSwift
import RxBlocking
import Nimble
import RxNimble
import OHHTTPStubs
@testableimport iGif
classiGifTests:XCTestCase{
// 테스트할 더미 JSON 객체
// 미리 정의된 데이터를 사용하면 데이터, 문자열 및 JSON 요청에 대한 테스트를 더 쉽게 작성 가능
letobj=["array":["foo","bar"],"foo":"bar"]as[String:AnyHashable]letrequest=URLRequest(url:URL(string:"http://raywenderlich.com")!)leterrorRequest=URLRequest(url:URL(string:"http://rw.com")!)overridefunc setUp(){
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
stub(condition:isHost("raywenderlich.com")){ _ inreturnHTTPStubsResponse(jsonObject:self.obj, statusCode:200, headers:nil)}stub(condition:isHost("rw.com")){ _ inreturnHTTPStubsResponse(error:RxURLSessionError.unknown)}}overridefunc tearDown(){
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()HTTPStubs.removeAllStubs()}
// 첫 번째 쓰기 테스트는 데이터 요청에 대한 테스트
// 다음 테스트를 테스트 사례 클래스에 추가하여 요청이 nil으로 반환되지 않는지 확인
func testData(){letobservable=URLSession.shared.rx.data(request:self.request)expect(observable.toBlocking().firstOrNil()).toNot(beNil())}
// {"array":["foo","bar"],"foo":"bar"} 또는, {"foo":"bar","array":["foo","bar"]}로 반환되는 지 확인
func testString(){letobservable=URLSession.shared.rx.string(request:self.request)letresult= observable.toBlocking().firstOrNil()??""letoption1="{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}"letoption2="{\"foo\":\"bar\",\"array\":[\"foo\",\"bar\"]}"expect(result == option1 || result == option2).to(beTrue())}
// 해당 JSON Dict로 반환 되는지 확인
func testJSON(){letobservable=URLSession.shared.rx.json(request:self.request)letobj=self.obj
letresult= observable.toBlocking().firstOrNil()expect(result as?[String:AnyHashable])== obj
}
// 오류가 제대로 반환되는지 확인
// 실제 테스트 시 stub 관련 반환이 NSURLError로 반환
// RxURLSessionError.unknown 타입으로 반환되도록 확인 필요
func testError(){varerroredCorrectly=falseletobservable=URLSession.shared.rx.json(request:self.errorRequest)do{
_ =try observable.toBlocking().first()assertionFailure()}catchlet error{
erroredCorrectly =trueprint(type(of: error))print(error.localizedDescription)}expect(erroredCorrectly)==true}}extensionBlockingObservable{func firstOrNil()->Element?{do{returntryfirst()}catch{returnnil}}}
Common available wrappers
RxSwift 커뮤니티는 매우 활발하며, 이미 많은 확장과 래퍼를 사용할 수 있다.
일부는 애플 컴포넌트를 기반으로 하는 반면, 다른 일부는 많은 iOS와 macOS 프로젝트에서 볼 수 있는 널리 사용되는 타사 라이브러리를 기반으로 한다. 커뮤니티에서 최신 래핑된 프레임워크 목록을 찾을 수 있다.
현재 가장 일반적인 래핑된 프레임워크 에 대한 간략한 개요는 다음과 같다.
RxDataSources
RxDataSources는 RxSwift의 UITableView 및 UICollectionView 데이터 소스이며 다음과 같은 몇 가지 훌륭한 기능을 제공한다.
차이를 계산하는 O(n) 알고리즘.
최소 수의 명령을 섹션으로 구분된 뷰로 보내기 위한 휴리스틱.
이미 구현된 뷰의 확장을 지원.
계층적 애니메이션을 지원.
모두 중요한 기능들이지만, 가장 중요한 건 두 데이터 소스를 구별하는 O(n) 알고리즘이다.
해당 기능은 응용 프로그램이 테이블 보기를 관리할 때 불필요한 계산을 수행하지 않도록 보장한다.
// RxCocoa 기반의 tableView 바인딩
// 단순한 데이터 세트에서는 완벽하게 작동하지만 애니메이션이 부족하고
// 여러 섹션에 대한 지원이 부족하며 확장성이 좋지 않음
letdata= Observable<[String]>.just(["1st place","2nd place","3rd place"])
data.bind(to: tableView.rx.items(cellIdentifier:"Cell")){ index, model, cell in
cell.placeLabel.text = model
}.disposed(by: bag)
// RxDataSources 기반의 datasource 바인딩
letdataSource=RxTableViewSectionedReloadDataSource<SectionModel<String,String>>()Observable.just([SectionModel(model:"Position", items:["1st","2nd","3rd"])]).bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: bag)
// 재사용 가능한 뷰를 기반으로 한 셀 구성
dataSource.configureCell ={ dataSource, tableView, indexPath, item inletcell= tableView.dequeueReusableCell(
withIdentifier:"Cell", for: indexPath
)
cell.placeLabel.text = item
return cell
}
// 타이틀 헤더 구성
dataSource.titleForHeaderInSection ={ dataSource, index inreturn dataSource.sectionModels[index].header
}
RxAlamofire
RxAlamofire는 가장 인기 있는 서드파티 HTTP 네트워킹 라이브러리인 Alamofire의 래핑된 버전.
데이터 호출 외에도, RxAlamofire는 만들 수 있는 편의 기능도 포함하고 있다.
파일을 다운로드하거나 업로드하고 진행률 정보를 검색하기 위해 관찰 가능.
// 모든 요청 세부 정보를 하나의 호출로 결합하고 서버 응답을 Observable<Data>로 반환
func data(_ method:_ url:parameters:encoding:headers:)->Observable<Data>
// 서버 응답을 Observable<String>로 반환
func string(_ method:_ url:parameters:encoding:headers:)->Observable<String>
// 서버 응답을 JSONSerialization을 사용하여 Observable<Any>로 반환
func json(_ method:_ url:parameters:encoding:headers:)->Observable<Any>
RxBluetoothKit
블루투스로 작업하는 것은 복잡할 수 있다.
일부 통화는 비동기식이며, 성공적으로 연결, 데이터 전송 및 장치 또는 주변 장치로부터 데이터를 수신하기 위해 통화 순서가 중요.
RxBluetoothKit는 Bluetooth에서 작업할 때 가장 고통스러운 부분을 추상화하고 몇 가지 기능을 제공.
RxBluetoothKit는 연결 복원을 적절하게 수행하고,
Bluetooth 상태를 모니터링하며, 단일 주변 장치의 연결 상태를 모니터링하는 기능도 갖추고 있다.
letmanager=CentralManager(queue:.main)
// 주변 장치를 검색하는 코드
manager
.scanForPeripherals(withServices:[serviceIds]).flatMap{ scannedPeripheral inletadvertisement= scannedPeripheral.advertisementData
// Do whatever we want with the advertisement.
}
// 하나의 기기에 연결
manager.scanForPeripherals(withServices:[serviceId]).take(1).flatMap{ $0.peripheral.establishConnection()}.subscribe(onNext:{ peripheral inprint("Connected to: \(peripheral)")})
// 매니저 외에도 특성과 주변기기에 대한 편리한 추상화
// 연결 가능한 주변 장치에 연결하려면 다음을 수행 가능
peripheral.establishConnection().flatMap{ $0.discoverServices([serviceId])}.subscribe(onNext:{ service inprint("Service discovered: \(service)")})
// 그리고 만약 장비 특성을 찾길 원한다면
peripheral.establishConnection().flatMap{ $0.discoverServices([serviceId])}.flatMap{Observable.from($0)}.flatMap{ $0.discoverCharacteristics([characteristicId])}.subscribe(onNext:{ characteristic inprint("Characteristic discovered: \(characteristic)")})
Where to go from here?
해당 챕터에서는 Apple 프레임워크를 구현하고 포장하는 방법을 알아보았다.
때때로 RxSwift와 더 잘 연결하기 위해 공식 Apple Framework 또는 타사 라이브러리를 추상화하는 것이 매우 유용.
래핑을 통한 추상화가 필요한 시점에 대한 정해진 규칙은 없지만
프레임워크가 다음 조건 중 하나 이상을 충족한다면 전략을 적용하는 것이 좋다.
완료 및 실패 정보와 함께 콜백을 사용.
많은 delelgate을 사용하여 정보를 비동기식으로 반환.
애플리케이션의 다른 RxSwift 부품과 상호 작동해야 할 경우.
또한 프레임워크에 데이터가 처리되어야 하는 스레드에 대한 제한이 있는지도 알아야 한다.
따라서 RxSwift 래퍼를 만들기 전에 공식문서를 충분히 읽는 것이 좋다.
그리고, 커뮤니티를 통해 구성되어 있는 서드파티를 찾는 것도 매력적이다.
Challenge
Add processing feedback
이 문제에서는 UI 이미지 처리에 대한 정보를 추가해보자.
현재 상태에서는 데이터를 처리할 수 없을 때 응용 프로그램이 빈 이미지를 수신한다.
시간을 내어 코드를 검토하고 기본 빈 개체를 제거하고 유형 변환이 제대로 되지 않으면 코드가 오류를 발생시킨다.
URLsession+Rx.swift의 RxURLsessionError에 이미 deserializationFailed라는 케이스가 포함되어 있다.
타입 변환이 실패하면 해당 케이스의 에러를 던진다.
시작하기 전에, 이 문제를 제기해야 하는 위치와 시기를 이해해보자.
Observable에 오류를 보내는 것은 시퀀스 종료이므로 오류를 보내는 것이 올바른지 확인하고.
문제를 혼자 해결할 수 없더라도 다음 솔루션을 통하여 개선해보자.
The text was updated successfully, but these errors were encountered:
Ch. 17 Creating Custom Reactive Extensions
RxSwift, RxCocoa를 소개하고 테스트를 만드는 방법을 배운 후, Apple이나 서드파티 프레임워크 위에 RxSwift를 사용하여 확장을 만드는 방법만 맛만 보았다. Apple 또는 타사 프레임워크의 구성 요소를 래핑하는 것은 RxCocoa에 관한 장에 소개되었으므로 프로젝트를 진행하면서 학습을 확장할 수 있다.
이번에는 엔드포인트와의 통신을 관리하고 일반적으로 일반 애플리케이션의 일부인 캐시 및 기타 것들을 관리하기 위해 URLSession 확장을 구성할 예정이다. 네트워킹과 함께 RxSwift를 사용하고 싶다면, 뒷부분에서도 다루는 RxAlamofire를 포함하여 이를 수행할 수 있는 여러 라이브러리의 도움도 받을 수 있다.
Getting started
시작하려면 웹에서 가장 인기 있는 GIF 서비스 중 하나인 Giphy의 API 키가 필요하다. API 키를 얻으려면 공식 문서로 가서 키를 구성하자, 해당 페이지에서 앱을 만들 때("Create an App" 버튼을 통해), Giphy "API"를 선택하면 작업하기에 충분한 개발 키를 받게 된다. 튜토리얼 프로젝트 내 ApiController.swift를 열고 키를 매핑하면, 실습할 준비는 끝난다.
How to create extensions
Cocoa 클래스나 프레임워크를 통해 확장을 만드는 것은 어려운 작업처럼 보일 수 있다; 프로세스가 까다로울 수 있으며 솔루션을 계속하기 전에 사전에 설계가 필요할 수 있다.
여기서 목표는 rx 네임스페이스로 URLSession을 확장하고, RxSwift 확장을 격리하고, 클래스를 더 확장해야 하는 경우 충돌이 일어나지 않는지 확인하는 거다.
How to extend URLSession with .rx
Reactive extension은 간단명료한 프로토콜 확장을 통해 URLSession을 통해 .rx 네임스페이스를 노출시킨다. 이것은 RxSwift로 URLSession을 확장하는 첫 번째 단계가 끝난다. 이제 진짜 포장지를 만들 시간이다.
How to create wrapper methods
URLSession을 통해 .rx 네임스페이스를 노출했으므로 이제 노출하려는 데이터 유형의 Observable을 반환하기 위해 래퍼 함수를 만들 수 있다. API는 다양한 유형의 데이터를 반환할 수 있으므로 앱이 기대하는 데이터 유형을 먼저 확인하는 것이 좋다. 다음 유형의 데이터를 처리하기 위한 래퍼를 만들어보자
• Data: 그냥 평범한 데이터.
• String: 텍스트로서의 데이터.
• JSON: JSON 객체의 인스턴스.
• Decodable: 디코딩 가능한 호환 객체로 디코딩.
• Image: 이미지의 인스턴스.
래퍼 함수는 필요한 정확한 타입을 얻을 수 있도록 할 거다. 그렇지 않으면 오류가 전송되고 응용 프로그램이 충돌하지 않고 오류가 발생하게 된다. 모든 래퍼를 만드는 데 사용될 래퍼는 HTTPURLResponse와 결과 데이터를 반환할 거다. 래퍼 구성의 목표는 나머지 세 개의 연산자를 만드는 데 사용될 Observable<Data>를 반환하는 거다:
먼저 주요 response 함수의 골격을 만들어 무엇을 반환해야 할지 정한다. 방금 만든 확장 프로그램 안에 추가해보자:
이미 이 함수가 무엇을 할 것인지 추측하고 있을 수도 있다. HTTPURLResponse는 요청이 성공적으로 처리되었는지 확인하는 부분이며, Data는 반환된 실제 데이터다.
URLSession은 콜백과 tasks을 기반으로 한다 예를 들어, 요청을 보내고 서버 응답을 다시 수신하는 내장 메서드는 dataTask(with:completionHandler:)다. 이 방법은 콜백을 사용하여 결과를 관리하므로, 관찰 가능한 논리는 필요한 클로저 내에서 관리되어야 한다. 그렇게 하려면, Observable.create 안에 다음을 추가해보자:
생성 후 바로 실행되지 않기 때문에 작업을 시작하도록 트리깅해야 하므로 resume() 메서드를 호출하여 트리거하자. 콜백은 나중에 요청 결과를 처리하게 되는 클로저로서 역할을 한다.
이제 task가 진행되었으므로, 진행하기 전에 수행해야 할 변경 사항이 있다. 이전 블록에서, 당신은 Observable이 폐기되면 아무것도 하지 않을 Disposable.create()를 반환하고 있었다. 리소스를 낭비하지 않도록 요청을 취소하는 것이 좋다.
이제 올바른 평생 전략으로 Observable을 받았으니, 이 인스턴스로 이벤트를 보내기 전에 받은 데이터를 검증해보자. guard 문을 활용하여 nil 값에 대한 검증을 한다.
두 guard 문 모두 모든 구독에 알리기 전에 요청이 성공적으로 수행되었음을 확인한다. 요청이 올바르게 완료되었는지 확인한 후, 이 Observable에게 필요로 하는 데이터를 전달하자. 이렇게 하면 모든 구독자에게 이벤트를 보낸 다음 즉시 onCompleted()을 통해 Observable을 완료한다. socket 통신과 같이 Observable을 유지하는 경우에는 완료하지 않고 다른 방식으로 구성하면 된다.
URLSession을 래핑하는 가장 기본적인 연산자이다. 애플리케이션이 올바른 타입의 데이터를 다루고 있는지 확인하기 위해 몇 가지를 더 래핑해야 한다. 좋은 건 이 래핑 함수를 재사용하여 나머지 편의 방법을 구축할 수 있다는 거다.
방금 했던 것처럼 확장을 모듈화할 때, 더 나은 composability를 허용합니다. 예를 들어, 마지막 observable인 Image는 다음과 같은 방식으로 시각화될 수 있다:
map()와 같은 RxSwift의 일부 연산자는 오버헤드가 되지 않도록 조립할 수 있으므로, 여러 map() 체인을 단일 호출하면서 과정을 최적화한다. 체인으로 계속 연결시키거나, 클로저 블록에 너무 많은 코드가 포함하는 것에 대해 걱정할 필요가 없다. 1개의 함수는 1개의 역할만 하도록 구성하면 된다.
How to create custom operators
RxCocoa에 관한 챕터에서 데이터를 캐시하는 메서드를 만들었었다. 이것은 일부 GIF 파일의 크기를 고려할 때 좋은 접근 방식처럼 보인다. 또한, 좋은 응용 프로그램은 가능한 한 로딩 시간을 최소화해야 한다.
이 경우 좋은 접근 방식은 튜플 타입 (HTTPURLResponse, Data)에서만 사용할 수 있는 데이터를 캐싱하는 특수 연산자를 만드는 거다. 목표는 가능한 한 데이터를 많이 캐시하는 것이므로, Observable<(HTTPURLResponse, Data)>에만 해당 연산자를 만들고 응답 객체를 사용하여 요청의 절대 URL을 검색하고 딕셔너리의 키로 사용하는 것이 합리적으로 보인다.
캐싱 전략은 간단한 딕셔너리로 구성된다; 나중에 이 기본 동작을 확장하여 캐시를 지속하고 앱을 다시 열 때 다시 로드할 수 있지만, 이것은 현재 프로젝트의 범위를 넘어서기에 다음에 알아보자. 캐싱 관련 확장은 아래와 같이 간단하게 구성된다.
그리고 해당 캐싱 기능을 위에서 구성한 함수에다 추가해보자. 래핑 함수들에서 선행적으로 실행되는 data(request: URLRequest) -> Observable 메서드에서 구성한 딕셔너리에 등록하는 로직을 추가하자.
이제 특정 타입의 Observable만 확장된 매우 기본적인 캐싱 시스템이 만들어졌다. 이것이 일반적인 솔루션인 점을 고려하면 동일한 방식을 재사용하여 다른 종류의 데이터를 캐시할 수 있습니다.
Using custom wrappers
URLSession을 둘러싼 일부 래퍼와 특정 타입의 Observable을 대상으로 하는 일부 커스텀 연산자를 만들었다. 이제 몇 가지 결과를 가져오고 재미있는 고양이 GIF를 표시해보자.
현재 프로젝트에는 이미 배터리가 포함되어 있으므로, 외부에서 받아와야 할 유일한 것은 Giphy API에서 오는 GiphyGif 구조 목록이다. ApiController.swift를 열고 search() 메서드를 살펴보자.
내부의 코드는 Giphy API에 대한 적절한 요청을 준비하지만, 맨 아래에서는 네트워크 호출을 하지 않는다. 이것은 placeholder 코드이기 때문에 대신 단순히 빈 Observable을 반환한다. 이제 URLSession 리액티브 확장을 완료했으므로, 맞춤형 메서드로 네트워크에서 데이터를 가져오고 적절한 모델로 디코딩할 수 있다.
주어진 쿼리 문자열에 대한 요청을 처리하지만, 데이터는 여전히 표시되지 않는다. GIF가 실제로 화면에 나타나기 전에 수행해야 할 마지막 단계가 하나 있다. Cell View로 이동해보자.
SingleAssignmentDisposable()의 사용은 일을 잘 유지하기 위해 필수적이다. GIF 다운로드가 시작되면, 사용자가 이미지 렌더링을 기다리지 않고 스크롤할 경우, 다운로드가 중단되었는지 확인해야 한다. 이것의 균형을 올바르게 맞추기 위해, prepareForReuse()의 스타터 코드에 이미 포함된 다음 두 줄이 있다: SingleAssignmentDisposable()은 모든 단일 셀에 대해 주어진 시간에 하나의 구독만 사용할 수 있도록 보장하므로 리소스를 낭비하지 않는다.
Testing custom wrappers
특히 다른 프레임워크를 래핑하거나 커스텀 모델에 대한 응답을 디코딩할 때 일부 테스트를 만들고 모든 것이 제대로 작동하는지 확인하는 것이 좋다. 테스트 제품군은 구현 상태를 양호하게 유지하며, 변경 사항이나 버그로 인해 코드가 실패한 위치를 찾는 데 도움이 된다.
How to write tests for custom wrappers
이전 장에서 테스트에 대해 알 수 있었다. 이 장에서는 Nimble이라는 Swift에 대한 테스트를 작성하는 데 사용되는 공통 라이브러리를 래핑한 RxNimble과 함께 사용하여 테스트를 진행해보자. RxNimble을 사용하면 테스트를 더 쉽게 작성할 수 있으며 코드를 보다 간결하게 만들 수 있다.
테스트 파일 iGifTests.swift를 열고 import 부분을 확인하면
사용되는 Nimble, RxNimble, OHTTPStub(네트워크 요청을 stub하는 데 사용), RxBlocking(비동기 작업을 차단 작업으로 변환)을 볼 수 있다.
파일 끝에는 다음과 같은 단일 기능으로 Blocking Observable에 대한 짧은 extension도 구성되어 있다.
이렇게 하면 테스트 파일 전체에서 try? 방법을 남용하는 것을 방지할 수 있다.
Common available wrappers
RxSwift 커뮤니티는 매우 활발하며, 이미 많은 확장과 래퍼를 사용할 수 있다.
일부는 애플 컴포넌트를 기반으로 하는 반면, 다른 일부는 많은 iOS와 macOS 프로젝트에서 볼 수 있는 널리 사용되는 타사 라이브러리를 기반으로 한다.
커뮤니티에서 최신 래핑된 프레임워크 목록을 찾을 수 있다.
현재 가장 일반적인 래핑된 프레임워크 에 대한 간략한 개요는 다음과 같다.
RxDataSources
RxDataSources는 RxSwift의 UITableView 및 UICollectionView 데이터 소스이며 다음과 같은 몇 가지 훌륭한 기능을 제공한다.
모두 중요한 기능들이지만, 가장 중요한 건 두 데이터 소스를 구별하는 O(n) 알고리즘이다.
해당 기능은 응용 프로그램이 테이블 보기를 관리할 때 불필요한 계산을 수행하지 않도록 보장한다.
RxAlamofire
RxAlamofire는 가장 인기 있는 서드파티 HTTP 네트워킹 라이브러리인 Alamofire의 래핑된 버전.
데이터 호출 외에도, RxAlamofire는 만들 수 있는 편의 기능도 포함하고 있다.
파일을 다운로드하거나 업로드하고 진행률 정보를 검색하기 위해 관찰 가능.
RxBluetoothKit
블루투스로 작업하는 것은 복잡할 수 있다.
일부 통화는 비동기식이며, 성공적으로 연결, 데이터 전송 및 장치 또는 주변 장치로부터 데이터를 수신하기 위해 통화 순서가 중요.
RxBluetoothKit는 Bluetooth에서 작업할 때 가장 고통스러운 부분을 추상화하고 몇 가지 기능을 제공.
RxBluetoothKit는 연결 복원을 적절하게 수행하고,
Bluetooth 상태를 모니터링하며, 단일 주변 장치의 연결 상태를 모니터링하는 기능도 갖추고 있다.
공식 문서
Where to go from here?
해당 챕터에서는 Apple 프레임워크를 구현하고 포장하는 방법을 알아보았다.
때때로 RxSwift와 더 잘 연결하기 위해 공식 Apple Framework 또는 타사 라이브러리를 추상화하는 것이 매우 유용.
래핑을 통한 추상화가 필요한 시점에 대한 정해진 규칙은 없지만
프레임워크가 다음 조건 중 하나 이상을 충족한다면 전략을 적용하는 것이 좋다.
또한 프레임워크에 데이터가 처리되어야 하는 스레드에 대한 제한이 있는지도 알아야 한다.
따라서 RxSwift 래퍼를 만들기 전에 공식문서를 충분히 읽는 것이 좋다.
그리고, 커뮤니티를 통해 구성되어 있는 서드파티를 찾는 것도 매력적이다.
Challenge
Add processing feedback
이 문제에서는 UI 이미지 처리에 대한 정보를 추가해보자.
현재 상태에서는 데이터를 처리할 수 없을 때 응용 프로그램이 빈 이미지를 수신한다.
시간을 내어 코드를 검토하고 기본 빈 개체를 제거하고 유형 변환이 제대로 되지 않으면 코드가 오류를 발생시킨다.
URLsession+Rx.swift의 RxURLsessionError에 이미 deserializationFailed라는 케이스가 포함되어 있다.
타입 변환이 실패하면 해당 케이스의 에러를 던진다.
시작하기 전에, 이 문제를 제기해야 하는 위치와 시기를 이해해보자.
Observable에 오류를 보내는 것은 시퀀스 종료이므로 오류를 보내는 것이 올바른지 확인하고.
문제를 혼자 해결할 수 없더라도 다음 솔루션을 통하여 개선해보자.
The text was updated successfully, but these errors were encountered: