posite
2026. 2. 22. 15:42
MVI
- 단방향 데이터 흐름 패턴
- Model, View Intent로 구성되어 있으며 각각의 역할은 다음과 같다
- Intent(의도) : 뷰에서 발생한 이벤트를 모델로 전달하는 매개체
- Model : UI에 표시될 상태 새로운 상태는 기존 상태를 수정하지 않고 새로운 객체를 생성하여 불변 데이터 구조임
- View : Model이 전달하는 상태를 그대로 렌더링할 뿐이며, 로직을 직접 수행하지 않음
- 특성
- 단방향 데이터 흐름 : Intent -> Model -> View -> Intent 순서로 한 방향으로만 데이터가 흐름
- 상태의 불변성 : 상태는 Reducer를 통해 새로운 상태를 생성할 뿐 바뀌지 않음 -> 멀티스레딩 환경에서도 일관성 유지 가능
- 상태의 단일화 : 모든 상태를 하나의 객체에 담아서 하나의 객체로 관리함
- 모든 사용자 행위를 Intent로 명시하고 상태를 계산하기 때문에 흐름이 명확하고 예측 가능성이 높으며 UI에 일관성이 생김
-
Reducer와 EffectHandler 구조를 통해 상태 변화와 부수 효과를 철저히 분리할 수 있어 테스트와 유지보수가 용이
- 상태와 UI가 일치하기 때문에 XML 보다 Compose에 더 적합함
- Intent, State, Reducer 등 정의할 코드가 많아 보일러플레이트 코드가 많이 생김
- 매번 새로운 상태 객체를 생성하므로 메모리 오버헤드 가능성 있음
예시)
State : 상태 정의 - 모든 상태들을 Data class로 정의함
data class UserProfileState(
val isLoading: Boolean = false,
val userName: String = "",
val userEmail: String = "",
val error: String? = null
)
Intent 정의 : 사용자가 할 수 있는 행동들을 정의 sealed class를 사용해 행동의 타입 제한
sealed class UserProfileIntent {
object LoadUserProfile : UserProfileIntent()
data class UpdateName(val newName: String) : UserProfileIntent()
object OnSaveClick : UserProfileIntent()
}
SideEffect 정의 : 부수 효과 정의
sealed class UserSideEffect {
data class ShowToast(val message: String) : UserSideEffect()
object NavigateToHome : UserSideEffect()
}
ViewModel : Intent를 받아 새로운 상태 발행 - Reducer 역할
class UserProfileViewModel : ViewModel() {
private val _state = MutableStateFlow(UserProfileState())
val state: StateFlow<UserProfileState> = _state
private val _effect = Channel<UserSideEffect>()
val effect = _effect.receiveAsFlow() // 외부에는 Flow로 노출
// Intent를 처리하는 함수 - Reducer
fun handleIntent(intent: UserProfileIntent) {
when (intent) {
is UserProfileIntent.LoadUserProfile -> loadData()
is UserProfileIntent.UpdateName -> updateUserName(intent.newName)
is UserProfileIntent.OnSaveClick -> {
// 저장 로직 수행 후...
// 2. Side Effect 발송
_effect.send(UserSideEffect.ShowToast("저장이 완료되었습니다!"))
_effect.send(UserSideEffect.NavigateToHome)
}
}
}
private fun loadData() {
viewModelScope.launch {
// 1. 로딩 상태 시작 (Copy를 통해 불변성 유지)
_state.value = _state.value.copy(isLoading = true)
try {
// API 호출 가정
val user = userRepository.getUser()
// 2. 성공 상태 업데이트
_state.value = _state.value.copy(
isLoading = false,
userName = user.name,
userEmail = user.email
)
} catch (e: Exception) {
// 3. 에러 상태 업데이트
_state.value = _state.value.copy(isLoading = false, error = e.message)
}
}
}
}
View : 상태를 관찰하고 이벤트가 발생하면 Intent 전달
@Composable
fun UserProfileScreen(viewModel: UserProfileViewModel, onNavigate: () -> Unit) {
val state by viewModel.state.collectAsState()
val context = LocalContext.current
Column {
if (state.isLoading) {
CircularProgressIndicator() // 로딩 중일 때
}
state.error?.let {
Text("에러 발생: $it", color = Color.Red)
}
Text("이름: ${state.userName}")
Text("이메일: ${state.userEmail}")
Button(onClick = {
// 사용자의 의도(Intent)를 ViewModel에 전달
viewModel.handleIntent(UserProfileIntent.LoadUserProfile)
}) {
Text("프로필 새로고침")
}
}
LaunchedEffect(viewModel.effect) {
viewModel.effect.collect { effect ->
when (effect) {
is UserSideEffect.ShowToast -> {
Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
}
is UserSideEffect.NavigateToHome -> {
onNavigate() // 화면 이동 콜백 실행
}
}
}
}
}