[Android] Navigation3

MariGold·2026년 1월 20일

[Android]

목록 보기
21/21

Jetpack Compose로 UI를 구현하다 보면 화면 전환을 위해 navigation-compose를 거의 필수적으로 사용하게 됩니다. 하지만 Navigation2는 Compose와 함께 쓰기에는 다소 아쉬운 점들이 있었고, 복잡한 앱일수록 구조 관리가 쉽지 않았습니다.

이런 문제를 해결하기 위해 Jetpack Compose Navigation3 (이하 Navigation3) 가 등장했습니다. 이번 글에서는 Navigation3가 기존 Navigation2에 비해 무엇이 좋아졌는지, 그리고 기본적인 사용 방법을 정리해 보려고 합니다.


🚧 Navigation2의 한계

Navigation2는 XML 기반 Navigation 개념을 Compose로 가져온 형태에 가까웠습니다. 그래서 다음과 같은 불편함이 있었습니다.

1. NavController 중심의 명령형 API

navController.navigate("detail/123")
  • 문자열 기반 route
  • 컴파일 타임 안전성 부족
  • 파라미터 실수 → 런타임 에러

2. 화면 상태와 네비게이션 상태의 분리

  • ViewModel의 상태와 Navigation 상태가 따로 놀기 쉬움
  • “이 화면이 왜 열렸지?”를 추적하기 어려움

3. 멀티 백스택, 복잡한 그래프 구성의 어려

  • BottomNavigation + Nested graph 구성 시 코드 가독성 급격히 하락
  • 테스트하기도 까다로움

🚀 Navigation3란?

Navigation 3는 Compose의 상태 중심(State-driven) 패러다임에 맞춰 완전히 새롭게 설계된 네비게이션 API입니다.

  • 안전한 타입 모델
  • NavController 의존도 감소
  • Navigation 상태를 UI State처럼 다룸
  • 테스트와 구조화에 훨씬 유리

🌱 Navigation3의 핵심 개념

1. 커스텀 타입 모델

Navigation 3에서는 문자열 route 대신 타입으로 화면을 정의합니다.

sealed interface Screen {
    data object Home : Screen
    data class Detail(val id: Long) : Screen
}
  • 컴파일 타임 안전성
  • 파라미터 실수 방지
  • IDE 자동완성 지원

2. Navigation 상태를 직접 관리

Navigation3에서는 화면 스택 자체를 상태(State) 로 관리합니다.

val backStack = rememberNavBackStack(Screen.Home)

backStack += Screen.Detail(id = 1L)

더 이상 navController.navigate()에 의존하지 않아도 됩니다.

3. NavHost → NavDisplay

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)
                  }
  				)
            }
        }
	)
}
  • Compose다운 선언적 구조
  • 화면 흐름을 한눈에 파악 가능

🧩 NavEntryDecorator란?

Navigation3에서는 화면 하나하나를 NavEntry라는 단위로 다룹니다. 그리고 각 NavEntry에 대해 어떤 부가 동작을 적용할지를 결정하는 것이 NavEntryDecorator입니다. 대표적으로 많이 쓰이는 것이 바로 아래 두 가지입니다.

rememberSaveableStateHolderNavEntryDecorator()
rememberViewModelStoreNavEntryDecorator()

이 둘은 화면 상태와 ViewModel 생명주기 관리를 담당합니다.

1. rememberSaveableStateHolderNavEntryDecorator

각 NavEntry마다 Saveable 상태를 분리해서 유지해 줍니다. 즉, 화면 A → 화면 B → 다시 A의 흐름대로 화면이 이동할 때, A의 rememberSaveable 상태가 유지됩니다. 만약 사용하지 않으면, 화면 이동 시 Composable이 dispose되어 다시 돌아오면 상태가 초기화됩니다. 입력 폼, 스크롤 위치, 탭 내부 상태 등을 유지해야할 때 사용합니다.

2. rememberViewModelStoreNavEntryDecorator

각 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에 맞는 네비게이션 패러다임으로의 전환이라고 볼 수 있습니다.

  • Navigation 2 → 명령형, 문자열 기반
  • Navigation 3 → 선언적, 상태 기반, 타입 안전

Compose가 상태 중심 UI인 만큼, 화면 이동 또한 상태로 관리하는 Navigation 3의 방향성은 매우 자연스럽습니다. 아직은 실험 단계지만, 앞으로 Compose Navigation의 표준이 될 가능성이 높은 만큼 지금부터 개념 정도는 익혀두면 충분히 가치가 있다고 생각합니다.

profile
많은 것을 알아가고 싶은 Android 주니어 개발자

0개의 댓글