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()은 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에 액세스하여 다른 화면으로의 네비게이션이 가능합니다. 이 아키텍처는 네비게이션을 명시적이고 예측 가능하게 만듭니다.
@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()에 대한 각 호출은 NavHost 내부 그래프에 고유한 라우트 식별자를 가진 정점을 생성합니다. NavController가 navigate()를 실행하면 라이브러리가 요청된 라우트를 등록된 모든 composable 정점과 비교하여 일치하는 항목을 찾습니다. 일치 후 NavBackStackEntry가 생성되어 네비게이션 스택에 배치되고 UI 컴포지션이 시작됩니다.
composable()의 내부 구현은 지연 초기화 메커니즘을 사용합니다: 화면 컴포지션은 해당 라우트로의 첫 번째 네비게이션 시에만 발생합니다. 즉, 사용자가 네비게이트한 적이 없는 화면은 메모리를 차지하지 않고 코드를 실행하지 않습니다. 이 접근 방식은 많은 화면을 가진 애플리케이션의 성능을 크게 향상시킵니다.
composable()의 key 매개변수를 사용하면 화면 재생성을 관리할 수 있습니다. 기본적으로 동일한 라우트로 반복 네비게이트할 때 composable은 재생성되지 않습니다 — NavHost가 기존 백 스택 항목을 사용합니다. 그러나 key가 전달되고 변경되면 NavHost가 composable 함수의 새 인스턴스를 생성합니다. 이는 다시 열 때 상태를 강제로 새로고침해야 하는 동적 데이터가 있는 화면에 유용합니다.
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()은 arguments 매개변수를 통해 유연한 인수 시스템을 지원합니다. 각 인수는 유형, 기본값 및 필수 여부를 정의하는 NavArgument 객체로 설명됩니다. 인수는 라우트에서 경로 매개변수(중괄호 사용) 또는 쿼리 매개변수(물음표 사용)로 전달됩니다.
경로 매개변수는 라우트 템플릿에 직접 지정됩니다: "profile/{userId}". "profile/42"로 네비게이트하면 NavHost가 자동으로 값 42를 추출하여 backStackEntry.arguments를 통해 액세스 가능하게 만듭니다. 쿼리 매개변수는 물음표 뒤에 추가됩니다: "search?query={text}"이며 라이브러리에 의해 자동으로 파싱됩니다.
인수를 추출할 때는 NavType.isNullableAllowed를 통해 매개변수의 필수 여부를 확인하고 NavArgument defaultValue를 통해 기본값을 제공하는 것이 중요합니다. 필수 매개변수가 누락된 경우 Navigation Compose가 IllegalArgumentException을 발생시켜 잘못된 라우트로 인한 미묘한 버그를 방지합니다.
| 인수 유형 | NavType | 라우트 예시 |
|---|---|---|
| Int | NavType.IntType | "item/{id}" |
| String | NavType.StringType | "user/{name}" |
| Boolean | NavType.BoolType | "filter?enabled={value}" |
| Float | NavType.FloatType | "map/{lat}/{lon}" |
| Long | NavType.LongType | "article/{timestamp}" |
복잡한 객체를 전달하려면 NavType.ParcelableType 또는 NavType.SerializableType을 사용하는 것이 좋습니다. 그러나 Google은 전송 데이터 크기를 최소화할 것을 권장합니다 — 식별자를 전달하고 화면 내에서 ID로 객체를 로드하는 것이 더 좋습니다. 이는 큰 직렬화 데이터 문제를 방지하고 구성 변경 처리를 단순화합니다.
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)
}
실제 애플리케이션에서는 중첩된 네비게이션 그래프를 구성해야 하는 경우가 많습니다 — 예를 들어 BottomNavigation 탭 내의 별도 화면 스택 등입니다. composable()은 중첩 NavHost를 통한 중첩을 지원합니다: composable 화면 내에서 독립적인 라우트 스택을 가진 자체 NavHost를 선언할 수 있습니다.
각 중첩 NavHost는 자체 NavController와 백 스택을 가집니다. 즉, 한 탭 내의 네비게이션이 다른 탭의 네비게이션에 영향을 미치지 않습니다 — 사용자는 각 탭 내의 네비게이션 기록을 잃지 않고 탭 간에 자유롭게 전환할 수 있습니다. 이 아키텍처를 Scoped Navigation이라고 하며 Google은 복잡한 다중 레벨 네비게이션이 있는 애플리케이션에 이를 권장합니다.
중첩 네비게이션을 구현할 때는 NavController 상태를 올바르게 관리하는 것이 중요합니다: 각 중첩 NavHost는 composable 함수의 스코프 내에 자체 rememberNavController를 저장해야 합니다. Android Developer Summit 2024에 따르면 3개 이상의 탭을 가진 Jetpack Compose 애플리케이션의 40% 이상이 모듈 간 네비게이션을 분리하기 위해 중첩 NavHost 아키텍처를 사용합니다.
// 탭이 있는 메인 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() }
}
}
Jetpack Compose 이전에는 Android에서 표준 네비게이션 방식이 Intent와 FragmentManager를 사용했습니다. Intent는 새 Activity를 시작하는 시스템 메시지로, 전체 View 트리의 재생성을 의미합니다. 이와 대조적으로 composable()은 단일 Activity 내에서 작동하며 Compose 트리의 일부만 교체하므로 훨씬 빠르고 메모리 효율적입니다.
composable()과 Intent 기반 네비게이션의 주요 차이점:
| 특성 | composable() | Intent / Fragment |
|---|---|---|
| 아키텍처 | 단일 Activity, Compose 트리 | 다중 Activity, Fragment 스택 |
| 데이터 전송 | 경로/쿼리 매개변수, 공유 ViewModel | Intent 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% 줄입니다.
가장 흔한 실수 중 하나는 재구성 중 NavController 재생성입니다. 상태 변경 시 재생성될 수 있는 부모 composable 수준에서 rememberNavController()를 통해 NavController가 생성된 경우 네비게이션이 손상됩니다 — 기록이 손실됩니다. 올바른 해결책은 NavController를 안정적인 composable 수준(예: Activity 수준 또는 애플리케이션의 루트 composable)으로 끌어올리는 것입니다.
두 번째 일반적인 문제는 네비게이션 중 무한 재구성입니다. 이는 navController.navigate()가 composable 함수 본문에 직접 배치될 때 발생합니다. 네비게이션이 NavHost 상태를 변경하므로 재구성이 트리거되고 다시 navigate()가 호출되어 루프가 생성됩니다. 모든 네비게이션 호출은 람다 핸들러(onClick, onButtonPressed)에 래핑되어야 하며 컴포지션에서 실행되지 않아야 합니다.
세 번째 실수는 BottomNavigation 사용 시 잘못된 백 스택 처리입니다. 각 탭 전환 시 navigate()를 통한 단순 네비게이션은 기존 항목으로 돌아가는 대신 스택에 새 항목을 추가합니다. BottomNavigation의 경우 restoreState = true 및 launchSingleTop = true가 포함된 navController.navigate()를 사용해야 하며, 이는 탭 전환 시 올바른 상태 복원을 보장합니다.
fun NavController.navigateToTab(route: String) {
navigate(route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
자주 묻는 질문
composable()은 어노테이션이 아니라 라우트를 UI에 바인딩하는 NavHost의 확장 함수입니다. 일반 @Composable 함수는 단순히 레이아웃을 설명하는 반면, composable()은 해당 레이아웃을 지정된 라우트로 네비게이션 그래프에 등록하여 NavController를 통한 네비게이션에 액세스 가능하게 만듭니다.
경로 매개변수를 통해 식별자(ID)만 전달하고 리포지토리 또는 ViewModel을 통해 ID로 화면 내에서 객체를 로드하는 것이 좋습니다. 여전히 객체를 전달해야 하는 경우 NavType.ParcelableType을 사용하되 1KB가 넘는 객체 전달은 피하세요 — TransactionTooLargeException이 발생할 수 있습니다.
화면 회전은 구성 변경을 유발하며 기본적으로 Activity를 재생성합니다. composable 화면의 상태를 유지하려면 단순 데이터의 경우 rememberSaveable을, 해당 화면의 스코프를 가진 ViewModel을 사용하세요. Navigation Compose는 재생성 후 백 스택을 복원하지만 composable() 함수 내의 상태는 rememberSaveable 없이 재설정됩니다.
아니요, composable()은 NavGraphBuilder의 확장 함수이며 NavHost 블록 내에서만 사용 가능합니다. 네비게이션 없는 단순 UI 교체에는 조건부 렌더링(when, if) 또는 AnimatedContent를 사용하세요. composable()은 백 스택 및 딥 링크 지원과 함께 라우팅 전용으로 설계되었습니다.
ViewModel 내에서 SavedStateHandle을 사용하세요: 첫 번째 네비게이션에서 handle.get("initialized")는 null을 반환합니다. 뒤로 가기 네비게이션에서는 저장된 값을 반환합니다. 또는 navController.previousBackStackEntry를 통해 백 스택의 현재 위치를 분석하세요 — null이면 네비게이션 스택의 첫 번째 화면입니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.