10
19

์ด์ œ ์ปดํฌ์ฆˆ์˜ ํ๋ฆ„์„ ํ”ผํ•ด๊ฐˆ ์ˆ˜ ์—†๋‹ค. ์ œ๋Œ€๋กœ ์ž…๋ฌธํ•˜๊ธฐ ์œ„ํ•ด MVI ํŒจํ„ด๋จผ์ € ๊ณต๋ถ€ํ•˜๊ณ , ์‹œ์ž‘ํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

# MVI?

MVI๋Š” Model-View-Intent๋กœ, 

Model์€ UI์˜ ์ƒํƒœ, View๋Š” UI, Intent๋Š” ์‚ฌ์šฉ์ž์™€์˜ ์ƒํ˜ธ์ž‘์šฉ์ด๋‚˜ ๋‹ค๋ฅธ ์š”์†Œ์— ์˜ํ•œ ์ด๋ฒคํŠธ(์˜๋„)๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

๋‚˜๋Š” ์ฒ˜์Œ์— Intent๊ฐ€ ์•ˆ๋“œ๋กœ์ด๋“œ์˜ Intent์ธ์ค„์•Œ๊ณ  ์ด๊ฑธ๋กœ ์–ด๋–ป๊ฒŒ ํŒจํ„ด์„ ๊ตฌ์„ฑํ–ˆ์ง€ ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์—ˆ๋‹ค. ๊ทผ๋ฐ ViewModel๊ณผ ACC ViewModel์ด ๋‹ค๋ฅด๋“ฏ์ด ์—ฌ๊ธฐ์„œ ๋งํ•˜๋Š” Intent๋„ ์™„์ „ํžˆ ๋‹ค๋ฅธ ๊ฐœ๋…์ด๋‹ค.

MVI์˜ ๊ฐ€์žฅ ํฐ ํŠน์ง•์€ ํ•จ์ˆ˜์˜ ์ž…๋ ฅ๋งŒ์ด ํ•จ์ˆ˜์˜ ๊ฒฐ๊ณผ์— ์˜ํ–ฅ์„ ์ฃผ๋Š” ์ˆœ์ˆ˜ํ•จ์ˆ˜ ์‹ธ์ดํด๋กœ ์ด๋ฃจ์–ด์ ธ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์œผ๋กœ ์ด๋ฃจ์–ด์ง„๋‹ค.

์ƒํƒœ๊ฐ€ ๊ทธ๋Ÿฌ๋ฉด model์ด ๋˜๊ฒ ๋‹ค. intentํ•จ์ˆ˜์˜ ๊ฒฐ๊ณผ๊ฐ€ model ํ•จ์ˆ˜์˜ ์ธ์ˆ˜๋กœ ์ „๋‹ฌ๋˜๊ณ , ๋ชจ๋ธ์˜ ๊ฒฐ๊ณผ๊ฐ€ ๋ทฐ๋กœ ์ „๋‹ฌ๋œ๋‹ค.

์™ธ๋ถ€์˜ ์˜ํ–ฅ์œผ๋กœ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€Œ์ง€์•Š๊ณ  ์ƒˆ ์ด๋ฒคํŠธ๊ฐ€ ๊ธฐ์กด ์ƒํƒœ์™€ ๋”๋ถˆ์–ด ์ƒˆ ์ƒํƒœ๋ฅผ ๋งŒ๋“ ๋‹ค.

๊ทธ๋ž˜์„œ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๊ด€๋ฆฌํ•ด์ฃผ๋Š” ์–ด๋–ค ์ง€์ ์ด ํ•„์š”ํ•œ๋ฐ, `State Reducer Pattern`์œผ๋กœ ์ด ๊ณผ์ •์„ ์ฒ˜๋ฆฌํ•œ๋‹ค. ์ด๊ฒŒ React์—์„œ Redux๋ž‘ ๋น„์Šทํ•˜๋‹ค. reducer, transformer, ๋‹ค์–‘ํ•œ ์ด๋ฆ„์œผ๋กœ ๋ถˆ๋ฆฐ๋‹ค.

 

์ˆœ์ˆ˜ํ•จ์ˆ˜๋ผ๊ณ  ํ•˜๋ฉด ๋”ฐ๋ผ์˜ค๋Š” ๊ฐœ๋… ์ค‘์— Side Effect๊ฐ€ ์žˆ๋‹ค. ํ•จ์ˆ˜ ์™ธ๋ถ€์˜ ์š”์†Œ๋‚˜ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๋Š” ๊ฑด๋ฐ, Intent์™€ Model ์‚ฌ์ด์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด๋ผ๊ณ  ๋ณด๋ฉด๋œ๋‹ค. ๋ทฐ์—๋Š” ์˜ํ–ฅ์„ ๋ผ์น˜์ง€์•Š์ง€๋งŒ, Model์—๋Š” ์˜ํ–ฅ์„ ๋ผ์น  ์ˆ˜ ์žˆ๋‹ค.

๋„ค๋น„๊ฒŒ์ด์…˜, ๋กœ๊น…, ํ† ์ŠคํŠธ(์Šค๋‚ต๋ฐ”), analytics๊ฐ™์€ ์ผํšŒ์„ฑ ์ด๋ฒคํŠธ๋“ค์€ Side Effect๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ๋˜๋Š” ๋ถ€๋ถ„์ด๋‹ค. ์ฆ‰ UI์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฒƒ๋“ค์ด๋ผ๊ณ  ์ดํ•ดํ•˜๋ฉด ๋œ๋‹ค. Jetpack Compose ์ž์ฒด์—๋„ SideEffect๋กœ ๋ถ„๋ฅ˜๋˜๋Š” ๊ฒƒ๋“ค์ด ์žˆ๊ธฐ๋•Œ๋ฌธ์— ์ด๊ฑฐ๋„ ํ•œ๋ฒˆ ์‚ดํŽด๋ด์•ผ๋œ๋‹ค.

# ๊ฐ€๋ณ๊ฒŒ ์‹ค์Šต

๊ตญ๋ฃฐ์ธ ์นด์šดํ„ฐ ์•ฑ์œผ๋กœ ํ•œ๋ฒˆ ์•Œ์•„๋ณด์ž.

 

๋จผ์ € UI๊ฐ€ ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜ํ•˜๊ฒ ๋‹ค.

data class CounterState(
    val count: Int = 0
)

์š” ์ƒํƒœ๋งŒ ๊ฐ–๊ณ  ์ด์ œ ui์—์„œ ์ฒ˜๋ฆฌํ•  ๊ฒƒ์ด๋‹ค. ์ด ์ƒํƒœ๋Š” ์นด์šดํŠธ ๊ฐ’์„ ๊ด€๋ฆฌํ•œ๋‹ค.

์ด์ œ Intent์— ํ•ด๋‹นํ•˜๋Š” ์•ก์…˜(์˜๋„)์„ ์ •์˜ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค. ๊ฐ’์„ ์ฆ๊ฐ€์‹œํ‚ค๋Š” ์•ก์…˜, ๊ฐ์†Œ์‹œํ‚ค๋Š” ์•ก์…˜์œผ๋กœ ๋‚˜๋ˆˆ๋‹ค.

sealed class CounterIntent {
    object Increment : CounterIntent()
    object Decrement : CounterIntent()
}

์ด์ œ ๋ทฐ๋ชจ๋ธ์—์„œ ์•ก์…˜์„ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•ด์•ผ๋œ๋‹ค.

class CounterViewModel : ViewModel() {
    private val _state = MutableStateFlow(CounterState())
    val state: StateFlow<CounterState> = _state.asStateFlow()

    fun processIntent(intent: CounterIntent) {
        when (intent) {
            is CounterIntent.Increment -> {
                _state.update { currentState ->
                    currentState.copy(count = currentState.count + 1)
                }
            }
            is CounterIntent.Decrement -> {
                _state.update { currentState ->
                    currentState.copy(count = currentState.count - 1)
                }
            }
        }
    }
}

์ƒํƒœ๊ฐ’์œผ๋กœ ๊ณ„์† flow๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๊ณ , ์•ก์…˜์ด ๋“ค์–ด์˜ค๋ฉด CounterState์˜ count๊ฐ’๋งŒ updateํ•ด์ค€๋‹ค.  

์˜๋„์— ๋งž๊ฒŒ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒŒ ์ด ํŒจํ„ด์˜ ํ•ต์‹ฌ์ด๋‹ค.

update๊ฐ€ ์ƒ์†Œํ•ด์„œ, ์ž์„ธํžˆ ์‚ดํŽด๋ดค๋‹ค.

public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T)

ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์ธ์ž๋กœ ๋ฐ›๊ณ , ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋กœ, MVI์˜ ๋ชฉ์ ์ธ ์ƒํƒœ์˜ ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•œ์ฑ„๋กœ ์ƒํƒœ๊ฐฑ์‹ ์„ ํ•ด์ค€๋‹ค๊ณ  ๋ณด๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค. ์ด update ๊ณผ์ •์—์„œ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋ฐ›์•„, ๊ทธ ์ƒํƒœ ์•ˆ์— ์žˆ๋Š” ๊ฐ’์„ dataclass์˜ ์œ ์šฉํ•œ ๋ฉ”์„œ๋“œ์ธ copy๋กœ ์ˆ˜์ •ํ•ด์„œ ์ƒˆ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ค€๋‹ค.

Row {
    Button(onClick = { viewModel.processIntent(CounterIntent.Decrement) }) {
        Text(text = "-")
    }
    Spacer(modifier = Modifier.width(16.dp))
    Button(onClick = { viewModel.processIntent(CounterIntent.Increment) }) {
        Text(text = "+")
    }
}

UI๋Š” ๊ทธ๋ƒฅ ๋ณ„๊ฑฐ์—†๋‹ค. ui๋กœ ์•ก์…˜์„ ์ „๋‹ฌํ•œ๋‹ค. ๊ทผ๋ฐ ๊ตฌํ˜„ ์ƒํƒœ๋ฅผ ๋ณด๋ฉด, ์ฒ˜๋ฆฌ ์ˆœ์„œ๋Š” ๋”ฐ๋กœ ๊ณ ๋ คํ•˜๊ณ  ์žˆ์ง€ ์•Š๋‹ค.

๊ทธ๋ž˜์„œ flow๋„ ์“ฐ๊ณ ์žˆ์œผ๋‹ˆ, channel์„ ์‚ฌ์šฉํ•ด์„œ ๊ด€๋ฆฌ๋ฅผ ํ•ด๋ณด์ž. Channel์€ ์ƒ์‚ฐ์ž-์†Œ๋น„์ž ํŒจํ„ด์„ ๋”ฐ๋ฅด๋ฉฐ, ์ด๋ฒคํŠธ๊ฐ€ ๋“ค์–ด์˜ค๋ฉด ํ์— ์Œ“์ด๊ณ  ์ˆœ์ฐจ์ ์œผ๋กœ ์†Œ๋น„๋œ๋‹ค.

๊ธฐ์กด์— processIntent์—์„œ ๋ฐ”๋กœ ์ฒ˜๋ฆฌํ•˜๋˜๊ฑธ channel๋กœ ๋ณด๋‚ด์„œ ์ˆœ์„œ๋ฅผ ๋งž์ถฐ์คฌ๋‹ค.

class CounterViewModel : ViewModel() {
    private val events = Channel<CounterIntent>(Channel.UNLIMITED)
    val state = MutableStateFlow(CounterState()) // ์™ธ๋ถ€์—์„œ ์ƒํƒœ๋ฅผ ๊ตฌ๋… ๊ฐ€๋Šฅ

    init {
        events.receiveAsFlow()
            .onEach { intent ->
                updateState(intent)  // handleIntent ๋Œ€์‹  updateState๋กœ ๋ณ€๊ฒฝ
            }
            .launchIn(viewModelScope) // viewModelScope์—์„œ ์‹คํ–‰
    }

    // ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ (์™ธ๋ถ€์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅ)
    fun handleEvent(intent: CounterIntent) {
        events.trySend(intent) // Channel์— ์ด๋ฒคํŠธ๋ฅผ ์ „์†ก
    }

    // ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ (๋‚ด๋ถ€์ ์œผ๋กœ๋งŒ ์‚ฌ์šฉ)
    private fun updateState(intent: CounterIntent) {
        when (intent) {
            is CounterIntent.Increment -> {
                state.update { currentState ->
                    currentState.copy(count = currentState.count + 1)
                }
            }
            is CounterIntent.Decrement -> {
                state.update { currentState ->
                    currentState.copy(count = currentState.count - 1)
                }
            }
        }
    }
}

events.receiveAsFlow()๋Š” Channel์—์„œ ์ด๋ฒคํŠธ๋ฅผ Flow๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ฑ„๋„์—์„œ ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋“ค์ด ์ˆœ์ฐจ์ ์œผ๋กœ ํ๋ฆ„์— ์˜ํ•ด ์ฒ˜๋ฆฌํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค. events.trySend(intent)๋Š” Channel์— ์ด๋ฒคํŠธ๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ „๋‹ฌํ•œ๋‹ค.

public fun trySend(element: E): ChannelResult<Unit>

์ด๋ฒคํŠธ๊ฐ€ ์ฑ„๋„๋กœ ๋„์ฐฉํ•˜์ง€ ์•Š์•„๋„ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค์ง€์•Š๋Š” safeํ•œ ๋ฉ”์„œ๋“œ๋‹ค.

# ์•„์ง ์•ˆ๋๋‚จ - ์™ธ๋ถ€ ์ƒํƒœ ๋ณ€๊ฒฝ๋ฌธ์ œ

๋ฌธ์ œ๊ฐ€ ํ•˜๋‚˜ ์žˆ๋‹ค. state๊ฐ€ MutableStateFlow๋ผ์„œ ์™ธ๋ถ€์—์„œ ๋ฐ”๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์ ์ด๋‹ค.

// ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” Flow, runningFold๋กœ ์ด๋ฒคํŠธ์— ๋”ฐ๋ผ ์ƒํƒœ๋ฅผ ๋ˆ„์ ํ•ด์„œ ๊ฐฑ์‹ 
val state: StateFlow<CounterState> = events.receiveAsFlow()
    .runningFold(CounterState(), ::reduceState)  // ๋ˆ„์ ํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ 
    .stateIn(viewModelScope, SharingStarted.Eagerly, CounterState())  // ์ƒํƒœ๋ฅผ StateFlow๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๊ตฌ๋… ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
    
// ๊ธฐ์กด์— ์žˆ๋˜ processIntent๋Œ€์‹  reduce๋กœ ๋ฐ”๊พผ๋‹ค
// ์ƒํƒœ ๊ฐฑ์‹ ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜
private fun reduceState(currentState: CounterState, intent: CounterIntent): CounterState {
    return when (intent) {
        is CounterIntent.Increment -> currentState.copy(count = currentState.count + 1)
        is CounterIntent.Decrement -> currentState.copy(count = currentState.count - 1)
    }
}

kotlin์—์„œ ์ œ๊ณตํ•˜๋Š” state reducer๊ฐ€ runningFold์ธ๋ฐ, ์ฃผ์–ด์ง„ ์ด๋ฒคํŠธ์™€ ์ƒํƒœ๋ฅผ ํ†ตํ•ด์„œ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ๋งŒ๋“ค์–ด๋‚ธ๋‹ค.

runningFold๋Š” Flow์˜ ์—ฐ์‚ฐ ํ•จ์ˆ˜ ์ค‘ ํ•˜๋‚˜๋กœ, ๋ˆ„์ ๋œ ๊ฐ’์„ ๊ณ„์‚ฐํ•˜๋ฉด์„œ ํ˜„์žฌ ๊ฐ’๊ณผ ์ƒˆ๋กœ์šด ๊ฐ’์„ ๊ฒฐํ•ฉํ•ด ๋‚˜๊ฐ„๋‹ค.  reduce๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋‹จ์ผ ๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ˜๋ฉด, runningFold๋Š” ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ๊ฐ’์„ ๋ฐœํ–‰ํ•˜๋ฉด์„œ ์ค‘๊ฐ„ ์ƒํƒœ๋ฅผ ๋ชจ๋‘ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

๋ˆ„์ ํ•จ์ˆ˜๋กœ reduceState๊ฐ€ ์“ฐ์ธ๊ฒƒ์ด๋‹ค.

public fun <T, R> Flow<T>.runningFold(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow<R> = flow {
    var accumulator: R = initial
    emit(accumulator)
    collect { value ->
        accumulator = operation(accumulator, value)
        emit(accumulator)
    }
}

์ดˆ๊ธฐ ์ƒํƒœ์™€ ์ด๋ฒคํŠธ ๋ณ€๊ฒฝ๋˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

์ „์ฒด์ ์ธ ํ๋ฆ„์„ ๋‹ค์‹œ ์งš์ž๋ฉด, 

  1. ์ด๋ฒคํŠธ๊ฐ€ ์ฑ„๋„์— ๋“ค์–ด์˜ค๋ฉด
  2. reduceState๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค(์ด๋•Œ runningFold์—์„œ ์ƒํƒœ๋ฅผ ๋ฐ›์•„ ๊ทธ๊ฑธ reduceStateํ˜ธ์ถœํ•  ๋•Œ ๋„ฃ๋Š”๋‹ค.)
  3. ๊ทธ๋Ÿฌ๋ฉด state๋ฅผ ๊ตฌ๋…ํ•˜๊ณ ์žˆ๋˜ UI ๋‹จ์—์„œ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ด๋ฃจ์–ด์ง€๊ฒŒ ๋œ๋‹ค.

 

MVI... ์–ด๋ ต๋‹ค.


::์ถœ์ฒ˜::

https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.linkedin.com%2Fpulse%2Funderstanding-react-reducer-javascript-state-ashim-rudra-paul&psig=AOvVaw1xQbV3yGK01ytdnGAqKU2b&ust=1729415101030000&source=images&cd=vfe&opi=89978449&ved=0CBMQjRxqFwoTCJCHt7iLmokDFQAAAAAdAAAAABAE

https://jaehochoe.medium.com/%EB%B2%88%EC%97%AD-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EB%A5%BC-%EC%9C%84%ED%95%9C-mvi-model-view-intent-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-165bda9dfbe7

๋„์›€์ด ๋๋‹ค๋ฉด ๋Œ“๊ธ€์ด๋‚˜ ๊ณต๊ฐ ๋ฒ„ํŠผ ํ•œ ๋ฒˆ์”ฉ ๋ˆ„๋ฅด๊ณ  ๊ฐ€์ฃผ์„ธ์š”!

 

๋ฐ˜์‘ํ˜•

'Android ๐Ÿ–ฅ๏ธ > ์•„ํ‚คํ…์ณ๐Ÿ“–' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

MVI, MVVM ํŒจํ„ด  (0) 2024.12.17
COMMENT