---
title: 메뉴 (네비게이션) 연동
description: 어드민 "디자인 > 메뉴"에서 관리하는 네비게이션을 헤더/푸터에 렌더
---

# 메뉴 (네비게이션) 연동

어드민(mysite.roottale.com)의 **디자인 > 메뉴**에서 저장한 네비게이션 트리를
사이트의 헤더·푸터에 렌더합니다. 고객이 직접 메뉴 항목(이름·주소·순서·하위
항목)을 바꿀 수 있어 코드 수정 없이 네비가 갱신됩니다.

- 메뉴는 **위치 핸들(slug)** 로 구분합니다 — 관례: `primary`(헤더), `footer`(푸터).
- 항목은 깊이 2 트리 (상위 + 드롭다운 1단).
- `url` 은 상대 경로(`/about`) 또는 절대 `http(s)` URL — 서버가 위험 스킴을
  제거한 안전한 값만 내려줍니다.
- 메뉴 저장 시 발행 웹훅(`post.updated`, paths: `["/"]`)이 발송되므로
  웹훅 수신 라우트(`revalidation-webhooks.md`)를 설정했다면 몇 초 내 반영됩니다.

## 서버 컴포넌트에서 메뉴 가져오기

```tsx
// components/site-nav.tsx (Server Component)
import Link from "next/link";
import { fetchMenu } from "@roottale/cms-client/server";

export async function SiteNav() {
  const menu = await fetchMenu({
    apiKey: process.env.ROOTTALE_API_KEY!,
    slug: "primary",
  });

  // 메뉴 미설정(null)이면 자체 fallback 네비를 렌더하세요.
  if (!menu) {
    return (
      <nav>
        <Link href="/blog">블로그</Link>
      </nav>
    );
  }

  return (
    <nav>
      {menu.items.map((item) => (
        <div key={item.id}>
          <Link
            href={item.url}
            {...(item.newTab
              ? { target: "_blank", rel: "noopener" }
              : {})}
          >
            {item.label}
          </Link>
          {item.children?.length ? (
            <div>
              {item.children.map((child) => (
                <Link key={child.id} href={child.url}>
                  {child.label}
                </Link>
              ))}
            </div>
          ) : null}
        </div>
      ))}
    </nav>
  );
}
```

`fetchMenu` 는 미존재/구 서버의 404 를 `null` 로 돌려주므로(fail-soft) 항상
fallback 분기를 두세요. 전체 메뉴 목록이 필요하면 `fetchMenus({ apiKey })`.

## 타입

```ts
interface RootTaleMenu {
  id: string;
  name: string;        // "헤더 메뉴"
  slug: string;        // "primary" | "footer" | ...
  items: RootTaleMenuItem[];
  updatedAt: string | null;
}

interface RootTaleMenuItem {
  id: string;
  label: string;
  url: string;         // "/about" 또는 "https://..."
  newTab?: boolean;    // target="_blank" rel="noopener" 로 렌더
  children?: RootTaleMenuItem[];
}
```

## 캐싱

메뉴 응답은 5분 캐시(+SWR)로 내려갑니다. 페이지가 ISR 이라면 발행 웹훅이
`/` 를 revalidate 할 때 함께 갱신되므로 별도 처리 없이 near-real-time 입니다.

## raw HTTP

JS 외 스택은 `GET /v1/cms/public/menus/{slug}` 를 직접 호출하세요 —
`api-reference.md` 참고.
