MVVM은 View 와 ViewModel 간의 양방향 데이터 흐름을 허용합니다.
MVVM에서는 ViewModel과 View가 각각 상태를 변경할 수 있습니다.
MVI는 이러한 문제를 해결하기 위해 단방향 데이터 흐름과 불변 상태 관리를 도입합니다. 모든 UI 상태는 단일 상태 객체(State)로 관리되고, 상태는 불변 객체로 유지된다.
MVVM의 데이터 흐름(양방향)
1. View → ViewModel (사용자 입력 전달)
2. ViewModel → View (상태 업데이트 전달)
데이터는 View와 ViewModel 사이에서 자유롭게 이동 (양방향).
class MainViewModel : ViewModel() {
val text = MutableLiveData<String>()
fun updateText(newText: String) {
text.value = newText
}
}
@Composable
fun MainScreen(viewModel: MainViewModel) {
val text by viewModel.text.observeAsState("")
Column {
TextField(value = text, onValueChange = { viewModel.updateText(it) })
Text(text = "You typed: $text")
}
}
문제점
MVI의 데이터 흐름(단방향)
1. View → Intent (사용자 이벤트 전달)
2. Intent → State (ViewModel에서 상태 업데이트)
3. State → View (View에서 상태를 구독하여 렌더링)
데이터는 항상 한 방향으로만 흐름 (단방향)
// 1. 상태(state) 정의
data class UiState(
val isLoading: Boolean = false,
val data: List<String> = emptyList(),
val error: String? = null
)
// 2. Intent 정의
sealed class UiIntent {
object LoadData : UiIntent()
data class Search(val query: String) : UiIntent()
}
// 3. ViewModel에서 상태 관리
class MainViewModel : ViewModel() {
private val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state
fun processIntent(intent: UiIntent) {
when (intent) {
is UiIntent.LoadData -> loadData()
is UiIntent.Search -> search(intent.query)
}
}
private fun loadData() {
viewModelScope.launch {
_state.value = UiState(isLoading = true)
// 데이터 로드 로직 (예: 네트워크 호출)
val data = listOf("Item 1", "Item 2", "Item 3")
_state.value = UiState(data = data)
}
}
private fun search(query: String) {
viewModelScope.launch {
val filteredData = listOf("Filtered Item 1")
_state.value = UiState(data = filteredData)
}
}
}
// 4. View에서 상태 구독 및 렌더링
@Composable
fun MainScreen(viewModel: MainViewModel) {
val state by viewModel.state.collectAsState()
Column {
if (state.isLoading) {
Text("Loading...")
} else if (state.error != null) {
Text("Error: ${state.error}")
} else {
state.data.forEach { item ->
Text(item)
}
}
Button(onClick = { viewModel.processIntent(UiIntent.LoadData) }) {
Text("Load Data")
}
}
}
상태가 불변 객체로 관리되므로 멀티스레드 환경에서도 안전합니다.
Intent 처리 로직이 순차적으로 실행되므로, 멀티스레드 문제를 줄일 수 있습니다.
제한 사항
- Intent 처리 로직이 명시적으로 직렬화되지 않으면 상태가 비정상적으로 변경될 수 있습니다.
- 예를 들어, ViewModel에서 여러 코루틴을 병렬로 실행하거나, 상태 갱신 로직에서 Mutable 객체를 사용할 경우 멀티스레드 문제가 발생할 수 있습니다.
- MVI에서 멀티스레드 안전성을 보장하려면, 상태 갱신은 항상 단일 스레드 컨텍스트에서 처리되어야 하며, Intent 순서를 명시적으로 관리해야 합니다.
상태 충돌(State Collision)이란, 여러 이벤트(Intent)가 동시에 발생하거나 비동기 작업의 결과가 순서대로 처리되지 않으면서 최종 상태(State)가 의도와 다르게 갱신되는 문제를 말합니다.
MVI(Model-View-Intent) 패턴은 Intent-Reducer 패턴을 활용해 상태(State) 관리와 상태 충돌 문제를 근본적으로 해결할 수 있습니다.
사용자의 동작(예: 버튼 클릭)이나 시스템 이벤트(예: 네트워크 요청 완료)를 명시적으로 표현한 객체입니다.
모든 이벤트는 Intent로 표현되며, ViewModel은 Intent를 받아 상태를 처리합니다.
Intent를 기반으로 현재 상태를 변경하여 새로운 상태를 반환하는 순수 함수(Pure Function)입니다.
Reducer는 이전 상태를 받아 Intent에 따라 새로운 상태(State)를 생성합니다.
상태 갱신은 Reducer를 통해서만 이루어지므로, 상태 변경 과정을 예측 가능하게 만듭니다.
다음 글에 좀 더 자세히 다뤄보겠습니다.