BottomNavigation: Jetpack Compose에서의 구조 및 설정

저자: IT Sectr 게시일: 2026-06-29 읽는 시간: 8 분

BottomNavigation(Material3의 NavigationBar)은 애플리케이션의 3~5개 메인 화면 간 내비게이션을 제공하는 Material Design 컴포넌트입니다. Jetpack Compose에서 하단 바는 NavigationBar 또는 BottomNavigation을 통해 구현되며, 각 항목은 아이콘과 텍스트 레이블로 표시됩니다. Google Material Design(2025)에 따르면 BottomNavigation은 iOS 및 Android에서 기본 내비게이션 패턴으로 권장됩니다. 패널은 WindowInsets을 통해 시스템 내비게이션(Gesture Navigation) 높이에 자동으로 조정됩니다. Material3의 NavigationBar는 다이나믹 컬러, 배지 및 적응형 여백을 지원합니다.

핵심 포인트

  • BottomNavigation — 3~5개 화면용 하단 내비게이션 바, Material3에서 NavigationBar로 구현
  • NavigationBarItem — 아이콘, 레이블 및 알림 배지를 포함하는 패널 항목
  • NavHost 연결 — 항목 선택 시 NavController에서 navigate()가 실행되어 그래프 전환
  • Material3는 다이나믹 컬러와 WindowInsets을 통한 자동 시스템 내비게이션 처리를 추가
  • Badge — 아이콘의 알림 배지, BadgedBox와 카운트로 구현

Jetpack Compose에서 BottomNavigation이란?

BottomNavigation은 3~5개의 메뉴 항목을 표시하는 하단 내비게이션 바용 Material Design 컴포넌트입니다. Jetpack Compose에서는 BottomNavigation(Material2) 및 NavigationBar(Material3) 컴포저블 함수를 통해 구현됩니다. 바는 화면 하단에 배치되며 애플리케이션의 주요 섹션에 빠르게 액세스할 수 있도록 합니다.

BottomNavigation의 주요 목적은 화면 수준 대상 간의 기본 내비게이션을 구성하는 것입니다. 각 패널 항목은 별도의 화면(홈, 검색, 프로필)을 나타냅니다. 탭하면 화면의 메인 영역 콘텐츠가 전환됩니다. BottomNavigation은 중첩 내비게이션용이 아닙니다 — 이를 위해 상단의 탭이 사용됩니다.

Material Design 가이드라인(2025)에 따르면 하단 바에는 애플리케이션의 어느 지점에서든 액세스할 수 있는 섹션만 포함되어야 합니다. 하위 화면에서 바를 숨기는 것(예: 세부 페이지 열기)은 표준 관행입니다. Compose에서 BottomNavigation의 표시 여부는 Scaffold 상태 또는 현재 NavController 경로에 따른 조건부 렌더링을 통해 제어됩니다.

Material3에서는 레거시 BottomNavigation 대신 NavigationBar를 사용하는 것이 좋습니다. NavigationBar에는 다이나믹 컬러, WindowInsets 지원 및 개선된 프레스 애니메이션이 포함됩니다. Google에 따르면 NavigationBar는 미래 지향적인 컴포넌트이며, M2의 BottomNavigation은 하위 호환성을 위해서만 유지됩니다.

NavigationBar 항목은 NavigationBarItem 컴포저블을 통해 구현됩니다. 매개변수: icon(아이콘), label(텍스트 레이블), selected(선택 상태), onClick(콜백). NavigationBarItem은 자동으로 선택된 색상(primary)을 적용하고 활성화 시 아이콘 크기 변경을 애니메이션합니다.

kotlin
@Composable
fun BottomNavBar() {
    val items = listOf(
        Screen.Home,
        Screen.Search,
        Screen.Profile
    )
    var selectedItem by remember { mutableIntStateOf(0) }
    NavigationBar {
        items.forEachIndexed { index, item ->
            NavigationBarItem(
                icon = { Icon(item.icon, contentDescription = item.label) },
                label = { Text(item.label) },
                selected = selectedItem == index,
                onClick = { selectedItem = index }
            )
        }
    }
}

NavigationBarItem의 아이콘은 선택된 상태에서는 채워짐(Filled), 선택되지 않은 상태에서는 윤곽선(Outlined)이어야 합니다. Material Icons는 두 세트를 모두 제공합니다: Icons.Filled.Home 및 Icons.Outlined.Home. 기본적으로 NavigationBarItem은 선택 시 채워짐 변형을 표시합니다. 다른 표시가 필요한 경우 조건부로 아이콘을 전달합니다: if (selected) Filled.X else Outlined.X.

텍스트 레이블 길이는 12자(라틴 문자) 또는 8자(키릴 문자)를 초과하지 않아야 합니다. 긴 레이블은 줄임표로 잘립니다. 레이블의 경우 maxLines = 1 및 ellipsis = TextTruncation.Ellipsis를 사용합니다. 레이블을 항상 표시해야 하는 경우 alwaysShowLabel = true 매개변수를 제거합니다(기본값은 true).

매개변수유형설명
icon@Composable () -> Unit패널 항목의 아이콘
label@Composable (() -> Unit)?아이콘 아래 텍스트 레이블
selectedBoolean선택된 항목 플래그
onClick() -> Unit클릭 시 콜백
badge@Composable (() -> Unit)?알림 개수가 있는 배지

BottomNavigation을 NavHost에 연결하기

selectedItem을 직접 업데이트해도 내비게이션이 실행되지 않습니다 — 화면 전환을 위해서는 NavController와의 통합이 필요합니다. 일반적인 패턴: NavigationBarItem의 onClick이 navController.navigate(route)를 호출하고, selectedItem은 현재 NavController 경로에서 결정됩니다.

NavController는 현재 백 스택을 저장합니다. 어떤 패널 항목이 선택되었는지 확인하려면 현재 경로(navController.currentBackStackEntryAsState().value?.destination?.route)를 항목 경로와 비교합니다. 경로가 일치하면 해당 항목이 선택된 것으로 간주됩니다. 이는 상태 중복을 제거합니다.

kotlin
@Composable
fun MainScreen(navController: NavController) {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route
    Scaffold(
        bottomBar = {
            NavigationBar {
                items.forEach { item ->
                    NavigationBarItem(
                        icon = { Icon(item.icon, contentDescription = item.label) },
                        label = { Text(item.label) },
                        selected = currentRoute == item.route,
                        onClick = {
                            navController.navigate(item.route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    ) { /* content */ }
}

navigate()의 주요 설정: popUpTo(startDestination)은 탭 전환 시 백 스택 축적을 방지하고, launchSingleTop은 스택 내 경로 중복을 방지하며, restoreState는 이전에 방문한 탭의 상태를 복원합니다. 이러한 매개변수가 없으면 각 클릭이 스택에 새 항목을 생성하여 잘못된 뒤로 가기 버튼 동작이 발생합니다.

Material3의 NavigationBar: 다이나믹 컬러 및 적응

Material3(M3)는 Material2의 BottomNavigation을 대체하는 NavigationBarNavigationBarItem을 제공합니다. M3의 주요 장점: 다이나믹 컬러, 적응형 높이, 전체 너비 리플 애니메이션 및 시스템 내비게이션용 WindowInsets 지원.

다이나믹 컬러는 기기 배경화면(Android 12+)에 따라 NavigationBar 색상을 자동으로 선택합니다. 활성 항목은 primary 색상으로, 비활성 항목은 onSurfaceVariant로 색상이 지정됩니다. 기본적으로 NavigationBar는 Modifier.navigationBarsPadding()을 통해 시스템 내비게이션 위로 올라갑니다. 수동 제어를 위해서는 windowInsets 매개변수를 사용합니다.

M3의 NavigationBar는 활성 항목 너비를 자동으로 애니메이션합니다 — 아이콘이 왼쪽으로 이동하고 레이블이 확장됩니다. 애니메이션은 300ms 동안 지속되며 표준 Compose 이징을 사용합니다. 이 동작이 원치 않는 경우 NavigationBarItem에서 alwaysShowLabel = true를 통해 항상 레이블을 표시합니다.

NavigationBar 색상 사용자 정의

색상을 재정의하려면 colors 매개변수에 NavigationBarColors를 전달합니다. 기본적으로 NavigationBarDefaults.colors()가 사용되지만 사용자 정의 값을 설정할 수 있습니다: containerColor(바 배경), contentColor(아이콘 색상), indicatorColor(활성 항목 배경). M2에서 BottomNavigationDefaults를 통해 색상이 설정된 것과 달리 M3는 통합된 NavColors 인터페이스를 사용합니다.

프로젝트가 M2(androidx.compose.material)의 BottomNavigation을 사용하는 경우 Google은 M3 NavigationBar로 마이그레이션할 것을 권장합니다. 마이그레이션에는 임포트 교체, 매개변수 업데이트 및 @OptIn(ExperimentalMaterial3Api::class) 추가가 포함됩니다. M2의 BottomNavigation은 더 이상 사용되지 않는 것으로 간주되며 새 기능을 받지 않습니다.

NavigationBar 상태는 현재 선택된 항목의 인덱스를 저장합니다. 단순한 경우 remember { mutableIntStateOf(0) }이 사용됩니다. 그러나 프로덕션 애플리케이션의 경우 인덱스는 내비게이션 그래프와 동기화되어야 하며 로컬 UI 상태가 아니어야 합니다.

상태 관리의 주요 접근 방식: NavController 기반(권장) 및 ViewModel 기반. 첫 번째 경우 selectedItem은 현재 NavController 경로에서 계산됩니다. 두 번째 경우 ViewModel이 현재 화면을 저장하고 NavigationBarItem이 ViewModel에서 StateFlow를 읽습니다. NavController 기반이 더 간단하고, ViewModel 기반은 복잡한 로직(인증, A/B 테스트)에 더 많은 제어를 제공합니다.

saveState 및 restoreState를 사용할 때 NavController는 각 탭 내의 LazyColumn 및 기타 컴포넌트의 상태를 자동으로 저장하고 복원합니다. 이는 스크롤 위치 저장을 위한 ViewModel의 필요성을 제거합니다. 그러나 저장은 입력 필드에 적용되지 않습니다 — 이러한 필드에는 SavedStateHandle 또는 ViewModel이 필요합니다.

항목의 알림 배지 및 카운터

Badge는 NavigationBarItem 아이콘에 표시되는 소형 알림 표시기입니다. Material3에서 배지는 BadgedBox를 통해 구현되며, 아이콘을 감싸고 배지를 오른쪽 상단 모서리에 배치합니다. 배지에는 숫자(알림 개수) 또는 점(간단한 존재 표시기)이 포함될 수 있습니다.

kotlin
@Composable
fun HomeItem(unreadCount: Int) {
    NavigationBarItem(
        icon = {
            BadgedBox(badge = {
                if (unreadCount > 0) {
                    Badge { Text(unreadCount.toString()) }
                }
            }) {
                Icon(Icons.Filled.Home, contentDescription = "홈")
            }
        },
        label = { Text("Home") },
        selected = false,
        onClick = { }
    )
}

BadgedBox는 자동으로 Badge를 자식 요소의 오른쪽 상단 모서리에 배치합니다. 개수가 99를 초과하면 Badge는 "99+"를 표시합니다. 값이 0일 때 배지를 숨기려면 if (count > 0)을 사용합니다. Material3의 Badge는 기본적으로 흰색 텍스트와 함께 error 색상(빨간색)을 가집니다. BadgeDefaults.colors(containerColor, contentColor)를 통해 사용자 정의합니다.

Material2에서는 배지가 없었습니다 — Canvas를 통해 수동으로 그리거나 타사 라이브러리를 사용해야 했습니다. Material3는 네이티브 컴포넌트로 이 문제를 해결했습니다. Android Developers(2025)에 따르면 BadgedBox는 NavigationBar를 사용하는 앱의 40%에서 사용되며, 그중 60%가 읽지 않은 메시지를 표시합니다.

자주 묻는 질문

BottomNavigation에는 몇 개의 항목이 있어야 하나요?

Material Design은 3~5개 항목을 권장합니다. 3개 미만은 실용적이지 않습니다(대신 탭을 사용하세요). 5개를 초과하면 레이블 가독성과 터치 대상 크기가 저하됩니다. 5개 이상의 섹션이 필요한 경우 Navigation Rail(태블릿) 또는 Drawer를 사용하세요.

하위 화면에서 BottomNavigation을 숨기려면 어떻게 하나요?

바가 표시되는 경로 목록을 정의합니다(일반적으로 루트 경로). Scaffold bottomBar에서 currentRoute가 bottomNavRoutes에 있는 경우에만 NavigationBar를 전달합니다. 대안은 bottomBar 없이 중첩된 화면을 위한 별도 그래프가 있는 NavHost입니다.

NavigationBar(M3)와 BottomNavigation(M2)의 차이점은 무엇인가요?

Material3의 NavigationBar는 다이나믹 컬러, BadgedBox, WindowInsets 및 애니메이션 활성 항목 너비를 지원합니다. Material2의 BottomNavigation은 이러한 기능이 없는 레거시 API입니다. Google은 새 프로젝트에는 NavigationBar를 사용하고 기존 프로젝트는 마이그레이션할 것을 권장합니다.

BottomNavigation 항목을 클릭하면 화면이 다시 생성되는 이유는 무엇인가요?

문제는 navigate()에 saveStaterestoreState가 없다는 것입니다. 이것들이 없으면 NavController가 이전 탭의 상태를 저장하지 않습니다. LazyColumn, 스크롤 위치 및 입력 필드를 보존하려면 NavOptions에 popUpTo(startDestination) { saveState = true } 및 restoreState = true를 추가하세요.

탭 간 전환 애니메이션을 사용자 정의하려면 어떻게 하나요?

전환 애니메이션은 BottomNavigation이 아닌 NavHost 수준에서 제어됩니다. 사용자 정의 애니메이션과 함께 composable(route, enterTransition, exitTransition)을 추가합니다. BottomNavigation의 경우 300ms 지속 시간의 페이드 애니메이션(fadeIn + fadeOut)이 일반적입니다.

요약

  • BottomNavigation(M3의 NavigationBar) — 애플리케이션의 3~5개 메인 화면 간 전환을 위한 하단 바
  • NavigationBarItem — 아이콘, 레이블, 배지 및 선택 상태가 있는 패널 항목
  • NavController 통합은 올바른 스택 관리를 위해 popUpTo, launchSingleTop 및 restoreState가 필요합니다
  • Material3 NavigationBar는 다이나믹 컬러, BadgedBox, WindowInsets 및 활성 항목 애니메이션을 지원합니다
  • BadgedBox — 항목 아이콘에 숫자나 점이 있는 알림 배지를 위한 컴포넌트
  • 선택 상태는 NavController와의 동기화를 위해 currentBackStackEntryAsState에서 계산됩니다
  • 하위 화면에서 바를 숨기려면 경로 목록을 기반으로 Scaffold bottomBar에서 조건부 렌더링을 사용합니다

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

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

프로젝트 논의

더 읽어보기