Transforming Operators

DEVJUN·2024년 3월 22일
0

RxSwift

목록 보기
6/9
post-thumbnail

이번 포스팅에선 방출된 값들을 다른 형태로 변화를 주고 방출하는 오퍼레이터인 Transformation Operator에 대해 알아봅시다~!

1. toArray (원본 옵저버블이 방출하는 모든 요소를 하나의 배열로 방출하는 연산자)

let disposeBag = DisposeBag()

let subject = PublishSubject<Int>()

subject
    .toArray()
    .subscribe { print($0) }
    .disposed(by: disposeBag)

subject.onNext(1)
subject.onNext(2) // 방출되지 않음
subject.onCompleted() // 방출

===============================================

결과
success([1, 2])

2. map (원본 옵저버블이 방출하는 요소를 대상으로 함수를 실행하고 결과를 새로운 옵저버블로 리턴하는 연산자)

let disposeBag = DisposeBag()
let skills = ["Swift", "SwiftUI", "RxSwift"]

Observable.from(skills)
    .map { $0.count }
    .subscribe { print($0) }
    .disposed(by: disposeBag)
    
Observable.from(skills)
    .map { "Hello, \($0)" }
    .subscribe { print($0) }
    .disposed(by: disposeBag)


===============================================

결과
next(5)
next(7)
next(7)
completed
next(Hello, Swift)
next(Hello, SwiftUI)
next(Hello, RxSwift)
completed

3. compactMap (옵저버블이 방출하는 이벤트에서 값을 꺼낸 후 옵셔널 형태로 바꾼 후 원하는 변환 실행 후 최종 변환 결과가 nil이면 필터링 하는 연산자)

let disposeBag = DisposeBag()

let subject = PublishSubject<String?>()

subject
    .filter { $0 != nil }
    .map { $0! }
    .subscribe { print($0) }
    .disposed(by: disposeBag)

subject
    .compactMap { $0 } // nil 필터링, 값은 언래핑됨
    .subscribe { print($0) }
    .disposed(by: disposeBag)

Observable<Int>.interval(.milliseconds(300), scheduler: MainScheduler.instance)
    .take(10)
    .map { _ in Bool.random() ? "⭐️" : nil }
    .subscribe(onNext: { subject.onNext($0) })
    .disposed(by: disposeBag)


===============================================

결과
next(⭐️)
next(⭐️)
next(⭐️)
next(⭐️)
next(⭐️)

위 코드에서 subject를 통해 두 번의 구독이 있다 첫 번재 구독에서는 filtermap 오퍼레이터를 사용하여 filter로 nil 값을 거르고 map을 통해 강제 언래핑을 하고 있다.

두 번째 구독에서는 compactMap만을 통해 동일한 결과를 내고 있다. 이처럼 compactMap 연산자는
전달받은 결과가 nil이면 필터링하고 또한 값은 언래핑 해주는 유용한 연산자이다.


4. flatMap (여러 스트림(옵저버블)의 데이터를 하나의 스트림으로 합치는 작업을 수행)

let disposeBag = DisposeBag()

let redCircle = "🔴"
let greenCircle = "🟢"
let blueCircle = "🔵"

let redHeart = "🩷"
let greenHeart = "💚"
let blueHeart = "💙"

Observable.from([redCircle, greenCircle, bludCircle])
	.flatMap { circle -> Observable<String> in 
    	switch circle {
        	case redCircle: 
            	return Observable.repeatElement(redHeart)
                	.take(5)
        	case greenCircle:
            	return Observable.repeatElement(greenHeart)
	                .take(5)
            case blueCircle:
            	return Observable.repeatElement(blueHeart)
	                .take(5)
            default:
				return Observable.just("")
        }
    }
    .subscribe { $0 }
    .disposed(by: disposeBag)


===============================================

결과
next(🩷)
next(🩷)
next(💚)
next(🩷)
next(💚)
next(💙)
next(🩷)
next(💚)
next(💙)
next(🩷)
next(💚)
next(💙)
next(💚)
next(💙)
next(💙)
completed

flatMap은 그냥 보기만 해도 더럽게 어렵다;;
위에 코드에선 from을 통해 Circle이미지들을 배열에 넣고 전달하여 flatMap을 통해 각 종류마다 하트로 바꾸고 take로 5개씩 방출하도록 짜여져 있다. 그렇다면 결과또한 순서대로 next(🩷)5개, next(💚)5개, next(💙)일 것으로 예상되었지만 이상하게 막 뒤죽박죽으로 나왔다.

그 이유는 뭐냐❗️

flatMap은 inner 옵져버블이 이벤트를 방출하면 Result 옵저버블을 통해 지연없이 방출한다. 따라서 뒤죽박죽인 것이다. 이를 Interleaving을 허용한다고 한다.

쉽게 코드의 동작을 순서대로 나타내면 다음과 같다

1️⃣. flatMap은 각 원에 대한 하트를 방출하는 새로운 옵저버블을 생성.
2️⃣. 이 옵저버블들은 독립적으로 동작하며, 생성된 순간부터 바로 값을 방출하기 시작. 따라서, 시스템의 스케줄링이나 처리 시간에 따라 어떤 하트가 먼저 방출될지 예측할 수 없음.
3️⃣. 결과적으로, 방출된 하트들은 예상한 순서와 다르게 섞여 나타나게 되는것이다.


🧹그렇다면 이 flatMap을 어디서 활용하느냐?

  • 데이터의 순서가 중요하지 않거나, 각 데이터 소스가 독립적으로 값을 방출해야 할 때
  • 여러 데이터 소스(스트림)에서 오는 데이터를 통합해야 할 때

5. flatMapFirst (가장 먼저 이벤트를 방출한 inner 옵저버블에서만 이벤트를 방출하는 연산자)

  • inner 옵저버블: 옵저버블이 방출하는 옵저버블
let disposeBag = DisposeBag()

let redCircle = "🔴"
let greenCircle = "🟢"
let blueCircle = "🔵"

let redHeart = "🩷"
let greenHeart = "💚"
let blueHeart = "💙"

Observable.from([redCircle, greenCircle, blueCircle])
	.flatMapFirst { circle -> Observable<String> in 
    	switch circle {
        	case redCircle: 
            	return Observable.repeatElement(redHeart)
                	.take(5)
        	case greenCircle:
            	return Observable.repeatElement(greenHeart)
	                .take(5)
            case blueCircle:
            	return Observable.repeatElement(blueHeart)
	                .take(5)
            default:
				return Observable.just("")
        }
    }
    .subscribe { $0 }
    .disposed(by: disposeBag)
    
    
Observable.from([greenCircle, redCircle, blueCircle])
	.flatMapFirst { circle -> Observable<String> in 
    	switch circle {
        	case redCircle: 
            	return Observable.repeatElement(redHeart)
                	.take(5)
        	case greenCircle:
            	return Observable.repeatElement(greenHeart)
	                .take(5)
            case blueCircle:
            	return Observable.repeatElement(blueHeart)
	                .take(5)
            default:
				return Observable.just("")
        }
    }
    .subscribe { $0 }
    .disposed(by: disposeBag)


===============================================

결과
next(🩷)
next(🩷)
next(🩷)
next(🩷)
next(🩷)
completed
next(💚)
next(💚)
next(💚)
next(💚)
next(💚)
completed

FlatMapFirst는 각 주기마다 처음 방출하는 이너 옵저버블만을 선택하고 나머지는 무시한다.


6. concatMap (인터리빙을 허용하지 않고 항상 방출 순서를 보장하는 연산자)

let disposeBag = DisposeBag()

let redCircle = "🔴"
let greenCircle = "🟢"
let blueCircle = "🔵"

let redHeart = "🩷"
let greenHeart = "💚"
let blueHeart = "💙"

Observable.from([redCircle, greenCircle, blueCircle])
    .concatMap { circle -> Observable<String> in
        switch circle {
        case redCircle:
            return Observable.repeatElement(redHeart)
                .take(5)
        case greenCircle:
            return Observable.repeatElement(greenHeart)
                .take(5)
        case blueCircle:
            return Observable.repeatElement(blueHeart)
                .take(5)
        default:
            return Observable.just("")
        }
    }
    .subscribe { print($0) }
    .disposed(by: disposeBag)


===============================================

결과
next(🩷)
next(🩷)
next(🩷)
next(🩷)
next(🩷)
next(💚)
next(💚)
next(💚)
next(💚)
next(💚)
next(💙)
next(💙)
next(💙)
next(💙)
next(💙)
completed

위 결과를 보면 FlatMap과 달리 이모티콘이 순서대로 내려오는 것을 볼 수 있다. concatMap은 inner 옵저버블을 생성된 순서대로 연결한다. concatMap은 원본 옵저버블이 이벤트를 방출하는 순서와 inner 옵저버블이 이벤트를 방출하는 순서가 동일하고, 또한 인터리빙을 허용하지 않는다.

concatMap은 각 입력 항목에 대한 처리가 순차적으로 이루어져야 할 때 유용하다.
예를 들어, 서버 요청을 순차적으로 처리하고 각 요청의 응답 순서를 보장해야 하는 네트워크 요청에 적합하다~!


7. scan (이전에 방출된 아이템과 새로 방출된 아이템을 결합해 현재 아이템을 생성)

let disposeBag = DisposeBag()

Observable.range(start: 1, count: 10)
    .scan(0, accumulator: +)
    .subscribe { print($0) }
    .disposed(by: disposeBag)


===============================================

결과
next(1)
next(3)
next(6)
next(10)
next(15)
next(21)
next(28)
next(36)
next(45)
next(55)
completed

scan 연산자는 작업 결과를 누적 시키면서 중간 결과와 최종 결과가 모두 필요할 때 사용한다.


8. buffer (특정 주기동안 옵저버블이 방출하는 항목을 수집하고 항목을 하나의 배열로 리턴하는 연산자)

let disposeBag = DisposeBag()

// 2초마다 3개씩 수집
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
    .buffer(timeSpan: .seconds(2), count: 3, scheduler: MainScheduler.instance)
    .take(5)
    .subscribe { print($0) }
    .disposed(by: disposeBag)

===============================================

결과
next([0])
next([1, 2, 3])
next([4, 5])
next([6, 7])
next([8, 9])
completed





Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
    .buffer(timeSpan: .seconds(5), count: 3, scheduler: MainScheduler.instance)
    .take(5)
    .subscribe { print($0) }
    .disposed(by: disposeBag)

===============================================

결과
next([0, 1, 2])
next([3, 4, 5])
next([6, 7, 8])
next([9, 10, 11])
next([12, 13, 14])
completed

위 코드의 동작원리는 우선 첫번째 구독에선 timespan이 2초고 count 3으로 2초마다 3개씩 수집한다 따라서 시간상의 오차로 보통은 배열에 2개의 요소가 담겨 전달되지만 1개인 경우 3개인 경우도 보인다.

아래 구독에선 5초마다 3개이므로 5초가 다 지나지 않고도 3개가 수집되면 그대로 방출한다. 따라서 오히려 수집 시간이 5초보다 빠르기 때문에 개수가 일정하게 방출된다.


9. window (버퍼 연산자와는 달리 수집된 항목을 방출하는 옵저버블을 리턴하는 연산자)

// 버퍼와 같이 원본 Observable이 방출하는 항목을 작은 단위의 Observable로 분리 (시간 오차 있음)
// 배열을 방출하는 버퍼와 단리 수집된 항목(Observable)을 방출하는 Observable(inner Observable)을 방출


let disposeBag = DisposeBag()

Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
    .window(timeSpan: .seconds(2), count: 3, scheduler: MainScheduler.instance)
    .take(5)
    .subscribe {
        print($0)
        
        if let observable = $0.element {
            observable.subscribe { print("inner: ", $0)}
        }
    }
    .disposed(by: disposeBag)

===============================================

결과
next(RxSwift.AddRef<Swift.Int>) 실제  옵저버블
inner:  next(0) // print로 찍은 요소
inner:  completed
next(RxSwift.AddRef<;Swift.Int>)
inner:  next(1)
inner:  next(2)
inner:  next(3)
inner:  completed
next(RxSwift.AddRef<Swift.Int>)
inner:  next(4)
inner:  next(5)
inner:  completed
next(RxSwift.AddRef<Swift.Int>)
inner:  next(6)
inner:  next(7)
inner:  completed
next(RxSwift.AddRef<Swift.Int>)
completed
inner:  next(8)
inner:  next(9)
inner:  completed



Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
    .window(timeSpan: .seconds(5), count: 3, scheduler: MainScheduler.instance)
    .take(5)
    .subscribe {
        print($0)
        
        if let observable = $0.element {
            observable.subscribe { print("inner: ", $0)}
        }
    }
    .disposed(by: disposeBag)

===============================================

결과
next(RxSwift.AddRef<Swift.Int>)
inner:  next(0)
inner:  next(1)
inner:  next(2)
inner:  completed
next(RxSwift.AddRef<Swift.Int>)
inner:  next(3)
inner:  next(4)
inner:  next(5)
inner:  completed
next(RxSwift.AddRef<Swift.Int>)
inner:  next(6)
inner:  next(7)
inner:  next(8)
inner:  completed
next(RxSwift.AddRef<Swift.Int>)
inner:  next(9)
inner:  next(10)
inner:  next(11)
inner:  completed
next(RxSwift.AddRef<Swift.Int>)
completed
inner:  next(12)
inner:  next(13)
inner:  next(14)
inner:  completed

window 연산자는 버퍼 연산자처럼 타임스탬프동안 수집된것을 작은 옵저버블로 방출한다. 옵저버블 안에서 방출되는 옵저버블을 inner Observable 이라고 한다. inner Observable은 지정된 최대 수만큼 방출하거나 시간이 지나면 종료된다.


10. groupBy (방출되는 요소를 원하는 기준에 따라 그룹핑하는 연산자)

// Observable이 방출하는 요소를 원하는 방식(키를 기준)으로 그룹핑 가능
// flatmap과 toArray 연산자를 활용하여 그룹핑 된 결과를 하나의 배열로 방출함

let disposeBag = DisposeBag()
let words = ["Apple", "Banana", "Orange", "Book", "City", "Axe"]

Observable.from(words)
    .groupBy { $0.count }
    .subscribe(onNext: { groupedObservable in
        print("== \(groupedObservable.key)")
        groupedObservable.subscribe { print("  \($0)") }
    })
    .disposed(by: disposeBag)


===============================================

결과
== 5
  next(Apple)
== 6
  next(Banana)
  next(Orange)
== 4
  next(Book)
  next(City)
== 3
  next(Axe)
  completed
  completed
  completed
  completed

// 글자수를 기준으로 그룹핑
Observable.from(words)
    .groupBy { $0.count }
    .flatMap { $0.toArray() }
    .subscribe { print($0) }
    .disposed(by: disposeBag)

===============================================

결과
next(["Banana", "Orange"])
next(["Book", "City"])
next(["Apple"])
next(["Axe"])
completed


// 첫 번째 문자를 기준으로 그룹핑
Observable.from(words)
    .groupBy { $0.first ?? Character(" ") }
    .flatMap { $0.toArray() }
    .subscribe { print($0) }
    .disposed(by: disposeBag)
===============================================

결과
next(["City"])
next(["Banana", "Book"])
next(["Orange"])
next(["Apple", "Axe"])
completed


// 홀수와 짝수로 그룹핑
Observable.range(start: 1, count: 10)
    .groupBy { $0.isMultiple(of: 2) }
    .flatMap { $0.toArray() }
    .subscribe { print($0) }
    .disposed(by: disposeBag)
===============================================

결과
next([1, 3, 5, 7, 9])
next([2, 4, 6, 8, 10])
completed



KXCoding 강의
Marble Diagram
ReactiveX 사이트

profile
🧑🏻‍💻iOS

0개의 댓글