
Compose Navigation을 쓰다 보면 화면 이동은 되는데 상태가 어디에 남는지 헷갈릴 때가 많습니다. route, argument, back stack, SavedStateHandle이 모두 비슷한 일을 하는 것처럼 보이기 때문입니다.
기준은 단순합니다. 화면 이동 정보와 화면 상태를 같은 곳에 넣지 않는 것입니다. Navigation은 이동 흐름을 다루고, 화면 상태는 ViewModel과 SavedStateHandle 경계에서 따로 봐야 합니다.

Android Developers의 Compose Navigation 문서는 composable destination과 route를 사용해 앱 안의 화면 이동을 구성하는 방법을 설명합니다. 이 말은 Navigation의 중심 책임이 ‘어디로 이동할지’라는 뜻입니다.
따라서 장바구니 목록, 입력 중인 폼 전체, 서버에서 받은 상세 데이터 같은 것을 route argument에 넣으려 하면 구조가 흔들립니다. route에는 화면을 다시 찾기 위한 최소 식별자만 두는 편이 안전합니다.
route와 argument는 화면 진입 계약이다
route는 화면 이름에 가깝고, argument는 그 화면을 열기 위한 최소 입력값입니다. 예를 들어 상세 화면에는 전체 게시글 객체가 아니라 postId처럼 다시 조회 가능한 식별자를 넘기는 편이 좋습니다.
composable("post/{postId}") { backStackEntry ->
val postId = backStackEntry.arguments?.getString("postId")
PostScreen(postId = postId)
}이 구조는 process death 이후에도 복원 가능성이 높습니다. 복잡한 객체를 직접 넘기는 방식은 직렬화, 버전 변경, 메모리 부담 문제를 만들 수 있습니다.
back stack은 화면 이력이지 화면 데이터 전체가 아니다
Navigation back stack은 사용자가 어떤 화면들을 거쳐왔는지 관리합니다. 뒤로 가기를 눌렀을 때 이전 destination으로 돌아갈 수 있는 이유가 여기에 있습니다.
하지만 back stack이 있다고 해서 모든 화면 데이터가 영원히 안전하게 보존되는 것은 아닙니다. 메모리 압박, 구성 변경, process death까지 고려하면 화면 데이터는 ViewModel, rememberSaveable, SavedStateHandle 같은 도구와 나누어 봐야 합니다.
SavedStateHandle은 최소 복원 값을 맡긴다
Android Developers의 SavedStateHandle 문서는 ViewModel이 saved state에 접근할 수 있게 해주는 API로 설명합니다. Navigation argument도 ViewModel의 SavedStateHandle에서 읽을 수 있습니다.
class PostViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val postId: String = checkNotNull(savedStateHandle["postId"])
}중요한 점은 SavedStateHandle도 모든 것을 넣는 창고가 아니라는 것입니다. 다시 만들 수 있는 화면 상태, 화면 진입 식별자, 사용자가 잃으면 곤란한 작은 값 정도를 맡기는 편이 좋습니다.
rememberSaveable과 ViewModel은 어디에 둘까
remember는 composition 동안만 값을 기억합니다. rememberSaveable은 구성 변경이나 일부 재생성 상황에서 복원 가능한 값을 저장하는 데 도움을 줍니다. 반면 ViewModel은 화면 단위 상태와 비즈니스 로직에 더 잘 맞습니다.
- UI 토글처럼 화면 내부 임시 값은 remember 또는 rememberSaveable을 검토한다.
- 서버 데이터, 로딩 상태, 에러 상태는 ViewModel이 맡는 편이 자연스럽다.
- 화면을 다시 열기 위한 ID는 Navigation argument와 SavedStateHandle에 둔다.
- 큰 객체나 오래된 캐시는 route argument에 넣지 않는다.
process death까지 생각하면 기준이 더 분명해진다
화면 회전만 생각하면 rememberSaveable로 충분해 보일 수 있습니다. 하지만 앱 프로세스가 죽었다가 다시 살아나는 상황까지 생각하면, 상태를 다시 만들기 위한 최소 정보가 어디에 남아야 하는지 점검해야 합니다.
예를 들어 상세 화면의 본문 전체가 아니라 postId가 남아 있으면 다시 repository에서 데이터를 가져올 수 있습니다. 이 기준은 Android SavedStateHandle 글에서 다룬 화면 상태와 비즈니스 상태 구분과도 이어집니다.
실무 예시: 목록에서 상세로 이동할 때 상태를 나누는 법
가장 흔한 실수는 목록 화면에서 선택한 전체 객체를 상세 화면으로 넘기려는 것입니다. 처음에는 편해 보이지만, 객체 구조가 바뀌거나 process death 이후 복원해야 할 때 문제가 커집니다.
// 좋은 방향: route에는 다시 조회 가능한 ID만 둔다.
navController.navigate("post/$postId")
composable("post/{postId}") { entry ->
val viewModel: PostViewModel = hiltViewModel()
PostRoute(viewModel = viewModel)
}
class PostViewModel(
savedStateHandle: SavedStateHandle,
private val repository: PostRepository
) : ViewModel() {
private val postId: String = checkNotNull(savedStateHandle["postId"])
val uiState = repository.observePost(postId)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), PostUiState.Loading)
}이 구조에서는 Navigation이 화면 진입 계약만 맡고, 데이터 조회와 화면 상태는 ViewModel이 맡습니다. 화면을 다시 만들 때도 postId만 남아 있으면 repository에서 상태를 복원할 수 있습니다.
실패 사례: back stack에 기대면 놓치는 것들
- 뒤로 가기에는 잘 동작하지만, 앱 프로세스가 죽었다가 복원될 때 필요한 ID가 남지 않는다.
- argument에 큰 객체를 넣어 화면 이동은 되지만 직렬화와 버전 변경에 약해진다.
- remember에만 의존해 화면 회전 후에는 괜찮지만 process death 후에는 상태가 사라진다.
- Navigation graph가 데이터 소유권까지 가진 것처럼 커져서 테스트가 어려워진다.
판단 체크리스트
- 이 값이 화면을 다시 열기 위한 식별자인가, 화면 안에서만 쓰는 임시 상태인가?
- 이 값이 없어져도 서버나 DB에서 다시 만들 수 있는가?
- 사용자가 입력 중인 값처럼 잃으면 곤란한 상태인가?
- Navigation argument, SavedStateHandle, ViewModel 중 어느 층이 가장 작은 책임으로 처리할 수 있는가?
정리
Compose Navigation 상태 보존은 Navigation 하나로 끝나는 주제가 아닙니다. route와 argument는 화면 진입 계약, back stack은 이동 이력, SavedStateHandle은 복원 가능한 최소 값, ViewModel은 화면 상태와 로직을 맡습니다. 이 경계를 나누면 화면 이동 후 상태가 사라지는 문제를 훨씬 차분하게 추적할 수 있습니다.