[Jetpack Compose] MVI 패턴

유민국·2025년 2월 24일

개요

많은 포스팅에서 MVI가 상태를 쉽게 관리해주기 때문에 상태가 중요한 Compose에서 적절한 패턴이라고 소개하고 있다

이 부분에서 의문이 들었다.

MVVM 패턴에서는 상태를 쉽게 관리해주지 않나?
MVVM 패턴에서는 단방향 흐름을 제공하지 못하나?

많은 고민을 하다가 내린 결론은 MVVM으로도 충분히 상태를 관리할 수 있다는 것이다.

다만, MVI는 MVVM을 더 엄격하게 구조화한 패턴이라고 생각된다.
즉, 상태를 쉽게 관리할 수 있다라는 말은 MVI패턴을 적용하면 상태를 더 엄격하게 관리할 수 있기 때문에, 예기치 못한 상태변경을 방지할 수 있고, 그렇기 때문에 상태관리가 더 쉬워진다라고 이해했다

MVI 패턴이란?

MVVM처럼 MVI도 관심사를 나누고 해당 관심사의 앞글자를 따서 만든 패턴이다

가장 큰 특징은 단방향 흐름 구조로 유지한다는 점이라고 할 수 있다

이 UDF 구조를 엄격하게 유지하기 위해 Intent라는 개념이 추가되었고, Intent는 사용자의 의도이다(UI 화면에서 어떤 행위를 할건지에 대한..)

UI 내에서 사용자의 의도를 Model로 전달하고, Model은 이를 캐치하여 상태를 변경하며, 이를 View에 적용시킨다

왜 Compose는 UDF 아키텍처 패턴이 좋은가?

선언형 UI 패턴과 자연스럽게 연결됨

  • UDF에서는 모든 상태 변경이 한 곳(ViewModel)에서만 이루어지고, UI는 State를 구독하는 역할만 함.
  • View가 직접 상태를 변경하지 않고, 항상 현재 State를 기반으로 UI를 그리기 때문에 예측 가능성이 높아짐.

상태(State) 추적이 쉬워 유지보수와 디버깅이 편리함

  • UDF를 사용하면 어디에서 상태가 변경되었는지 쉽게 추적할 수 있어.
  • MVP 패턴에서는 상태를 여러 곳에서 변경할 수 있기 때문에, 예상치 못한 버그가 발생할 가능성이 높다
  • UDF에서는 "모든 상태 변경은 ViewModel에서만 발생"하므로, 디버깅이 훨씬 쉬워짐.

테스트가 용이해지고, 앱의 안정성이 증가함

  • UDF에서는 상태 변경이 한 곳(ViewModel)에서만 이루어지므로, 유닛 테스트가 훨씬 쉬워짐.
@Test
fun testCounterViewModel() = runTest {
    val viewModel = CounterViewModel()

    viewModel.increase()
    assertEquals(1, viewModel.count.first())  // ✅ 상태가 정확하게 변경되었는지 확인
}

MVI 구조

abstract class BaseViewModel<Event: UiEvent, State : UiState, Effect : UiEffect> : ViewModel() {
    private val initialState : State by lazy {createInitialState()}
    abstract fun createInitialState() : State

    private val _uiState : MutableStateFlow<State> = MutableStateFlow(initialState)
    val currentState: State get() = _uiState.value

    val uiState =  _uiState.asStateFlow()

    private val _event : MutableSharedFlow<Event> = MutableSharedFlow()
    val event = _event.asSharedFlow()

    private val _effect : Channel<Effect> = Channel()
    val effect = _effect.receiveAsFlow()

    init {
        viewModelScope.launch {
            event.collect{
                handleEvent(it)
            }
        }
    }

    abstract fun handleEvent(event : Event)

    fun setEvent(event:Event){
        val newEvent = event
        viewModelScope.launch {
            _event.emit(newEvent)
        }
    }
    protected fun setState(state: State){
        _uiState.value = state
    }
    
    protected fun setEffect(effect : Effect){
        viewModelScope.launch {
            _effect.send(effect)
        }
    }
}

참고

Android 공식문서: Compose에서 UDF 아키텍처 패턴 권장
찰리의 안드로이드

profile
안녕하세요 😊

0개의 댓글