RxSwift) why

5 minute read

RxSwift/Why.md at main · ReactiveX/RxSwift

GitHub - RxSwift document 를 번역 및 정리한 내용입니다.

Why

Rx 를 사용하면 declarative way(선언적 방식)으로 앱을 빌드할 수 있습니다.

Bindings

Observable.combineLatest(firstName.rx.text, lastName.rx.text) { $0 + " " + $1 }
    .map { "Greetings, \($0)" }
    .bind(to: greetingLabel.rx.text)

이것은 UITableView 와 UICollectionView 에서도 작동합니다.

viewModel
    .rows
    .bind(to: resultsTableView.rx.items(cellIdentifier: "WikipediaSearchCell", cellType: WikipediaSearchCell.self)) { (_, viewModel, cell) in
        cell.title = viewModel.title
        cell.url = viewModel.url
    }
    .disposed(by: disposeBag)

공식적인 제안은 단순한 바인딩에서 필요하지 않더라도 .disposed(by: disposeBag) 을 사용하는 것입니다.

Retries(재시도)

API 가 실패하지 않으면 좋겠지만, 불행하게도 실패합니다. 다음과 같은 API 메서드가 있다고 해보겠습니다.

func doSomethingIncredible(forWho: String) throws -> IncredibleThing

이 기능을 그대로 사용하면 실패할 경우 재시도하기가 정말 어렵습니다. exponential backoffs 모델리의 복잡성은 말할 것도 없습니다. 물론 가능하지만, 코드에는 당신이 신경 쓰지 않는 일시적인 상태가 많이 포함되어 있고, 이는 재사용할 수 없습니다.

이상적으로, 재시도의 본질을 파악하고, 이를 모든 작업에서 적용할 수 있기를 원할 것입니다.

이것이 Rx 로 간단한 재시도를 할 수 있는 방법입니다.

doSomethingIncredible("me")
    .retry(3)

사용자 지정 재시도 연산자 역시 쉽게 만들 수 있습니다.

Delegates

지루하고 non-expressive 한 일을 하는 대신

public func scrollViewDidScroll(scrollView: UIScrollView) { [weak self] // what scroll view is this bound to?
    self?.leftPositionConstraint.constant = scrollView.contentOffset.x
}

다음과 같이 작성할 수 있습니다.

self.resultsTableViewㅡ
    .rx.contentOffset
    .map { $0.x }
    .bind(to: self.leftPositionConstraint.rx.constant)

KVO(Key-Value-Observing)

Instead of:

`TickTock` was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object.
// key value observers 가 등록되어 있는 동안 'TickTock' 이 할당 해제되었다고 하겠습니다. 이때 observation info 가 유출되어서 다른 object 에 연결될 수도 있습니다.

and

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context

rx.observerx.observeWeakly 를 사용하여 다음과 같이 사용할 수 있습니다.

view.rx.observe(CGRect.self, "frame")
    .subscribe(onNext: { frame in
        print("Got new frame \(frame)")
    })
    .disposed(by: disposeBag)

or

someSuspiciousViewController
    .rx.observeWeakly(Bool.self, "behavingOk")
    .subscribe(onNext: { behavingOk in
        print("Cats can purr? \(behavingOk)")
    })
    .disposed(by: disposeBag)

Notifications

Instead of using:

@available(iOS 4.0, *)
public func addObserverForName(name: String?, object obj: AnyObject?, queue: NSOperationQueue?, usingBlock block: (NSNotification) -> Void) -> NSObjectProtocol

just write

NotificationCenter.default
    .rx.notification(NSNotification.Name.UITextViewTextDidBeginEditing, object: myTextView)
    .map {  /*do something with data*/ }
    ....

Transient state(일시적인 상태)

비동기 프로그램을 작성할 때 transient state(일시적인 상황) 에도 많은 문제가 있습니다. 일반적인 예는 자동 완성 검색 상자입니다.

Rx 없이 코드를 작성한다면 첫 번째 문제는 abcc 가 입력되고 ab 에 대한 보류 중인 요청이 있을 때 이 요청이 취소된다는 것 입니다. 그리 어렵지 않습니다. 보류 중인 요청에 대한 참조를 가질 추가 변수를 생성하면 됩니다.

다음 문제는 요청이 실패하면 지저분한 재시도 로직을 수행해야 합니다. 하지만, 정리해야 하는 재시도 횟수를 캡처하는 필드가 몇개 더 있습니다.

프로그램이 서버에 대한 요청을 실행하기 전에 일정 시간 기다리면 좋을 것 같습니다. 누군가가 매우 긴 내용을 입력하는 과정을 대비하여 서버에 스팸 메일을 보내고 싶지 않습니다. 추가적인 타이머 필드가 있을까요?

검색이 실행되는 동안 화면에 표시되어야 하는 항목과 모든 재시도에도 실패할 경우에 표시되어야 하는 항목은 어떻게 해야할까요?

이 모든 것을 작성하고 테스트하는 것은 지루할 것입니다. Rx 로 동일한 논리를 작성해 보겠습니다.

searchTextField.rx.text
    .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
    .distinctUntilChanged()
    .flatMapLatest { query in
        API.getSearchResults(query)
            .retry(3)
            .startWith([]) // clears results on new search term
            .catchErrorJustReturn([])
    }
    .subscribe(onNext: { results in
      // bind to ui
    })
    .disposed(by: disposeBag)

추가적인 플래그나 필드가 필요하지 않습니다. Rx 는 일시적인 혼란을 처리합니다.

Compositional disposal(구성 폐기)

table view 에 흐린 이미지를 표시하려는 시나리오를 가정하겠습니다.

  • 먼저, 이미지를 URL 에서 가져온 다음 디코딩한 다음 흐리게 처리해야 합니다.
  • 블러 처리를 위한 비용이 존재하기 때문에 셀이 보이는 table view 영역을 벗어나면 프로세스가 취소될 수 있다면 좋을 것입니다.
  • 사용자가 매우 빠르게 스와이프하면 많은 요청들이 시작되고 취소될 수 있기 때문에 셀이 보이는 영역에 들어가면 이미지를 가져오는 것을 즉시 시작하지 않아도 좋을 것입니다.
  • 또한, 동시에 이미지 작업의 수를 제한할 수 있다면 좋을 것입니다.

아래는 Rx 를 사용하여 구현한 것입니다.

// this is a conceptual solution
let imageSubscription = imageURLs
    .throttle(.milliseconds(200), scheduler: MainScheduler.instance)
    .flatMapLatest { imageURL in
        API.fetchImage(imageURL)
    }
    .observeOn(operationScheduler)
    .map { imageData in
        return decodeAndBlurImage(imageData)
    }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { blurredImage in
        imageView.image = blurredImage
    })
    .disposed(by: reuseDisposeBag)

이 코드는 모든 작업을 수행하며 imageSubscription 이 삭제되면, 종속된 모든 비동기 작업ㅇ르 취소하고 불량 이미지가 UI 에 바인딩되지 않도록 합니다.

Aggregating network requests(네트워크 요청 집계)

두 개의 요청을 실행하고 둘 다 완료되었을 때 결과를 집계해야 하는 경우는 어떻게 해야할 까요?

zip operator 가 있습니다.

let userRequest: Observable<User> = API.getUser("me")
let friendsRequest: Observable<[Friend]> = API.getFriends("me")

Observable.zip(userRequest, friendsRequest) { user, friends in
    return (user, friends)
}
.subscribe(onNext: { user, friends in
    // bind them to the user interface
})
.disposed(by: disposeBag)

그렇다면 이러한 APIs 가 백그라운드 스레드에서 결과를 반환하고 main UI 스레드에서 바인딩이 발생해야 하는 경우에는 어떻게 할까요?

observeOn 이 있습니다.

let userRequest: Observable<User> = API.getUser("me")
let friendsRequest: Observable<[Friend]> = API.getFriends("me")

Observable.zip(userRequest, friendsRequest) { user, friends in
    return (user, friends)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { user, friends in
    // bind them to the user interface
})
.disposed(by: disposeBag)

State(상태)

mutation 을 허용하는 언어를 사용하면 전역 상태에 쉽게 접근할 수 있고 변경 할 수 있습니다. 공유된 전역 상태의 제어되지 않은 mutation 은 쉽게 combinatorial explosion(문제의 조합이 받는 영향에 따라 문제의 복잡성이 급격히 증가)을 일으킬 수 있습니다.

그러나 다른 한편으로는, 스마트한 방식으로 사용될 때 명령형 언어는 하드웨어에 더 가깝게 더 효율적인 코드를 작성할 수 있습니다.

combinatorial explosion 과 싸우는 일반적인 방법은 상태를 가능한 단순하게 유지하고, unidirectional data flows(단방향 데이터 흐름)을 사용하여 파생 데이터를 모델링하는 것입니다.

이것은 Rx 가 정말 빛나는 곳입니다. Rx 는 기능적 세계와 명령적 세계 사이의 sweet spot 입니다. 변경할 수 없는 정의와 순수 함수를 사용하여 안정적인 구성 가능한 방식으로 변경 가능한 상태의 스냅샷을 처리할 수 있습니다.

실제적인 예시가 있나요?

Easy integration(쉬운 통합)

자신만의 observable 을 생성해야 한다면?

꽤나 쉽습니다. 아래의 코드는 RxCocoa 에서 가져왔으며 URLSession 으로 HTTP 요청을 래핑하는데 필요한 모든 것입니다.

extension Reactive where Base: URLSession {
    public func response(request: URLRequest) -> Observable<(Data, HTTPURLResponse)> {
        return Observable.create { observer in
            let task = self.base.dataTask(with: request) { (data, response, error) in
            
                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(data, httpResponse))
                observer.on(.completed)
            }

            task.resume()

            return Disposables.create(with: task.cancel)
        }
    }
}

Benefits(이익)

요약하면 Rx 를 사용하면 코드가 다음과 같이 만들어 집니다.

  • Composable(구성 가능한) ← Rx 는 composition 의 별명이기 때문입니다.
  • Reuable(재사용 가능) ← composable 하기 때문입니다.
  • Declarative(선언적) ← 정의는 변경할 수 없고 오직 데이터만 변경되기 떄문입니다.
  • Understandable and concise(이해하기 쉽고 간결합니다.) ← 추상화 수준을 높이고 일시적인 상태를 제거합니다.
  • Stable(안정성) ← Rx 코드는 철저하게 단위 테스트를 거쳤기 때문입니다.
  • Less stateful ← 애플리케이션을 단방향 데이터 흐름으로 모델링하기 때문입니다.
  • Without leaks(누수 없음) ← 리소스 관리가 쉽기 때문입니다.

It’s not all or nothing

Rx 를 사용하여 최대한 많은 애플리케이션을 모델링하는 것이 좋습니다.

그러나 모든 operator 를 알지 못하고 특정 사례의 모델링하는 연산자가 있는지 여부도 모른다면 어떻게 될까요?

Rx 연산자는 수학을 기반으로 하며 직관적이어야 합니다. 좋은 소식은 약 10-15명의 operators 가 일반적인 사용사례를 다룬다는 것입니다. map, filter, zip, observeOn 등이 있습니다.

all Rx operators 의 목록이 있습니다.

각 operators 에 대해 작동 방식을 설명하는데 도움이 되는 marble diagram 이 있습니다.

하지만 해당 목록에 없는 operator 가 필요하면 어떻게 할까요? 나만의 operator 를 만들 수 있습니다.

어떤 이유로 그런 종류의 operator 를 만드는 것이 정말 어렵거나 작업해야하는 일부 레거시 상태 저장 코드가 있는 경우 어떻게 할까요? Rx monads 에서 쉽게 빠져나와 데이터를 처리하고 다시 돌아올 수 있습니다.

Categories:

Updated: