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

🍎 Apple/Combine & Rx

[RxSwift] Driver, Signal

inu 2022. 2. 24. 15:21
반응형

이번엔 RxCocoa의 Driver와 Signal에 대해 알아보아요.


RxCocoa Trait?

Driver와 Singal 모두 UI 요소를 위한 기능이지만 Single, Completable, Maybe와 마찬가지로 Observable을 wrapping하여 구성되기 때문에 RxCocoa의 Trait이라고 볼 수 있습니다.


Driver

Driver는 UILayer에서 반응형 작업을 좀 더 직관적으로 사용할 수 있도록 제공되는 개념입니다. Observable은 상황에 따라 BackgroundScheduler를 명확히 지정해주어야하지만, Driver는 MainScheduler로 지정되어 있습니다. UI 관련적용에는 따로 Scheduler를 지정해줄 필요없이 Driver를 사용하여 실수를 방지하는 것이 좋습니다. (cf. 이름이 Driver인 이유는 목적 자체가 애플리케이션을 구동(Drive)하는 것에 있기 때문이라고 합니다. 재밌네요!)

 

Dirver의 특징으로는 3가지가 있습니다.

  • 에러를 반환하지 않음
  • MainScheduler에서 돌아감
  • side effect를 share함 (share(replay: 1, scope: .whileConnected))

 

cf. share?

 

한번 생성한 시퀀스를 공유해 사용할 수 있도록 하는 Operator입니다. share를 사용하면 발생한 이벤트가 버퍼에 저장되고, 새로운 subscription은 새로운 데이터 시퀀스를 생성하는 것이 아니라 버퍼에 저장된 이벤트를 전달받게 됩니다. replay는 버퍼의 사이즈를 의미하며, scope은 .forever, .whileConnected 중 하나를 선택할 수 있습니다. .forever를 선택할 경우 버퍼가 subscription의 존재 여부에 관계없이 유지되며, .whileConnected를 선택할 경우 1개 이상의 subscription이 존재하는 동안에만 버퍼가 유지됩니다.

 

예시를 보면서 이 특징의 필요성에 대해 확인해봅시다.

let results = query.rx.text
    .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
    .flatMapLatest { query in
        fetchAutoCompleteItems(query)
    }

results
    .map { "\($0.count)" }
    .bind(to: resultCount.rx.text)
    .disposed(by: disposeBag)

results
    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
        cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)
  • throttle operator는 일정시간만큼 이벤트를 받지않고 대기하도록 하는 operator입니다. 계속해서 데이터시퀀스가 전달될 것을 방지하는 역할을 합니다.
  • 이렇게 받아온 query를 통해 서버에 데이터를 요청합니다.
  • 이를 UI 요소에 적용합니다. (resultCount, resultsTableView)

이 코드의 문제점은 무엇일까요?

  • 먼저 fetchAutoCompleteItems가 에러를 발생시키면 자연스럽게 데이터시퀀스가 종료될 것이고, UI는 새로운 쿼리에 대해 어떤 반응도 취하지 못할 것입니다.
  • 또 fetchAutoCompleteItems가 background thread에서 동작해버리면, UI 요소에 접근하면서 충돌(에러)이 발생할 수 있습니다.
  • 마지막으로 두개의 bind를 수행하면서 같은 쿼리에 대해 두 번의 쿼리요청이 발생합니다.

이 문제점을 해결해보겠습니다.

let results = query.rx.text
    .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
    .flatMapLatest { query in
        fetchAutoCompleteItems(query)
            .observeOn(MainScheduler.instance)  // results are returned on MainScheduler
            .catchErrorJustReturn([])           // in the worst case, errors are handled
    }
    .share(replay: 1)                           // HTTP requests are shared and results replayed
                                                // to all UI elements

results
    .map { "\($0.count)" }
    .bind(to: resultCount.rx.text)
    .disposed(by: disposeBag)

results
    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
        cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)
  • catchErrorJustReturn로 에러를 방지하고
  • observeOn을 통해 MainScheduler에서 작업이 처리되도록 하고
  • share를 적용해 데이터 시퀀스를 공유하도록 했습니다.

분명 문제를 해결됐지만 규모가 큰 프로젝트에서 이들을 일일히 적용하는 것은 귀찮은 일입니다. 또 실수가 발생할 가능성도 있겠죠. 이럴 때 사용하면 좋은 것이 Driver입니다.

let results = query.rx.text.asDriver()        // This converts a normal sequence into a `Driver` sequence.
    .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
    .flatMapLatest { query in
        fetchAutoCompleteItems(query)
            .asDriver(onErrorJustReturn: [])  // Builder just needs info about what to return in case of error.
    }

results
    .map { "\($0.count)" }
    .drive(resultCount.rx.text)               // If there is a `drive` method available instead of `bind(to:)`,
    .disposed(by: disposeBag)              // that means that the compiler has proven that all properties
                                              // are satisfied.
results
    .drive(resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
        cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)
  • asDriver를 통해 ControlProperty Trait을 Driver로 만들어주었습니다. (onErrorJustReturn을 통해 error 처리도 가능합니다.)
  • bind 대신 drive를 통해 이를 적용했습니다. (drive는 Driver에 정의된 메서드로, 이를 활용하면 해당 데이터시퀀스가 절대 오류를 방출하지 않으며 메인 스레드에서 돌아가고, 데이터시퀀스를 share함을 명시적으로 파악할 수 있습니다.)

하나씩 처리해주어야 했던 작업이 Driver 하나를 통해 간단하게 해결되었습니다. UI를 구현할때는 유용하게 사용할만 하겠어요!

Signal

Driver와 유사하지만 버퍼를 가지지 않고 지난 데이터를 replay하지 않는다는 차이점이 있습니다. 특징을 정리해보면 아래와 같겠네요.

  • 에러가 발생하지 않음
  • MainScheduler에서 돌아감
  • side effect를 share함 (share(scope: .whileConnected))
  • 지난 데이터를 replay하지 않음

참고

반응형