[Combine] Map / FlatMap / SwitchToLatest 에 대해 알아봅시다.
안녕하세요 🙇♂️
이 글은 콤린이로써 FlatMap이 너무 헷갈려서 게시하게 됐습니다.
Combine이 아니더라도 Map / CompactMap / FlatMap 은 자주 쓰잖아요?
이 글에서는 Map과 FlatMap을 비교하고 Rx에 있는 FlatMapLatest는 Combine에서 어떻게 구현해야되는지 알아보겠습니다.
CompactMap은 이 글과 약간 관련이 없어서 제외했습니다. :)
자 시작해보겠습니다. 🔥
Map(_:)
먼저 Map입니다.
func map<T>(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map<Self, T>
제공된 closure를 사용하여 업스트림 publisher의 모든 요소를 변환합니다.
네네 맞습니다. 일반적인 Map과도 비슷하죠? 뭐 같다고 볼 수도 있을 것 같아요.
자 예제를 보면서 후딱 알아봅시다.
let numbers = [5, 4, 3, 2, 1, 0]
let romanNumeralDict: [Int : String] =
[1:"I", 2:"II", 3:"III", 4:"IV", 5:"V"]
cancellable = numbers.publisher
.map { romanNumeralDict[$0] ?? "(unknown)" }
.sink { print("\($0)", terminator: " ") }
// 결과값
// V IV III II I (unknown)
먼저 numbers를 통해 publisher를 만듭니다.
그리고 map을 통해 romanNumeralDict에 numbers에서 publish 되는 Int값을 통해 Dictionary의 value값을 return 해줍니다.
또, romanNumeralDict에 존재하지 않으면 unknown을 반환하게 했습니다.
결과값을 확인해보면 5, 4, 3, 2, 1, 0의 인덱스 순(numbers의 값들)으로 romanNumeralDict의 value가 반환되는 것을 볼 수 있습니다.
간단합니다!
이제 FlatMap입니다. 개인적으로는 이해하기가 참 어려웠어요.😭
FlatMap(maxPublishers:_:)
func flatMap<T, P>(
maxPublishers: Subscribers.Demand = .unlimited,
_ transform: @escaping (Self.Output) -> P
) -> Publishers.FlatMap<P, Self> where T == P.Output, P : Publisher, Self.Failure == P.Failure
이거봐요 복잡하죠? 힘들었습니다 아주
업스트림 publisher에서 사용자가 지정한 최대 publisher 수까지 모든 요소를 새 publisher로 변환합니다.
자 이게 무슨 말일까요?
일단 업스트림 publisher를 통해 새로운 publisher로 변환한다는 말인데..
이번에는 마블 다이어그램을 직접 그려보면서 코드와 함께 이해봅시다!
Rx를 공부하신분이라면 마블 다이어그램이 익숙하실겁니다.
익숙하지 않으셔도 이 글에서는 설명을 하지 않습니다.😢 Rx 홈페이지를 통해 학습 부탁드립니다!
struct Person {
let name: CurrentValueSubject<String, Never>
}
Person은 간단하게 요렇게 생겼다고 합시다!
자, 그림을 보면 flatMap은 맨 위의 publisher의 Person을 다른 publisher로 변환해줍니다.
flatMap의 closure를 확인해보면 Person의 name 프로퍼티를 새로운 publisher로 바꿔주네요.
그러면 3명의 Person을 통해 FlatMap은 총 3개의 publisher를 생성하고,
결과적으로는 하나의 publisher만 생성이 되는 모습을 볼 수 있습니다.
이 마블 다이어그램을 code로 살펴봅시다.
let personA = Person(name: .init("Felix")
let personB = Person(name: .init("James")
let personC = Person(name: .init("Dochoi")
let subject = PassthroughSubject<Person, Never>()
let cancellable = subject
.flatMap { $0.name }
.sink { print($0) }
subject.send(personA)
subject.send(personB)
subject.send(personC)
// 결과값
// Felix
// James
// Dochoi
여기까지는 뭔가 당연한거 같은데 뭐가 어렵다는거지? 라고 생각하실 수 있습니다.
다음은 personB의 name 값을 변경해보도록 하겠습니다.
... 생략 ...
subject.send(personA)
subject.send(personB)
subject.send(personC)
personB.send("Itzel")
// 결과값
// Felix
// James
// Dochoi
// Itzel
네, 저는 이 부분을 이해하는게 어려웠습니다😢
subject에 마지막으로 send한 사람은 personC인데 왜 personB를 바꾼값이 print에 찍히지? 요런 생각이 들었습니다.
위에 코드를 다시 마블 다이어그램으로 표현해봅시다.
조금 이해가 되시나요?
raywenderlich에서는 이것에 대해 이렇게 말해줍니다
it will buffer as many publishers as you send it to update the single publisher it emits downstream.
다운스트림에 방출하는 단일 publisher를 업데이트하기 위해 많은 publisher들을 버퍼링 합니다.
위 예제에서는 마지막에 생성되는 단일 publisher를 업데이트 하기 위해 1, 2, 3 번의 publisher들을 버퍼링 하고 있는 것입니다.
그렇기 때문에 personB.send(Itzel)을 했을 때, 단일 publisher가 업데이트 되는 것을 확인할 수 있습니다.
또 raywenderlich는 이 부분에 대해 요런 문제가 있다고 말합니다.
This can pose a memory concern
메모리 문제가 발생할 수 있습니다.
그렇겠죠? 이 상태에서 subject에 다른 person을 계속 send를 하게 된다면
단일 publisher를 업데이트하기 위해 send되는 모든 person을 버퍼링 해야 될 것입니다.
여기서 사용되는 것이 Subscriber.Demand 값을 받는 maxPublishers라는 인자입니다.
flatMap의 정의에서도 확인할 수 있고 직접 경험한대로 default값은 .unlimited입니다.
자 다시 코드와 마블 다이어그램으로 maxPublishers를 사용해봅시다.
... 생략 ...
let cancellable = subject
.flatMap(maxPublishers: .max(2)) { $0.name }
.sink { print($0) }
subject.send(personA)
subject.send(personB)
subject.send(personC)
personB.send("Itzel")
// 결과값
// Felix
// James
// Itzel
예상했던 결과가 나왔나요?? 어렵습니다 참🤯
마블 다이어그램도 확인해봅시다.
최대 publisher의 개수를 2개로 제한 했기 때문에 personA와 personB에 의해 생성된 publisher들만
단일 publisher를 업데이트 해줍니다. 여기서는 1번과 2번 publisher가 되겠죠?
위에 설명했듯이 flatMap operator를 사용할 때는 메모리 이슈가 생길 수 있기 때문에 maxPublishers 인자를 잘 활용하도록 합시다!
switchToLatest()
switchToLatest()는 Rx의 flatMapLatest()처럼 사용할 수 있게 만들어주는 operator입니다.
Rx의 flatMapLatest는 새로운 값이 publish 되면 이전에 publish된 값들은 cancel 하게 되고 가장 최신의 publisher로 단일 publisher를 업데이트 해줍니다.
이것도 간단한 코드와 마블 다이어그램으로 알아봅시다!
이번엔 switchToLatest() 기능을 좀 더 명확히 알아보기 위해 다른 예제를 준비했습니다.
let red = CurrentValueSubject<String, Never>("Red")
let green = CurrentValueSubject<String, Never>("Green")
let blue = CurrentValueSubject<String, Never>("Blue")
let subject = PassthroughSubject<CurrentValueSubject<String, Never>, Never>()
let queue = DispatchQueue(label: "Felix")
let cancellable = subject
.map { color -> AnyPublisher<String, Never> in
test(color.value)
}
.switchToLatest()
.sink {
print($0)
}
subject.send(red)
subject.send(green)
subject.send(blue)
func test(_ value: String) -> AnyPublisher<String, Never> {
return Future { promise in
queue.asyncAfter(deadline: .now() + 1) {
promise(.success(value))
}
}
.eraseToAnyPublisher()
}
좀 길어졌나요..😭
자 아무튼 test라는 함수는 Future를 사용해 인자로 받은 value를 1초 후에 그대로 publisher로 반환해 주는 간단한 함수입니다.
queue는 serial한 동작이 되어야할 것 같아서 serial queue로 생성해봤습니다.
cancellable을 살펴보면 map + switchToLatest를 통해 Rx의 flatMapLatest를 만들어줬습니다.
왜 flatMap에다가 해주지 헷갈리게 map에다가 했을까요?
제가 모르는 어떤 것이 있겠죠? 나중에 한번 찾아서 작성해봐야 될 것 같습니다. 🔥
그리고 subject에는 red, green, blue를 send 해줍시다.
자 여기까지 동작을 마블 다이어그램으로 확인해볼까요?
red와 green이 test를 통해 success 되기 전에 blue가 들어왔기 때문에 가장 최근에 publish된 blue의 publisher만 cancel되지 않는 모습을 볼 수 있습니다.
코드의 결과값 또한 Blue만 print 되는 것을 확인하실 수 있습니다.
flatMap(maxPublishers: .max(1)) 와 map + switchToLatest() 를 비교해봤을 때
전자는 Oldest한 publisher만 가지고 있는 모습이고
후자는 Latest한 publisher만 가지고 있는 것을 알 수 있습니다.
조건에 따라서 사용하면 굉장히 유용할 것 같지 않나요?😅
자자! 이번 게시글에서는 Combine의 Map, FlatMap, SwitchToLatest에 대해서 알아봤습니다.
콤린이로써 어려운 부분이 있었지만 이렇게 정리하고 나니까 어느정도 친숙해진 느낌은 있는 것 같습니다 ㅎㅎ
혹시 설명 중에 잘못된 부분이나 이해하기 어려운 문장이 있으시다면 댓글로 지적 부탁드립니다.
감사합니다 :)🎉
Reference
http://reactivex.io/documentation/ko/operators/flatmap.html
https://navdeepsinghh.medium.com/transforming-operators-map-flatmap-and-flatmaplatest-c233a536b38b
https://developer.apple.com/documentation/combine/publisher/flatmap(maxpublishers:_:)-3k7z5
https://developer.apple.com/documentation/combine/publisher/switchtolatest()-453ht