RxSwift Operator 살펴보기(map, flatMap, combineLastest, withLatestFrom)

2021. 4. 3. 18:05iOS/RxSwift

오늘은 RxSwift에 대해 이해해보고 공부해보면서 왜 RxSwift가 편한지!! 느껴볼 수 있는 여러 가지 'Sugar API'들을 알아보겠습니다.

map, flatMap

먼저 살펴볼 것은 기본적은 Swift 사용자들에게도 익숙한 map과 flatMap입니다. 이는 Swift의 고차 함수로 Sequnece 자료형에서 각 element에 대한 동일한 동작을 수행하여 새로운 Sequenece 자료형을 return 하는 함수이고, map과 flatMap 이 둘의 동작에는 약간의 차이점이 존재합니다. 

RxSwift에서도 이 두 함수에는 일반 Swift와는 다른 기능적 차이가 존재합니다. 하나씩 살펴보도록 하겠습니다.

map

map은 reactive.io 에서 'Observerble에 의해 방출되는 아이템들에 대해 각각 함수를 적용하여 변환한다' 라고 설명하고 있습니다. 아래의 그림을 보면 이해하기 쉬울 텐데요. 

하나의 스트림에서 1, 2, 3의 숫자들이 emit되었을 때 각 아이템에 10을 곱하여 방출하는 동작을 수행하게 해 줌으로써 최종적으로 emit 되는 아이템은 10, 20, 30이 방출됩니다.

이와 관련하여 예제 코드입니다.

Observable<String>
    .of("1", "2", "3", "asd")
    .map{ Int($0) }
    .subscribe(onNext: { print($0) })

// Print
Optional(1)
Optional(2)
Optional(3)
nil

flatMap

다음은 flatMap입니다. flatMap은 map과는 다르게 스트림에서 방출되는 아이템들을 다른 observable로 만드는 역할을 하며 각각 만들어진 Observable 또한 아이템들을 방출하게 됩니다.

다음의 예제입니다.  

let sequenceInt = Observable.of(1, 2, 3)    // Int 타입 시퀀스
let sequenceString = Observable.of("A", "B", "C", "D")    // String 타입 시퀀스

sequenceInt
    .flatMap { (x: Int) -> Observable<String> in
        print("Emit Int Item : \(x)")
        return sequenceString.map{ "\(x):\($0)"}
        //            return sequenceString
    }
    .subscribe(onNext: {
        self.arr.append($0)
    })

print(arr)

// Print
Emit Int Item : 1
Emit Int Item : 2
Emit Int Item : 3
["1:A", "1:B", "2:A", "1:C", "2:B", "3:A", "1:D", "2:C", "3:B", "2:D", "3:C", "3:D"]

예제 처럼 1, 2, 3이 방출되는 스트림에서 각각 Int값과 "A", "B" "C", "D"를 합친 문자열로 변환한 스트림을 return 하도록 하였을 때 결과입니다. 여기서 중요한 것은 flatMap을 통해 변환되는 각각 Observable들을 평평하게(flat) 한 스트림을 생성하여 방출된다는 점입니다. 아래 그림을 봅시다.

flatMap을 통해 각각 세개의 Observable이 생성되는데요. 각각의 Observable에서 방출하는 아이템들이 하나의 스트림으로 다시 방출된다는 점입니다. 결국 위에서 subscribe을 하는 부분은 아래 스트림이 되겠습니다.

이러한 flatMap의 장점은 flatMap을 통해 한번에 여러가지 스트림을 사용할 수 있고 각기 스트림에서 방출하는 아이템들에 대한 누락 없이 모든 이벤트들에 대한 observing이 가능하다는 점입니다.

다음은 이런 flatMap의 특징을 활용한 Operator를 알아보겠습니다.

flatMapFirst

flatMapFirst는 flatMap과 유사하게 동작합니다. 하지만 flatMap을 통해 먼저 생성된 스트림의 동작이 다 끝날 때까지 새로 방출하는 아이템들이 무시되게 됩니다.

sequenceInt
    .flatMapFirst {
        (x: Int) -> Observable<String> in
        print("Emit Int Item : \(x)")
        return sequenceString.map{ "\(x):\($0)"}
    }
    .subscribe(onNext: {
        self.arr.append($0)
    })
    
// Print
Emit Int Item : 1
["1:A", "1:B", "1:C", "1:D"]

flatMap과 똑같은 동작을 하도록 구현을 하였는데요? 하지만 결과는 다른 모습을 보입니다. 1, 2, 3이 방출되는 sequenceInt에서 flatMapLatest를 통해 새로운 스트림을 생성하게 된다면, 먼저 방출되는 아이템 "1"에 대해 동작이 수행되는 시점이라면, 그다음으로 들어오는 아이템들에 대해 동작을 무시하는 모습을 확인할 수 있습니다.

아래 그림처럼 동작이 된다고 할 수 있겠죠

flatMapLatest

flatMapLastest 또한 flatMapFirst와는 다른 동작을 수행합니다. flatMapLatest는 새로운 스트림을 만들고 동작을 수행하는 도중, 새로운 아이템이 방출되게 된다면, 이전 스트림을 dispose 하고 새롭게 들어오게 되는 아이템에 대해 스트림을 생성하여 동작하게 됩니다. 예제를 보시죠

sequenceInt
    .flatMapLatest {
        (x: Int) -> Observable<String> in
        print("Emit Int Item : \(x)")
        return sequenceString.map{ "\(x):\($0)"}
    }
    .subscribe(onNext: {
        self.arr.append($0)
    })

// Print
Emit Int Item : 1
Emit Int Item : 2
Emit Int Item : 3
["1:A", "2:A", "3:A", "3:B", "3:C", "3:D"]

위의 예제처럼 각 각 아이템들이 모두 방출되며, 새롭게 방출되는 아이템에 따라 이전 동작을 모두 멈춘 것을 확인해 볼 수 있습니다. 아래 그림을 보시면 이해하기 수월하실 것 같습니다.

 

combineLastest

combineLastest 두가지 스트림 중 하나에서 아이템이 방출될 때 지정한 함수를 통해 각 스트림에서 방출된 최신 아이템들을 결합하고, 이 함수의 return값에 따른 아이템을 방출하게 됩니다.

아래 예제처럼 두개 이상의 스트림을 생성하고 각 스트림에 대해 이벤트를 주었을 때 가장 최근에 방출된 아이템들을 결합한 새로운 아이템을 방출하게 됩니다.

let sequenceInt = PublishSubject<Int>()
let sequenceString = PublishSubject<String>()

let combine = Observable.combineLatest(sequenceInt, sequenceString, resultSelector: { (sInt, sString)  in
    "\(sInt):\(sString)"
})
.subscribe(onNext: { print($0) })
    
sequenceInt.onNext(1)
sequenceString.onNext("A")
sequenceInt.onNext(2)
sequenceString.onNext("B")
sequenceString.onNext("C")
sequenceInt.onNext(3)
sequenceString.onNext("D")

// Print
1:A
2:A
2:B
2:C
3:C
3:D

 

아래 그림처럼 말이죠 ! 이때 생각할 점은 새롭게 생성된 스트림의 시작 지점은 결합된 두 스트림에서 모두 하나 이상의 아이템들이 방출되는 시점이라는 것입니다.!

이는 일반적으로 회원가입, 로그인과 같은 필수적으로 입력해야 하는 정보와 입력된 각 정보들에 대해 검사하여 다음으로 넘어가게 만들거나 혹은 그렇게 못하게 만드는(버튼을 disabled 처리하는) 로직을 처리할 때 매우 간편하게 만들 수 있을 것입니다.

withLatestFrom

combineLatestFrom과 비슷한 Operator로 withLastestFrom과 비슷하지만 

새로운 이벤트가 발생한 스트림에서 지정한 스트림의 가장 최신 아이템을 얻을 수 있다는 점입니다. 예제를 보며 빠르게 이해해 봅시다.

let withLastestFrom = sequenceInt.withLatestFrom(sequenceString) { (iItem, sItem) in
"\(iItem):\(sItem)"
}
withLastestFrom.subscribe(onNext: { print($0) })

sequenceInt.onNext(1)
sequenceString.onNext("A")
sequenceInt.onNext(2)
sequenceString.onNext("B")
sequenceString.onNext("C")
sequenceInt.onNext(3)
sequenceString.onNext("D")

// Print
2:A
3:C

이 withLastestFrom의 특징은 하나의 기준이 되는 스트림이 존재한다는 점입니다. 실제로 호출부를 보면 sequenceInt라는 스트림에서 다른 sequenceString이라는 스트림을 지정하는 모습을 볼 수 있었는데요.

여기서 호출하는 스트림이 sequenceInt라는 의미는 sequenceInt에서 새로운 이벤트가 발생할 때 withLastestFrom으로 생성한 새로운 스트림에서 이벤트가 발생한다는 것입니다.

그림을 보며 쉽게 이해해 봅시다.

앞서 코드에서 보았듯 기준이 되는 스트림인 sequenceInt 가 되고 해당 스트림에서 새로운 이벤트가 발생했을 때 지정한 스트림에서 가장 최근 방출한 아이템을 가져오게 됩니다. 물론 combineLastest와 비슷하게 지정한 스트림에서 아무 이벤트가 발생하지 않았다면, withLastestFrom으로 생성한 스트림에도 이벤트가 생성되지 않습니다.

이 Operator 또한 회원가입, 로그인과 같은 화면을 구성할 때 유용하게 사용할 수 있게 되는데요.

combineLatest에서 각 입력값에 대한 처리를 통해 버튼을 enable 상태로 만들었다면, 버튼을 눌렀을 때 해당 입력값을 저장하거나 서버에 보내게 될 텐데요. 버튼에 대한 터치 이벤트가 발생했을 때 지정한 스트림에서 가장 최신에 방출된 데이터를 가져와 처리를 할 수 있다는 점에서 이러한 Operator를 사용하면 간단한 처리가 가능할 것 같습니다.

여기까지 몇 가지 Operator들에 대해 정리를 해보았는데요!!

다음 시간에는 조금 더 깊게 사용할 수 있는 Operator나 RxCocoa, RxGesture에서 사용하는 Operator들도 소개해드리도록 하겠습니다.

감사합니다!!