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() // 화면 이동 콜백 실행
                }
            }
        }
    }
}