MVVM·단방향 데이터 흐름(UDF)
UI 계층의 표준 패턴이 **MVVM(Model-View-ViewModel)**이고, 그 데이터가 흐르는 규칙이 **UDF(단방향 데이터 흐름)**다. 핵심 한 줄 — ViewModel이 화면 상태를 들고, 화면은 그 상태를 그리며(상태↓), 사용자 동작은 이벤트로 올려보낸다(이벤트↑). 이 단방향성이 상태를 예측 가능하게 하고 화면 회전·재구성에도 안정적으로 만든다. (출처: Android — UI layer / UI events / [Now in Android].)
MVVM — 세 역할
- Model — 데이터·비즈니스(데이터 계층). 화면을 모른다.
- View — 화면(Compose/Activity/Fragment). 상태를 그리고 이벤트를 위임할 뿐, 로직을 갖지 않는다(수동적).
- ViewModel — 화면 상태를 보유하고 이벤트를 받아 상태를 갱신한다. 화면(View)을 참조하지 않는다(그래서 회전에도 살아남고 테스트 가능).
ViewModel — 화면보다 오래 산다
안드로이드 ViewModel(Jetpack)은 화면(Activity/Fragment)보다 긴 생명주기를 갖는다 — 화면 회전으로 Activity가 재생성돼도 ViewModel은 유지되어 상태가 날아가지 않는다. 그래서 "화면 상태와 그 상태를 만드는 로직"을 ViewModel에 둔다.
class ListViewModel(private val repo: MemberRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow() // 상태를 읽기 전용으로 노출
fun onRefresh() = viewModelScope.launch { // 이벤트 처리
_uiState.value = UiState.Loading
runCatching { repo.load() }
.onSuccess { _uiState.value = UiState.Success(it) }
.onFailure { _uiState.value = UiState.Error(it.message ?: "오류") }
}
}
viewModelScope(코루틴 스코프)는 ViewModel이 소멸되면 자동 취소([07 코루틴])라 누수가 없다.
UDF — 상태는 아래로, 이벤트는 위로
단방향 데이터 흐름의 두 방향:
- 상태(state)는 아래로(↓) — ViewModel의
StateFlow를 화면이 구독해 그린다. 화면은 상태의 함수 — 같은 상태면 같은 화면. - 이벤트(event)는 위로(↑) — 클릭·입력 같은 사용자 동작을 화면이 ViewModel의 함수 호출로 올려보내고, ViewModel이 상태를 갱신한다.
@Composable
fun ListScreen(viewModel: ListViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle() // 상태 ↓ 구독
when (state) {
is UiState.Loading -> Spinner()
is UiState.Success -> MemberList(
members = state.data,
onRefresh = viewModel::onRefresh // 이벤트 ↑ 위임
)
is UiState.Error -> ErrorView(state.message)
}
}
화면은 절대 상태를 직접 바꾸지 않는다 — 항상 이벤트를 ViewModel에 올려 ViewModel이 새 상태를 만들어 내려보낸다. 이 규칙이 "상태가 어디서 바뀌는지"를 ViewModel 한 곳으로 모아 디버깅·테스트를 쉽게 한다.
UI 상태는 불변 객체로
UI 상태는 *불변(immutable)*으로 모델링한다 — 보통 data class나 sealed([03 클래스]). 갱신은 copy로 새 상태를 만든다. 부분 변경을 직접 하지 않으니 경쟁 조건·예측 불가가 줄어든다.
data class ScreenState(
val isLoading: Boolean = false,
val items: List<Member> = emptyList(),
val error: String? = null
)
_state.update { it.copy(isLoading = true) } // 불변 갱신
일회성 이벤트는 SharedFlow로
토스트·화면 이동 같은 일회성 이벤트를 상태(StateFlow)로 표현하면 회전 후 재실행 같은 버그가 난다. 이런 건 SharedFlow([08 Flow])나 이벤트 래퍼로 한 번만 소비되게 한다.
정리 — 단방향이 예측 가능성을 만든다
MVVM+UDF의 핵심 — ViewModel이 화면 상태를 보유(회전에도 생존), 상태는 아래로(StateFlow 구독)·이벤트는 위로(함수 호출), 상태는 불변(copy 갱신), 일회성 이벤트는 SharedFlow. 화면을 상태의 함수로 만들면 — 같은 상태면 같은 화면이라 추론·테스트·재구성이 쉬워진다. 이게 Compose 선언형 UI와 천생연분인 이유다.
한 줄 요약 — MVVM: View(수동적 그리기)·ViewModel(상태 보유·화면 모름·회전 생존)·Model(데이터). UDF: 상태↓(StateFlow)·이벤트↑(함수 호출), 화면은 상태의 함수. 상태는 불변
data class+copy, 일회성은 SharedFlow.viewModelScope로 누수 없는 비동기.
(출처: Android — UI layer architecture / ViewModel / UI events / Now in Android.)