Jetpack Navigation Compose 2026: 타입 안전 내비게이션과 면접 질문
Navigation Compose 2.10의 타입 안전 API 완벽 가이드. Kotlin Serialization 라우트 정의, 중첩 그래프, 딥링크, 면접 빈출 질문까지 상세히 다룹니다.

Jetpack Navigation Compose 2.10은 Kotlin Serialization을 통해 Android 내비게이션에 컴파일 타임 타입 안전성을 도입합니다. 이 접근 방식은 인자 불일치로 인한 런타임 크래시를 제거하고 대규모 코드베이스에서의 리팩토링을 크게 단순화합니다.
Navigation Compose 2.10.0은 2026년 8월에 출시되었으며 최소 SDK 24가 필요합니다. 타입 안전 라우트는 문자열 기반 목적지 대신 @Serializable 데이터 클래스를 사용하여 내비게이션 오류를 컴파일 시점에 포착합니다.
Navigation 2.10에서 타입 안전 내비게이션 설정하기
타입 안전 내비게이션 API에는 Kotlin Serialization이 필요합니다. 모듈 수준 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를 사용합니다:
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<T> 블록은 특정 목적지 타입을 처리합니다:
@Composable
fun AppNavigation(
navController: NavHostController = rememberNavController()
) {
NavHost(
navController = navController,
startDestination = Home
) {
composable<Home> {
HomeScreen(
onNavigateToProducts = { navController.navigate(ProductList) }
)
}
composable<ProductList> {
ProductListScreen(
onProductClick = { productId ->
navController.navigate(ProductDetail(productId))
}
)
}
composable<ProductDetail> { backStackEntry ->
// 백스택 엔트리에서 타입이 지정된 라우트 추출
val route: ProductDetail = backStackEntry.toRoute()
ProductDetailScreen(
productId = route.productId,
onCheckout = { cartId ->
navController.navigate(Checkout(cartId))
}
)
}
composable<Checkout> { backStackEntry ->
val route: Checkout = backStackEntry.toRoute()
CheckoutScreen(
cartId = route.cartId,
promoCode = route.promoCode
)
}
}
}toRoute<T>() 확장 함수는 목적지의 완전히 타입이 지정된 인스턴스를 반환합니다. 데이터 클래스의 프로퍼티 이름을 변경하면 IDE 리팩토링을 통해 모든 사용처가 자동으로 업데이트됩니다.
기능 모듈을 위한 중첩 내비게이션 그래프
대규모 애플리케이션에서는 중첩 내비게이션 그래프가 유용합니다. 각 기능 모듈은 자체 그래프를 정의하고 루트 그래프가 이들을 구성합니다:
@Serializable
object AuthGraph // 그래프 식별자
@Serializable
object Login
@Serializable
object Register
@Serializable
data class PasswordReset(val email: String)
fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
navigation<AuthGraph>(startDestination = Login) {
composable<Login> {
LoginScreen(
onRegisterClick = { navController.navigate(Register) },
onForgotPassword = { email ->
navController.navigate(PasswordReset(email))
},
onLoginSuccess = {
navController.navigate(Home) {
popUpTo<AuthGraph> { inclusive = true }
}
}
)
}
composable<Register> {
RegisterScreen(
onRegistrationComplete = {
navController.navigate(Home) {
popUpTo<AuthGraph> { inclusive = true }
}
}
)
}
composable<PasswordReset> { backStackEntry ->
val route: PasswordReset = backStackEntry.toRoute()
PasswordResetScreen(email = route.email)
}
}
}popUpTo<AuthGraph>와 inclusive = true를 조합하면 로그인 성공 후 전체 인증 플로우가 백스택에서 제거되어 사용자가 로그인 화면으로 돌아가는 것을 방지합니다.
Android 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
라우트 인자로서의 Value Class
Navigation 2.9.0-alpha03에서 라우트의 value class 지원이 추가되었습니다. 이 패턴은 ID에 대한 타입 안전성을 강제하고 동일한 원시 타입의 파라미터를 혼동하는 것을 방지합니다:
@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 블록에서 정의합니다:
composable<ProductDetail>(
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에 딥링크를 등록합니다:
<!-- AndroidManifest.xml -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="example.com"
android:pathPrefix="/products" />
</intent-filter>
</activity>Navigation Compose의 예측 뒤로 가기 제스처
Android 14 이상에서는 사용자가 커밋 전에 이전 화면을 미리 볼 수 있는 예측 뒤로 가기 제스처를 지원합니다. Navigation Compose 2.10은 SeekableTransitionState를 통해 이를 통합합니다:
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을 사용합니다:
@HiltViewModel
class ProductViewModel @Inject constructor(
private val repository: ProductRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
// SavedStateHandle에서 타입이 지정된 라우트 추출
private val productId: String = savedStateHandle.toRoute<ProductDetail>().productId
val product = repository.getProduct(productId)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
}Compose에서 내비게이션을 테스트하려면 어떻게 해야 합니까?
내비게이션 테스트는 사용자 액션이 올바른 목적지 변경을 트리거하는지 확인합니다:
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<ProductDetail>()
assertThat(currentRoute).isNotNull()
}
}NavController가 파괴된 후 사용되면 어떻게 됩니까?
Navigation 2.9.0-alpha01에서 이 동작이 변경되었습니다. 호스팅하는 Activity나 Fragment가 파괴된 후 NavController를 사용하면 이제 IllegalStateException이 발생합니다. 이전 버전에서는 조용히 실패하거나 정의되지 않은 동작을 보였습니다. 이 변경으로 라이프사이클 버그가 개발 초기에 표면화됩니다.
인증 상태에 따른 조건부 내비게이션을 어떻게 처리합니까?
일반적인 패턴은 인증 상태를 관찰하고 그에 따라 리다이렉트합니다:
@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<Home> { HomeScreen() }
}
}LaunchedEffect는 인증 상태 변경에 반응하고 popUpTo(0) { inclusive = true }로 전체 백스택을 비워 잘못된 플로우로의 뒤로 가기 내비게이션을 방지합니다.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
문자열 라우트에서 타입 안전 내비게이션으로 마이그레이션
기존 프로젝트는 점진적으로 마이그레이션할 수 있습니다. 문자열 라우트와 타입 안전 라우트는 동일한 NavHost 내에서 공존할 수 있습니다:
NavHost(
navController = navController,
startDestination = "home" // 레거시 문자열 라우트
) {
// 레거시 문자열 기반 목적지
composable("home") {
HomeScreen(onNavigateToProduct = { id ->
navController.navigate(ProductDetail(id)) // 새로운 타입 안전 내비게이션
})
}
// 새로운 타입 안전 목적지
composable<ProductDetail> { 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 면접 준비에서 이러한 패턴을 심층적으로 다룹니다
- 관련 UI 개념은 Jetpack Compose 면접 질문도 참조하세요
Android 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 9월 1일 업데이트
태그
공유
관련 기사

2026년 Android 모듈화: 멀티 모듈 아키텍처와 면접 질문
Android 앱의 멀티 모듈 설계에 대해 상세히 알아봅니다. Gradle 버전 카탈로그, 컨벤션 플러그인, 모듈 간 통신 패턴, 그리고 기술 면접에서 자주 출제되는 질문과 답변을 소개합니다.

Android WorkManager 2026 완벽 가이드: 백그라운드 작업, 제약 조건, 면접 질문
Android WorkManager를 활용한 백그라운드 작업 처리 방법을 상세히 설명합니다. 제약 조건 설정, 주기적 실행, 체인 처리, 2026년 기술 면접에서 자주 출제되는 질문까지 포괄적으로 다룹니다.

Kotlin Flow vs StateFlow vs SharedFlow: 2026 안드로이드 면접 질문
2026년 안드로이드 면접관이 묻는 Kotlin Flow vs StateFlow vs SharedFlow 질문을 명확한 답변, 비교 표, 실무용 코드와 함께 정리했습니다.