
rememberSaveable SavedStateHandle 차이는 Compose 상태 보존을 공부할 때 자주 막히는 지점입니다. 둘 다 상태를 살리는 도구처럼 보이지만 책임이 다릅니다.
기준은 Composable 내부 UI 상태는 rememberSaveable, ViewModel이 복원해야 할 최소 키는 SavedStateHandle로 나누어 보는 것입니다.
SavedStateHandle과 rememberSaveable 책임 분리

rememberSaveable은 작은 UI 상태에 가깝다
Android Developers의 Compose state saving 문서는 rememberSaveable이 Bundle에 저장 가능한 값을 보존하는 데 쓰인다고 설명합니다.
var query by rememberSaveable { mutableStateOf("") }검색어, 선택된 탭, 펼침 여부처럼 화면 안에서만 의미가 있는 작은 UI 상태에 잘 맞습니다.
SavedStateHandle은 ViewModel의 최소 복원 값이다
SavedStateHandle 문서는 ViewModel이 saved state에 접근할 수 있게 해주는 API로 설명합니다.
class DetailViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val itemId: String = checkNotNull(savedStateHandle["itemId"])
}전체 객체를 저장하려고 하지 않는다
상세 객체 전체를 저장하기보다 다시 가져올 수 있는 ID를 남기는 편이 안전합니다. 큰 객체는 직렬화, 오래된 데이터, 메모리 문제를 만들 수 있습니다.
정리
rememberSaveable과 SavedStateHandle은 경쟁 관계가 아닙니다. 하나는 composable 내부의 작은 UI 상태, 다른 하나는 ViewModel이 복원해야 할 최소 키에 가깝습니다.

SavedStateHandle과 rememberSaveable을 구분하는 가장 쉬운 질문
둘 중 무엇을 쓸지 헷갈릴 때는 “이 값의 주인이 누구인가”를 먼저 묻는 편이 좋습니다. TextField의 현재 입력값처럼 화면 내부 표현에 가까우면 rememberSaveable이 자연스럽습니다. 반면 화면을 다시 만들 때 필요한 id, filter, selectedTab 같은 값이 ViewModel 로직의 입력이면 SavedStateHandle이 더 잘 맞습니다.
- 스크롤 위치, 임시 입력값, 열림/닫힘 상태: rememberSaveable 후보
- 상세 화면의 itemId, 검색 조건, 탭 id: SavedStateHandle 후보
- 네트워크 응답 전체, 큰 리스트, 이미지 데이터: 둘 다 저장 대상으로 부적합
- 도메인 상태의 원본: Repository, DB, 서버를 우선 고려
process death를 기준으로 보면 더 분명해진다
화면 회전만 생각하면 remember나 ViewModel만으로도 충분해 보일 수 있습니다. 하지만 앱 프로세스가 죽었다가 복원되는 상황을 생각하면 기준이 달라집니다.
rememberSaveable은 가능한 값을 Bundle에 저장해 Composable 상태를 복원합니다. SavedStateHandle은 ViewModel이 새로 만들어질 때 필요한 작은 상태를 제공해서, 다시 데이터를 로드하거나 화면 상태를 재구성할 수 있게 돕습니다.
잘못 나누면 생기는 문제
- rememberSaveable에 큰 객체를 넣으면 Bundle 크기와 직렬화 문제가 생길 수 있다
- SavedStateHandle에 UI 장식 상태까지 넣으면 ViewModel이 화면 세부 표현에 묶인다
- ViewModel에만 값을 두면 process death 이후 필요한 입력값이 사라질 수 있다
- 서버에서 다시 받을 수 있는 데이터를 저장하려 하면 복원 책임이 과해진다
Compose 화면 예시로 보는 분리
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
var queryDraft by rememberSaveable { mutableStateOf("") }
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
SearchTextField(
value = queryDraft,
onValueChange = { queryDraft = it },
onSearch = { viewModel.search(queryDraft) }
)
SearchResultList(uiState.items)
}
class SearchViewModel(
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
private var lastQuery: String
get() = savedStateHandle["lastQuery"] ?: ""
set(value) { savedStateHandle["lastQuery"] = value }
fun search(query: String) {
lastQuery = query
// repository.search(query)
}
}이 예시에서 입력 중인 임시 문자열은 화면 표현에 가깝습니다. 반면 마지막 검색어는 ViewModel이 다시 검색을 재개할 때 필요한 입력값이므로 SavedStateHandle에 남길 수 있습니다.
실무 체크리스트
- 값이 화면 내부 표현인가, 화면 로직의 입력인가
- Bundle에 넣어도 될 만큼 작고 단순한 값인가
- 프로세스가 죽은 뒤에도 반드시 복원되어야 하는가
- 복원보다 재조회가 더 안전한 데이터는 아닌가
- Navigation argument와 중복 저장하고 있지는 않은가
관련해서 같이 보면 좋은 글
Compose Navigation 상태 보존도 함께 보면 이 글의 기준을 더 쉽게 연결할 수 있습니다.
Android SavedStateHandle은 언제 필요할까도 함께 보면 이 글의 기준을 더 쉽게 연결할 수 있습니다.