구성 변경(화면 회전 등)을 넘어 살아남으며 UI 상태를 보유하는 컴포넌트.
생명주기가 화면보다 길다
회전 시
- Activity — onDestroy → onCreate (새 인스턴스) ViewModel 그대로 유지된다
최종 종료 시 (뒤로가기 · finish()) ViewModel 의 onCleared() 가 불리고 소멸한다
"회전했더니 데이터를 다시 불러온다" 는 문제가 이것으로 사라진다.
MVVM의 세 역할
- Model — 데이터와 비즈니스 (Repository · UseCase)
- View — 화면 (Activity · Fragment · Composable) — 상태를 그리기만 한다
- ViewModel — 상태를 보유하고 이벤트를 처리한다. View 를 모른다
ViewModel이 View를 모른다는 것이 핵심이다 — 그래서 테스트에서 안드로이드 없이 돌릴 수 있다.
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: UserRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
fun onRefresh() = viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
val users = repository.getUsers()
_uiState.update { it.copy(isLoading = false, users = users) }
}
}
절대 하면 안 되는 것 — Context 참조
// ❌ Activity 가 파괴돼도 ViewModel 이 참조를 들고 있다 → 메모리 누수
class MyViewModel(private val activity: Activity) : ViewModel()
class MyViewModel : ViewModel() { var view: TextView? = null }
// ✅ 필요하면 Application Context 를 쓴다 (AndroidViewModel)
// 더 나은 방법: Context 가 필요한 일을 Repository 로 내린다
SavedStateHandle — 프로세스 죽음에서 살아남기
ViewModel 은 '구성 변경' 은 넘지만 '프로세스 종료' 는 못 넘는다
- 백그라운드에서 시스템이 앱을 죽이면 ViewModel 도 사라진다
SavedStateHandle 에 넣은 값은 그것도 넘는다 (Bundle 로 저장된다)
- 검색어 · 선택된 탭 · 스크롤 위치처럼 작은 것만 넣는다
val query: StateFlow<String> = savedStateHandle.getStateFlow("query", "")
fun onQueryChange(q: String) { savedStateHandle["query"] = q }
면접 함정
- ❌ "ViewModel은 모든 상황에서 데이터를 유지한다" → 프로세스 종료는 못 넘는다.
SavedStateHandle이 필요하다. - ❌ "ViewModel에 LiveData/StateFlow만 있으면 MVVM" → View를 모르는 것이 본질이다.
viewModelScope — 코루틴이 자동으로 취소된다
viewModelScope.launch { ... } // onCleared() 시 자동 취소
왜 중요한가
- GlobalScope.launch 를 쓰면 화면이 사라져도 작업이 계속 돈다
- 이미 없는 화면의 상태를 갱신하려다 참조를 붙들고 있는다 → 누수
viewModelScope 는 내부적으로 SupervisorJob + Dispatchers.Main.immediate 다
- 자식 하나가 실패해도 형제가 죽지 않는다
상태를 어떻게 노출하나 — stateIn
val uiState: StateFlow<HomeUiState> = repository.observeUsers()
.map { HomeUiState(users = it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000), // 구독 끊기고 5초 뒤 중단
initialValue = HomeUiState(isLoading = true)
)
WhileSubscribed(5_000) 의 5초가 뭘 푸나 화면 회전 중에는 잠깐 구독자가 0 이 된다 즉시 중단하면 회전 직후 네트워크를 다시 탄다
- 5초 유예를 두면 회전은 넘기고, 진짜 떠난 경우는 정리한다
어느 ViewModel 인스턴스를 받나
- by viewModels() — 이 Fragment/Activity 에 묶인 것
- by activityViewModels() — 호스트 Activity 에 묶인 것 (Fragment 끼리 공유)
- by hiltNavGraphViewModels(R.id.flow) 네비게이션 그래프 범위
공유 범위를 넓게 잡으면 화면을 떠나도 상태가 남아 다음에 들어왔을 때 옛 데이터가 보인다