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

발행: (2026년 1월 16일 오후 06:35 GMT+9)
12 min read
원문: Dev.to

Source: Dev.to

탭 개요

소개

HarmonyOS NextArkUI/ArkTS로 앱을 개발하고 있다면, 부드러운 사용자 경험이 얼마나 중요한지 잘 알고 있을 것입니다. 개발자들이 흔히 겪는 문제 중 하나는 사용자가 다른 화면으로 이동할 때 탭 바가 보이지 않게 되는 상황입니다.

이 글에서는 다음을 다룹니다:

  1. 라우터를 사용할 때 탭이 사라지는 이유를 설명합니다.
  2. TabsNavigation 컴포넌트를 결합한 견고한 해결책을 제시합니다.

문제: 탐색 시 탭 사라짐

ArkUI에서는 Tabs 컴포넌트를 사용해 Home, Profile, Settings 등과 같은 섹션을 만들곤 합니다. 탭은 화면 하단에 위치해 사용자가 주요 뷰 간에 전환할 수 있게 합니다.

탭만 전환할 때는 모든 것이 정상적으로 동작합니다. 문제는 탭 내부에 있는 버튼을 눌러 ArkTS의 router를 사용해 새로운 페이지로 이동할 때 발생합니다.

Tabs disappearing on navigation

왜 이런 현상이 발생할까요?
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 barPlaced Tabs at the app root (Index.ets)
Independent navigation per tabUsed Navigation inside a tab (NavigationExample.ets)
Seamless deep navigation without losing tabsManaged 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%')
}
  • @Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()
    이 라인은 매우 중요합니다. NavPathStackNavigation이 이 탭 내에서 화면 기록을 관리하는 데 사용됩니다. @ProvideNavigation이 올바르게 설정되도록 보장합니다.

  • pageMap([@builder](https://dev.to/builder))
    이것을 콘텐츠 맵이라고 생각하면 됩니다. 정의한 각 “경로”(예: '2' 또는 '3')에 대해 Navigation이 어떤 UI(NavDestination)를 표시할지 알려줍니다.

  • Navigation(this.pageInfos) { … }
    이것이 Navigation 컨테이너이며, {} 안에 들어 있는 초기 콘텐츠가 사용자가 이 탭을 열었을 때 처음 보는 내용입니다.

  • Button().onClick(…)
    버튼을 탭하면 this.pageInfos.pushPathByName()이 호출되어 pageMap에 정의된 새로운 “화면”을 Navigation 스택에 추가합니다. 이를 통해 메인 탭을 떠나지 않고 새로운 콘텐츠를 표시할 수 있습니다.

출력

son.gif

결과: 원활한 사용자 경험

이 설정을 사용하면 사용자가 Tab A에 있을 때 목록이 표시됩니다. **“Go to Child 1,”**을 클릭하면 화면 상단에 Child 1의 상세 내용이 표시되지만, **tab bar (A, B, C, D)**는 여전히 하단에 보입니다. 그런 다음 사용자는 Tab A의 기록 내에서 쉽게 뒤로 이동하거나 Tab B, C, 또는 D로 전환하여 컨텍스트를 잃지 않을 수 있습니다.

이 접근 방식의 장점

  • 항상 표시되는 탭 – 앱의 주요 탭이 화면에 계속 표시되어 일관된 탐색을 제공합니다.
  • 독립적인 탐색 – 각 탭은 자체 히스토리 스택을 가질 수 있어 복잡한 앱을 더 쉽게 관리하고 이해할 수 있습니다.
  • 향상된 사용자 흐름 – 사용자는 탭 내에서 깊은 콘텐츠를 탐색한 후 다른 주요 섹션으로 전환해도 길을 잃은 느낌이 없습니다.
  • 깨끗한 코드 – 탭별로 탐색 로직을 분리함으로써 코드베이스가 더 체계적이고 유지보수가 쉬워집니다.

결론

HarmonyOS Next의 TabsNavigation 컴포넌트를 지능적으로 결합하면 사라지는 탭 바라는 일반적인 문제를 극복할 수 있습니다. 이 접근 방식은 UI 요소가 고정된 상태로 유지되는 앱을 쉽고 강력하게 구축할 수 있게 하여 전체 사용자 경험을 크게 향상시킵니다.

참고 자료

작성자: Muhammet Ali Ilgaz

Back to Blog

관련 글

더 보기 »