버미

[안드로이드] Compose Navigation 개괄적 이해 본문

안드로이드

[안드로이드] Compose Navigation 개괄적 이해

Bum_2 2025. 7. 9. 20:21

Compose UI 기반 Navigation에 사용되는 요소는 어떤게 있으며, 어떻게 사용하는지 살펴보고자 한다.


 

공식 문서에 나와있는 Navigation에 필요한 요소들이다.

개념 목적 유형
Host 화면을 담고 있는 컨테이너
사용자가 navigate하면, 앱(Host)에서는 destination(화면)을 스왑하여 보여준다.
Compose: NavHost
Fragments: NavHostFragment
Graph 앱 내에 있는 모든 목적지(Navigation Destnation, 화면)을 정의하고 그것들이 어떻게 연결하는지 정의하는 데이터 구조 NavGraph
Controller 목적지 사이에서 관리하는 컨트롤러. 이것은 목적지 사이에서 네비게이션하는 방법, 딥링크 핸들링, 백스택 관리 등을 담당한다. NavController
Destination Navigation Graph에서의 노드 한 개를 의미.
사용자가 Node로 navigate하면, Host는 해당 Node를 보여준다.
NavDestination
Route destination으로 사용하는 유일한 이름. destination에 필요한 데이터가 포함할 수 있다. Any Serializable data type

1. Host

NavHost는 화면이 바뀌는 공간(또는, 컨테이너)을 제공해주고 NavController을 통해 화면을 swap하여 보여준다.

NavHostFragment는 ComposeView를 Fragment 안에 넣어서 사용하는 앱 서비스의 경우 사용한다. NavHostFragment는 잘 사용하지 않아서 NavHost을 알아보자. 

/**
 * Provides in place in the Compose hierarchy for self contained navigation to occur.
 *
 * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
 * the provided [navController].
 *
 * The builder passed into this method is [remember]ed. This means that for this NavHost, the
 * contents of the builder cannot be changed.
 *
 * @param navController the navController for this host
 * @param startDestination the route for the start destination
 * @param modifier The modifier to be applied to the layout.
 * @param contentAlignment The [Alignment] of the [AnimatedContent]
 * @param route the route for the graph
 * @param enterTransition callback to define enter transitions for destination in this host
 * @param exitTransition callback to define exit transitions for destination in this host
 * @param popEnterTransition callback to define popEnter transitions for destination in this host
 * @param popExitTransition callback to define popExit transitions for destination in this host
 * @param builder the builder used to construct the graph
 */
@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.Center,
    route: String? = null,
    enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
        { fadeIn(animationSpec = tween(700)) },
    exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
        { fadeOut(animationSpec = tween(700)) },
    popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
        enterTransition,
    popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
        exitTransition,
    builder: NavGraphBuilder.() -> Unit
) {
    NavHost(
        navController,
        remember(route, startDestination, builder) {
            navController.createGraph(startDestination, route, builder)
        },
        modifier,
        contentAlignment,
        enterTransition,
        exitTransition,
        popEnterTransition,
        popExitTransition
    )
}

 

 

NavHost는 화면을 보여주는 컨테이너 역할을 해줄 뿐이다.

NavHost 자체가 핵심이라기보다, 실제 기능을 맡고 있는 요소는 navController와 NavGraph이다.

 

navController가 현재 destination(화면), back stack, navigation event 관리를 담당하고 있다.

NavGraph는 NavHost 안에 어떤 화면을 구성할 때 사용한다. 앱의 화면 구조 설계를 담당하는 근원적인 기능을 담당한다. startDestination 파라미터와 builder 블록에서 NavGraph를 어떻게 구성할지 정의한다.

 


2. Graph

NavGraph는 화면 전환에 필요한 NavDestination의 모음집이다.

NavGraph 자체는 화면 스택에 올라가지 않지만, NavGraph도 가상의 목적지다. 

 

NavHost 파라미터 중, startDestination과 builder: NavGraphBuilder.() -> Unit 블럭, Route String을 사용하여 navController에 NavGraph가 반환된다.

 

🔎참고

NavGraph도 NavDestination을 상속하고 있다. 코드를 보고 싶은 독자를 위해 링크를 달아놓겠다.

public actual open class NavGraph actual constructor(navGraphNavigator: Navigator<out NavGraph>) : NavDestination(navGraphNavigator), Iterable<NavDestination> 

 


3. NavController

NavHost 안에서 보여지는 화면 이동을 관리한다.

보통 NavController을 직접 만드는 경우는 거의 없으며, rememverNavController()나, 유틸리티로 함수를 만들어 사용한다.

NavHost는 NavGraph를 통해 만들어진 흐름을 통해 이동한다.

NavController에서 화면의 스택을 관리하는 backStack를 관리하고 있다. 정확하게 말하자면, NavController class의 internal class인 NavControllerImpl의 backQueue에서 관리하고 있다. (KMP 멀티 대응 버전 이후)

개념적으로 알고 있던 backStack은 실제로는 backQueue : ArrayDeque<NavBackStackEntry>에서 관리하고 있다. 이는 자료 구조 상 Vector를 기반으로하는 Stack보다 Array를 기반으로하는 ArrayDeque의 장점을 이용하기 위해서다.

 

- 코드 일부

internal class NavControllerImpl(
    val navController: NavController,
    var updateOnBackPressedCallbackEnabledCallback: () -> Unit,
) {
    internal var navigatorStateToRestore: SavedState? = null
    internal var backStackToRestore: Array<SavedState>? = null

    internal val backQueue: ArrayDeque<NavBackStackEntry> = ArrayDeque()
    
    val navContext: NavContext
        get() = navController.navContext

    internal var _graph: NavGraph? = null

    internal var graph: NavGraph
        @MainThread
        get() {
            checkNotNull(_graph) { "You must call setGraph() before calling getGraph()" }
            return _graph as NavGraph
        }
        @MainThread
        @CallSuper
        set(graph) {
            setGraph(graph, null)
        } 
    // ...생략...
}

 

더 자세히 보고 싶다면 NavController 코드와 기능 위임을 위해 따로 빼놓은 Impl 코드를 보기 바란다. 

 


4. Destination

NavDestination은 전체 NavGraph에서 하나의 목적지를 말한다. 

각 목적지는 Navigator로 연결되어있는데 이 Navigator가 어떻게 이동할지 알고 있다.

각 목적지는 인자를 가질 수 있고 기본 값을 지정할 수 있고, 이동할 시점에 덮어쓰기가 가능하다.

Desination을 만들기 위해 NavGraphBuilder의 확장 함수인, composable과 navigation, dialog 함수를 사용한다. 이에 대해 간략하게 알아보자.

 

composable 확장 함수를 사용하여 Destination을 등록하면 내부적으로 ComposeNavigator가 Destination을 등록해준다. 따라서 개발자는 Composable로 Desination을 생성하기만 하면 된다.

 

public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    enterTransition: (@JvmSuppressWildcards
        AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = null,
    exitTransition: (@JvmSuppressWildcards
        AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = null,
    popEnterTransition: (@JvmSuppressWildcards
        AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? =
            enterTransition,
    popExitTransition: (@JvmSuppressWildcards
        AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? =
            exitTransition,
    content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
) {
    addDestination(
        ComposeNavigator.Destination(
            provider[ComposeNavigator::class],
            content
        ).apply {
            this.route = route
            arguments.forEach { (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach { deepLink ->
                addDeepLink(deepLink)
            }
            this.enterTransition = enterTransition
            this.exitTransition = exitTransition
            this.popEnterTransition = popEnterTransition
            this.popExitTransition = popExitTransition
        }
    )
}

 

 

navigation 확장 함수는 하위 SubGraph를 만들 때 사용한다. 하위 Destination을 묶고 startDestination 으로 이동시키는 역할을 한다. 이전에 NavGraph에서와 같이, navigation 확장 함수로 SubGraph를 만들 수 있고 이 Destination 은 가상 목적지가 된다. 

즉, NavGraph가 루트인 셈이고 navigation 를 사용하면 SubGraph가 만들어져 연결된다. 각 Graph는 곧바로 startDestination으로 이동하도록 구성되어있다.

public fun NavGraphBuilder.navigation(
    startDestination: String,
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? =
        null,
    exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? =
        null,
    popEnterTransition: (
    AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
    )? = enterTransition,
    popExitTransition: (
    AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
    )? = exitTransition,
    builder: NavGraphBuilder.() -> Unit
) {
    addDestination(
        NavGraphBuilder(provider, startDestination, route).apply(builder).build().apply {
            arguments.forEach { (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach { deepLink ->
                addDeepLink(deepLink)
            }
            if (this is ComposeNavGraphNavigator.ComposeNavGraph) {
                this.enterTransition = enterTransition
                this.exitTransition = exitTransition
                this.popEnterTransition = popEnterTransition
                this.popExitTransition = popExitTransition
            }
        }
    )
}

 

 

dialog 확장 함수는 composable과 달리 전체 화면이 아닌 다이얼로그로 콘텐츠를 표시하기 위해 사용한다. 

public fun NavGraphBuilder.dialog(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    dialogProperties: DialogProperties = DialogProperties(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    addDestination(
        DialogNavigator.Destination(
            provider[DialogNavigator::class],
            dialogProperties,
            content
        ).apply {
            this.route = route
            arguments.forEach { (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach { deepLink ->
                addDeepLink(deepLink)
            }
        }
    )
}

5. Route

Route는 목적지와 목적지에 필요한 데이터를 고유하게 식별하는 문자열이다.
Route를 사용해 탐색 시 목적지로 이동하며, 경로 안에 인자를 포함할 수도 있다.
주로 문자열로 표현되며, 직렬화 가능한 데이터 유형(숫자, 문자열 등)도 직렬화하여 포함 가능하다.