Menu Store (useMenu)
iCignal 레이아웃의 탭 네비게이션, 브레드크럼, 북마크를 관리하는 Zustand 스토어입니다.
목차
개요
useMenu는 @vortex/ui-icignal에서 제공하는 Zustand 스토어로, 다음 기능을 통합 관리합니다.
| 기능 | 설명 |
|---|---|
| 탭 관리 | 열린 페이지를 브라우저 탭처럼 관리 |
| 메뉴 검색 | 계층적 메뉴 트리에서 URL로 항목 검색 |
| 브레드크럼 | pathname 기반 브레드크럼 자동 생성 |
| 북마크 | 즐겨찾기 페이지 추가/제거/토글 |
| 영속성 | localStorage에 탭과 북마크 상태를 저장 |
설치
@vortex/ui-icignal 패키지에 포함되어 있습니다.
import { useMenu } from "@vortex/ui-icignal"기본 사용법
Store 구독 (Selector 패턴)
// 필요한 값만 구독 (권장)
const tabs = useMenu((state) => state.tabs)
const push = useMenu((state) => state.push)
// 여러 값 동시 구독
const { tabs, push, pop } = useMenu((state) => ({
tabs: state.tabs,
push: state.push,
pop: state.pop,
}))메뉴 아이템 초기화
앱 진입 시 사이드바 메뉴 데이터를 스토어에 등록합니다.
import { useMenu, type MenuItem } from "@vortex/ui-icignal"
const menuItems: MenuItem[] = [
{
type: "link",
title: "대시보드",
url: "/dashboard",
items: [
{ title: "개요", url: "/dashboard/overview" },
{ title: "분석", url: "/dashboard/analytics" },
],
},
{
type: "page",
title: "사용자 관리",
url: "/users",
},
]
function AppLayout() {
const setMenuItems = useMenu((state) => state.setMenuItems)
useEffect(() => {
setMenuItems(menuItems)
}, [setMenuItems])
return <>{/* ... */}</>
}탭 관리
열린 페이지를 브라우저 탭처럼 관리합니다. 탭 상태는 localStorage에 자동 저장됩니다.
탭 추가 (push)
const push = useMenu((state) => state.push)
// 탭 추가 (이미 열린 URL이면 무시)
push({ title: "대시보드", url: "/dashboard" })- 동일 URL의 탭이 이미 있으면 중복 추가하지 않습니다.
maxTabs를 초과하면 가장 오래된 탭이 자동 제거됩니다.
탭 닫기 (pop)
const pop = useMenu((state) => state.pop)
// 특정 인덱스의 탭 닫기
pop(2)
// 마지막 탭 닫기 (인덱스 생략)
pop()다른 탭 모두 닫기 (closeOthers)
const closeOthers = useMenu((state) => state.closeOthers)
// 인덱스 1번 탭만 남기고 나머지 닫기
closeOthers(1)전체 탭 닫기 (closeAll)
const closeAll = useMenu((state) => state.closeAll)
closeAll()최대 탭 수 설정 (setMaxTabs)
const setMaxTabs = useMenu((state) => state.setMaxTabs)
// 최대 15개 탭까지 허용 (기본값: 10)
setMaxTabs(15)메뉴 검색
URL로 메뉴 아이템 찾기 (findByUrl)
계층적 메뉴 트리를 재귀 탐색하여 URL이 일치하는 항목을 반환합니다.
const findByUrl = useMenu((state) => state.findByUrl)
const menuItem = findByUrl("/dashboard/analytics")
// → { title: "분석", url: "/dashboard/analytics" } 또는 undefined검색 쿼리 (setSearchQuery)
메뉴 검색 UI에 연결할 수 있는 검색 쿼리 상태입니다.
const searchQuery = useMenu((state) => state.searchQuery)
const setSearchQuery = useMenu((state) => state.setSearchQuery)
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="메뉴 검색..."
/>브레드크럼
자동 브레드크럼 생성 (getBreadcrumbItems)
현재 pathname을 세그먼트별로 분해하고, 각 경로에 매칭되는 메뉴 항목의 제목을 가져와 브레드크럼을 자동 생성합니다.
import { useMenu } from "@vortex/ui-icignal"
import { Breadcrumb } from "@vortex/ui-icignal"
function AppBreadcrumb() {
const getBreadcrumbItems = useMenu((state) => state.getBreadcrumbItems)
const pathname = usePathname()
const items = getBreadcrumbItems(pathname)
// 예: pathname="/dashboard/analytics"
// → [
// { label: "대시보드", href: "/dashboard", type: "link" },
// { label: "분석", href: "/dashboard/analytics", type: "page" },
// ]
return <Breadcrumb items={items} />
}
getBreadcrumbItems는setMenuItems로 등록된 메뉴 트리를 기반으로 동작합니다. 메뉴에 등록되지 않은 경로 세그먼트는 브레드크럼에서 건너뜁니다.
북마크
즐겨찾기 페이지를 관리합니다. 북마크 상태는 localStorage에 자동 저장됩니다.
북마크 추가/제거
const addBookmark = useMenu((state) => state.addBookmark)
const removeBookmark = useMenu((state) => state.removeBookmark)
// 북마크 추가
addBookmark({ title: "대시보드", url: "/dashboard" })
// 북마크 제거
removeBookmark("/dashboard")북마크 토글
const toggleBookmark = useMenu((state) => state.toggleBookmark)
// 있으면 제거, 없으면 추가
toggleBookmark({ title: "대시보드", url: "/dashboard" })URL로 북마크 토글 (toggleBookmarkByUrl)
메뉴 트리에서 URL에 해당하는 항목을 찾아 자동으로 title을 채워줍니다.
const toggleBookmarkByUrl = useMenu((state) => state.toggleBookmarkByUrl)
// 메뉴에서 자동으로 title을 찾아서 토글
toggleBookmarkByUrl("/dashboard")북마크 여부 확인 (isBookmarked)
const isBookmarked = useMenu((state) => state.isBookmarked)
<Button
variant={isBookmarked("/dashboard") ? "filled" : "outlined"}
onClick={() => toggleBookmarkByUrl("/dashboard")}
>
{isBookmarked("/dashboard") ? "★" : "☆"}
</Button>CachedOutlet 연동
useMenu는 CachedOutlet 컴포넌트와 함께 사용되어 페이지 캐싱 + 탭 자동 관리를 제공합니다.
동작 흐름
- 사용자가 페이지를 이동하면
CachedOutlet이findByUrl로 메뉴 항목을 찾음 - 찾은 메뉴 항목을
push로 탭에 자동 추가 - 방문한 페이지 컴포넌트를 메모리에 캐싱 (display:none으로 숨김)
- 탭을 닫으면 해당 페이지의 캐시도 함께 제거
import { CachedOutlet } from "@vortex/ui-icignal"
// 라우트 맵 기반 (Next.js)
const routes = {
"/dashboard": DashboardPage,
"/users": UsersPage,
"/settings": SettingsPage,
}
function MainContent() {
return <CachedOutlet routes={routes} fallback={NotFoundPage} />
}TabNavigation과의 조합
useMenu의 탭 상태를 TabNavigation 컴포넌트와 연결하면 완전한 탭 기반 네비게이션을 구현할 수 있습니다.
import { useMenu, TabNavigation } from "@vortex/ui-icignal"
import { useRouter, usePathname } from "next/navigation"
function AppTabs() {
const tabs = useMenu((state) => state.tabs)
const pop = useMenu((state) => state.pop)
const router = useRouter()
const pathname = usePathname()
const tabItems = tabs.map((tab) => ({
value: tab.url,
label: tab.title,
}))
return (
<TabNavigation
items={tabItems}
value={[pathname]}
onValueChange={([url]) => router.push(url)}
closable
onClose={(url) => {
const index = tabs.findIndex((t) => t.url === url)
if (index !== -1) pop(index)
}}
/>
)
}API Reference
State
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
items | MenuItem[] | [] | 전체 메뉴 트리 |
tabs | MenuItem[] | [] | 열린 탭 목록 |
maxTabs | number | 10 | 최대 탭 수 |
searchQuery | string | "" | 메뉴 검색 쿼리 |
bookmarks | BookmarkItem[] | [] | 북마크 목록 |
Actions
| 메서드 | 시그니처 | 설명 |
|---|---|---|
push | (item: MenuItem) => void | 탭 추가 (중복 무시) |
pop | (index?: number) => void | 탭 닫기 (인덱스 또는 마지막) |
closeOthers | (index: number) => void | 지정 탭 외 모두 닫기 |
closeAll | () => void | 전체 탭 닫기 |
findByUrl | (url: string) => MenuItem | undefined | URL로 메뉴 아이템 검색 |
setMaxTabs | (maxTabs: number) => void | 최대 탭 수 설정 |
setMenuItems | (items: MenuItem[]) => void | 메뉴 트리 설정 |
setSearchQuery | (query: string) => void | 검색 쿼리 설정 |
getBreadcrumbItems | (pathname: string) => BreadcrumbItemProps[] | 브레드크럼 아이템 생성 |
addBookmark | (item: BookmarkItem) => void | 북마크 추가 |
removeBookmark | (url: string) => void | 북마크 제거 |
toggleBookmark | (item: BookmarkItem) => void | 북마크 토글 |
toggleBookmarkByUrl | (url: string) => void | URL로 북마크 토글 |
isBookmarked | (url: string) => boolean | 북마크 여부 확인 |
MenuItem
| 속성 | 타입 | 설명 |
|---|---|---|
type | "link" | "page" | "ellipsis" | 메뉴 타입 (선택) |
title | string | 메뉴 제목 (필수) |
url | string | 메뉴 URL (필수) |
icon | ReactNode | 아이콘 (선택) |
items | MenuItem[] | 하위 메뉴 (선택) |
BookmarkItem
| 속성 | 타입 | 설명 |
|---|---|---|
title | string | 페이지 제목 |
url | string | 페이지 URL |
영속성 (persist)
useMenu는 Zustand persist 미들웨어를 사용하며, 다음 상태만 localStorage에 저장합니다.
| 저장 항목 | localStorage 키 |
|---|---|
tabs | icignal:menu |
bookmarks | icignal:menu |
items,maxTabs,searchQuery는 저장되지 않습니다. 앱 시작 시setMenuItems로 메뉴를 다시 설정해야 합니다.
Best Practices
✅ 권장 사항
| 영역 | 권장 사항 |
|---|---|
| Selector | useMenu((s) => s.tabs) 형태로 필요한 값만 구독 |
| 초기화 | 레이아웃 최상위에서 setMenuItems 1회 호출 |
| CachedOutlet | 탭 + 페이지 캐싱이 필요하면 CachedOutlet과 함께 사용 |
| 북마크 토글 | 가능하면 toggleBookmarkByUrl로 title 자동 매칭 |
| maxTabs | 성능을 위해 적절한 상한 설정 (기본 10) |
⚠️ 피해야 할 것
setMenuItems를 매 렌더링마다 호출 →useEffect내에서 1회만 호출- 전체 store 구독 → Selector 패턴으로 필요한 값만 구독
findByUrl결과를 캐싱 없이 렌더링마다 호출 → 변수에 저장 후 사용
관련 문서
- TabNavigation - 탭 UI 컴포넌트
- State Management - Zustand 패턴 가이드
- Navigation Patterns - 네비게이션 구현 패턴