Appearance
Mobile Navigation
@jennifersoft/apm-components의 모바일 GNB 문서입니다. 이 문서는 @jennifersoft/apm-components@1.4.1 기준으로 정리했습니다.
데스크톱 GNB는 NavigationBar를 참고하세요. 모바일은 하단 탭 바(MobileNavigationBar)와 바텀시트(MobileNavigationSheet) 두 컴포넌트로 구성됩니다.
인터랙티브 데모
IMPORTANT
MobileNavigationSheet는 내부에서 <Teleport to="body"> 를 사용합니다. 따라서 위 프레임 안에 넣어도 시트는 body로 빠져나가 화면 전체를 덮습니다. 데모에서 더보기를 누르면 문서 전체 위에 시트가 뜨는 것이 정상 동작입니다. 조상 요소에 transform이나 overflow: hidden을 줘도 가둘 수 없습니다.
실제 앱에서는 시트가 뷰포트를 덮는 것이 의도된 동작이므로 문제가 되지 않지만, 시트를 특정 영역 안에 가두려는 시도는 통하지 않는다는 점을 알고 있어야 합니다.
메뉴 데이터 계약
typescript
interface MobileNavigationItem {
key: MobileNavigationItemKey;
label: string;
icon: IconTypes; // @jennifersoft/vue-components-v2의 ICON_TYPE
route: string | null; // null이면 라우팅 없이 select만 발생
selected?: boolean;
disabled?: boolean;
badge?: number | string; // 숫자는 99를 넘으면 99+로 표시
spotlight?: boolean; // 강조 표시(예: Chat)
}
const MOBILE_NAVIGATION_ITEM_KEY = {
DASHBOARD: 'dashboard',
CUSTOM_DASHBOARD: 'custom-dashboard',
CHAT: 'chat',
TALK: 'talk',
NOTIFICATIONS: 'notifications',
SETTINGS: 'settings',
MORE: '__more__',
} as const;MobileNavigationItemKey는 위 상수 외에 임의 문자열도 허용합니다(string & {}).
기본 메뉴와 노출 순서
메뉴 구성은 하나로 통일되어 있습니다. mobilePrimaryItems를 넘기지 않으면 아래 내장 기본 메뉴가 그대로 쓰입니다.
typescript
MOBILE_NAVIGATION_ITEM_SEQUENCE
// ['dashboard', 'custom-dashboard', 'talk', 'notifications', 'settings']| key | 기본 라벨 | route |
|---|---|---|
dashboard | Dashboard | /mobile/dashboard |
custom-dashboard | Custom dashboard | /mobile/dashboard/custom |
talk | Talk | /mobile/talk |
notifications | Notifications | /mobile/notifications |
settings | Settings | /mobile/settings |
기본 메뉴는 영문 라벨이므로, 다국어가 필요한 제품 앱은 번역한 메뉴를 직접 넘겨야 합니다.
IMPORTANT
mobilePrimaryItems를 넘겨도 최종 노출 항목과 순서는 mobilePrimaryItemSequence가 결정합니다. 이를 생략하면 위 내장 순서가 쓰이고, 그 순서에 없는 key는 넘겨도 렌더되지 않습니다. 내장 순서에 없는 메뉴(예: chat)를 넣으려면 mobilePrimaryItemSequence를 함께 넘기세요.
vue
<MobileNavigationBar
:mobile-primary-items="primaryItems"
:mobile-primary-item-sequence="['dashboard', 'chat', 'settings']"
/>MobileNavigationBar
하단 고정 탭 바입니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
mobilePrimaryItems | readonly MobileNavigationItem[] | 내장 기본 메뉴 | 탭 바에 노출할 항목 |
mobilePrimaryItemSequence | readonly MobileNavigationItemKey[] | 내장 순서 | 노출 항목과 순서를 결정 |
mobileOverflowItems | readonly MobileNavigationItem[] | undefined | 더보기 시트로 밀어낼 항목 |
selectedKey | MobileNavigationItemKey | null | undefined | 현재 선택 항목 |
sheetOpen | boolean | false | 시트 열림 상태 (v-model) |
mobileMaxBarWidth | number | 512 | 바 최대 폭 |
sheetId | string | 'mobile-navigation-sheet' | aria-controls 연결용 |
ariaLabel | string | 'Mobile primary navigation' | |
class | string | undefined |
이벤트
typescript
emit('select', item: MobileNavigationItem)
emit('open-sheet')
emit('update:sheetOpen', value: boolean)밀도 자동 전환
항목 폭이 MOBILE_NAVIGATION_DENSITY_THRESHOLD(62px) 아래로 좁아지면 density가 'compact'로 자동 전환되어 라벨 표시가 줄어듭니다. ResizeObserver로 관측하므로 호출자가 계산할 필요는 없습니다.
배지는 MOBILE_NAVIGATION_BADGE_MAXIMUM(99)을 넘으면 잘라서 표시합니다.
MobileNavigationSheet
더보기 및 하위 메뉴를 표시하는 바텀시트입니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
open | boolean | — (필수) | 열림 여부 |
stack | readonly MobileNavigationSheetStackItem[] | — (필수) | 탐색 단계 스택 |
dismissOnDrag | boolean | undefined | 드래그로 닫기 허용 |
sheetId | string | 'mobile-navigation-sheet' | |
ariaLabel | string | 'Mobile navigation sheet' | |
backLabel | string | 'Back' | |
closeLabel | string | 'Close' | |
expandable | boolean | false | 그래버를 위로 끌어 전체 높이로 확장 |
expanded | boolean | false | 확장 상태 (v-model) |
dragZoneHeight | number | 0 | 패널 상단에서 드래그를 시작할 수 있는 높이(px) |
class | string | undefined |
typescript
interface MobileNavigationSheetStackItem {
key: string;
title: string;
items: readonly MobileNavigationItem[];
parentKey?: string;
}stack의 마지막 원소가 현재 화면입니다. 하위 메뉴로 들어가면 push, back을 받으면 pop 하는 구조입니다.
이벤트
typescript
emit('select', item: MobileNavigationItem)
emit('back')
emit('close')
emit('update:expanded', expanded: boolean)dragZoneHeight
기본값 0이면 그래버만 잡을 수 있습니다. 시트가 자체 헤더를 그리고 그 영역까지 손잡이로 쓰고 싶을 때, 그래버를 포함한 전체 높이를 지정합니다.
IMPORTANT
드래그 영역 안이라도 버튼 같은 조작 요소를 누른 경우에는 드래그를 시작하지 않습니다. 헤더에 버튼을 배치해도 클릭이 먹히므로 dragZoneHeight를 줄일 필요가 없습니다.
드래그로 패널 높이의 1/3(MOBILE_NAVIGATION_SHEET_DRAG_CLOSE_RATIO) 이상 내리면 닫힙니다.
본문 스크롤 잠금
시트가 열린 동안 배경이 스크롤되지 않도록 잠그는 유틸입니다. 호출 횟수를 세는 방식이라 중첩 호출이 안전하고, 마지막 unlock에서만 원래 값으로 복구합니다.
typescript
import {
lockMobileBodyScroll,
unlockMobileBodyScroll,
} from '@jennifersoft/apm-components';
onMounted(lockMobileBodyScroll);
onUnmounted(unlockMobileBodyScroll);MobileNavigationSheet는 내부에서 이미 사용합니다. 직접 만든 오버레이에만 호출하세요.
사용 예제
vue
<script setup lang="ts">
import { ref, computed } from 'vue';
import {
MobileNavigationBar,
MobileNavigationSheet,
} from '@jennifersoft/apm-components';
import type {
MobileNavigationItem,
MobileNavigationItemKey,
MobileNavigationSheetStackItem,
} from '@jennifersoft/apm-components';
const sheetOpen = ref(false);
const expanded = ref(false);
const selectedKey = ref('dashboard');
const stack = ref<MobileNavigationSheetStackItem[]>([]);
// 노출 항목과 순서는 sequence 가 결정한다. 내장 순서에 의존하지 않도록 직접 지정한다
const primaryItemSequence: MobileNavigationItemKey[] = [
'dashboard',
'talk',
'notifications',
'settings',
];
const primaryItems: MobileNavigationItem[] = [
{ key: 'dashboard', label: '대시보드', icon: ICON_TYPE.dashboard, route: '/mobile/dashboard' },
{ key: 'talk', label: '토크', icon: ICON_TYPE.talk, route: '/mobile/talk' },
{ key: 'notifications', label: '알림', icon: ICON_TYPE.bell, route: '/mobile/notifications' },
{ key: 'settings', label: '설정', icon: ICON_TYPE.settings, route: '/mobile/settings' },
];
const overflowItems: MobileNavigationItem[] = [
{ key: 'about', label: '정보', icon: ICON_TYPE.info, route: null },
];
function onSelect(item: MobileNavigationItem) {
if (item.route === null) return;
selectedKey.value = item.key;
router.push(item.route);
}
function onOpenSheet() {
stack.value = [{ key: 'more', title: '더보기', items: overflowItems }];
sheetOpen.value = true;
}
</script>
<template>
<MobileNavigationBar
:mobile-primary-items="primaryItems"
:mobile-primary-item-sequence="primaryItemSequence"
:mobile-overflow-items="overflowItems"
:selected-key="selectedKey"
v-model:sheet-open="sheetOpen"
@select="onSelect"
@open-sheet="onOpenSheet"
/>
<MobileNavigationSheet
:open="sheetOpen"
:stack="stack"
expandable
v-model:expanded="expanded"
dismiss-on-drag
back-label="뒤로"
close-label="닫기"
@select="onSelect"
@back="stack.pop()"
@close="sheetOpen = false"
/>
</template>상수
typescript
import {
MOBILE_NAVIGATION_ITEM_KEY,
MOBILE_NAVIGATION_MORE_ITEM,
MOBILE_NAVIGATION_DEFAULT_MAX_BAR_WIDTH, // 512
MOBILE_NAVIGATION_DENSITY_THRESHOLD, // 62
MOBILE_NAVIGATION_BADGE_MAXIMUM, // 99
MOBILE_NAVIGATION_DEFAULT_SHEET_ID,
MOBILE_NAVIGATION_DEFAULT_ARIA_LABEL,
MOBILE_NAVIGATION_DEFAULT_SHEET_ARIA_LABEL,
MOBILE_NAVIGATION_DEFAULT_BACK_LABEL,
MOBILE_NAVIGATION_DEFAULT_CLOSE_LABEL,
} from '@jennifersoft/apm-components';NOTE
1.4.1에서 MOBILE_NAVIGATION_TYPE과 mobileNavigationType prop이 제거되었습니다. LLM 사용 여부로 GNB를 나누던 구성이 하나로 통일되면서, MOBILE_NAVIGATION_DEFAULT_ITEMS와 MOBILE_NAVIGATION_ITEM_SEQUENCE도 타입별 맵에서 단일 배열로 바뀌었습니다.
주의사항
MobileNavigationSheet는<Teleport to="body">로 렌더됩니다. 조상의transform/overflow로 가둘 수 없고, 항상 뷰포트를 덮습니다.- 메뉴는
mobilePrimaryItems+mobilePrimaryItemSequence로 결정합니다.1.4.1에서mobileNavigationTypeprop 은 제거되었습니다. - 넘긴 항목이 안 보이면 대부분
mobilePrimaryItemSequence에 그 key가 없어서입니다. - 내장 기본 메뉴 라벨은 영문입니다. 다국어 앱은
mobilePrimaryItems를 직접 번역해 넘기세요. route: null항목은 라우팅 없이select만 발생합니다. 모달 열기 등에 사용합니다.stack을 직접 mutate하지 말고 새 배열로 교체하는 편이 전환 애니메이션과 잘 맞습니다.