frontend

Jetpack Compose Navigation 딥링크 완벽 가이드 — 백스택과 딥링크 처리

Jetpack Compose Navigation에서 딥링크 설정, 백스택 관리, 인자 전달 방식을 실전 예제와 함께 정리하였다.

★★★★☆추천
AndroidJetpackComposeNavigation딥링크Kotlin
Jetpack Compose Navigation 딥링크 완벽 가이드 — 백스택과 딥링크 처리
  • ·Compose Navigation은 NavHost + NavController 조합으로 화면 전환을 선언적으로 관리한다
  • ·딥링크는 navDeepLink { uriPattern = "앱스킴://경로" } 형태로 등록한다
  • ·백스택 동작은 navigate()의 popUpTo와 launchSingleTop으로 제어한다
  • ·타입 안전 내비게이션은 Navigation 2.8.0부터 @Serializable 어노테이션으로 지원된다
Compose Navigation을 처음 적용했을 때 딥링크로 진입한 경우 뒤로 가기 버튼을 눌렀더니 홈 화면 대신 앱이 종료됐다. popUpTo 설정을 잘못 이해해 백스택이 비어 있었기 때문이었다. inclusive와 saveState 옵션의 조합을 여러 번 시험한 끝에 기대한 백스택 동작을 구현할 수 있었고, 이후 딥링크 진입 시나리오를 테스트케이스로 남겨두는 습관이 생겼다.

Compose Navigation 기본 구조

Jetpack Compose NavHost 설정과 화면 라우트 등록 방법

Compose Navigation은 NavHost 컴포저블 안에 composable 함수로 각 화면을 등록하는 구조다. NavController를 rememberNavController()로 생성해 NavHost의 navController 파라미터에 전달하고, startDestination으로 앱 시작 화면을 지정한다. 각 화면은 라우트 문자열로 식별되며 composable("home") { HomeScreen(navController) } 형태로 선언한다. 화면 전환은 navController.navigate("detail/42")처럼 라우트 경로를 직접 문자열로 전달하는 방식과, Navigation 2.8.0 이후부터 지원하는 타입 안전 내비게이션 두 가지가 있다. 타입 안전 방식은 @Serializable 데이터 클래스를 라우트로 사용해 컴파일 타임에 인자 타입을 검사할 수 있어 문자열 오타로 인한 런타임 크래시를 예방한다. 새 프로젝트라면 타입 안전 방식으로 시작하는 것을 권장한다.

val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(navController) }
    composable(
        "detail/{postId}",
        arguments = listOf(navArgument("postId") { type = NavType.IntType })
    ) { backStackEntry ->
        val postId = backStackEntry.arguments?.getInt("postId") ?: return@composable
        DetailScreen(postId, navController)
    }
}

Compose Navigation 딥링크 구현

Jetpack Compose Navigation 딥링크 등록과 AndroidManifest 설정

Compose Navigation에서 딥링크를 등록하려면 composable 함수에 deepLinks 파라미터를 추가하고 navDeepLink { uriPattern = "앱스킴://detail/{postId}" } 형태로 URI 패턴을 선언한다. 여기에 더해 AndroidManifest.xml의 MainActivity intent-filter에 딥링크 URI 스킴을 등록해야 외부 앱이나 알림에서 진입이 가능해진다. HTTPS 딥링크를 쓰려면 assetlinks.json을 서버에 배포하고 Digital Asset Links를 설정해야 앱 링크로 인식된다. 딥링크로 진입할 때 백스택은 기본적으로 비어 있으므로 뒤로 가기 시 앱이 바로 종료된다. 딥링크 진입 시에도 홈 화면이 백스택에 있어야 한다면 NavDeepLinkRequest와 함께 handleDeepLink()를 사용하거나 커스텀 백스택 구성을 해야 한다.

composable(
    "detail/{postId}",
    deepLinks = listOf(navDeepLink {
        uriPattern = "https://example.com/post/{postId}"
    })
) { ... }

Compose Navigation 백스택 관리 — popUpTo와 launchSingleTop 옵션

navigate() 호출 시 popUpTo와 launchSingleTop 옵션으로 백스택 동작을 세밀하게 제어할 수 있다. popUpTo("home") { inclusive = false }는 home까지의 스택을 유지하면서 그 위에 새 화면을 쌓고, inclusive = true이면 home 자체도 스택에서 제거한다. 하단 탭 내비게이션을 구현할 때 탭 전환 시 이전 탭의 스택이 남아 있어야 하면 saveState = true와 restoreState = true를 함께 사용한다. launchSingleTop = true는 현재 백스택 최상단에 동일한 화면이 있으면 새 인스턴스를 생성하지 않고 기존 것을 재사용하는 옵션으로, 중복 탭 클릭 처리에 유용하다. 이 옵션들의 조합을 처음 접하면 직관적이지 않아 헷갈리기 쉬우니 각 경우의 기대 동작을 직접 실행해 확인하는 것이 이해에 가장 빠르다.

Compose Navigation 상태 공유와 ViewModel 범위

Compose Navigation 그래프 범위 ViewModel로 화면 간 상태 공유하기

Compose Navigation에서 여러 화면이 상태를 공유해야 할 때 Activity 범위 ViewModel을 쓰면 생명주기가 너무 길어진다. 대신 navController.getBackStackEntry("routeName")로 특정 라우트의 백스택 엔트리를 얻고 viewModel(it)을 호출하면 해당 네비게이션 그래프 범위에 묶인 ViewModel을 생성할 수 있다. 이 ViewModel은 해당 그래프에서 벗어나는 순간 소멸되므로 불필요한 상태 유지를 방지한다. 멀티 스텝 폼처럼 여러 화면을 걸쳐 동일 상태를 유지해야 하는 경우에 특히 유용하다. 단, nested navigation graph를 사용할 때 백스택 엔트리 라우트를 정확히 지정하지 않으면 IllegalArgumentException이 발생하므로 라우트 이름을 상수로 관리하는 습관이 필요하다.

자주 묻는 질문

Compose Navigation에서 화면 전환 애니메이션은 어떻게 추가하나요?+

composable의 enterTransition, exitTransition 파라미터에 AnimatedContentTransitionScope를 사용합니다. slideInHorizontally, fadeIn 등의 빌트인 애니메이션을 조합할 수 있습니다.

이전 화면으로 결과값을 전달하려면 어떻게 하나요?+

NavController.previousBackStackEntry?.savedStateHandle에 키-값을 저장하고 이전 화면에서 savedStateHandle.getStateFlow(key, default)로 수신합니다.

Compose Navigation 2.8의 타입 안전 내비게이션은 어떻게 사용하나요?+

@Serializable data class DetailRoute(val postId: Int)를 선언하고 composable<DetailRoute> { }와 navController.navigate(DetailRoute(42)) 형태로 사용합니다.