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

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])

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
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를 통해 두 번의 구독이 있다 첫 번재 구독에서는 filter와 map 오퍼레이터를 사용하여 filter로 nil 값을 거르고 map을 통해 강제 언래핑을 하고 있다.
두 번째 구독에서는 compactMap만을 통해 동일한 결과를 내고 있다. 이처럼 compactMap 연산자는
전달받은 결과가 nil이면 필터링하고 또한 값은 언래핑 해주는 유용한 연산자이다.

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을 어디서 활용하느냐?
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는 각 주기마다 처음 방출하는 이너 옵저버블만을 선택하고 나머지는 무시한다.
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은 각 입력 항목에 대한 처리가 순차적으로 이루어져야 할 때 유용하다.
예를 들어, 서버 요청을 순차적으로 처리하고 각 요청의 응답 순서를 보장해야 하는 네트워크 요청에 적합하다~!

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 연산자는 작업 결과를 누적 시키면서 중간 결과와 최종 결과가 모두 필요할 때 사용한다.

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초보다 빠르기 때문에 개수가 일정하게 방출된다.

// 버퍼와 같이 원본 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은 지정된 최대 수만큼 방출하거나 시간이 지나면 종료된다.

// 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