Jetpack Compose Navigation (면접 대비)
Source: Dev.to

About this article: 실제 프로젝트를 직접 리뷰하면서 만든 내용입니다. 기억보다 이해를 목표로 합니다 — 면접관은 언제나 차이를 알아차릴 수 있기 때문이죠.
Screen.Details.route vs Screen.Details.createRoute(name) — 차이점은?
많은 사람들이 헷갈리는데, 두 개가 모두 “경로”처럼 보이기 때문입니다. 사실 그렇지 않습니다.
-
Screen.Details.route는 템플릿입니다 — 자리표시자를 포함합니다:"details/{pokemonName}"이 값을
NavHost안에서 목적지를 등록할 때 사용합니다. 네비게이션 시스템이 경로 형태와 추출할 인자를 알 수 있게 해 줍니다. -
Screen.Details.createRoute(name)은 실제 URL을 만들어 줍니다:"details/Pikachu"이 값을 네비게이션할 때 사용합니다 —
navController.navigate(...)는 실제 데이터가 들어간 주소가 필요합니다.
⚠️ 두 곳 모두 .route를 사용하면 어떻게 될까?
NavHost는 여전히 경로를 매칭하지만, 추출된 인자는 실제 포켓몬 이름이 아니라 문자 그대로 "{pokemonName}"가 됩니다. 앱이 크래시되지는 않지만, 잘못된 데이터가 조용히 전달됩니다.
핵심 규칙 → 등록용은 템플릿, 네비게이션용은 실제 URL.
NavConstants에 경로를 정의해도 등록되지 않는다
NavConstants.kt에 Search와 Settings를 정의했지만 AppNavigation에 나타나지 않았다면, navController.navigate(Screen.Search.route)를 호출했을 때 어떤 일이 일어날까요?
앱이 런타임에 크래시됩니다:
IllegalArgumentException:
"navigation destination 'search' is unknown to this NavController"
왜 사람들이 혼란스러워 할까?
NavConstants.kt는 문자열 사전일 뿐입니다 — 거기에 경로를 정의해도 아무 비용도 들지 않고, 아무 보장도 없습니다.AppNavigation.kt가 실제 목적지가 존재하는 곳입니다 —NavHost안에composable()이 매칭될 때 비로소 경로가 실체를 갖게 됩니다.NavConstants는 오타를 방지해 주지만, 목적지를 등록하지 않은 실수를 막아 주지는 못합니다. 두 실수 모두 앱을 크래시시킵니다.
핵심 규칙 → 문자열 과 composable() 등록, 두 가지가 모두 필요합니다.
nullable = false 와 if (name != null) 은 서로 모순되지 않는다
navArgument("pokemonName") { nullable = false } 를 선언하고 바로 아래에서 ?. 와 if (name != null) 를 사용하는 모습을 보면 모순된 것처럼 보일 수 있습니다. 실제로는 그렇지 않습니다.
두 개는 완전히 다른 레이어에서 작동합니다:
| 레이어 | 역할 |
|---|---|
| Navigation system | 런타임에 네비게이션 인자를 검사합니다. nullable = false 가 위반되면 크래시합니다. |
| Kotlin type system | 컴파일 타임에 타입을 검사합니다. Bundle.getString() 은 언제나 String? 를 반환하므로, 컴파일러는 널 처리를 강제합니다. |
Bundle.getString() 은 네비게이션 계약과 무관하게 항상 String? 를 반환합니다 — Kotlin 컴파일러는 navArgument 규칙을 알지 못합니다. 따라서 if (name != null) 은 컴파일러를 만족시키기 위한 널 체크이며, 런타임에 실제로 널이 올 거라고 기대해서 넣은 것이 아닙니다.
핵심 규칙 → 네비게이션 계약과 Kotlin 타입은 서로 독립된 시스템이며, 서로 대화하지 않습니다.
popBackStack() vs navigate(Home) — 절대 대체하지 말 것
두 메서드 모두 사용자를 Home 화면으로 보이게 할 수 있지만, 백스택에 미치는 영향은 완전히 다릅니다.
-
popBackStack()은 현재 화면을 제거하고 그 아래에 있던 화면을 드러냅니다.Before: [Home, Details] After: [Home] ← Details 가 제거되고 Home 가 드러남 -
navigate(Screen.Home.route)은 새로운 Home 화면을 추가합니다.Before: [Home, Details] After: [Home, Details, Home] ← 새로운 Home 이 추가됨
⚠️ 왜 중요한가? navigate() 후에 뒤로 가기 버튼을 누르면 사용자는 여전히 Details 화면으로 돌아갑니다 — 떠났다고 생각했던 화면이 남아 있는 것이죠. 다시 한 번 누르면 원래 Home 화면이 나오고, 백스택이 조용히 늘어나면서 흐트러진 사용자 경험을 만들게 됩니다.
핵심 규칙 → 뒤로 갈 때는 popBackStack()을, 앞으로 갈 때는 navigate()를 사용합니다. 절대 서로 대체하지 마세요.
언제 backStackEntry 가 실제로 필요할까?
NavHost 안의 composable() 람다에서 매개변수 이름을 backStackEntry 로 지정할 수 있습니다. 일부 컴포저블은 이를 사용하고, 일부는 사용하지 않는데, 이유는 무엇일까요?
- 정적 경로 (
"home"혹은"profile"등) 는 인자를 포함하지 않으므로, 추출할 것이 없어서backStackEntry를 무시하거나 이름을 붙이지 않아도 됩니다. - 동적 경로 (
"details/{pokemonName}") 는 인자를 포함합니다.navController.navigate("details/Pikachu")가 호출되면, 네비게이션 시스템은"Pikachu"를backStackEntry의arguments번들에 저장합니다. 이후backStackEntry.arguments?.getString("pokemonName")로 꺼낼 수 있습니다.
핵심 규칙 → 경로에 인자가 있을 때만 backStackEntry 가 필요합니다.
Compose Navigation 의 세 레이어
세 레이어를 각각 이해하면 나머지는 자동으로 맞물립니다.
🔵 Navigation System Layer
- 런타임에
NavHost/NavController가 강제합니다. - 모든 이동 대상은
composable()로 등록돼 있어야 합니다. 그렇지 않으면 크래시. nullable = false는 여기서 적용되는 계약이며, 위반 시 예외 발생.- Kotlin 타입을 알지 못합니다.
🟣 Kotlin Type System Layer
- 컴파일 타임에 컴파일러가 강제합니다.
Bundle.getString()은 언제나String?를 반환하므로, 널 처리 코드를 강제합니다.- 네비게이션 계약이나
navArgument규칙을 전혀 인식하지 못합니다. - 백스택 상태를 알 수 없습니다.
🟢 Back Stack Layer
- 런타임 상태 — 사용자가 거쳐 온 화면들의 히스토리.
navigate()는 항상 새로운 목적지를 푸시합니다.popBackStack()은 항상 최상위 목적지를 팝합니다.- 시스템 뒤로 가기 버튼도 같은 동작을 합니다.
- 팝되지 않는 한 계속 쌓입니다.
흔히 혼동되는 두 레이어 비교
| 레이어 | 특성 | 예시 |
|---|---|---|
| Kotlin Type System | 컴파일‑타임 규칙 | getString() returns String? |
| Back Stack | 런타임 상태 | [Home, Details] grows with navigate() |
Navigation Hoisting — 화면은 NavController 가 아니라 Lambda 를 받는다
Navigation hoisting 은 화면 컴포저블이 직접 navController 에 접근하는 대신, 네비게이션 동작을 람다 파라미터 로 전달받는 방식을 의미합니다.
// AppNavigation 이 navController 를 소유하고, 액션을 람다로 전달
HomeScreen(
onNavigateToProfile = { navController.navigate(Screen.Profile.route) },
onPokemonClick = { name -> navController.navigate(Screen.Details.createRoute(name)) }
)
// HomeScreen 은 navController 를 전혀 알지 못한다
@Composable
fun HomeScreen(
onNavigateToProfile: () -> Unit,
onPokemonClick: (String) -> Unit
)
왜 이런 패턴을 쓰는가?
- UI
