study record

[Combine] map+switchLatest로 flatMapLatest 대체하기 본문

Swift/Combine

[Combine] map+switchLatest로 flatMapLatest 대체하기

asong 2025. 1. 13. 23:55

RxSwift에서 유용하게 사용하는 flatMapLatest (가장 최근에 생성된 Observable의 값만 만들고자 할 때)

Combine에서 map + switchLatest를 통해 flatMapLatest처럼 사용하기 알아봅시다!

 

 

map이 단순하게 값을 변형하는 것이라면,

flatMap

동작 방식

  • 입력 데이터를 새로운 스트림(Publisher/Observable)로 변환한 뒤, 이 스트림을 병합하여 단일 스트림으로 반환합니다.
  • 입력당 여러 개의 출력이 가능하며, 모든 스트림이 병렬로 실행됩니다.

사용하는 상황

  • 입력값으로 새로운 스트림을 생성해야 할 때.
  • 병렬적으로 여러 작업을 처리하며, 결과를 단일 스트림으로 병합하고 싶을 때.

 

import RxSwift

let disposeBag = DisposeBag()

let numbers = Observable.of(1, 2, 3)

numbers.flatMap { number -> Observable<String> in
    return Observable.just("Number: \(number)")
}
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

// 출력:
// Number: 1
// Number: 2
// Number: 3

 

import Combine

let numbers = [1, 2, 3].publisher

numbers.flatMap { number in
    Just("Number: \(number)")
}
.sink { print($0) }

// 출력:
// Number: 1
// Number: 2
// Number: 3

 

 

flatMapLatest

동작 방식

  • 입력 데이터를 새로운 스트림으로 변환한 뒤, 이 스트림 중 가장 최신의 스트림만 구독합니다.
  • 이전 스트림은 자동으로 취소됩니다.

사용하는 상황

  • 최신 작업만 유지하고, 이전 작업은 취소해야 할 때.
  • 사용자의 입력값에 따라 새로운 작업이 시작되며, 이전 작업은 무시해야 하는 상황(예: 검색창 자동완성).

 

 

import RxSwift

let disposeBag = DisposeBag()

let subject = PublishSubject<String>()

subject.flatMapLatest { text -> Observable<String> in
    return Observable.just("Mapped: \(text)")
}
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

subject.onNext("First")  // 출력: Mapped: First
subject.onNext("Second") // 출력: Mapped: Second

 

 

Combine에는 flatMapLatest가 직접적으로 제공되지 않지만, RxSwift와 비슷한 동작을 구현하려면 커스텀 연산자를 만들거나 switchToLatest를 사용해야 합니다.

import Combine

let subject = PassthroughSubject<String, Never>()

subject
    .map { Just("Mapped: \($0)") } // 새로운 Publisher 생성
    .switchToLatest() // 가장 최신의 Publisher만 구독
    .sink { print($0) }

subject.send("First")  // 출력: Mapped: First
subject.send("Second") // 출력: Mapped: Second

 

+) 정리

  • map: 단순한 값 변환이 필요할 때 사용.
  • flatMap: 입력값으로 비동기 작업이나 여러 스트림을 생성해야 할 때 사용.
  • flatMapLatest: 이전 작업을 취소하고, 최신 작업만 유지해야 할 때 사용.

 

flatMap과 flatMapLatest의 차이

  • flatMap: 모든 입력값에서 생성된 스트림을 병렬로 실행하고 병합합니다.
  • flatMapLatest가장 최신 스트림만 실행하고 이전 스트림은 자동으로 취소합니다.
import RxSwift

let disposeBag = DisposeBag()

let userInputs = PublishSubject<String>()

userInputs
    .flatMap { query -> Observable<String> in
        return Observable.create { observer in
            print("Searching for: \(query)")
            DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
                observer.onNext("Results for \(query)")
                observer.onCompleted()
            }
            return Disposables.create()
        }
    }
    .subscribe(onNext: { result in
        print(result)
    })
    .disposed(by: disposeBag)

// 사용자 입력 시뮬레이션
userInputs.onNext("A")
userInputs.onNext("B")
userInputs.onNext("C")

// 출력:
// Searching for: A
// Searching for: B
// Searching for: C
// Results for A
// Results for B
// Results for C

 

동작

  • 사용자가 "A", "B", "C"를 입력하면, 각 입력값에 대해 새로운 요청 스트림이 생성됩니다.
  • 모든 요청이 병렬로 실행되며, 각 결과가 병합되어 출력됩니다.
  • 이전 요청이 취소되지 않으므로 "A"의 결과도 출력됩니다.
import RxSwift

let disposeBag = DisposeBag()

let userInputs = PublishSubject<String>()

userInputs
    .flatMapLatest { query -> Observable<String> in
        return Observable.create { observer in
            print("Searching for: \(query)")
            DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
                observer.onNext("Results for \(query)")
                observer.onCompleted()
            }
            return Disposables.create()
        }
    }
    .subscribe(onNext: { result in
        print(result)
    })
    .disposed(by: disposeBag)

// 사용자 입력 시뮬레이션
userInputs.onNext("A")
userInputs.onNext("B")
userInputs.onNext("C")

// 출력:
// Searching for: A
// Searching for: B
// Searching for: C
// Results for C

동작

  • 사용자가 "A", "B", "C"를 입력하면, 각 입력값에 대해 새로운 요청 스트림이 생성됩니다.
  • 이전 스트림(A, B)은 취소되고, 최신 스트림(C)만 실행됩니다.
  • 결과적으로, "A"와 "B"는 출력되지 않고, "C"의 결과만 출력됩니다.

flatMapLatest는 가장 최신 스트림만 유지하고 이전 스트림을 취소해야 하는 상황에 적합!

 

 

 

 

Combine에서 switchToLatest 사용만으로 flatMapLatest를 대체하지 못 하는 이유?

드디어 본론..!

 

Combine에서 switchToLatest는 RxSwift의 flatMapLatest와 유사하지만, flatMapLatest를 완전히 대체하지 못하는 이유switchToLatest가 직접적으로 새로운 스트림을 생성하지 않기 때문입니다.

 

 

 

Combine의 switchToLatest

switchToLatest는 Publisher를 방출하는 Publisher에서 가장 최신의 내부 Publisher만 유지하며 이전 Publisher를 자동으로 취소합니다.

특징:

  1. 스트림 생성:
    • 새로운 스트림을 생성하지 않습니다.
    • 이미 존재하는 Publisher 스트림에서 가장 최신의 Publisher만 구독합니다.
  2. Publisher<Publisher<Output, Failure>> 필요:
    • switchToLatest는 반드시 Publisher를 방출하는 Publisher에서만 사용 가능합니다.

 

 

RxSwift의 flatMapLatest

flatMapLatest는:

  1. 입력값마다 새로운 스트림을 생성합니다.
  2. 생성된 스트림 중 가장 최신 스트림만 유지하고, 이전 스트림은 자동으로 취소합니다.

 

 

주요 차이:

  • RxSwift의 flatMapLatest는 입력값을 새로운 Observable로 변환하는 작업을 포함합니다.
  • Combine의 switchToLatest는 이와 달리 새로운 스트림 생성 기능을 제공하지 않습니다. 대신 이미 생성된 Publisher를 선택적으로 구독합니다.

 

따라서 switchToLatest를 사용하려면, Publisher를 방출하는 Publisher가 이미 존재해야 합니다. 하지만, 일반적으로 RxSwift의 flatMapLatest는:

  1. 입력값에 따라 새로운 Publisher를 생성하고,
  2. 가장 최신의 스트림만 유지합니다.

 

Combine에서 flatMapLatest를 구현하려면 

map을 사용해 새로운 스트림을 생성한 후, switchToLatest를 적용해야 합니다.

 

  1. map으로 입력값을 새로운 Publisher로 변환.
  2. 생성된 Publisher를 switchToLatest로 연결.
import Combine

let subject = PassthroughSubject<String, Never>()

let cancellable = subject
    .map { input in
        Just("Processed \(input)") // 새로운 Publisher 생성
            .delay(for: .seconds(1), scheduler: RunLoop.main) // 비동기 처리 시뮬레이션
            .eraseToAnyPublisher()
    }
    .switchToLatest() // 가장 최신의 Publisher만 유지
    .sink { result in
        print(result)
    }

// 테스트
subject.send("A")
subject.send("B")
subject.send("C")

// 출력 (1초 지연 후):
// Processed C

 

Publisher<AnyPublisher<String, Never>, Never> 타입으로 map을 통해 변환되었고,

Publisher<Publisher<Output, Failure>> 형태를 띄게 된다.

  • switchToLatest는 기존에 존재하는 Publisher를 선택적으로 구독할 뿐, 입력값에 따라 새로운 Publisher를 생성하는 기능은 없습니다.
  • 새로운 스트림을 생성하려면 map이나 flatMap을 추가로 사용해야 합니다.

 

map + switchToLatest:

  • 단순히 입력값을 하나의 Publisher로 변환하고 최신 값만 유지.
  • 예: 검색어 입력 시 최신 검색 결과 하나만 반환.

 

flatMap + switchToLatest가 안 되는 이유

switchToLatest는 Publisher<Publisher<Output, Failure>>를 필요로 하는데

Combine의 flatMap 사용하면, 이 타입을 띄지 않기 때문입니다.

flatMap은 클로저 내부에서 반환된 Publisher를 펼쳐서(flatten), 방출된 값만을 결과로 내보냅니다. 즉, 중첩된 Publisher를 자동으로 구독하고, 최종적으로 내부 Publisher의 값만 방출합니다.

 

즉, AnyPublisher<String, Never> 내부에서 실제로 방출되는 String 값만 결과로 얻습니다.

 

let cancellable = subject
    .flatMap { input in
        Just("Processed \(input)")
            .delay(for: .seconds(1), scheduler: RunLoop.main)
            .eraseToAnyPublisher()
    }
    // flatMap의 결과: Publisher<String, Never>
    .sink { result in
        print(result)
    }

 

 

map과 flatMap의 차이

변환된 타입의 차이

  • map은 입력값(input)을 변환하고, 결과를 그대로 방출합니다. 결과적으로 중첩된 Publisher가 생성될 수 있습니다.
    • 예: Publisher<AnyPublisher<String, Never>, Never>
  • flatMap은 입력값(input)을 변환한 후, 새로 생성된 Publisher를 자동으로 펼쳐서 최종적으로 방출된 값만 구독합니다.
    • 예: Publisher<String, Never>

 

이렇게 flatMap + switchToLatest 안 되는 이유까지 알아봤습니다! 끝!

 

 

 

 

 

참고

- https://limjs-dev.tistory.com/140

'Swift > Combine' 카테고리의 다른 글

[Combine] 1. Hello Combine!  (1) 2024.08.28
[Swift Combine] Future, Deferred  (0) 2024.03.31
[Swift] Combine이란?  (0) 2023.01.03