|

MVVM과 MVI 차이: 안드로이드 화면 상태가 복잡해질 때 무엇이 더 버티기 쉬울까

MVVM과 MVI 차이를 설명하는 대표 이미지
패턴 이름보다 상태가 복잡해질 때 무엇이 더 버티는지가 중요하다

MVVM과 MVI 차이는 개념 정의만 보면 금방 이해되는 것 같지만, 실무로 들어가면 오히려 더 헷갈립니다. 이유는 간단합니다. 둘 다 화면 상태를 다루지만, 상태가 복잡해질 때 버티는 방식이 다르기 때문입니다.

이번 글에서는 “어느 쪽이 더 최신인가”보다, event 처리와 state explosion, 단방향 데이터 흐름 관점에서 무엇이 더 버티기 쉬운지 정리하겠습니다.

이 글은 특정 라이브러리 문법보다 상태 흐름을 어떻게 구조화할지에 초점을 맞춥니다. 즉 프레임워크 선택보다 화면 복잡도에 어떤 방식이 더 잘 버티는지를 보려는 글입니다.


가장 짧은 차이: 상태를 얼마나 한 곳으로 모으는가

MVVM은 화면 상태를 ViewModel 중심으로 관리하지만, 실제 필드와 이벤트 처리 방식은 팀 구현에 따라 넓게 달라집니다. 반면 MVI는 보통 state, intent, reducer 흐름처럼 상태 변화를 더 한 줄기로 모으는 쪽에 가깝습니다.

  • MVVM: 유연하지만 구현 편차가 크다
  • MVI: 흐름이 더 엄격하지만 읽는 규칙이 분명하다

왜 상태가 단순할 때는 MVVM이 편할까

화면 상태가 단순하고, 이벤트 종류도 많지 않다면 MVVM은 충분히 가볍고 빠릅니다. ViewModel에 필요한 state와 action만 두고 바로 UI에 연결할 수 있기 때문입니다.

즉 구조 비용보다 개발 속도가 더 중요할 때는 MVVM이 자연스럽습니다.


왜 상태가 복잡해지면 MVI가 자주 언급될까

문제는 화면이 커지고 이벤트가 많아질 때입니다. 로딩, 성공, 실패, 페이징, 재시도, 입력 검증, 일회성 이벤트까지 섞이기 시작하면 상태 분기가 빠르게 늘어납니다. 이때는 상태를 하나의 모델로 모으고, 변화 경로를 추적하기 쉬운 구조가 더 도움이 됩니다.

MVI가 자주 언급되는 이유는 바로 이 추적 가능성입니다. 무엇이 어떤 state를 만들었는지 읽기 쉬운 편이기 때문입니다.



코드 예시로 보면 차이가 더 선명하다

MVVM 쪽 예시

data class UiState(
    val loading: Boolean = false,
    val items: List<String> = emptyList(),
    val error: String? = null,
)

class SampleViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState

    fun load() {
        viewModelScope.launch {
            _uiState.update { it.copy(loading = true, error = null) }
            runCatching { repository.loadItems() }
                .onSuccess { items ->
                    _uiState.update { it.copy(loading = false, items = items) }
                }
                .onFailure { e ->
                    _uiState.update { it.copy(loading = false, error = e.message) }
                }
        }
    }
}

이 방식은 단순하고 빠릅니다. 하지만 이벤트 종류가 늘어나면 상태 변경 지점이 ViewModel 여러 메서드로 흩어질 수 있습니다.

MVI 쪽 예시

data class UiState(
    val loading: Boolean = false,
    val items: List<String> = emptyList(),
    val error: String? = null,
)

sealed interface UiIntent {
    data object Load : UiIntent
    data object Retry : UiIntent
}

class SampleStore : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState

    fun dispatch(intent: UiIntent) {
        when (intent) {
            UiIntent.Load, UiIntent.Retry -> loadItems()
        }
    }

    private fun loadItems() {
        viewModelScope.launch {
            reduce { it.copy(loading = true, error = null) }
            runCatching { repository.loadItems() }
                .onSuccess { items -> reduce { it.copy(loading = false, items = items) } }
                .onFailure { e -> reduce { it.copy(loading = false, error = e.message) } }
        }
    }

    private fun reduce(block: (UiState) -> UiState) {
        _uiState.update(block)
    }
}

이 예시는 단순화한 형태지만, intent와 state 변화 경로를 더 의식적으로 한 줄기에 모읍니다. 화면이 커질수록 이런 추적 가능성이 장점으로 느껴질 수 있습니다.

즉 차이는 문법보다 상태 변경을 흩어지게 둘지, reducer 성격으로 모을지의 차이에 더 가깝습니다.

event 처리에서 체감이 갈린다

MVVM에서는 이벤트 처리가 비교적 자유롭습니다. LiveData, StateFlow, SharedFlow, 단일 이벤트 래퍼 등 구현 폭이 넓습니다. 반대로 MVI는 intent -> reducer -> new state 같은 흐름을 더 강하게 의식합니다.

  1. 디버깅과 재현성이 중요하면 MVI가 유리한 순간이 있다
  2. 작은 화면을 빠르게 만드는 일은 MVVM이 더 단순할 수 있다
  3. 팀 합의가 약하면 MVVM은 금방 구현 편차가 커질 수 있다

자주 하는 오해

MVI가 무조건 더 고급이다

그렇지 않습니다. 상태가 단순한 화면까지 모두 엄격한 reducer 구조로 가져가면 오히려 과할 수 있습니다.

MVVM은 구조가 약하다

MVVM 자체가 약한 것이 아니라, 팀마다 구현이 너무 달라질 수 있다는 점이 문제입니다. 합의된 규칙이 있으면 충분히 강한 구조가 됩니다.


실전 판단 기준

  1. 화면 상태 분기가 많아지는가
  2. 이벤트 원인 추적이 중요한가
  3. 팀이 단방향 데이터 흐름 규칙을 지킬 준비가 되었는가
  4. 속도가 더 중요한지, 일관성이 더 중요한지 분명한가

이 기준으로 보면 작은 화면은 MVVM이, 복잡한 상태 기계에 가까운 화면은 MVI가 더 자연스러울 때가 많습니다.

상태 모델링 자체는 sealed class vs enum 글, 앱 구조 흐름은 single activity 구조 글과 함께 보면 더 선명합니다.


마무리

MVVM과 MVI 차이는 패턴 이름보다 상태 복잡도를 어떻게 감당하느냐의 차이에 가깝습니다. MVVM은 유연하고 가볍고, MVI는 흐름 추적과 일관성에 더 강한 편입니다.

결국 중요한 것은 어떤 패턴이 더 멋있어 보이는지가 아니라, 지금 화면 상태가 얼마나 복잡하고 팀이 어떤 일관성을 필요로 하는가입니다.

함께보면 좋은 글