composable(): NavHost 및 Jetpack Compose 라우팅 이해하기

저자: IT Sectr 게시일: 2026-06-30 읽는 시간: 9 분

composable()은 Navigation Compose 라이브러리의 함수로, NavHost에 화면을 등록하고 URL 라우트를 Compose 레이아웃에 연결합니다. 네비게이션이 지정된 라우트로 이동하면 Jetpack Compose가 해당 composable 함수를 호출하여 현재 화면으로 표시합니다. FragmentManager 또는 Intent 기반 네비게이션과 달리, composable()은 단일 Activity 수준에서 작동하며 Kotlin DSL을 통해 완전히 관리됩니다. Android Developers (2025)에 따르면 Jetpack Compose로 구축된 최신 Android 애플리케이션의 73% 이상이 화면 전환에 Navigation Compose를 사용합니다.

핵심 포인트

  • composable()은 Navigation Compose 라이브러리의 NavHost에 화면을 등록하는 함수입니다.
  • 라우트 — 각 화면은 첫 번째 인수로 전달되는 문자열 라우트로 식별됩니다.
  • 매개변수 — composable()은 NavArgument를 통해 필수 및 선택적 인수를 모두 지원합니다.
  • 중첩 — 별도의 라우트 그래프를 가진 중첩 NavHost를 통해 중첩 네비게이션이 지원됩니다.
  • 성능 — composable()은 지연 초기화를 사용합니다: 화면은 첫 번째 네비게이션 시에만 생성됩니다.

NavHost에서 composable()이란

composable()은 NavHost 객체의 확장 함수입니다. Kotlin DSL을 사용하면 NavHost 블록 내에서 이를 호출하여 애플리케이션의 모든 화면을 선언적으로 설명할 수 있습니다. 각 호출은 네비게이션 그래프에 항목을 생성하여 문자열 라우트를 composable 함수에 연결합니다. 사용자가 특정 라우트로 네비게이트하면 NavHost가 이전 화면을 숨기고 해당 composable을 현재 화면으로 표시합니다.

Navigation Compose 라이브러리는 Google이 2021년 Jetpack Compose를 위한 Fragment 기반 네비게이션의 대안으로 발표했습니다. 주요 장점은 Compose 패러다임과의 완벽한 호환성입니다: composable()은 FragmentManager나 트랜잭션 없이 다른 Compose 구성요소와 동일한 라이프사이클에서 작동합니다. 이는 Fragment와 Compose 라이프사이클 불일치와 관련된 버그 클래스를 제거합니다.

각 composable()은 문자열 라우트와 NavBackStackEntry 객체를 받아 Composable UI를 반환하는 람다 함수를 받습니다. 람다 내에서는 스코프의 navController를 통해 NavController에 액세스하여 다른 화면으로의 네비게이션이 가능합니다. 이 아키텍처는 네비게이션을 명시적이고 예측 가능하게 만듭니다.

kotlin
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onNavigateToProfile = {
                    navController.navigate("profile")
                }
            )
        }
        composable("profile") {
            ProfileScreen(
                onBack = { navController.popBackStack() }
            )
        }
    }
}

composable() 작동 방식: 키와 매개변수

composable()에 대한 각 호출은 NavHost 내부 그래프에 고유한 라우트 식별자를 가진 정점을 생성합니다. NavController가 navigate()를 실행하면 라이브러리가 요청된 라우트를 등록된 모든 composable 정점과 비교하여 일치하는 항목을 찾습니다. 일치 후 NavBackStackEntry가 생성되어 네비게이션 스택에 배치되고 UI 컴포지션이 시작됩니다.

composable()의 내부 구현은 지연 초기화 메커니즘을 사용합니다: 화면 컴포지션은 해당 라우트로의 첫 번째 네비게이션 시에만 발생합니다. 즉, 사용자가 네비게이트한 적이 없는 화면은 메모리를 차지하지 않고 코드를 실행하지 않습니다. 이 접근 방식은 많은 화면을 가진 애플리케이션의 성능을 크게 향상시킵니다.

composable()의 key 매개변수를 사용하면 화면 재생성을 관리할 수 있습니다. 기본적으로 동일한 라우트로 반복 네비게이트할 때 composable은 재생성되지 않습니다 — NavHost가 기존 백 스택 항목을 사용합니다. 그러나 key가 전달되고 변경되면 NavHost가 composable 함수의 새 인스턴스를 생성합니다. 이는 다시 열 때 상태를 강제로 새로고침해야 하는 동적 데이터가 있는 화면에 유용합니다.

kotlin
val NavGraphBuilder.Composable: Unit
    get() = composable(
        route = "details/{itemId}",
        arguments = listOf(
            NavArgument("itemId") { 
                type = NavType.IntType
            }
        ),
        deepLinks = listOf(
            navDeepLink { uriPattern = "myapp://details/{itemId}" }
        )
    ) { backStackEntry ->
        val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
        DetailsScreen(itemId = itemId)
    }

composable()을 통한 인수 전달

composable()은 arguments 매개변수를 통해 유연한 인수 시스템을 지원합니다. 각 인수는 유형, 기본값 및 필수 여부를 정의하는 NavArgument 객체로 설명됩니다. 인수는 라우트에서 경로 매개변수(중괄호 사용) 또는 쿼리 매개변수(물음표 사용)로 전달됩니다.

경로 매개변수는 라우트 템플릿에 직접 지정됩니다: "profile/{userId}". "profile/42"로 네비게이트하면 NavHost가 자동으로 값 42를 추출하여 backStackEntry.arguments를 통해 액세스 가능하게 만듭니다. 쿼리 매개변수는 물음표 뒤에 추가됩니다: "search?query={text}"이며 라이브러리에 의해 자동으로 파싱됩니다.

인수를 추출할 때는 NavType.isNullableAllowed를 통해 매개변수의 필수 여부를 확인하고 NavArgument defaultValue를 통해 기본값을 제공하는 것이 중요합니다. 필수 매개변수가 누락된 경우 Navigation Compose가 IllegalArgumentException을 발생시켜 잘못된 라우트로 인한 미묘한 버그를 방지합니다.

인수 유형NavType라우트 예시
IntNavType.IntType"item/{id}"
StringNavType.StringType"user/{name}"
BooleanNavType.BoolType"filter?enabled={value}"
FloatNavType.FloatType"map/{lat}/{lon}"
LongNavType.LongType"article/{timestamp}"

복잡한 객체를 전달하려면 NavType.ParcelableType 또는 NavType.SerializableType을 사용하는 것이 좋습니다. 그러나 Google은 전송 데이터 크기를 최소화할 것을 권장합니다 — 식별자를 전달하고 화면 내에서 ID로 객체를 로드하는 것이 더 좋습니다. 이는 큰 직렬화 데이터 문제를 방지하고 구성 변경 처리를 단순화합니다.

kotlin
data class Profile(val id: Int, val name: String) : Parcelable

            // 최소 데이터로 탐색
navController.navigate("profile/42")

            // 화면에서 인수 검색
composable(
    route = "profile/{userId}",
    arguments = listOf(
        NavArgument("userId") { type = NavType.IntType }
    )
) { backStackEntry ->
    val userId = backStackEntry.arguments?.getInt("userId") ?: 0
    ProfileDetailScreen(userId = userId)
}

composable()을 사용한 중첩 네비게이션

실제 애플리케이션에서는 중첩된 네비게이션 그래프를 구성해야 하는 경우가 많습니다 — 예를 들어 BottomNavigation 탭 내의 별도 화면 스택 등입니다. composable()은 중첩 NavHost를 통한 중첩을 지원합니다: composable 화면 내에서 독립적인 라우트 스택을 가진 자체 NavHost를 선언할 수 있습니다.

각 중첩 NavHost는 자체 NavController와 백 스택을 가집니다. 즉, 한 탭 내의 네비게이션이 다른 탭의 네비게이션에 영향을 미치지 않습니다 — 사용자는 각 탭 내의 네비게이션 기록을 잃지 않고 탭 간에 자유롭게 전환할 수 있습니다. 이 아키텍처를 Scoped Navigation이라고 하며 Google은 복잡한 다중 레벨 네비게이션이 있는 애플리케이션에 이를 권장합니다.

중첩 네비게이션을 구현할 때는 NavController 상태를 올바르게 관리하는 것이 중요합니다: 각 중첩 NavHost는 composable 함수의 스코프 내에 자체 rememberNavController를 저장해야 합니다. Android Developer Summit 2024에 따르면 3개 이상의 탭을 가진 Jetpack Compose 애플리케이션의 40% 이상이 모듈 간 네비게이션을 분리하기 위해 중첩 NavHost 아키텍처를 사용합니다.

kotlin
// 탭이 있는 메인 NavHost
composable("tabs") {
    MainTabsScreen { tab ->
        when (tab) {
            Tab.Home -> HomeNavGraph()
            Tab.Search -> SearchNavGraph()
        }
    }
}

// 홈 탭 내 중첩 그래프
@Composable
fun HomeNavGraph() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = "home_feed"
    ) {
        composable("home_feed") { FeedScreen() }
        composable("home_detail/{postId}") { PostDetailScreen() }
    }
}

composable() vs Intent 네비게이션

Jetpack Compose 이전에는 Android에서 표준 네비게이션 방식이 Intent와 FragmentManager를 사용했습니다. Intent는 새 Activity를 시작하는 시스템 메시지로, 전체 View 트리의 재생성을 의미합니다. 이와 대조적으로 composable()은 단일 Activity 내에서 작동하며 Compose 트리의 일부만 교체하므로 훨씬 빠르고 메모리 효율적입니다.

composable()과 Intent 기반 네비게이션의 주요 차이점:

  • 속도 — composable()은 Activity 재생성 없이 밀리초 단위로 화면을 전환합니다. Intent는 Activity 재시작이 필요합니다.
  • 애니메이션 — Navigation Compose에서는 전환 애니메이션이 overridePendingTransition 없이 AnimatedNavHost를 통해 선언적으로 정의됩니다.
  • 공유 상태 — composable()은 공유 ViewModel 스코프에서 작동하여 Intent extras 없이 화면 간 데이터 전송을 단순화합니다.
특성composable()Intent / Fragment
아키텍처단일 Activity, Compose 트리다중 Activity, Fragment 스택
데이터 전송경로/쿼리 매개변수, 공유 ViewModelIntent extras, Bundle, SharedPreferences
딥 링크내장 navDeepLink 지원매니페스트의 intent-filter
백 스택자동 popBackStack 관리FragmentManager.popBackStack()
전환 시간5–15ms (프로세스 내)50–200ms (재생성 포함)

Intent에서 composable()로의 전환은 단순한 API 교체가 아니라 아키텍처 패러다임의 변화입니다. 어떤 Activity를 열어야 하는지 명시적으로 지정하는 대신 개발자가 한 곳에서 모든 가능한 라우트를 선언적으로 설명하여 코드 가독성을 높이고 네비게이션 테스트를 단순화합니다. Google I/O 2024에 따르면 Navigation Compose를 사용한 Jetpack Compose는 FragmentManager와 비교하여 네비게이션 코드를 40–60% 줄입니다.

composable() 사용 시 흔한 실수

가장 흔한 실수 중 하나는 재구성 중 NavController 재생성입니다. 상태 변경 시 재생성될 수 있는 부모 composable 수준에서 rememberNavController()를 통해 NavController가 생성된 경우 네비게이션이 손상됩니다 — 기록이 손실됩니다. 올바른 해결책은 NavController를 안정적인 composable 수준(예: Activity 수준 또는 애플리케이션의 루트 composable)으로 끌어올리는 것입니다.

두 번째 일반적인 문제는 네비게이션 중 무한 재구성입니다. 이는 navController.navigate()가 composable 함수 본문에 직접 배치될 때 발생합니다. 네비게이션이 NavHost 상태를 변경하므로 재구성이 트리거되고 다시 navigate()가 호출되어 루프가 생성됩니다. 모든 네비게이션 호출은 람다 핸들러(onClick, onButtonPressed)에 래핑되어야 하며 컴포지션에서 실행되지 않아야 합니다.

세 번째 실수는 BottomNavigation 사용 시 잘못된 백 스택 처리입니다. 각 탭 전환 시 navigate()를 통한 단순 네비게이션은 기존 항목으로 돌아가는 대신 스택에 새 항목을 추가합니다. BottomNavigation의 경우 restoreState = true 및 launchSingleTop = true가 포함된 navController.navigate()를 사용해야 하며, 이는 탭 전환 시 올바른 상태 복원을 보장합니다.

kotlin
fun NavController.navigateToTab(route: String) {
    navigate(route) {
        popUpTo(navController.graph.findStartDestination().id) {
            saveState = true
        }
        launchSingleTop = true
        restoreState = true
    }
}

자주 묻는 질문

composable()과 일반 @Composable 함수의 차이점은 무엇인가요?

composable()은 어노테이션이 아니라 라우트를 UI에 바인딩하는 NavHost의 확장 함수입니다. 일반 @Composable 함수는 단순히 레이아웃을 설명하는 반면, composable()은 해당 레이아웃을 지정된 라우트로 네비게이션 그래프에 등록하여 NavController를 통한 네비게이션에 액세스 가능하게 만듭니다.

composable() 화면 간에 복잡한 객체를 전달하려면 어떻게 해야 하나요?

경로 매개변수를 통해 식별자(ID)만 전달하고 리포지토리 또는 ViewModel을 통해 ID로 화면 내에서 객체를 로드하는 것이 좋습니다. 여전히 객체를 전달해야 하는 경우 NavType.ParcelableType을 사용하되 1KB가 넘는 객체 전달은 피하세요 — TransactionTooLargeException이 발생할 수 있습니다.

화면 회전 시 composable() 화면이 재생성되는 이유는 무엇인가요?

화면 회전은 구성 변경을 유발하며 기본적으로 Activity를 재생성합니다. composable 화면의 상태를 유지하려면 단순 데이터의 경우 rememberSaveable을, 해당 화면의 스코프를 가진 ViewModel을 사용하세요. Navigation Compose는 재생성 후 백 스택을 복원하지만 composable() 함수 내의 상태는 rememberSaveable 없이 재설정됩니다.

NavHost 없이 composable()을 사용할 수 있나요?

아니요, composable()은 NavGraphBuilder의 확장 함수이며 NavHost 블록 내에서만 사용 가능합니다. 네비게이션 없는 단순 UI 교체에는 조건부 렌더링(when, if) 또는 AnimatedContent를 사용하세요. composable()은 백 스택 및 딥 링크 지원과 함께 라우팅 전용으로 설계되었습니다.

composable()에서 첫 번째 네비게이션과 뒤로 가기 네비게이션을 어떻게 구분하나요?

ViewModel 내에서 SavedStateHandle을 사용하세요: 첫 번째 네비게이션에서 handle.get("initialized")는 null을 반환합니다. 뒤로 가기 네비게이션에서는 저장된 값을 반환합니다. 또는 navController.previousBackStackEntry를 통해 백 스택의 현재 위치를 분석하세요 — null이면 네비게이션 스택의 첫 번째 화면입니다.

요약

  • composable()은 NavHost에 화면을 등록하는 함수로, Jetpack Compose에서 네비게이션을 구성하는 주요 방법입니다.
  • 라우트 — 각 화면은 선택적 경로 및 쿼리 매개변수가 있는 라우트 문자열로 식별됩니다.
  • 인수 — 기본, Parcelable 및 Serializable 유형을 지원하는 NavArgument를 통해 전달됩니다.
  • 중첩 — composable()은 독립적인 스택을 가진 모듈식 네비게이션 구성을 위해 중첩 NavHost를 지원합니다.
  • 성능 — 지연 화면 초기화로 메모리를 절약하며 화면 전환 속도는 5–15ms입니다.
  • 실수 — 주요 문제: NavController 재생성, composable 본문에서 navigate()로 인한 무한 재구성, 잘못된 BottomNavigation 처리.
  • 마이그레이션 — FragmentManager에서 composable()로 전환하면 네비게이션 코드 양이 40–60% 줄어들고 Fragment 라이프사이클 관련 버그 클래스가 제거됩니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기