개요
상품 상세 화면에서 상품 상세 정보(DetailState)와 퀵 메시지(QuickMessageUiModel) 정보의 결합을 통해 UI 상태 관리를 할 데이터(QuickMessageViewState)를 생성해야했다.
상품 상세정보는 Server API를 통해 화면 진입 시 먼저 가져오고, 퀵 메시지 정보는 Firebase Remote Config를 통해 가져온다.
이 과정에서 당연히 상황에 알맞은 연산자를 사용해야 했음에도 불구하고 가볍게 생각하고 지나쳐버렸다.
(Zip을 사용해야 하는 상황인데… Combine을..?)
Before
/**
* 상품 상세 정보와 퀵 메시지 결합
*/
fun getCombinedData(): Flow<Pair<DetailState, QuickMessageUiModel>> {
return combine(_detailState, _quickMessage) { detailState, quickMessage ->
Pair(detailState, quickMessage)
}
}
그 후에는 update 되어있는 상품 상세 정보(detailState)와 퀵 메시지 데이터(quickMessage)를 결합하였다.
바로 이 부분에서 문제가 되었다.
combine의 용도를 잘 모른다면, 퀵 메시지와 상품 상세 정보와의 연관성을 모른다면 그냥 지나칠 수 있는 코드이다. (난 알았는데 왜,,? 🤷♂️)
이 코드는 반드시 Combine보다 Zip 연산자를 사용하는 것이 맞다.
Combine과 Zip에 대해 설명하기 전에 상황을 먼저 간략하게 설명하자면, 각각의 퀵 메시지를 클릭하면 어떠한 동작을 하게 된다.
이 때 퀵 메시지의 내용과 상품 상세 정보에서 받아온 itemIdx를 함께 알고 있어야한다.
즉 퀵메시지와 상품 상세정보는 매칭이 되어야 하고, 두 개가 짝을 이뤘을 때 방출이 되는 것이 맞다.
zip vs combine
이 둘의 공통점은 ‘결합’을 한다는 것이고 아래와 같은 차이점이 있다.
zip

- 여러 Flow의 요소를 동기화하고 결합해야 하는 경우에 적합.
- output Flow는 두 input Flow 모두 발행할 때만 방출하고 그렇기에 순서가 유지된다.
- 두 input Flow 중 하나가 다른 Flow보다 더 자주 발행하는 경우 느린 Flow가 따라잡을 때까지 대기한 후 결합된 요소를 발행한다.
ex) 사용자 ID와 사용자의 이름을 매칭하여 방출할 때
private fun main(): Unit = runBlocking {
println("\nCombine")
val combine = getIds().combine(getNames()) { id, name ->
"$id: $name"
}
combine.collect {
log(it)
}
}
private fun getIds() = flow {
for (i in 0 until 3) {
delay(100L)
emit(ids[i])
}
}
private fun getNames() = flow {
for (i in 0 until 3) {
delay(200L)
emit(names[i])
}
}

combine

- 별다른 동기화 요구 사항 없이 여러 Flow의 요소를 결합하고 입력 흐름이 생성될 때 마다 업데이트를 내보내려는 경우에 유용.
- input Flow 중 하나가 발행할 때마다 output이 발행된다.
- 제공된 transform 함수는 각 Flow에서 최신 요소를 결합하여 결합된 요소를 생성한다.
- output Flow는 한 개의 inputFlow보다 자주 요소를 발행할 수 있고, input Flow가 발행하는 속도에 따라 달라진다.
ex) 온도와 습도를 함께 보여주는 온도계에서 온도와 습도가 각각 업데이트 될 때 마다 방출할 때
private fun main(): Unit = runBlocking {
val zip = getIds().zip(getNames()) { id, name ->
"$id: $name"
}
println("\nZip")
zip.collect {
log(it)
}
println("\nCombine")
val combine = getIds().combine(getNames()) { id, name ->
"$id: $name"
}
combine.collect {
log(it)
}
}
private fun getIds() = flow {
for (i in 0 until 3) {
delay(100L)
emit(ids[i])
}
}
private fun getNames() = flow {
for (i in 0 until 3) {
delay(200L)
emit(names[i])
}
}

위와 같은 차이점들로 인해 둘이 함께 방출되어야 하는 내 상황에서는 combine을 쓰는 것은 비효율적이다
After
/**
* 상품 상세 정보와 퀵 메시지 결합
*/
fun bindGetItemDetailQuickMessage() {
viewModelScope.launch {
_detailState.zip(fetchQuickMessage()) { itemDetail, quickMessage ->
QuickMessageViewState(itemDetail, quickMessage)
}.collect { result ->
_quickMessageState.update {
it.copy(
detail = result.detail,
quickMessage = result.quickMessage
)
}
}
}
}