# Jetpack Navigation Compose 2026: 타입 안전 내비게이션과 면접 질문 > Navigation Compose 2.10의 타입 안전 API 완벽 가이드. Kotlin Serialization 라우트 정의, 중첩 그래프, 딥링크, 면접 빈출 질문까지 상세히 다룹니다. - Published: 2026-09-01 - Updated: 2026-09-01 - Author: Anthony Fillion-Maillet - Tags: android, jetpack-compose, navigation, kotlin, type-safe - Reading time: 12 min --- Jetpack Navigation Compose 2.10은 Kotlin Serialization을 통해 Android 내비게이션에 컴파일 타임 타입 안전성을 도입합니다. 이 접근 방식은 인자 불일치로 인한 런타임 크래시를 제거하고 대규모 코드베이스에서의 리팩토링을 크게 단순화합니다. > **Navigation 2.10 안정 버전** > > Navigation Compose 2.10.0은 2026년 8월에 출시되었으며 최소 SDK 24가 필요합니다. 타입 안전 라우트는 문자열 기반 목적지 대신 `@Serializable` 데이터 클래스를 사용하여 내비게이션 오류를 컴파일 시점에 포착합니다. ## Navigation 2.10에서 타입 안전 내비게이션 설정하기 타입 안전 내비게이션 API에는 Kotlin Serialization이 필요합니다. 모듈 수준 `build.gradle.kts`에 다음 의존성을 추가합니다: ```kotlin // build.gradle.kts plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0" } dependencies { implementation("androidx.navigation:navigation-compose:2.10.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") } ``` 목적지는 직렬화 가능한 타입으로 정의합니다. 인자가 없는 화면에는 `object`를, 파라미터를 받는 화면에는 `data class`를 사용합니다: ```kotlin // Routes.kt import kotlinx.serialization.Serializable @Serializable object Home @Serializable object ProductList @Serializable data class ProductDetail(val productId: String) @Serializable data class Checkout(val cartId: String, val promoCode: String? = null) ``` 컴파일러는 모든 라우트 클래스에 `@Serializable` 어노테이션이 있는지 확인합니다. Navigation 2.9 이상에서는 어노테이션이 누락되면 린트 오류가 발생합니다. ## 직렬화 가능 라우트로 NavHost 구축하기 `NavHost` 컴포저블은 직렬화 가능한 타입을 직접 받습니다. 각 `composable` 블록은 특정 목적지 타입을 처리합니다: ```kotlin // AppNavigation.kt @Composable fun AppNavigation( navController: NavHostController = rememberNavController() ) { NavHost( navController = navController, startDestination = Home ) { composable { HomeScreen( onNavigateToProducts = { navController.navigate(ProductList) } ) } composable { ProductListScreen( onProductClick = { productId -> navController.navigate(ProductDetail(productId)) } ) } composable { backStackEntry -> // 백스택 엔트리에서 타입이 지정된 라우트 추출 val route: ProductDetail = backStackEntry.toRoute() ProductDetailScreen( productId = route.productId, onCheckout = { cartId -> navController.navigate(Checkout(cartId)) } ) } composable { backStackEntry -> val route: Checkout = backStackEntry.toRoute() CheckoutScreen( cartId = route.cartId, promoCode = route.promoCode ) } } } ``` `toRoute()` 확장 함수는 목적지의 완전히 타입이 지정된 인스턴스를 반환합니다. 데이터 클래스의 프로퍼티 이름을 변경하면 IDE 리팩토링을 통해 모든 사용처가 자동으로 업데이트됩니다. ## 기능 모듈을 위한 중첩 내비게이션 그래프 대규모 애플리케이션에서는 중첩 내비게이션 그래프가 유용합니다. 각 기능 모듈은 자체 그래프를 정의하고 루트 그래프가 이들을 구성합니다: ```kotlin // AuthNavigation.kt @Serializable object AuthGraph // 그래프 식별자 @Serializable object Login @Serializable object Register @Serializable data class PasswordReset(val email: String) fun NavGraphBuilder.authNavGraph(navController: NavHostController) { navigation(startDestination = Login) { composable { LoginScreen( onRegisterClick = { navController.navigate(Register) }, onForgotPassword = { email -> navController.navigate(PasswordReset(email)) }, onLoginSuccess = { navController.navigate(Home) { popUpTo { inclusive = true } } } ) } composable { RegisterScreen( onRegistrationComplete = { navController.navigate(Home) { popUpTo { inclusive = true } } } ) } composable { backStackEntry -> val route: PasswordReset = backStackEntry.toRoute() PasswordResetScreen(email = route.email) } } } ``` `popUpTo`와 `inclusive = true`를 조합하면 로그인 성공 후 전체 인증 플로우가 백스택에서 제거되어 사용자가 로그인 화면으로 돌아가는 것을 방지합니다. ## 라우트 인자로서의 Value Class Navigation 2.9.0-alpha03에서 라우트의 [value class](https://kotlinlang.org/docs/inline-classes.html) 지원이 추가되었습니다. 이 패턴은 ID에 대한 타입 안전성을 강제하고 동일한 원시 타입의 파라미터를 혼동하는 것을 방지합니다: ```kotlin // Domain.kt @JvmInline @Serializable value class ProductId(val value: String) @JvmInline @Serializable value class UserId(val value: String) @Serializable data class ProductDetail(val productId: ProductId) @Serializable data class UserProfile(val userId: UserId) ``` 이제 컴파일러는 `ProductId`가 예상되는 곳에 `UserId`를 전달하는 것을 방지합니다. 이는 문자열 기반 내비게이션에서 흔히 발생하는 버그입니다. ## 타입 안전 라우트를 사용한 딥링크 딥링크는 외부 URI를 직렬화 가능한 라우트에 매핑합니다. 패턴은 `composable` 블록에서 정의합니다: ```kotlin // AppNavigation.kt composable( deepLinks = listOf( navDeepLink { uriPattern = "https://example.com/products/{productId}" } ) ) { backStackEntry -> val route: ProductDetail = backStackEntry.toRoute() ProductDetailScreen(productId = route.productId) } ``` `{productId}` 플레이스홀더는 `ProductDetail` 데이터 클래스의 `productId` 프로퍼티에 직접 매핑됩니다. Navigation은 인자를 자동으로 추출하고 역직렬화합니다. `AndroidManifest.xml`에 딥링크를 등록합니다: ```xml ``` ## Navigation Compose의 예측 뒤로 가기 제스처 Android 14 이상에서는 사용자가 커밋 전에 이전 화면을 미리 볼 수 있는 예측 뒤로 가기 제스처를 지원합니다. Navigation Compose 2.10은 `SeekableTransitionState`를 통해 이를 통합합니다: ```kotlin // AppNavigation.kt NavHost( navController = navController, startDestination = Home, enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start) }, exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start) }, popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End) }, popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End) } ) { // destinations } ``` 예측 뒤로 가기 제스처는 사용자가 스와이프할 때 이전 목적지를 실시간으로 보여줍니다. 임계값에 도달하기 전에 제스처를 놓으면 내비게이션이 취소되고 현재 화면으로 돌아갑니다. ## Navigation Compose 면접 빈출 질문 시니어 Android 면접에서는 내비게이션 아키텍처 결정에 대해 자주 심층적으로 질문합니다. 다음은 경험 있는 지원자를 구별하는 질문들입니다. ### 왜 문자열 기반 라우트 대신 타입 안전 내비게이션을 사용합니까? `"product/{productId}"`와 같은 문자열 기반 라우트는 인자의 철자 오류나 잘못된 타입으로 인해 런타임 크래시를 유발합니다. 타입 안전 라우트는 이러한 오류를 컴파일 시점에 포착합니다. 라우트 파라미터 리팩토링은 IDE 도구를 통해 모든 사용처를 업데이트하지만, 문자열 기반 라우트는 코드베이스 전체에서 수동 찾기-바꾸기가 필요합니다. ### 큰 객체를 전달하지 않고 목적지 간에 데이터를 공유하려면 어떻게 해야 합니까? > **큰 라우트 인자 피하기** > > 라우트는 저장된 인스턴스 상태로 직렬화됩니다. 상품 카탈로그 같은 큰 객체를 전달하면 `TransactionTooLargeException` 위험이 있습니다. ID를 전달하고 리포지토리나 공유 ViewModel에서 데이터를 로드하는 방식을 사용합니다. 데이터 공유 패턴에는 스코프가 지정된 ViewModel을 사용합니다: ```kotlin // ProductViewModel.kt @HiltViewModel class ProductViewModel @Inject constructor( private val repository: ProductRepository, savedStateHandle: SavedStateHandle ) : ViewModel() { // SavedStateHandle에서 타입이 지정된 라우트 추출 private val productId: String = savedStateHandle.toRoute().productId val product = repository.getProduct(productId) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) } ``` ### Compose에서 내비게이션을 테스트하려면 어떻게 해야 합니까? 내비게이션 테스트는 사용자 액션이 올바른 목적지 변경을 트리거하는지 확인합니다: ```kotlin // NavigationTest.kt class NavigationTest { @get:Rule val composeTestRule = createComposeRule() @Test fun clickingProduct_navigatesToDetail() { val navController = TestNavHostController(ApplicationProvider.getApplicationContext()) navController.navigatorProvider.addNavigator(ComposeNavigator()) composeTestRule.setContent { AppNavigation(navController = navController) } // 상품 목록으로 이동 composeTestRule.onNodeWithText("View Products").performClick() // 첫 번째 상품 클릭 composeTestRule.onNodeWithTag("product_item_0").performClick() // 현재 목적지 확인 val currentRoute = navController.currentBackStackEntry?.toRoute() assertThat(currentRoute).isNotNull() } } ``` ### NavController가 파괴된 후 사용되면 어떻게 됩니까? Navigation 2.9.0-alpha01에서 이 동작이 변경되었습니다. 호스팅하는 `Activity`나 `Fragment`가 파괴된 후 `NavController`를 사용하면 이제 `IllegalStateException`이 발생합니다. 이전 버전에서는 조용히 실패하거나 정의되지 않은 동작을 보였습니다. 이 변경으로 라이프사이클 버그가 개발 초기에 표면화됩니다. ### 인증 상태에 따른 조건부 내비게이션을 어떻게 처리합니까? 일반적인 패턴은 인증 상태를 관찰하고 그에 따라 리다이렉트합니다: ```kotlin // MainScreen.kt @Composable fun MainScreen( authState: AuthState, navController: NavHostController = rememberNavController() ) { LaunchedEffect(authState) { when (authState) { is AuthState.Authenticated -> { navController.navigate(Home) { popUpTo(0) { inclusive = true } } } is AuthState.Unauthenticated -> { navController.navigate(AuthGraph) { popUpTo(0) { inclusive = true } } } else -> { /* loading, do nothing */ } } } NavHost( navController = navController, startDestination = if (authState is AuthState.Authenticated) Home else AuthGraph ) { authNavGraph(navController) composable { HomeScreen() } } } ``` `LaunchedEffect`는 인증 상태 변경에 반응하고 `popUpTo(0) { inclusive = true }`로 전체 백스택을 비워 잘못된 플로우로의 뒤로 가기 내비게이션을 방지합니다. ## 문자열 라우트에서 타입 안전 내비게이션으로 마이그레이션 기존 프로젝트는 점진적으로 마이그레이션할 수 있습니다. 문자열 라우트와 타입 안전 라우트는 동일한 `NavHost` 내에서 공존할 수 있습니다: ```kotlin // AppNavigation.kt NavHost( navController = navController, startDestination = "home" // 레거시 문자열 라우트 ) { // 레거시 문자열 기반 목적지 composable("home") { HomeScreen(onNavigateToProduct = { id -> navController.navigate(ProductDetail(id)) // 새로운 타입 안전 내비게이션 }) } // 새로운 타입 안전 목적지 composable { backStackEntry -> val route: ProductDetail = backStackEntry.toRoute() ProductDetailScreen(productId = route.productId) } } ``` 이 접근 방식을 통해 전체 내비게이션 그래프를 다시 작성하지 않고 한 번에 한 화면씩 마이그레이션할 수 있습니다. ## 2026년 Jetpack Navigation Compose 핵심 요약 - Navigation 2.10.0은 최소 SDK 24를 요구하는 현재 안정 버전입니다 - 타입 안전 라우트는 문자열 목적지 대신 `@Serializable` 데이터 클래스를 사용합니다 - Value class는 ID에 추가 타입 안전성을 제공하고 파라미터 혼동을 방지합니다 - 중첩 내비게이션 그래프는 독립적인 백스택으로 기능 모듈을 구성합니다 - 딥링크는 URI 패턴을 라우트 프로퍼티에 직접 매핑합니다 - 예측 뒤로 가기 제스처는 표준 트랜지션과 자동으로 통합됩니다 - 라우트 인자는 가볍게 유지해야 합니다: ID를 전달하고 리포지토리에서 데이터를 로드합니다 - 문자열 라우트에서의 마이그레이션은 혼합 내비게이션 그래프로 점진적으로 수행할 수 있습니다 - [Android 면접 준비](/technologies/android)에서 이러한 패턴을 심층적으로 다룹니다 - 관련 UI 개념은 [Jetpack Compose 면접 질문](/blog/android/jetpack-compose-interview-questions)도 참조하세요 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/android/jetpack-navigation-compose-type-safe-2026