ViewModel이 프로세스 종료까지 상태를 넘기게 해 주는 Bundle 기반 저장소.
두 종류의 죽음
-
① 구성 변경 (회전 · 다크모드 전환 · 폰트 크기 변경)
- Activity 는 죽고 ViewModel 은 산다
-
② 프로세스 종료 (백그라운드에서 시스템이 메모리를 회수)
- Activity 도 ViewModel 도 죽는다
- 사용자는 앱이 죽은 줄 모른다 — 돌아오면 그 화면이 복원돼야 한다
구성 변경 프로세스 종료
remember ✗ ✗
rememberSaveable ✓ ✓
ViewModel ✓ ✗
ViewModel + SavedStateHandle ✓ ✓
쓰는 법
@HiltViewModel
class SearchViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: SearchRepository
) : ViewModel() {
val query: StateFlow<String> = savedStateHandle.getStateFlow(KEY_QUERY, "")
val results: StateFlow<List<Item>> = query
.debounce(300)
.flatMapLatest { repository.search(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
fun onQueryChange(q: String) { savedStateHandle[KEY_QUERY] = q }
private companion object { const val KEY_QUERY = "query" }
}
네비게이션 인자도 여기로 들어온다
// route = "detail/{itemId}" 로 이동했다면
val itemId: Long = checkNotNull(savedStateHandle["itemId"])
Fragment의 arguments를 ViewModel에서 직접 읽을 필요가 없어진다 — 안드로이드 의존이 하나 줄어든다.
크기 제한이 실재한다
Bundle 은 Binder 트랜잭션으로 전달된다
-
전체 트랜잭션 버퍼가 약 1MB
-
넘으면 TransactionTooLargeException 으로 앱이 죽는다
-
넣어도 되는 것 — 검색어 · 선택된 탭 인덱스 · 스크롤 위치 · 화면 인자 ID 넣으면 안 되는 것 목록 데이터 · 비트맵 · 큰 JSON
큰 데이터는 ID 만 저장하고 복원 시 DB·네트워크에서 다시 가져온다
어떻게 테스트하나
@Test fun `저장된 검색어로 복원된다`() = runTest {
val handle = SavedStateHandle(mapOf("query" to "안드로이드"))
val vm = SearchViewModel(handle, FakeRepository())
assertEquals("안드로이드", vm.query.value)
}
개발 중에 어떻게 재현하나
개발자 옵션 → "활동 유지 안 함" 을 켠다
- 또는 — adb shell am kill <package> (백그라운드로 보낸 뒤)
에뮬레이터에서 회전만 시켜 보고 "복원 잘 된다" 고 결론 내리는 것이 가장 흔한 검증 누락이다 — 회전은 ①만 검증한다
면접 함정
- ❌ "ViewModel이 있으니 상태 저장은 필요 없다" → 프로세스 종료를 못 넘는다.
- ❌ "rememberSaveable이면 다 된다" →
Bundle직렬화가 가능한 타입만이고 크기 제한도 같다.