이번 포스팅에선 방출된 값들을 다른 형태로 변화를 주고 방출하는 오퍼레이터인 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