Glacier's Daily Log

Android Compose + MVI 아키텍처 개발 그라운드 룰 본문

Coding/Android

Android Compose + MVI 아키텍처 개발 그라운드 룰

h__glacier_ 2025. 5. 20. 22:08
반응형

 회사에서 Compose를 최초 도입하는 프로젝트를 리딩하고, 팀원들이 Compose를 도입하기 편하게 초기 작업들을 많이 진행하고 있다.

Compose는 Recomposition과 상태관리 관점에서 많은 이점을 얻을 수 있는 MVI 아키텍처와 잘 어울리는데,
따라서 상용 운영중인 앱에 Compose를 도입할때는 MVI아키텍처로 리팩토링도 함께 진행하고 있다.

 

Android Compose + MVI 아키텍처를 도입할 때 주의해야 할 점을 그라운드 룰로 정리해본다.


Jetpack Compose에서 MVI(Model–View–Intent) 아키텍처를 준수하며 개발할 때, 구조적 일관성과 유지보수성을 높이기 위한 Ground Rule


1. 단방향 데이터 흐름 (Unidirectional Data Flow)

  • 상태 직접 변경 금지 (uiState.value = ... 등은 금지)
  • View → ViewModel (event) → ViewModel → State/Effect → View로만 흐름

2. 초기 데이터 로딩 방식 통일 (논의 필요)

방법1 - MVI 원칙은 따르나, 데이터 로딩 시점과 UI 렌더링 시점을 잘 고려해야함

  • Init 이벤트를 명시적으로 호출하여 상태 사이클 돌리기
  • LaunchedEffect(Unit) 또는 remember { ... } 내에서 명시적으로 viewModel.sendEvent(MyEvent.Init) 호출
    • LaunchedEffect(Unit) { viewModel.sendEvent(MyEvent.Init) }

방법2 - 모든 흐름은 event로 부터 시작된다는 MVI 원칙에는 위배되지만, 초기 데이터 로딩에는 적합

  • StateFlow Flow.stateIn() 하기 전에 onStart {} 내에서 초기 데이터 로딩 함수 호출

방법3 - 비추천

  • ViewModel init {} 블록 내에서 초기 데이터 로딩 함수 호출

3. 상태(State)와 부수효과(SideEffect)는 명확히 분리

항목 예시

UiState 로딩 상태, 리스트 데이터, 선택 항목 등 UI에 반영될 정보
SideEffect 토스트, 네비게이션, 다이얼로그, 팝업 등 일회성 UI 동작
  • 상태는 StateFlow, 부수효과는 SharedFlow/Channel로 관리

4. Intent/Event는 명시적으로 설계

  • 모든 사용자 액션, UI 진입, 리트라이 등은 Event로 정의
sealed interface MyEvent {
    object Init : MyEvent
    data class OnClickItem(val id: String) : MyEvent
    object Retry : MyEvent
}

5. ViewModel은 상태 중심 로직만 관리

  • ViewModel은 다음만 처리:
    • Event 처리 (reduceEvent(event: MyEvent))
    • 상태 변경 (uiState.copy { ... })
    • Effect 발행 (sendEffect(...))
    • Repository 등 도메인 계층 호출
  • 절대 ViewModel에서 직접 UI 관련 코드 호출하지 말 것

6. Composable은 상태만 소비하고 이벤트만 전송

  • Composable은 state를 collectAsState()로 수신
  • 사용자 입력 등은 sendEvent(...)로 ViewModel에 전달
  • 직접 Repository 호출, 상태 변경 금지

7. 상태는 Immutable하게 구성

  • data class 사용하여 불변성 유지
  • .copy(...)를 통해 상태 변경
  • UIState는 명확히 스냅샷처럼 구성
data class MyState(
    val isLoading: Boolean = false,
    val items: List<Item> = emptyList(),
    val error: String? = null
)

8. ViewModel의 상태 스트림은 stateIn 로 관리

val uiState: StateFlow<MyState> = _event
    .onStart { emit(MyEvent.Init) }
    .runningFold(initialState, ::reduce)
    .stateIn(viewModelScope, SharingStarted.Eagerly, initialState)

9. 비동기 작업은 반드시 ViewModel 내부에서 처리

  • viewModelScope.launch { ... }로 처리
  • Repository 호출 시 결과를 받아 상태/이펙트로 반영
  • Composable이나 UI에서 suspend fun 직접 호출 금지

10. Error Handling은 일관되게: State or Effect

처리 유형 방법

화면 내 에러 메시지 uiState.errorMessage 등으로 상태에 포함
일회성 팝업/토스트 SideEffect.ShowErrorPopup(...) 등으로 처리 후 UI에서 소비

Layer 정리

UI (Compose)
 └─ State + Effect 소비 + 이벤트 전송

ViewModel
 └─ Event 수신 → 비즈니스 처리 → State / Effect 발행

UseCase/Repository
 └─ 데이터 처리, 네트워크 호출, 결과 반환

반응형
Comments