Skip to Content
PatternsApp Shell (ui-cals)

App Shell (ui-cals)

@vortex/ui-cals의 내비게이션 스토어(createNavigationStore)와 화면 유지 컴포넌트(CachedOutlet) 사용법입니다. 메뉴 트리·열린 탭·breadcrumb·즐겨찾기 상태를 하나의 zustand 스토어로 다룹니다.


개요

이름역할
createNavigationStore옵션으로 탭 키·개수·저장·닫기 규칙을 정해 내비게이션 스토어를 만든다
CachedOutlet방문한 화면을 숨겨 유지하고, 탭에서 빠진 화면을 지운다 (선택)

메뉴 데이터 출처, 화면 메타, 권한, 이벤트 실행은 스토어가 다루지 않습니다. 앱이 옵션 콜백으로 연결합니다.


스토어 만들기

import { createNavigationStore, type NavigationItem } from "@vortex/ui-cals" export const useAppMenu = createNavigationStore()

기본값은 개수 제한 없음, 저장 안 함, 즐겨찾기 끔입니다. 필요한 것만 옵션으로 켭니다.

옵션

옵션타입기본값설명
getKey(item: T) => stringitem.url탭·즐겨찾기·findByKey·breadcrumb의 키
maxTabsnumber제한 없음최대 탭 수
overflow"drop-oldest" | "reject""drop-oldest"최대 탭 수를 넘을 때 오래된 탭을 닫을지, 새 탭을 막을지
bookmarksbooleanfalse즐겨찾기 사용
persist{ key; include?; storage? }저장 안 함저장소 키·저장할 상태·저장소
onBeforeClose(tab: T) => boolean-false면 그 탭은 닫히지 않는다
onClose(tabs: T[]) => void-탭이 빠진 뒤 빠진 탭으로 한 번 부른다(모든 닫기 경로)
isPinned(tab: T) => boolean-닫히지 않는 탭
closeDialogScopebooleantrue탭이 빠질 때 그 탭 키 scope의 dialog를 닫는다
getBreadcrumb(key: string, items: T[]) => T[]메뉴 트리 조상breadcrumb 계층 규칙
createRouteTab(url: string, title?: string) => T | null경로·제목으로 만든 항목메뉴에 없는 경로의 탭. null이면 열지 않는다

상태와 액션

이름설명
items / setMenuItems(items)메뉴 트리
tabs열린 탭
push(item)탭을 연다. 같은 키가 있으면 그대로 둔다. 탭이 열려 있으면 true
close(key)키로 탭을 닫는다. { closed, next }를 돌려준다
update(key, patch)탭을 제자리에서 고친다. 없거나 바뀐 키가 겹치면 false
closeOthers(key) / closeAll()닫을 수 있는 탭만 닫고, 닫힌 탭을 돌려준다
findByKey(key) / findByUrl(url)메뉴 트리에서 항목을 찾는다
openFromRoute(url, title?)경로로 탭을 연다. 메뉴에 있으면 그 항목, 없으면 createRouteTab 결과
getBreadcrumbItems(key)breadcrumb 항목(label·href·type)
bookmarks, toggleBookmark(item) 등즐겨찾기. bookmarks: true일 때만 동작
setMaxTabs(n)최대 탭 수를 바꾼다. 0이면 제한 없음

설정 예시

키와 닫기 규칙을 앱이 정하는 탭

화면마다 고유 ID가 있고, 같은 경로라도 ID가 다르면 다른 탭으로 엽니다. 닫기 전 확인과 고정 탭, 저장 키를 앱이 정합니다.

interface ScreenTab extends NavigationItem { id: string items?: ScreenTab[] } export const useScreenTabs = createNavigationStore<ScreenTab>({ getKey: (tab) => tab.id, onBeforeClose: (tab) => !isBusy(tab.id), // 앱의 판단 isPinned: (tab) => tab.url === "/", persist: { key: "my-app:tabs" }, })

URL 없는 탭

경로 이동 없이 내부 상태로만 화면을 바꾸는 편집기형 앱입니다. 키를 ID로 두면 url이 없어도 탭이 열립니다.

const useEditorTabs = createNavigationStore<ScreenTab>({ getKey: (tab) => tab.id }) useEditorTabs.getState().push({ id: "doc-1", title: "문서 1" })

기본 설정

const useSimpleMenu = createNavigationStore() // 키 url, 제한 없음, 저장 안 함

탭 닫기와 다음 탭

close는 이동하지 않습니다. 닫힌 탭이 활성 탭이었다면 돌려받은 next로 앱이 이동합니다. 키 기본값이 url이라 아래 예시는 url을 키로 넘깁니다.

const { pathname, navigate } = useRouterContext() function closeTab(key: string) { const wasActive = key === pathname const { closed, next } = useAppMenu.getState().close(key) if (closed && wasActive) navigate(next?.url ?? "/") }

탭 바 잇기 (TagView)

닫기 확인은 스토어 onBeforeClose 한 곳에 둡니다. TagView는 닫기 요청을 스토어에 넘기기만 합니다.

import { TagView } from "@vortex/ui-cals" const tabs = useScreenTabs((s) => s.tabs) const close = useScreenTabs((s) => s.close) <TagView items={tabs.map((t) => ({ value: t.id, label: t.title }))} // value는 스토어 키 closable onClose={(v) => close(v)} />

제자리 수정과 닫힌 뒤 정리

// 제목만 바꾼다. 탭 순서는 그대로다 useScreenTabs.getState().update("doc-1", { title: "문서 1 (수정됨)" }) // 어느 경로로 닫혀도(닫기·다른 탭 닫기·전체 닫기·최대 개수 정리) 한 번 불린다 createNavigationStore<ScreenTab>({ getKey: (tab) => tab.id, onClose: (tabs) => tabs.forEach((t) => releaseScreen(t.id)), // 앱의 정리 })

url 메서드

findByUrl·openFromRoute·toggleBookmarkByUrl은 라우팅 연동용입니다. 경로에서 탭을 찾거나 열 때 쓰고, 탭 조작은 키로 합니다.


경로가 바뀔 때 탭 열기

CachedOutlet 없이 라우터 출력만 쓰는 앱은 경로가 바뀔 때 직접 openFromRoute를 부릅니다. 라우터 출력은 useRouterContext()의 outlet으로 받습니다.

import { useEffect } from "react" import { useRouterContext } from "@vortex/ui-cals" function RouteOutlet() { const { pathname, routeTitle, outlet } = useRouterContext() const openFromRoute = useAppMenu((s) => s.openFromRoute) useEffect(() => { openFromRoute(pathname, routeTitle) }, [openFromRoute, pathname, routeTitle]) return outlet }

탭을 경로가 아닌 기준(화면 메타 등)으로 여는 앱은 이 effect 대신 자기 시점에 push합니다.


화면 유지 (CachedOutlet)

기본은 라우터 출력입니다(위 「경로가 바뀔 때 탭 열기」). 화면을 바꿀 때마다 새로 마운트되고, 남겨야 할 상태는 앱 스토어에 둡니다.

탭을 바꿔도 입력값·스크롤을 컴포넌트 인스턴스째 남겨야 할 때만 CachedOutlet을 씁니다. 경로가 바뀌면 store의 openFromRoute를 부르고, 탭에서 빠진 경로의 화면을 지웁니다.

<ViteRouterAdapter> <CachedOutlet store={useAppMenu} /> </ViteRouterAdapter>

store는 필수입니다. createNavigationStore로 만든 스토어를 넘깁니다.

캐시는 경로 기준입니다. url이 있는 탭만 화면이 유지되고, url이 같은 탭은 화면 하나를 공유합니다.

Last updated on