Jetpack Compose Navigation (면접 대비)

발행: (2026년 6월 11일 AM 02:58 GMT+9)
9 분 소요
원문: Dev.to

Source: Dev.to

Cover image for Jetpack Compose Navigation(Interview Prep)

Aalaa Fahiem

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.ktSearchSettings를 정의했지만 AppNavigation에 나타나지 않았다면, navController.navigate(Screen.Search.route)를 호출했을 때 어떤 일이 일어날까요?

앱이 런타임에 크래시됩니다:

IllegalArgumentException:
"navigation destination 'search' is unknown to this NavController"

왜 사람들이 혼란스러워 할까?

  • NavConstants.kt문자열 사전일 뿐입니다 — 거기에 경로를 정의해도 아무 비용도 들지 않고, 아무 보장도 없습니다.
  • AppNavigation.kt가 실제 목적지가 존재하는 곳입니다 — NavHost 안에 composable()이 매칭될 때 비로소 경로가 실체를 갖게 됩니다.
  • NavConstants는 오타를 방지해 주지만, 목적지를 등록하지 않은 실수를 막아 주지는 못합니다. 두 실수 모두 앱을 크래시시킵니다.

핵심 규칙 → 문자열 composable() 등록, 두 가지가 모두 필요합니다.


nullable = falseif (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"backStackEntryarguments 번들에 저장합니다. 이후 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 에 접근하는 대신, 네비게이션 동작을 람다 파라미터 로 전달받는 방식을 의미합니다.

// 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
0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...