apple/UIKit & ReactiveX

[week5] 🌟Transforming Observables

lgvv 2021. 7. 10. 17:07

✅ 이번시간에는 rx에서 가장 중요한 연산자인 Transforming Observables에 대해서 살펴볼 예정이야.

 

나의 실습 소스코드 위치 : https://github.com/lgvv/MyRxSwift

 

lgvv/MyRxSwift

나의 RxSwift 공부 기록장. Contribute to lgvv/MyRxSwift development by creating an account on GitHub.

github.com

 

 

이번에는 커리큘럼의 내용을 기반으로 공식문서를 곁들여 보도록 하자!

https://github.com/lgvv/RxSwiftStudy/blob/main/week5.md

 

lgvv/RxSwiftStudy

RxSwift를 공부하는 Repository입니다.🐍. Contribute to lgvv/RxSwiftStudy development by creating an account on GitHub.

github.com

 

(목차)

1. Transforming elements

 - toArray

 - map + enumerated

2. Transforming inner observabels

 - flatMap

 - flatMapLatest

3. Observing evnets

 - materialize and dematerialize

4. 공식문서에 나와있는 Transforming Observables

 - observeOn & subscribeOn

 - groupby

 - scan

 - window

5.

 

✅ toArray 

 - Observable은 from just 와 같이 독립적으로 요소들을 방출하지만 observable을 tableView 또는 collectionView와 바인딩 하는 것처럼 이 것을 조합하고 싶을 수 있습니다. 이럴 때 아래의 toArray() operator를 사용한다.

toArray()는 대표적으로 To에 속하여 공식문서에서 이쪽으로 안내한다.

To

그럼 코드를 함께 보도록 할까?

        print(" ===== toArray ===== ")
        Observable.of("A", "B", "C")
            .toArray()
            .subscribe(onSuccess: {
                print($0)
                
            }, onError: { error in
                print("\(error.localizedDescription)")
            }).disposed(by: disposeBag)
            
// 결과값
["A", "B", "C"]

배열로 묶인 것을 확인할 수 있다.

그럼 이번에는 onNext가 아닌 onSuccess를 써야하는건 어떤 경우일까?

싱글 타입일 때는 이렇게 해요!

 

✅ map + enumerated

 - map은 각 항목에 함수를 적용하여 이에 따른 결과를 방출한다.

 - 스위프트의 map과 같으나, 여기에 있는 map은 Observable에서만 작동한다.

map

        print(" ===== map ===== ")
        let formatter = NumberFormatter()
        formatter.numberStyle = .spellOut
        
        Observable<NSNumber>.of(123, 4, 56)
            .map {
                formatter.string(from: $0) ?? ""
            }
            .subscribe(onNext: {
                print($0)
            }).disposed(by: disposeBag)
            
        print(" ===== map + enumerated ===== ")
        Observable.of(1,2,3,4,5,6)
            .enumerated()
            .map{ index, value in
                index > 2 ? value * 2 : value
            }
            .subscribe(onNext: {
                print($0)
            })
            .disposed(by: disposeBag)           
            
 
// 결과값
 ===== map ===== 
one hundred twenty-three
four
fifty-six
 ===== map + enumerated ===== 
1
2
3
8
10
12

코드를 보면 그렇게 어렵지 않다. 다른 포스팅에서 이와 같은 작업을 할때, 조금 더 코드가 길었었는데, 이렇게 하면 명확하게 처리할 수 있다.

 

✅ flatMap

 - Observable에 의해 방출된 항목은 Observable로 변환한 다음 다시 그 방출값을 단인 Observable로 평면화 한다.

 - 이건 확실히 좀 어려운 것 같다.. 꼼꼼하게 봐보자!

flatMap

struct Student {
    var score: BehaviorSubject<Int>
}        
        
        print(" ===== flatmap ===== ")
        let ryan = Student(score: BehaviorSubject(value: 80)) // 80으로 초기화
        let charlotte = Student(score: BehaviorSubject(value: 90)) // 90으로 초기화
        
        let student = PublishSubject<Student>() // student타입의 subject를 생성
        
        student
            .flatMap{ $0.score } // 이를 통해 student타입의 시퀀스인 student를 Student 내에 있는 score로 transform함
            .subscribe(onNext: { print($0) })
            .disposed(by: disposeBag)
        
        student.onNext(ryan) // student에게 라이언 넣어줌
        ryan.score.onNext(85) // 라이언의 선언된 behavior에 85를 건네줌
        
        student.onNext(charlotte)
        ryan.score.onNext(95)
        
        charlotte.score.onNext(100)
        
        
 
// 결과값
80
85
90
95
100

이렇게 나오게 된다. 이거 코드를 봐도 확 와닿지 않는데, 꼼꼼하게 살펴볼 필요가 있겠다.

🟠 결론부터 말하자면 flatMap연산자의 특징은 생성된 모든 observable의 Element들의 변화를 관찰할 수 있다는 것이다.

 

아이패가 있어서 ㅜㅜ 손으로 바로 그려서 여기에 올리면 더할 나위 없이 좋은건데, 없으니까 내가 알아서 잘 만들어 보자!

내가 그림 예제 그림을 통한 이해

❗️ 내 이해를 바탕으로 그린 그림이다. 이해가 잘못되어서 틀릴 수 있음.

이런식으로 되서 flatMap은 안의 모든 subjects를 확인할 수 있다.

 

✅ flatMapLatest

  - 이 연산자는 map과 switchLatest연산자를 합친 것

  - switchLatest에 대해선 다음 챕터에서 더 자세하게 배울 것입니다. 여기서 맛보기만 하자면, switchLatest는 가장 최근 observable의 값만 제공하고, 그 이전의 observable에 대해선 구독하지 않는 연산자입니다.

 - flatMap과는 달리 Observable의 마지막 요소의 변화만 관찰해주는 연산자

flatMapLatest

        print(" ===== flatMapLatest ===== ")
        let appech = Student(score: BehaviorSubject(value: 80)) // 80으로 초기화
        let muzi = Student(score: BehaviorSubject(value: 90)) // 90으로 초기화
        
        let friends = PublishSubject<Student>() // student타입의 subject를 생성
        
        
        friends.flatMapLatest{
            $0.score
        }
        .subscribe(onNext : { print($0) })
        .disposed(by: disposeBag)
        
        friends.onNext(appech)
        appech.score.onNext(85)
        friends.onNext(muzi)
        appech.score.onNext(95)
        muzi.score.onNext(100)
        
   
//결과값
80
85
90
100

코드를 보자 확 이해가 가는가? 사실 확 이해가 가긴 하는데, 그럼에도 불구하고 그림을 통해 보도록 하자.

flatMapLatest 내가 그린 예제

이해가 가지? 어렵지 않다!

 

materialize and dematerialize

우선 material부터 확인해볼까?

material은 언제 사용하는지의 설명
materialize

그럼 코드로 함께 알아보자.

enum MyError : Error {
    case anError
}
       
       
        print(" ===== materialize ===== ")
        let chulsu = Student(score: BehaviorSubject(value: 80)) // 80으로 초기화
        let younghee = Student(score: BehaviorSubject(value: 100)) // 100으로 초기화
        
        let couple = BehaviorSubject(value: chulsu)// student타입의 subject를 생성
        
        let coupleScore = couple.flatMapLatest{
            $0.score
        }
        
        coupleScore.subscribe(onNext : {
            print($0)
        }).disposed(by: disposeBag)
        
        chulsu.score.onNext(85)
        chulsu.score.onError(MyError.anError)
        chulsu.score.onNext(90)
        
        couple.onNext(younghee)
        
   
// 결과값
80
85
Unhandled error happened: anError

1. 에러를 하나 만들어 둔다. (중요) 

2. 철수와 영히라는 두개의 스튜던트 타입의 인스턴스를 생성하고, 초기값으로을 철수를 같는 커플 비헤비어 서브젝트를 만든다.

3. 플랫맵레이티스트를 사용하여, 커플의 스코어 값을 옵저버블로 만든 커플스코어를 만들어준다.

4. 커플 스코어를 구독한 뒤 방출하는 스코어 값을 프린트한다.

5. 철수에 새점수 추가하고 에러를 추가한다. 그리고 다시 새 점수를 추가한다.

6. 커플에 새 스튜던트 값인 영희를 추가한다.

 

결과에서 주목해봐야 하는 점은 error코드로 인하여 Final Sequence가 죽는다는 사실이다.

그렇다면 error로 인해 죽어버리면 더이상 들어오는 데이터를 처리할 수 없는데, 더 들어오는 정보에 대해서 처리할 수 있는 방법은 없을까?

그것이 바로 materialize이다.

이렇게 코드를 수정한다면? -> 각각이 Observable로 바뀌어서 error가 있어도 final Sequence가 죽지 않는다.

에러는 여전히 coupleScore 시퀀스를 종료시키지만 couple 시퀀스는 그대로 살아있다.

내가 그린 그림을 통한 이해

이렇게 되기 때문에, Fianl seqence가 죽어서 전달이 안된다는 말이다.

그렇다면 Materialize한 시퀀스를 알아볼까?

 

그림을 통한 이해

각각을 flatmap을 사용해서 모든 서브젝트를 다 관찰하고 있으면서도, 어느 한 곳에서 에러가 발생하도 final sequence를 죽지 않고 안전하게 사용할 수 있다는 장점이 있다.

하지만 이렇게 할 경우에는 이벤트는 받을 수 있지만, 요소들은 받을 수 없기 때문에 dematerialize 사용해야한다.

 

그럼 다음은 dematerialize에 대해서 알아볼까?

dematerialize

코드로 한번 볼까?

        let dechulsu = Student(score: BehaviorSubject(value: 80)) // 80으로 초기화
        let deyounghee = Student(score: BehaviorSubject(value: 100)) // 90으로 초기화
        
        let decouple = BehaviorSubject(value: dechulsu)// student타입의 subject를 생성
        
        let decoupleScore = decouple.flatMapLatest{
            $0.score.materialize()
        }
        
        decoupleScore.filter{
            guard $0.error == nil else {
                print($0.error!)
                return false
            }
            
            return true
        }
        .dematerialize()
        .subscribe(onNext : { print($0) })
        .disposed(by: disposeBag)
        
        dechulsu.score.onNext(85)
        dechulsu.score.onError(MyError.anError)
        dechulsu.score.onNext(90)
        
        decouple.onNext(deyounghee)
        
// 결과값
80
85
anError
100

코드를 확인해보면, 에러가 방출되면 출력할 수 있도록 guard문을 작성한다.

다음으로는 dematerialize를 이용해 decouplescore의 observable을 원래 모양으로 return 하고 점수와 정이 이벤트를 방출할 수 있도록 한다.

material이랑 짝꿍으로 쓰이는데, demateralize가 들어가는 것을 확인할 수 있다.

 

그러니까 meterialize를 통해서 에러가 나도 죽지 않게 이벤트가 있어!! 만 전달한다면 그에 대한 요소는 demateralize를 통해 그 이벤트를 까봐야지~ 이렇게 처리한는거다. 그 요소가 설사 error이여도 final sequence에는 이벤트 랩핑되어 전달되어 요소를 까본다는 개념으로 생각해보자.

 

✅ delay

 - 일정 시간 지연 후 방출

delay

        print(" ===== delay ===== ")
        Observable.of(1,2,3,4,5)
            .delay(3, scheduler: MainScheduler.instance)
            .subscribe(onNext : {print($0) })

// 결과값
(3초 지연)
1
2
3
4
5

 

✅ Do

 - 다양한 옵저버블 라이프 사이클 이벤트에 대해 수행할 작업을 등록

do

 

여기에는 엄청 많은 메소드들이 있다.

do를 입력했을떄 사용 가능한 것들

 

        print(" ===== do ===== ")
        Observable.of("A","B","C")
            .do(onNext: {print("do on Next -> \($0)")} )
            .subscribe(onNext : {
                print("subscribe ->\($0)")
            }).disposed(by: disposeBag)
       
// 결과값
do on Next -> A
subscribe ->A
do on Next -> B
subscribe ->B
do on Next -> C
subscribe ->C

이렇게 사용할 수 있다.

다양한 메소드들은 직접 한번 해보길 바란다!

 

✅ observeOn & subscribeOn

 - observeOn : 내 아래로 스케줄러를 변경함. -> 작성하는 위치 중요

 - subscribeOn : 현재 subscribe의 시작지점부터 스케줄러를 변경함. -> 어디에 작성하든 맨위에 적용 돼

 

이건 지난 시간에 본 적이 있어. 쉽게 말해서 네트워크 같은 헤비한 작업은 백그라운드로 돌리고, UI를 처리해야할 때, 메인 스케줄러로 가져와서 한다면 사용자에게 더 좋은 결과를 제공해줄 수 있겠지?

 

스케줄러를 이해하기 위한 사진

색으로 보면 알겠지만 쉽게 이해할 수 있다.

 

코드를 보자

//선언부
    @IBOutlet weak var observeOnBtn: UIButton!
    @IBOutlet weak var observeOnLabel: UILabel!        
    var obseveOnCount = 0
    let backgroundScheduler = SerialDispatchQueueScheduler(qos: .default)        
    
   
// 구현부
        print(" ===== observeOn ===== ")
        observeOnBtn.rx.tap
            .subscribe(onNext : {self.observeOnTest()})
            
       
       func observeOnTest() {
        Observable.of(2,3)
            .map{ n -> Int in
                print("this is background working")
                return n * 2
            }
            .subscribeOn(backgroundScheduler)
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { emit in
                    print("emit value = \(emit)")
                    self.obseveOnCount += 1
                    self.observeOnLabel.text = String(emit)})
            .disposed(by: disposeBag)
            
            
       
// 결과값
this is background working
this is background working
emit value = 4
emit value = 6

이건 학습을 위해 코드를 이렇게 작성하였는데, 우선 subscribeOn을 이용하여 전체가 백그라운드에서 실행하게 되고, UI를 이용하기 전에 메인 스케줄러 쪽으로 넘기는 모습을 볼 수 있다.

 

✅ groupby

 - observable의 정의

"divide an Observable into a set of Observables that each emit a different subset of items from the original Observable"

 

정의는 공식문서에 따르면 저러한데, 저게 잘 와닿지가 않는다.

groupby

공식문서의 마블 다이어그램을 봐도 사실 이해가 잘 가지 않는데, 그럼 코드로 한번 볼까?

        print(" ===== GroupBy ===== ")
        var first = Observable.of(1,2,3,4,5,6,7,8,9,10)
        
        first.groupBy { i -> String in
            if i%2 == 0 {
                return "odd"
            } else {
                return "even"
            }
        }
        .flatMap { o -> Observable<String> in
            if o.key == "odd" {
                return o.map { v in
                    "odd \(v)"
                }
            } else {
                return o.map { v in
                    "even \(v)"
                }
            }
        }
        .subscribe{ event in
            switch event {
            case let .next(value) :
                print(value)
            default :
                print("finished")
            }
        }.disposed(by: disposeBag)
        
//결과값
even 1
odd 2
even 3
odd 4
even 5
odd 6
even 7
odd 8
even 9
odd 10
finished

그룹을 만들건데, flatmap을 통해서 각 분기로 나눠진 여러 개의 시퀀스를 모두 참조해야한다. 그리고 그것을 맵해서, 문자와 합친다. 그리고 그 이벤트를 내보내주고, 넥스트가 아니면 finish를 보내준다. 

코드를 보니까 어렵지 않다!! 

 

✅ scan

 - 옵저버블을 각 항목에 순차적으로 적용하고 방출한다.

scan

        print(" ===== scan ===== ")
        Observable.of(1,2,3,4,5)
            .scan(0) { prev, next in
                return prev + next
            }
            .subscribe(onNext: { print($0) })
            .disposed(by: disposeBag)
    
// 결과값
1
3
6
10
15

스캔은 이전 값을 계속 참조해서 갱신하게 돼.

따라서 스캔의 파라미터는 2개를 갖는데, 처음에는 참조할 value가 없어서 scan의 첫번쨰 인자로 초기값을 하나 주는거고 나머지는 그냥 사용하면 돼.

스캔의 활용에는 아래의 예제가 있는데,

스캔의 활용 예시들

// scan을 활용하여 텍스트 필드의 입력글자 제한하기

myTextField.rx.text.orEmpty.asObservable()
   .scan("") { (oldValue, newValue) in
      return newValue.isNumber ? newValue : oldValue
   }
   .bind(to: myTextField.rx.text)
   .disposed(by: disposeBag)

extension String {
   var isNumber: Bool {
   do {
      let regex = try NSRegularExpression(pattern: "^[0-9]+$", options: .caseInsensitive)
      if let _ = regex.firstMatch(in: self, options: .reportCompletion, range: NSMakeRange(0, count)) { return true }
      } catch { return false }
      return false
   }
}

// 결과값
입력 : 12ab3

Event next(1)
text: 1
Event next(12)
text: 12
Event next(12a)
text: 12
Event next(12b)
text: 12
Event next(123)
text: 123

 

scan의 활용

 

스캔을 활용할 때는, 상태에 대한 변화가 필요하면 스캔을 우선적으로 생각해보기!

 

 

✅ buffer & window 

 

 

window & buffer

두 연산자의 선언을 한번 보자

두 연산자의 선언

buffer나 window모두 다음 두가지 조건 중 1가지가 만족되기 전까지는 이벤트를 방출하지 않습니다.

  1. 첫 구독 시점 혹은 가장 최근 이벤트 발생 후로 timespan으로 지정한 시간이 지났다(타임 아웃)
  2. 첫 구독 시점 혹은 가장 최근 이벤트 발생 후로 원본 Observable에서 count만큼의 이벤트가 발생했다.

 

이 두가지 조건중 하나가 만족되기 전까지는, 원본 Observable의 이벤트는 내부 버퍼에 저장됩니다. 그러다 이벤트 발생 시점이 되면, 해당 버퍼의 이벤트들을 모두 내보내고, 버퍼를 비우고 타이머를 다시 시작합니다.

buffer와 window의 차이는 단지 이 버퍼가 Array의 형태로 방출되는가 또 다른 Observable의 형태로 방출되는 가의 차이입니다.

 

        //선언부
        let timer1 = Observable<Int>.interval(RxTimeInterval.seconds(1), scheduler: MainScheduler.instance).map({"o1: \($0)"})
        
        timer1.buffer(timeSpan: RxTimeInterval.seconds(3), count: 2, scheduler: MainScheduler.instance)
            .subscribe { event in
            switch event {
            case let .next(value):
                print(value)
            default:
                print("finished")
            }
            
            }.disposed(by: disposeBag)

//결과값
["o1: 0", "o1: 1"]
["o1: 2", "o1: 3"]
["o1: 4", "o1: 5"]
....


        timer1.window(timeSpan: RxTimeInterval.seconds(3), count: 2, scheduler: MainScheduler.instance)
            .subscribe { event in
                switch event {
                case let .next(observable):
                    observable.subscribe { e in
                        switch e {
                        case let .next(value):
                            print(value)
                        default:
                            print("inner finished")
                        }
                    }
                default:
                    print("finished")
                }
                
            }.disposed(by: disposeBag)
        
        timer1.window(timeSpan: RxTimeInterval.seconds(3), count: 2, scheduler: MainScheduler.instance)
            .subscribe { event in
                switch event {
                case let .next(observable):
                    observable.subscribe { e in
                        switch e {
                        case let .next(value):
                            print(value)
                        default:
                            print("inner finished")
                        }
                    }
                default:
                    print("finished")
                }
                
            }.disposed(by: disposeBag)
    
 // 결과값
o1: 0
o1: 1
inner finished
o1: 2
o1: 3
inner finished
o1: 4
o1: 5
inner finished
...

이것도 상당히 어렵다. 사실 작성하면서도 완벽하게 이해는 안가는데, 비슷한 걸로는 groupby가 있는 것 같다. 

 

 

 

 

 

 

 

 

 

(참고)

https://jusung.github.io/scan/

 

[RxSwift] Scan

Scan 연산자는 상태를 다루는데 유용한 연산자입니다. 이번 포스트에서는 Scan 연산자의 동작과 활용에 대해 살펴보도록 하겠습니다.

jusung.github.io

https://jcsoohwancho.github.io/2019-09-11-Rxswift%EC%97%B0%EC%82%B0%EC%9E%90-buffer,window/

 

RxSwift연산자-buffer,window

이번 포스트에서는 이벤트를 묶음으로 전달하는 buffer와 window 연산자에 대해서 알아보겠습니다. buffer와 window의 선언은 다음과 같습니다. public func buffer(timeSpan: RxTimeInterval, count: Int, scheduler: Schedu

jcsoohwancho.github.io

 

'apple > UIKit & ReactiveX' 카테고리의 다른 글

[week7] ⏰ Time Based Operators(cold? hot?)  (0) 2021.07.12
[week6] Combining Observables  (0) 2021.07.11
[week4] Filtering Observables  (0) 2021.07.10
[week3] Subjects  (0) 2021.07.10
🐉 RxSwift(Operators) Creating Observables  (0) 2021.07.09