HarmonyOS Next: 내비게이션 컴포넌트를 사용한 지속적인 탭 만들기
Source: Dev.to

소개
HarmonyOS Next와 ArkUI/ArkTS로 앱을 개발하고 있다면, 부드러운 사용자 경험이 얼마나 중요한지 잘 알고 있을 것입니다. 개발자들이 흔히 겪는 문제 중 하나는 사용자가 다른 화면으로 이동할 때 탭 바가 보이지 않게 되는 상황입니다.
이 글에서는 다음을 다룹니다:
- 라우터를 사용할 때 탭이 사라지는 이유를 설명합니다.
- Tabs와 Navigation 컴포넌트를 결합한 견고한 해결책을 제시합니다.
문제: 탐색 시 탭 사라짐
ArkUI에서는 Tabs 컴포넌트를 사용해 Home, Profile, Settings 등과 같은 섹션을 만들곤 합니다. 탭은 화면 하단에 위치해 사용자가 주요 뷰 간에 전환할 수 있게 합니다.
탭만 전환할 때는 모든 것이 정상적으로 동작합니다. 문제는 탭 내부에 있는 버튼을 눌러 ArkTS의 router를 사용해 새로운 페이지로 이동할 때 발생합니다.

왜 이런 현상이 발생할까요?
Tabs 컴포넌트는 보통 단일 페이지에 배치됩니다. router를 이용해 다른 페이지로 이동하면 탭을 호스팅하고 있던 원래 페이지를 떠나게 되므로 탭 바가 사라집니다. 사용자는 중요한 네비게이션 요소를 잃게 되어 혼란스러울 수 있습니다.
우리는 사용자가 앱의 특정 섹션을 얼마나 깊게 들어가든 탭이 계속 표시되기를 원합니다.
해결책: 탭과 Navigation 결합
HarmonyOS Next는 더 나은 접근 방식을 제공합니다: Tabs 컴포넌트를 together Navigation 컴포넌트와 함께 사용합니다. 이 조합을 사용하면:
- 탭 바를 화면 하단에 고정할 수 있습니다.
- 각 탭마다 독립적인 네비게이션 스택을 가질 수 있습니다.
따라서 탭 바가 화면에 남아 있는 동안 탭 within 상세 화면으로 이동할 수 있습니다.
왜 Navigation을 Router보다 선택해야 할까?
코드에 들어가기 전에, 이 시나리오에서 Navigation이 선호되는 이유를 명확히 하겠습니다.
*“애플리케이션에서 라우팅 프레임워크로 **Component Navigation (Navigation)*을 사용하도록 권장합니다. 이는 향상된 기능과 커스터마이징 능력을 제공합니다.” – Official HarmonyOS documentation
- Router – 다른 페이지 간 전환을 수행합니다.
- Navigation – 단일 페이지 내에서 컴포넌트‑레벨 라우팅을 처리합니다.
지속적인 탭 패턴은 같은 페이지(탭을 포함하는 페이지)에 머무르는 것을 요구하기 때문에, Navigation이 적합한 도구입니다. 탭 바는 그대로 두고 탭의 콘텐츠 영역만 변경하며, 복잡한 UI 흐름을 보다 세밀하게 제어할 수 있습니다.
지속적인 탭 UI 만들기: 단계별
단계 1 – 메인 탭 구조 설정
엔트리 컴포넌트를 생성하여 Tabs 컴포넌트를 호스트합니다. 이것이 애플리케이션의 루트가 됩니다.
// Index.ets (Main Tabs Component)
@Entry
@Component
export struct Index {
build() {
Tabs({ barPosition: BarPosition.End }) { // Tabs at the bottom
// Tab A – will use Navigation for its content
TabContent() {
NavigationExample() // Custom component for Tab A
}.tabBar('A') // Tab label
// Tab B – simple placeholder
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#007DFF')
}.tabBar('B')
// Tab C – simple placeholder
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#FFBF00')
}.tabBar('C')
// Tab D – simple placeholder
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#E67C92')
}.tabBar('D')
}.barBackgroundColor('#ffb9fdf4') // Tab bar background color
}
}
핵심 포인트
@Entry는 이 컴포넌트를 앱의 진입점으로 지정합니다.Tabs({ barPosition: BarPosition.End })는 하단에 탭 바를 생성합니다.- 탭 A는 사용자 정의
NavigationExample컴포넌트를 포함하며, 자체 내비게이션 스택을 관리합니다. - 탭 B‑D는 시연용으로 색상이 입힌 간단한 컬럼입니다.
단계 2 – 탭 내부 내비게이션 구현
이제 탭 A 안에 들어갈 컴포넌트를 생성합니다. 이 컴포넌트는 자체 내비게이션 히스토리를 보유합니다.
// NavigationExample.ets (Content for Tab A)
@Component
struct NavigationExample {
// Dummy data for a simple list
private arr: number[] = [1, 2, 3];
// Provide a NavPathStack so child pages can access navigation state
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
// -------------------------------------------------
// Step 2.1 – Define the Navigation container
// -------------------------------------------------
build() {
Navigation({
// Optional: customize transition animations, etc.
}) {
// Page 1 – List view
Page() {
Column() {
ForEach(this.arr, (item) => {
Button(`Item ${item}`)
.onClick(() => {
// Push a new page onto the navigation stack
this.pageInfos.push({
name: `DetailPage${item}`,
params: { id: item }
});
});
})
}
}
// Page 2 – Detail view (generated dynamically)
Page({ name: (info) => info.name }) {
Column() {
Text(`Detail for item ${this.pageInfos.current?.params?.id}`)
Button('Back')
.onClick(() => this.pageInfos.pop());
}
}
}
}
}
설명
@Provide('pageInfos')는 이 탭의 내비게이션 히스토리를 저장하는 NavPathStack 인스턴스를 생성합니다.Navigation컴포넌트는 스택 기반 내비게이션 흐름을 정의합니다:- 페이지 1은 아이템 리스트를 보여줍니다. 아이템을 클릭하면 디테일 페이지가 스택에 푸시됩니다.
- 페이지 2는 선택된 아이템의 상세 정보를 표시하고, 스택을 팝하는 Back 버튼을 제공합니다.
내비게이션이 NavigationExample 컴포넌트 내부에서 이루어지기 때문에, 외부에 있는 Tabs 컴포넌트는 사라지지 않습니다.
요약
| ✅ 달성한 목표 | 📌 구현 방법 |
|---|---|
| Persistent bottom tab bar | Placed Tabs at the app root (Index.ets) |
| Independent navigation per tab | Used Navigation inside a tab (NavigationExample.ets) |
| Seamless deep navigation without losing tabs | Managed navigation state with NavPathStack and @Provide |
Source: (원본 링크가 제공되지 않았습니다.)
탐색 목적지
// This @Builder function tells Navigation what content to show for each path.
@Builder
pageMap(name: string) {
NavDestination() {
if (name === '1') {
Text("NavDestinationContent " + name)
} else if (name === '2') { // Example detail page for '2'
Text("Detail Content for Item " + name)
} else if (name === '3') { // Example detail page for '3'
Text("Another Detail Content for Item " + name)
} else {
// Fallback for other items, showing their number
Text("NavDestination Content for Item " + name)
}
}
.title("NavDestinationTitle " + name) // Title for the navigation bar
.onShown(() => {
console.info("show NavDestinationTitle " + name)
})
.onHidden(() => {
console.info("hide NavDestinationTitle " + name)
})
}
// Step 2.2: Building the Navigation Container and its Initial Content
build() {
Column() {
// The Navigation component itself
Navigation(this.pageInfos) { // It uses our NavPathStack for managing history
List({ space: 12 }) { // Initial content: A list of items
ForEach(this.arr, (item: number) => {
ListItem() {
Button("Go to Child " + item)
.width('100%')
.onClick(() => {
// When button is clicked, push a new path onto the Navigation stack
this.pageInfos.pushPathByName(item.toString(), '')
})
}
})
}
}
.navDestination(this.pageMap) // Link Navigation to our pageMap for destination definitions
}
.height('100%')
.width('100%')
}
NavigationExample의 핵심 부분 분석
-
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()–
이 라인은 매우 중요합니다.NavPathStack은Navigation이 이 탭 내에서 화면 기록을 관리하는 데 사용됩니다.@Provide는Navigation이 올바르게 설정되도록 보장합니다. -
pageMap([@builder](https://dev.to/builder))–
이것을 콘텐츠 맵이라고 생각하면 됩니다. 정의한 각 “경로”(예:'2'또는'3')에 대해Navigation이 어떤 UI(NavDestination)를 표시할지 알려줍니다. -
Navigation(this.pageInfos) { … }–
이것이Navigation컨테이너이며,{}안에 들어 있는 초기 콘텐츠가 사용자가 이 탭을 열었을 때 처음 보는 내용입니다. -
Button().onClick(…)–
버튼을 탭하면this.pageInfos.pushPathByName()이 호출되어pageMap에 정의된 새로운 “화면”을Navigation스택에 추가합니다. 이를 통해 메인 탭을 떠나지 않고 새로운 콘텐츠를 표시할 수 있습니다.
출력
결과: 원활한 사용자 경험
이 설정을 사용하면 사용자가 Tab A에 있을 때 목록이 표시됩니다. **“Go to Child 1,”**을 클릭하면 화면 상단에 Child 1의 상세 내용이 표시되지만, **tab bar (A, B, C, D)**는 여전히 하단에 보입니다. 그런 다음 사용자는 Tab A의 기록 내에서 쉽게 뒤로 이동하거나 Tab B, C, 또는 D로 전환하여 컨텍스트를 잃지 않을 수 있습니다.
이 접근 방식의 장점
- 항상 표시되는 탭 – 앱의 주요 탭이 화면에 계속 표시되어 일관된 탐색을 제공합니다.
- 독립적인 탐색 – 각 탭은 자체 히스토리 스택을 가질 수 있어 복잡한 앱을 더 쉽게 관리하고 이해할 수 있습니다.
- 향상된 사용자 흐름 – 사용자는 탭 내에서 깊은 콘텐츠를 탐색한 후 다른 주요 섹션으로 전환해도 길을 잃은 느낌이 없습니다.
- 깨끗한 코드 – 탭별로 탐색 로직을 분리함으로써 코드베이스가 더 체계적이고 유지보수가 쉬워집니다.
결론
HarmonyOS Next의 Tabs와 Navigation 컴포넌트를 지능적으로 결합하면 사라지는 탭 바라는 일반적인 문제를 극복할 수 있습니다. 이 접근 방식은 UI 요소가 고정된 상태로 유지되는 앱을 쉽고 강력하게 구축할 수 있게 하여 전체 사용자 경험을 크게 향상시킵니다.
참고 자료
작성자: Muhammet Ali Ilgaz
