[RxSwift] #7 Filtering Operators / Throttle & Debounce

20197 단어 swiftrxswiftiOSiOS

raywenderlich RxSwift Ch5~6

1. Ignoring Operators

Swift 의 경우는 최신 버젼에서는 문법에 맞게, elementAt -> element(at: ), takeWhile -> take(while: ) 이런식으로 바뀌어 있었다.

1. .ignoreElements()

  • .next 무시
  • .completed, .error 는 허용

2. .element(at: index)

  • 해당 index에 있는 요소만 방출
  • index는 0부터 시작.

3. .filter( ((Any) -> Bool) )

  • Any 에는 앞의 Observable 에서 next 로 오는 값이 들어간다.
  • 안에 들어가는 함수(클로저) 의 return 값이 true 인 것만 남는다.



2. Skipping Operators

1. .skip(count)

  • 처음부터 뛰어넘을 개수 count 를 입력해서 뛰어넘는다.

2. .skip(while: ((Any) -> Bool) )

  • 안에 들어가는 함수(클로저) 의 return 값이 true 인 것을 건너뛴다.
  • 그럼 filter 랑 반대이기만 하고 똑같은데 왜 씀?
  • 조건에 맞지 않는 하나를 보내고 나면 그 뒤에는 skip 하지 않음.
Observable.of(2, 2, 3, 4, 4)
    .skip(while: {
        // 이 경우에는 조건에 맞지 않는 3부터 출력된다. 
        $0 % 2 == 0
    })
    .subscribe(onNext: {
        print($0)
    })
    .disposed(by: disposeBag)

// 3 4 4

3. .skip(until: Observable)

  • 다른 observable 이 .next 를 emit 하기 전까지는 다 skip 됨.



3. Taking Operators

<-> Skipping

+) Observable 에도 .enumerated() 를 사용할 수 있다.

1. take(count)

  • 처음부터 count 만큼 방출.

2. take(while: ((Any) -> Bool) )

  • 인자로 받은 함수의 반환값이 true 인 것만 방출.
  • skipWhile 이 skip 하다가 조건에 안맞는게 나왔을 때부터 방출하던 것과 반대로
  • take 하다가 조건에 안맞는게 나왔을 때부터 방출 안함.

3. take(until: ((Any) -> Bool))

  • 다른 Observable 이 .next 를 emit 하기 전까지만 방출



4. Distinct Operators

중복해서 이어지는 값을 없애준다.

1. .distinctUntilChanged()

  • 바로 이어지는 값의 중복을 없애준다.
  • 1 1 2 2 1 을 1 2 1 로 만들어준다.

2. .distinctUntilChanged(_: ((Any) -> Void) )

  • distinctUntilChanged 와 기본 로직은 같다 ( 바로 이어지는 값의 중복만 없애줌 )
  • 하지만, custom 가능
distinctUntilChanged {
   $0.lastName == $1.lastName
}




5. Refactoring

#6에서 진행했던 프로젝트를 Filtering Object 를 이용해서 기능을 추가하고 리팩토링 해보았다. 역시 따라가면서 기억에 남는 것들 위주로 포스팅해보려고 한다.


1. share()

Observable 은 구독될 때마다 create 를 호출하기 때문에, 같은 Observable 을 구독해도 다른 결과가 나올 수 있다. 게다가 같은 데이터를 받을 건데 Observable 을 구독자 별로 다르게 유지하는 것은 비효율적이다.

그래서 하나의 구독을 공유하기 위해서 share() 를 이용한다.

let newImage = photosViewController.selectedPhotos.share()
// newImage 를 구독하여 각각 처리를 해주면 된다.

  • create : share() 를 이용하면 구독자 수가 1이 될 때 구독을 생성하고, 이미 생성된 구독을 다른 구독자와 공유한다.
  • dispose : 공유된 sequence 에 대한 모든 구독이 dispose 되면 공유된 sequence 도 dispose 한다.
  • method
    • share() : 구독이 영향을 받기 전까지는 어떤 값도 방출하지 않는다.
    • share(replay: , scope: ) 는 마지막 몇 개의 방출값에 대한 버퍼를 제공한다. (새로운 구독자에게 마지막 몇 개를 바출한다.)
    • sharing 의 경우 completed 되지 않는 Observable 에 사용한다.

2. caching

Observable 은 현재 값이나 히스토리를 제공하지 않기 때문에 특정 요소를 확인하기 위해서는 각 요소 자체가 스스로를 추적하게 해야한다.

// imageCache 배열로, 이미 있는 이미지인지 확인하는 과정을 거쳤다.
.filter { [weak self] newImage in
  // 보통 assetId 같은 것을 이용하지만, 예제라서 이미지 크기를 사용하여 비교.
  let len = newImage.pngData()?.count ?? 0
  guard self?.imageCache.contains(len) == false else { return false }
  self?.imageCache.append(len)
  return true
}

3. 코드 리팩토링에서 중요한 것.

10줄짜리 코드를 3줄로 줄이는 것만이 좋은 리팩토링 방식이 아니다!! 예제에서는 PHPhotoLibrary 에 사진 접근 권한 관련 코드를 쓸 때, 이런 방식으로 코드를 짰다.

// 승인이 되어 있으면 true 
// 안되어 있으면 false 보내고 다시 권한 요청해서 결과값
extension PHPhotoLibrary {
  static var authorized: Observable<Bool> {   
    return Observable.create() { observer in  
    
      if authorizationStatus() == .authorized {
        observer.onNext(true)
        observer.onCompleted()
        
      } else {
        observer.onNext(false)
        requestAuthorization { newStatus in
          observer.onNext(newStatus == .authorized)
          observer.onCompleted()
        }
      }
      return Disposables.create()
    }
  }
}
  1. 꼭 쓸 필요가 없어도 코드를 한두줄 추가해서 의미가 명확해지고 읽기 편해진다면 그렇게 하자!
// 어차피 앱을 맨 처음 켜고 첫번째 값은 false 임.
// 권한 승인을 해줘서 값이 true 일 때
let authorized = PHPhotoLibrary.authorized.share()
authorized
     .skipWhile { $0 == false }
     .take(1) // 쓸 필요가 없지만 의미가 명확해진다.
     .subscribe(onNext: { [weak self] _ in
         self?.photos = PhotosViewController.loadPhotos()
         DispatchQueue.main.async {
             self?.collectionView?.reloadData()
         }
     })
     .disposed(by: bag)

filter{$0 == false} == filter{!$0} == filter{!}
물론 줄여서 많이 사용하는 것은 줄여 써도 괜찮음.

  1. Observable 에서 넘어오는 데이터의 sequence 가 절대불변 할 로직이 아니라면 로직을 생략하지 말자.
// 권한 승인을 거절해서 값이 false 일 때
// 어차피 앱 처음 켠 false, 권한 승인 거절 false 로 2개의 값이 오지만 skip, takeLast를 둘 다 한 것 처럼 로직을 생략하지 말자.
// 
_ = authorized
      .skip(1)
      .takeLast(1)
      .filter { $0 == false }
      .subscribe(onNext: { [weak self] _ in
        guard let errorMessage = self?.errorMessage else { return }
        DispatchQueue.main.async(execute: errorMessage)
      })

3. throttle

너무 많은 사용자 입력값을 걸러주는 코드로
여러번 들어오는 동일 이벤트를 한번만 전달한다.

// 0.5 초간 들어오는 입력을 모아서 반응한다.
// 즉각적으로 반응하지 않게!
.throttle(0.5, scheduler: MainScheduler.instance)

우리가 만든 예제에서는 사용자가 사진을 선택할 때마다 새로운 콜라주를 만드는게 자원 낭비이므로, 0.5초 내에 선택되는 여러개의 사진에 대해서는 일일이 콜라주를 만들지 않게 throttle 을 걸었다.


이 외에도 textField 입력할 때나
모달이 뜨기 전에 버튼을 연타했을 때 모달이 여러개 뜨지 않도록 방지 한다던가
드래그 할 때 드래그 끝나는 위치만 알고 싶을 때 사용할 수 있다.


4. Debounce

debounce 는 예제에는 없었지만 throttle 과 항상 묶여서 다니는 개념인 것 같아서 함께 찾아봤다.

debounce 는 일정 시간동안 발생하는 이벤트를 모아서!! 한번에 전달한다.

debounce(for: .seconds(2), lastest: true, scheduler: MainScheduler.instance)

지정된 시간 2초 안에서 마지막에 들어온 것을 emit하는 옵션인 lastest 를 설정할 수 있다. 기본값이 true! (throttle 도 설정할 수 있다.)

latest: false 이면 첫번째 값만을 emit 하고 2초 동안 들어오는 값은 다 무시한 뒤, 2초가 지나고 나서 언젠가 또 들어오는 첫번째 값만 emit 하게 된다.



느낀점

filtering operator 가 그렇게 어렵진 않다고 생각했는데, takeWhile, skipWhile 이 생각보다 헷갈렸다. 그리고 여기 있는게 다가 아니라 또 아주 많은.. operator 들이 있다고 생각하니까 적시적기에 맞는 것을 찾아 쓰는 것도 어려울 것 같다는 생각이 들었다.

PHPhotoLibrary.autorized 를 Observable 을 이용해서 다루는게 신기했다. 여태 함수 return 값으로만 줬었는데, computed property 를 사용해서도 Observable 을 사용할 수 있다는게.. 배울 부분이 엄청 많은 것 같다.



출처 - https://github.com/fimuxd/RxSwift/tree/master/Lectures
이미지 출처 - https://reactivex.io/

좋은 웹페이지 즐겨찾기