Jetpack Compose로 UI를 구현하다 보면 화면 전환을 위해 navigation-compose를 거의 필수적으로 사용하게 됩니다. 하지만 Navigation2는 Compose와 함께 쓰기에는 다소 아쉬운 점들이 있었고, 복잡한 앱일수록 구조 관리가 쉽지 않았습니다.
이런 문제를 해결하기 위해 Jetpack Compose Navigation3 (이하 Navigation3) 가 등장했습니다. 이번 글에서는 Navigation3가 기존 Navigation2에 비해 무엇이 좋아졌는지, 그리고 기본적인 사용 방법을 정리해 보려고 합니다.
Navigation2는 XML 기반 Navigation 개념을 Compose로 가져온 형태에 가까웠습니다. 그래서 다음과 같은 불편함이 있었습니다.
navController.navigate("detail/123")
Navigation 3는 Compose의 상태 중심(State-driven) 패러다임에 맞춰 완전히 새롭게 설계된 네비게이션 API입니다.
Navigation 3에서는 문자열 route 대신 타입으로 화면을 정의합니다.
sealed interface Screen {
data object Home : Screen
data class Detail(val id: Long) : Screen
}
Navigation3에서는 화면 스택 자체를 상태(State) 로 관리합니다.
val backStack = rememberNavBackStack(Screen.Home)
backStack += Screen.Detail(id = 1L)
더 이상 navController.navigate()에 의존하지 않아도 됩니다.
Navigation3에서는 NavHost 대신 NavDisplay를 사용합니다.
@Composable
fun App() {
val backStack = rememberNavBackStack(Screen.Home)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() }
) { screen ->
when (screen) {
is Screen.Home -> HomeScreen(
onClick = {
backStack += Screen.Detail(1L)
}
)
is Screen.Detail -> DetailScreen(screen.id)
}
}
}
또는 when문이 아닌 entryProvider를 사용할 수도 있습니다.
@Composable
fun App() {
val backStack = rememberNavBackStack(Screen.Home)
NavDisplay(
backStack = backStack,
entryProvider = entryProvider {
entry<Screen.Home> {
HomeScreen(
onClick = {
backStack += Screen.Detail(1L)
}
)
}
}
)
}
Navigation3에서는 화면 하나하나를 NavEntry라는 단위로 다룹니다. 그리고 각 NavEntry에 대해 어떤 부가 동작을 적용할지를 결정하는 것이 NavEntryDecorator입니다. 대표적으로 많이 쓰이는 것이 바로 아래 두 가지입니다.
rememberSaveableStateHolderNavEntryDecorator()
rememberViewModelStoreNavEntryDecorator()
이 둘은 화면 상태와 ViewModel 생명주기 관리를 담당합니다.
각 NavEntry마다 Saveable 상태를 분리해서 유지해 줍니다. 즉, 화면 A → 화면 B → 다시 A의 흐름대로 화면이 이동할 때, A의 rememberSaveable 상태가 유지됩니다. 만약 사용하지 않으면, 화면 이동 시 Composable이 dispose되어 다시 돌아오면 상태가 초기화됩니다. 입력 폼, 스크롤 위치, 탭 내부 상태 등을 유지해야할 때 사용합니다.
각 NavEntry마다 독립적인 ViewModelStore를 제공합니다. 즉, 화면마다 ViewModel 생명주기가 명확히 분리되어 backStack에서 화면이 제거되면 ViewModel도 함께 clear됩니다.
실제 사용 시에는 둘을 함께 사용하는 것이 거의 표준입니다.
NavDisplay(
backStack = backStack,
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Home> {
HomeScreen()
}
entry<Screen.Detail> {
DetailScreen()
}
}
)
Navigation 3는 단순한 API 개선이 아니라 Compose에 맞는 네비게이션 패러다임으로의 전환이라고 볼 수 있습니다.
Compose가 상태 중심 UI인 만큼, 화면 이동 또한 상태로 관리하는 Navigation 3의 방향성은 매우 자연스럽습니다. 아직은 실험 단계지만, 앞으로 Compose Navigation의 표준이 될 가능성이 높은 만큼 지금부터 개념 정도는 익혀두면 충분히 가치가 있다고 생각합니다.