RxSwift - Why Rx?

Groot·2022년 10월 29일
0

TIL

목록 보기
91/153
post-thumbnail

TIL

🌱 난 오늘 무엇을 공부했을까?

📌 RxSwift - Why Rx? 문서보기

📍 Why Rx?

  • Rx는 선언 방식으로 앱을 빌드할 수 있는 방법.

🔗 Bindings

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

// UITableView, UICollectionViews
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는 실패할 가능성이 있다.
  • 아래와 같은 코드가 실패했을때 재시도 하기 어렵다.
func doSomethingIncredible(forWho: String) throws -> IncredibleThing
  • Rx로 간단한 재시도를 할 수 있는 방법
doSomethingIncredible("me")
    .retry(3)

🔗 Delegates

  • Rx의 간단한 표현
public func scrollViewDidScroll(scrollView: UIScrollView) { [weak self]
    self?.leftPositionConstraint.constant = scrollView.contentOffset.x
}
self.resultsTableView
    .rx.contentOffset
    .map { $0.x }
    .bind(to: self.leftPositionConstraint.rx.constant)

🔗 Notifications

  • 기존의 방법대신
@available(iOS 4.0, *)
public func addObserverForName(name: String?, object obj: AnyObject?, queue: NSOperationQueue?, usingBlock block: (NSNotification) -> Void) -> NSObjectProtocol
  • Rx
NotificationCenter.default
    .rx.notification(NSNotification.Name.UITextViewTextDidBeginEditing, object: myTextView)
    .map {  /*do something with data*/ }
    ....

🔗 Transient state

  • 비동기 프로그램을 작성할 때 일시적인 상태에도 많은 문제가 있다. 예) 자동완성 검색 상자
  • Rx를 사용하지 않고 자동완성 기능을 사용할 때 생기는 문제
    • 'abc'의 'c'가 입력되고 '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

  • 테이블 뷰에 흐린 이미지를 표시하려는 시나리오가 있다고 가정해보자. 먼저 이미지를 URL에서 가져온 다음 디코딩한 다음 흐리게 처리한다.
  • 또한 블러 처리를 위한 대역폭과 프로세서 시간이 비싸기 때문에, 셀이 보여지는 테이블 뷰 영역을 벗어나면 전체 프로세스가 취소될 수 있으면 좋다.
  • 사용자가 매우 빠르게 스와이프하면 많은 요청이 시작되고 취소될 수 있기 때문에 셀이 가시 영역에 들어가면 즉시 이미지 가져오기를 시작하지 않는 것도 좋을 것이다.
  • 또한 이미지를 흐리게 하는 것은 비용이 많이 드는 작업이기 때문에 동시 이미지 작업의 수를 제한할 수 있다면 좋을 것이다.
  • 위의 내용을 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을 사용한 예시
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)
  • API가 백그라운드 스레드에서 결과를 반환하고 기본 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)

🔗 Easy integration

  • 자신만의 옵저버블을 생성해야 한다면? 꽤 쉽다.
  • 이 코드는 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는 컴포지션 닉네임이기 때문에 구성이 가능하다.
  • 구성이 가능하기 때문에 재사용이 가능하다.
  • 정의는 변경할 수 없고 데이터만 변경되기 때문에 선언형이다.
  • 추상화 수준 높이기 및 일시적 상태 제거해서 이해하기 쉽고 간결함.
  • Rx 코드는 철저하게 단위 테스트를 거쳐서 안정적이다.
  • 단방향 데이터 흐름으로 애플리케이션을 모델링하기 때문에 상태 저장이 적다
  • 리소스 관리가 쉽기 때문에 누출이 없다.

Rx의 모든 연산자들
마블 다이어그램
사용방법

profile
I Am Groot

0개의 댓글