728x90
MVI 패턴이란?
Model - View - Intent의 약자로, 단방향 데이터 흐름을 가지는 패턴입니다.
State를 중심으로 UI에 전달하는 방식으로서 여기서 Intent는 기존에 알고 있는 Intent와는 다른 개념입니다.
- Model: 상태(State)로, UI에서 보일 데이터 또는 상태입니다.
- View: 화면(UI)을 구성하는 요소로, Model의 상태 변화를 관찰하여 이를 반영합니다.
- Intent: 사용자의 액션이나 이벤트를 정의한 것으로, 이를 통해 Model이 업데이트됩니다.
상태가 중심인 MVI에서는 데이터는 불변의 구조로 구성되며, 그로 인해 상태가 변할 때 필요한 부분만 갱신되면서 효율성이 증가합니다.
MVI 패턴 장점
1. 단방향 데이터 흐름 (Unidirectional Data Flow)
- 상태(State)가 한 방향으로만 흐르기 때문에 코드의 예측 가능성이 높아지고, 디버깅이 쉬워집니다.
- Intent → ViewModel → Model → View 순으로 흐름이 고정되어 있어 데이터 관리가 명확합니다.
2. 상태 기반 UI (State-driven UI)
- 상태를 중심으로한 UI 렌더링 구조로 인해 UI와 데이터의 불일치 문제를 방지합니다.
- 모든 UI는 특정한 상태에 의해 결정되기 때문에 일관된 화면 구성이 가능합니다.
3. Testability (테스트 용이성)
- View와 비즈니스 로직이 분리되어 있어 단위 테스트와 UI 테스트가 쉬워집니다.
- Intent와 State를 분리하여 테스트 시 의존성을 제거할 수 있습니다.
4. 확장성 및 유지보수 용이 (Scalable and Maintainable)
- 상태(State)가 하나의 Immutable 객체로 관리되기 때문에 상태 전이가 명확하고 유지보수가 용이합니다.
- 기능이 추가될 때도 Intent와 State를 쉽게 추가할 수 있습니다.
5. 비동기 작업 처리에 강함 (Effective Handling of Asynchronous Operations)
- 네트워크 요청 같은 비동기 작업을 처리할 때, 로딩 상태(Loading), 성공(Success), 실패(Failure) 같은 상태를 명확하게 정의하여 비동기 처리를 직관적으로 구현할 수 있습니다.
6. 모든 상태의 중앙 집중화 (Centralized State Management)
- 상태를 ViewModel 등에서 중앙에서 관리하므로 어느 시점에서든 상태를 쉽게 추적할 수 있습니다.
- 이로 인해 상태 관리가 일관성 있게 유지됩니다.
7. UI와 비즈니스 로직의 분리 (Separation of Concerns)
- View와 비즈니스 로직이 분리되어 서로 독립적으로 개발 및 유지보수가 가능합니다.
- UI는 상태를 구독하고 그리기만 할 뿐, 비즈니스 로직에 대한 관심사를 가지지 않습니다.
8. 구독과 자동 업데이트 (Reactive Updates)
- StateFlow, LiveData 같은 반응형 스트림과 함께 사용하면 UI가 상태 변화를 자동으로 구독하고 업데이트합니다.
- 사용자가 액션을 수행하면 즉각적으로 화면에 반영됩니다.
위와 같은 장점이 있지만.. 무엇보다 러닝 커브가 높고, 상태 관리와 구성을 위해 단순 MVP 방식에 비해 많은 코드가 필요할 수 있습니다.
MVI 패턴 적용 예시
무료로 랜덤 Advice를 제공하는 api를 활용하여 Compose Text에 보여주는 로직을 구성해 보겠습니다.
interface ApiService {
@GET("/advice")
suspend fun fetchAdvice(): Response<AdviceResponse>
}
State Model을 선언하여 호출 처리에 대한 개별 상태 값을 구성해 줍니다.
sealed class AdviceState {
object Loading : AdviceState()
data class Success(val advice: String) : AdviceState()
data class Error(val errorMessage: String) : AdviceState()
}
Intent class를 선언하여 사용자의 동작에 대해 정의해 줍니다.
//Intent : 사용자의 액션이나 이벤트를 정의
sealed class AdviceIntent {
object FetchAdvice : AdviceIntent() // API 요청을 보내는 Intent
}
ViewModel에서는 init 시점에 API 호출을 처리하고 uiState에 따라 로딩 -> 성공 or 실패에 따른 UI 처리를 진행하게 됩니다.
@HiltViewModel
class AdviceViewModel @Inject constructor(
@NetworkModule.Advice private val apiService: ApiService
) : ViewModel() {
private val _uiState = MutableStateFlow<AdviceState>(AdviceState.Loading)
val uiState: StateFlow<AdviceState> = _uiState
init {
callIntent(AdviceIntent.FetchAdvice)
}
private fun callIntent(intent: AdviceIntent) {
when (intent) {
is AdviceIntent.FetchAdvice -> fetchAdvice()
}
}
private fun fetchAdvice() {
viewModelScope.launch {
_uiState.value = AdviceState.Loading
try {
val response = apiService.fetchAdvice()
if (response.isSuccessful) {
val advice = response.body()?.slip?.advice ?: "No advice found"
_uiState.value = AdviceState.Success(advice)
} else {
_uiState.value = AdviceState.Error("Failed to fetch advice")
}
} catch (e: Exception) {
_uiState.value = AdviceState.Error(e.message ?: "Unknown error")
}
}
}
}
ViewModel을 통해 Advice Intent가 요청되고 State에 따른 구성을 진행합니다.
@Composable
fun AdviceScreen(viewModel: AdviceViewModel) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is AdviceState.Loading -> {
CircularProgressIndicator() // 로딩 상태 UI
}
is AdviceState.Success -> {
Text(text = (uiState as AdviceState.Success).advice) // 성공 상태 UI
}
is AdviceState.Error -> {
Text(text = (uiState as AdviceState.Error).errorMessage) // 에러 상태 UI
}
}
}
728x90