테마 전환

shadcn/ui로 관리자 페이지 뼈대 만들기: Sidebar + Layout 모범 사례

Easton editorial illustration: developer problem-solving desk

Sidebar 컴포넌트부터 Next.js Layout 통합까지, 바로 활용할 수 있는 전체 코드로 확장 가능한 관리자 시스템의 뼈대를 단계별로 만들어 봅니다.


지난주 관리자 시스템 프로젝트를 맡으면서 가장 먼저 shadcn/ui가 떠올랐습니다. 솔직히 이전에 Ant Design과 MUI를 사용했을 때는 스타일을 커스터마이징하기가 꽤 번거롭다고 느꼈습니다. 수많은 스타일을 덮어써야 하거나 프레임워크의 디자인 철학에 묶이기 일쑤였습니다.

shadcn/ui는 다릅니다. “Copy-paste” 방식이라 코드가 프로젝트에 직접 들어가고, 원하는 대로 수정할 수 있습니다. 2주 동안 사용해 보니 확실히 매력적이었습니다. 특히 Sidebar 컴포넌트를 Next.js App Router와 함께 사용하면 관리자 페이지의 뼈대를 훨씬 깔끔하게 구축할 수 있습니다.

이 글에서는 제가 실제로 적용한 과정을 정리합니다. 처음부터 시작해 확장 가능한 관리자 레이아웃을 함께 만들어 보겠습니다.


1. 왜 shadcn/ui Sidebar를 선택해야 할까요?

먼저 제가 겪었던 문제부터 이야기해 보겠습니다.

이전에 Ant Design Pro를 사용할 때는 바로 쓸 수 있다는 점이 정말 편했습니다. 하지만 프로젝트가 길어지자 골치 아픈 일이 생기기 시작했습니다. 사이드바 스타일 하나를 바꾸려 해도 문서를 한참 뒤져야 했고, 조금 특별한 상호작용을 구현하려 하면 프레임워크의 제약이 너무 많았습니다. MUI도 비슷했습니다. 테마 커스터마이징이 유연하기는 하지만, Material Design 작성법을 알아야 한다는 전제가 붙습니다.

기존 방식의 문제점

Ant Design: 기능은 완전하지만 커스터마이징 비용이 큽니다. 사이드바 모서리 하나를 둥글게 바꾸려고 스타일을 세 겹이나 덮어써야 할 수도 있습니다.

MUI: 디자인 체계는 완성도가 높지만 학습 곡선이 가파릅니다. Styled Components 작성법에 팀의 새 구성원이 익숙해지려면 일주일은 필요합니다.

직접 구현: 모든 것을 제어할 수 있지만, 반응형 디자인과 접근성, 키보드 탐색까지 지원하는 사이드바를 처음부터 만들려면 아무리 적게 잡아도 사흘은 걸립니다.

shadcn/ui의 해결 방식

shadcn/ui는 다른 길을 택했습니다.

  • Copy-paste 방식: 컴포넌트 코드가 프로젝트에 직접 들어가므로 블랙박스 의존성이 없습니다.
  • Radix UI 기반: 접근성이 내장되어 키보드 탐색과 ARIA 속성 등을 알아서 처리합니다.
  • Tailwind CSS 기반: 클래스 이름으로 스타일을 구성하므로 원하는 대로 바꿀 수 있고 스타일을 덮어쓸 필요도 없습니다.

MUI에서 shadcn/ui로 이전한 팀을 적지 않게 보았습니다. 이유는 간단합니다. 그들이 원하는 것은 즉시 쓸 수 있는 템플릿이 아니라 직접 제어할 수 있는 코드이기 때문입니다.

적합한 사용 사례

다음과 같은 프로젝트를 만들고 있다면:

  • 중소 규모의 관리자 시스템
  • SaaS 제품의 콘솔
  • 내부 도구 또는 운영 플랫폼

shadcn/ui Sidebar를 사용해 볼 만합니다. 완성된 관리자 템플릿을 제공하지는 않지만, 충분히 유연한 뼈대를 제공합니다.


2. Sidebar 컴포넌트 아키텍처 살펴보기

직접 구현하기 전에 shadcn/ui Sidebar의 컴포넌트 체계를 이해해 보겠습니다. 공식 문서에서도 비교적 명확하게 설명된 부분이므로 여기서는 빠르게 훑어보겠습니다.

핵심 컴포넌트 목록

Sidebar는 역할이 명확히 나뉜 여러 컴포넌트로 구성됩니다.

SidebarProvider   // 애플리케이션 전체를 감싸는 상태 컨텍스트
Sidebar          // 사이드바 컨테이너
SidebarHeader    // Logo를 배치하는 상단 고정 영역
SidebarContent   // 메뉴를 배치하는 스크롤 가능 콘텐츠 영역
SidebarGroup     // 메뉴 그룹
SidebarMenu      // 메뉴 목록
SidebarMenuItem  // 메뉴 항목
SidebarMenuButton // Link를 지원하는 메뉴 버튼
SidebarFooter    // 사용자 정보를 배치하는 하단 고정 영역
SidebarTrigger   // 접기/펼치기 버튼
SidebarInset     // 메인 콘텐츠 영역 래퍼

컴포넌트가 많아 보여도 관계는 명확합니다.

SidebarProvider
├── Sidebar
│   ├── SidebarHeader
│   ├── SidebarContent
│   │   └── SidebarGroup
│   │       └── SidebarMenu
│   │           └── SidebarMenuItem
│   │               └── SidebarMenuButton
│   └── SidebarFooter
└── SidebarInset
    └── {children}

상태 관리 방식

Sidebar의 접힘 상태는 SidebarProvider가 관리합니다. 두 가지 방식이 있습니다.

비제어 모드(권장):

<SidebarProvider defaultOpen={true}>
  <Sidebar />
</SidebarProvider>

제어 모드:

const [open, setOpen] = useState(true);

<SidebarProvider open={open} onOpenChange={setOpen}>
  <Sidebar />
</SidebarProvider>

대부분의 경우에는 비제어 모드로 충분합니다. 사용자 설정에 스위치가 있는 경우처럼 다른 곳에서 Sidebar 상태를 제어해야 할 때만 제어 모드를 사용하면 됩니다.

반응형 디자인의 원리

Sidebar에는 반응형 기능이 내장되어 있습니다.

  • 데스크톱: 사이드바가 왼쪽에 고정되며 SidebarTrigger로 접을 수 있습니다.
  • 모바일: 자동으로 서랍형(Sheet) UI로 바뀌며 Trigger를 누르면 열립니다.

이 로직은 컴포넌트 내부에서 처리됩니다. SidebarProvider만 올바르게 설정하면 나머지는 컴포넌트에 맡길 수 있습니다.


3. Next.js Layout 통합 실전

핵심 개념을 살펴봤으니 이제 가장 중요한 단계인 Sidebar와 Next.js Layout 시스템의 통합으로 넘어가겠습니다.

3.1 프로젝트 구조 설계

Next.js의 Route Groups로 레이아웃을 구성하는 방식을 권장합니다. URL 구조에는 영향을 주지 않으면서 기능별 페이지에 서로 다른 레이아웃을 적용할 수 있습니다.

app/
├── layout.tsx              # Root Layout(전역)
├── (marketing)/            # 마케팅 페이지 그룹(Landing, About)
│   ├── layout.tsx          # Sidebar 없음
│   └── page.tsx            # 홈페이지
├── (dashboard)/            # 관리자 페이지 그룹
│   ├── layout.tsx          # Sidebar가 있는 레이아웃
│   ├── page.tsx            # Dashboard 홈페이지
│   ├── users/
│   │   └── page.tsx        # 사용자 관리
│   └── settings/
│       └── page.tsx        # 시스템 설정
└── (auth)/                 # 인증 페이지 그룹
    ├── layout.tsx          # 가운데 정렬 레이아웃
    ├── login/
    │   └── page.tsx        # 로그인 페이지
    └── register/
        └── page.tsx        # 회원가입 페이지

이 구조의 장점은 다음과 같습니다.

  1. 레이아웃 분리: 마케팅 페이지에는 Sidebar가 필요 없고 관리자 페이지에는 필요합니다. Route Groups를 사용하면 자연스럽게 분리할 수 있습니다.
  2. 간결한 URL: (dashboard)는 URL에 나타나지 않으므로 /users는 그대로 /users가 됩니다.
  3. 쉬운 확장: 새 페이지 그룹을 추가할 때 폴더만 만들면 됩니다.

3.2 Root Layout 설정

Root Layout은 애플리케이션 전체의 진입점입니다. 여기에서 테마, 폰트, SidebarProvider 같은 전역 요소를 설정합니다.

// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { SidebarProvider } from "@/components/ui/sidebar";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Admin Dashboard",
  description: "Built with shadcn/ui and Next.js",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;html lang="zh-CN"&gt;
      &lt;body className={inter.className}&gt;
        &lt;SidebarProvider&gt;
          {children}
        &lt;/SidebarProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}

여기서 SidebarProvider는 Dashboard Layout이 아니라 Root Layout에 배치해야 합니다. 그래야 /users에서 /settings로 이동하는 경우처럼 페이지를 전환해도 Sidebar의 접힘 상태가 유지됩니다.

3.3 Dashboard Layout 구현

Dashboard Layout은 관리자 페이지의 핵심 레이아웃이며, 여기에서 Sidebar를 가져옵니다.

// app/(dashboard)/layout.tsx
import { AppSidebar } from "@/components/app-sidebar";
import { SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;&gt;
      &lt;AppSidebar /&gt;
      &lt;SidebarInset&gt;
        &lt;header className="flex h-16 shrink-0 items-center gap-2 border-b px-4"&gt;
          &lt;SidebarTrigger className="-ml-1" /&gt;
          &lt;Separator orientation="vertical" className="mr-2 h-4" /&gt;
          &lt;Breadcrumb&gt;
            &lt;BreadcrumbList&gt;
              &lt;BreadcrumbItem className="hidden md:block"&gt;
                &lt;BreadcrumbLink href="/dashboard"&gt;
                  관리자 페이지
                &lt;/BreadcrumbLink&gt;
              &lt;/BreadcrumbItem&gt;
              &lt;BreadcrumbSeparator className="hidden md:block" /&gt;
              &lt;BreadcrumbItem&gt;
                &lt;BreadcrumbPage&gt;개요&lt;/BreadcrumbPage&gt;
              &lt;/BreadcrumbItem&gt;
            &lt;/BreadcrumbList&gt;
          &lt;/Breadcrumb&gt;
        &lt;/header&gt;
        &lt;main className="flex-1 p-4 pt-6"&gt;{children}&lt;/main&gt;
      &lt;/SidebarInset&gt;
    &lt;/&gt;
  );
}

이 레이아웃은 다음 요소로 구성됩니다.

  1. AppSidebar: 커스텀 사이드바 컴포넌트입니다. 다음 절에서 구현합니다.
  2. SidebarInset: Sidebar를 접었을 때 너비를 자동으로 처리하는 메인 콘텐츠 영역 래퍼입니다.
  3. Header: SidebarTrigger와 브레드크럼을 포함하는 상단 탐색 모음입니다.
  4. Main: 메인 콘텐츠 영역입니다.

3.4 AppSidebar 컴포넌트 구현

이제 사이드바 자체를 구현해 보겠습니다. 탐색 메뉴를 설정 파일에 저장하고 컴포넌트가 설정에 따라 렌더링하는 설정 기반 방식을 권장합니다.

먼저 탐색 설정을 정의합니다.

// lib/navigation.ts
import {
  Home,
  Users,
  Settings,
  FileText,
  BarChart3,
  Shield,
} from "lucide-react";

export interface NavItem {
  title: string;
  href: string;
  icon: React.ComponentType&lt;{ className?: string }&gt;;
  badge?: string;
}

export const navConfig: NavItem[] = [
  {
    title: "개요",
    href: "/dashboard",
    icon: Home,
  },
  {
    title: "사용자 관리",
    href: "/users",
    icon: Users,
    badge: "12", // 배지
  },
  {
    title: "데이터 분석",
    href: "/analytics",
    icon: BarChart3,
  },
  {
    title: "콘텐츠 관리",
    href: "/content",
    icon: FileText,
  },
  {
    title: "시스템 설정",
    href: "/settings",
    icon: Settings,
  },
  {
    title: "권한 관리",
    href: "/permissions",
    icon: Shield,
  },
];

그다음 AppSidebar를 구현합니다.

// components/app-sidebar.tsx
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
} from "@/components/ui/sidebar";
import { navConfig } from "@/lib/navigation";
import { Logo } from "@/components/logo";
import { UserNav } from "@/components/user-nav";

export function AppSidebar() {
  const pathname = usePathname();

  return (
    &lt;Sidebar&gt;
      &lt;SidebarHeader className="border-b border-border"&gt;
        &lt;Logo /&gt;
      &lt;/SidebarHeader&gt;

      &lt;SidebarContent&gt;
        &lt;SidebarGroup&gt;
          &lt;SidebarGroupLabel&gt;탐색 메뉴&lt;/SidebarGroupLabel&gt;
          &lt;SidebarGroupContent&gt;
            &lt;SidebarMenu&gt;
              {navConfig.map((item) =&gt; {
                const isActive = pathname === item.href;

                return (
                  &lt;SidebarMenuItem key={item.href}&gt;
                    &lt;SidebarMenuButton
                      asChild
                      isActive={isActive}
                      tooltip={item.title}
                    &gt;
                      &lt;Link href={item.href}&gt;
                        &lt;item.icon className="h-4 w-4" /&gt;
                        &lt;span&gt;{item.title}&lt;/span&gt;
                        {item.badge && (
                          &lt;span className="ml-auto text-xs bg-primary text-primary-foreground rounded-full px-2 py-0.5"&gt;
                            {item.badge}
                          &lt;/span&gt;
                        )}
                      &lt;/Link&gt;
                    &lt;/SidebarMenuButton&gt;
                  &lt;/SidebarMenuItem&gt;
                );
              })}
            &lt;/SidebarMenu&gt;
          &lt;/SidebarGroupContent&gt;
        &lt;/SidebarGroup&gt;
      &lt;/SidebarContent&gt;

      &lt;SidebarFooter className="border-t border-border"&gt;
        &lt;UserNav /&gt;
      &lt;/SidebarFooter&gt;
    &lt;/Sidebar&gt;
  );
}

여기서 핵심은 현재 경로 강조 표시입니다. usePathname()으로 현재 경로를 가져와 item.href와 비교합니다. 두 값이 일치하면 SidebarMenuButtonisActive={true}를 전달하고, 컴포넌트가 자동으로 활성화 스타일을 적용합니다.

3.5 다단계 메뉴 구현

관리자 페이지에 2단계 메뉴가 필요하다면 CollapsibleSidebarGroup을 감쌀 수 있습니다.

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";

// SidebarMenu 내부
&lt;Collapsible defaultOpen&gt;
  &lt;SidebarMenuItem&gt;
    &lt;CollapsibleTrigger asChild&gt;
      &lt;SidebarMenuButton&gt;
        &lt;Settings className="h-4 w-4" /&gt;
        &lt;span&gt;시스템 설정&lt;/span&gt;
        &lt;ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" /&gt;
      &lt;/SidebarMenuButton&gt;
    &lt;/CollapsibleTrigger&gt;
    &lt;CollapsibleContent&gt;
      &lt;SidebarMenuSub&gt;
        &lt;SidebarMenuSubItem&gt;
          &lt;SidebarMenuSubButton href="/settings/general"&gt;
            &lt;span&gt;기본 설정&lt;/span&gt;
          &lt;/SidebarMenuSubButton&gt;
        &lt;/SidebarMenuSubItem&gt;
        &lt;SidebarMenuSubItem&gt;
          &lt;SidebarMenuSubButton href="/settings/security"&gt;
            &lt;span&gt;보안 설정&lt;/span&gt;
          &lt;/SidebarMenuSubButton&gt;
        &lt;/SidebarMenuSubItem&gt;
      &lt;/SidebarMenuSub&gt;
    &lt;/CollapsibleContent&gt;
  &lt;/SidebarMenuItem&gt;
&lt;/Collapsible&gt;

4. 고급 기능 구현

기본 레이아웃을 완성했으니 실용적인 고급 기능을 살펴보겠습니다.

4.1 권한 제어(RBAC)

많은 관리자 시스템은 사용자 역할에 따라 서로 다른 메뉴를 표시해야 합니다. 구현 방법은 간단합니다. 탐색 설정에 roles 필드를 추가하고 렌더링할 때 필터링하면 됩니다.

먼저 설정을 수정합니다.

// lib/navigation.ts
export interface NavItem {
  title: string;
  href: string;
  icon: React.ComponentType&lt;{ className?: string }&gt;;
  roles?: string[]; // 접근이 허용된 역할
}

export const navConfig: NavItem[] = [
  {
    title: "개요",
    href: "/dashboard",
    icon: Home,
    // roles를 설정하지 않으면 누구나 볼 수 있음
  },
  {
    title: "사용자 관리",
    href: "/users",
    icon: Users,
    roles: ["admin", "manager"], // admin과 manager만 볼 수 있음
  },
  {
    title: "권한 관리",
    href: "/permissions",
    icon: Shield,
    roles: ["admin"], // admin만 볼 수 있음
  },
];

그다음 AppSidebar에서 사용자 역할에 따라 필터링합니다.

// components/app-sidebar.tsx
import { useAuth } from "@/hooks/use-auth"; // auth hook이 있다고 가정

export function AppSidebar() {
  const pathname = usePathname();
  const { user } = useAuth(); // 현재 사용자 가져오기

  const filteredNav = navConfig.filter((item) =&gt; {
    if (!item.roles) return true; // 역할 제한이 없으면 누구나 볼 수 있음
    return item.roles.some((role) =&gt; user?.roles?.includes(role));
  });

  return (
    &lt;Sidebar&gt;
      {/* ... */}
      &lt;SidebarMenu&gt;
        {filteredNav.map((item) =&gt; {
          // ...
        })}
      &lt;/SidebarMenu&gt;
      {/* ... */}
    &lt;/Sidebar&gt;
  );
}

이제 일반 사용자가 로그인하면 “권한 관리” 메뉴 항목이 표시되지 않습니다.

4.2 외부 링크와 구분선

사이드바에 문서나 도움말 센터 같은 외부 링크를 추가하거나 구분선으로 메뉴를 나눠야 할 때가 있습니다. shadcn/ui Sidebar는 이 기능도 지원합니다.

&lt;SidebarGroup&gt;
  &lt;SidebarGroupLabel&gt;주요 기능&lt;/SidebarGroupLabel&gt;
  &lt;SidebarGroupContent&gt;
    &lt;SidebarMenu&gt;
      {/* 주요 메뉴 항목 */}
    &lt;/SidebarMenu&gt;
  &lt;/SidebarGroupContent&gt;
&lt;/SidebarGroup&gt;

&lt;SidebarGroup&gt;
  &lt;SidebarGroupLabel&gt;도움말 및 지원&lt;/SidebarGroupLabel&gt;
  &lt;SidebarGroupContent&gt;
    &lt;SidebarMenu&gt;
      &lt;SidebarMenuItem&gt;
        &lt;SidebarMenuButton asChild&gt;
          &lt;a href="https://docs.example.com" target="_blank" rel="noopener"&gt;
            &lt;BookOpen className="h-4 w-4" /&gt;
            &lt;span&gt;사용 설명서&lt;/span&gt;
            &lt;ExternalLink className="ml-auto h-3 w-3" /&gt;
          &lt;/a&gt;
        &lt;/SidebarMenuButton&gt;
      &lt;/SidebarMenuItem&gt;
      &lt;SidebarMenuItem&gt;
        &lt;SidebarMenuButton asChild&gt;
          &lt;a href="mailto:[email protected]"&gt;
            &lt;HelpCircle className="h-4 w-4" /&gt;
            &lt;span&gt;문의하기&lt;/span&gt;
          &lt;/a&gt;
        &lt;/SidebarMenuButton&gt;
      &lt;/SidebarMenuItem&gt;
    &lt;/SidebarMenu&gt;
  &lt;/SidebarGroupContent&gt;
&lt;/SidebarGroup&gt;

4.3 검색창과 빠른 작업

많은 관리자 페이지가 사이드바에 검색창이나 전역 검색(Cmd+K)을 배치합니다. shadcn/ui의 Command 컴포넌트로 구현할 수 있습니다.

import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command";

&lt;SidebarGroup&gt;
  &lt;SidebarGroupContent&gt;
    &lt;Command className="rounded-lg border shadow-md"&gt;
      &lt;CommandInput placeholder="메뉴 검색..." /&gt;
      &lt;CommandList&gt;
        &lt;CommandEmpty&gt;검색 결과가 없습니다&lt;/CommandEmpty&gt;
        &lt;CommandGroup heading="추천"&gt;
          {navConfig.map((item) =&gt; (
            &lt;CommandItem key={item.href} onSelect={() =&gt; router.push(item.href)}&gt;
              &lt;item.icon className="mr-2 h-4 w-4" /&gt;
              {item.title}
            &lt;/CommandItem&gt;
          ))}
        &lt;/CommandGroup&gt;
      &lt;/CommandList&gt;
    &lt;/Command&gt;
  &lt;/SidebarGroupContent&gt;
&lt;/SidebarGroup&gt;

5. 성능 최적화와 모범 사례

마지막으로 실제 프로젝트에서 유용한 최적화 방법 몇 가지를 살펴보겠습니다.

Server Components 우선 사용

Next.js App Router에서는 기본적으로 모든 컴포넌트가 Server Component입니다. Logo나 고정 메뉴 항목 같은 Sidebar의 정적 부분은 Server Component로 유지하고, 현재 경로 강조 표시나 접힘 상태처럼 상호작용이 필요한 부분에만 "use client"를 사용할 수 있습니다.

제가 사용하는 방식은 다음과 같습니다.

  • usePathname을 사용하므로 AppSidebar"use client"를 지정합니다.
  • SidebarHeaderSidebarFooter의 정적 부분은 별도의 Server Component로 분리합니다.
  • 탐색 설정은 서버에서 생성해 클라이언트 컴포넌트에 전달합니다.

이렇게 하면 클라이언트 JavaScript 번들 크기를 줄일 수 있습니다.

대규모 메뉴 지연 로딩

관리자 페이지에 메뉴 항목이 수십 개 있다면 지연 로딩을 고려할 수 있습니다. React.lazy 또는 Next.js의 dynamic을 사용합니다.

import dynamic from "next/dynamic";

const AdminMenu = dynamic(() =&gt; import("./admin-menu"), {
  loading: () =&gt; &lt;SidebarMenuSkeleton /&gt;,
});

하지만 솔직히 대부분의 관리자 페이지에는 메뉴 항목이 그렇게 많지 않으므로, 이 최적화가 필요한 경우는 드뭅니다.

접근성 핵심 사항

shadcn/ui의 Sidebar는 Radix UI를 기반으로 하므로 기본적인 접근성 기능이 내장되어 있습니다. 그래도 다음 사항은 주의해야 합니다.

  1. 아이콘 + 텍스트: 아이콘만 사용하지 마세요. 스크린 리더 사용자는 아이콘을 인식할 수 없습니다.
  2. 포커스 표시: 기본 focus 스타일을 덮어쓰지 마세요.
  3. 키보드 탐색: Tab 키와 방향키로 정상적으로 탐색할 수 있어야 합니다.

Radix UI가 대부분을 처리하지만, 컴포넌트를 커스터마이징했다면 키보드 탐색도 꼭 테스트하세요.


6. 자주 묻는 질문

Q1: 새로고침하면 Sidebar 상태가 사라지나요?

SidebarProvider를 Root Layout이 아니라 Dashboard Layout에 배치하면 경로를 전환할 때 상태가 초기화됩니다. Provider를 Root Layout으로 올리면 됩니다.

Q2: 모바일에서 Sidebar를 자동으로 닫으려면 어떻게 하나요?

shadcn/ui의 Sidebar는 모바일에서 자동으로 Sheet로 전환됩니다. 메뉴 항목을 클릭한 뒤 직접 닫아야 합니다.

const { setOpenMobile } = useSidebar();

&lt;SidebarMenuButton
  onClick={() =&gt; setOpenMobile(false)}
&gt;

Q3: Sidebar 너비는 어떻게 커스터마이징하나요?

CSS 변수를 사용합니다.

&lt;Sidebar
  style={{
    "--sidebar-width": "280px",
    "--sidebar-width-mobile": "100%",
  }}
&gt;

또는 sidebar.tsxSIDEBAR_WIDTH 상수를 수정할 수 있습니다.


마무리

shadcn/ui Sidebar와 Next.js Layout을 함께 사용하면 관리자 페이지의 뼈대를 효율적으로 구축할 수 있습니다. 핵심 내용을 정리하면 다음과 같습니다.

  1. 컴포넌트 체계: SidebarProvider, Sidebar, SidebarContent 등 각 컴포넌트의 역할을 이해합니다.
  2. Layout 통합: Route Groups로 서로 다른 레이아웃을 분리하고 SidebarProvider는 Root Layout에 배치합니다.
  3. 설정 기반 구성: 탐색 메뉴를 설정 파일에 저장하고 컴포넌트가 이를 렌더링하게 하면 유지보수가 쉬워집니다.
  4. 현재 경로 강조 표시: usePathname()isActive prop으로 간단하게 구현할 수 있습니다.
  5. 권한 제어: 설정에 roles를 추가하고 렌더링할 때 필터링합니다.

저는 여러 프로젝트에서 이 아키텍처를 사용했고 확장성도 좋았습니다. 새 페이지를 추가할 때 navConfig에 항목 하나만 더하면 나머지는 컴포넌트가 처리합니다.

궁금한 점이 있다면 댓글에서 이야기해 주세요. 다음 글에서는 shadcn/ui DataTable의 실전 활용법을 다룰 예정이니 관심 있다면 계속 지켜봐 주세요.


참고 자료

shadcn/ui Sidebar + Next.js Layout 관리자 페이지 뼈대 구축하기

사이드바, 현재 경로 강조 표시, 권한 제어를 포함한 확장 가능한 관리자 시스템 레이아웃을 처음부터 구축합니다.

⏱️ Estimated time: 45 min

  1. 1

    Step 1: shadcn/ui 설치 및 Sidebar 컴포넌트 추가

    CLI 명령을 실행해 프로젝트를 초기화하고 컴포넌트를 추가합니다.

    ```bash
    npx shadcn@latest init
    npx shadcn@latest add sidebar
    ```

    설치 과정에서 스타일 설정을 묻는 메시지가 나오면 기본값을 선택하면 됩니다. 완료 후 components/ui 디렉터리에 sidebar.tsx 파일이 생성됩니다.
  2. 2

    Step 2: Root Layout 설정

    app/layout.tsx에서 SidebarProvider로 감쌉니다.

    • SidebarProvider 컴포넌트 가져오기
    • body 태그 안에서 {children} 감싸기
    • lang="ko" 언어 속성 설정

    이렇게 하면 Sidebar 상태가 전역에서 유지됩니다.
  3. 3

    Step 3: Dashboard Layout 만들기

    app/(dashboard)/layout.tsx에 관리자 페이지 전용 레이아웃을 만듭니다.

    • Route Groups 문법인 (dashboard) 사용
    • AppSidebar와 SidebarInset 가져오기
    • 상단 Header와 브레드크럼 탐색 추가

    Route Groups는 URL 구조에 영향을 주지 않으므로 /dashboard 경로가 직접 루트 경로에 매핑됩니다.
  4. 4

    Step 4: 탐색 설정 정의

    lib/navigation.ts 설정 파일을 만듭니다.

    • NavItem 인터페이스(title, href, icon, badge) 정의
    • navConfig 배열 내보내기
    • 필요하다면 roles 필드를 추가해 권한 제어 구현

    설정 기반 방식을 사용하면 메뉴를 추가할 때 한 곳만 수정하면 됩니다.
  5. 5

    Step 5: AppSidebar 컴포넌트 구현

    components/app-sidebar.tsx를 만듭니다.

    • "use client"로 클라이언트 컴포넌트 지정
    • usePathname으로 현재 경로 가져오기
    • navConfig를 순회하며 메뉴 항목 렌더링
    • 경로가 일치하면 isActive 속성을 설정해 강조 표시
  6. 6

    Step 6: 권한 제어 추가(선택 사항)

    RBAC 권한 필터링을 구현합니다.

    • NavItem 인터페이스에 roles 필드 추가
    • AppSidebar에서 useAuth로 사용자 역할 가져오기
    • filter 메서드로 메뉴 항목 필터링

    roles 필드가 없는 메뉴는 기본적으로 모든 사용자에게 표시됩니다.

FAQ

shadcn/ui Sidebar와 Ant Design 사이드바의 차이점은 무엇인가요?
shadcn/ui는 Copy-paste 방식을 사용하므로 컴포넌트 코드가 프로젝트에 직접 들어가고 완전히 제어할 수 있습니다. Ant Design은 바로 사용할 수 있는 완전한 디자인 시스템이지만 커스터마이징 비용이 큽니다. 높은 수준의 커스터마이징이 필요하면 shadcn/ui를, 빠른 개발이 중요하면 Ant Design을 선택하세요.
SidebarProvider는 Root Layout과 Dashboard Layout 중 어디에 두어야 하나요?
Root Layout(app/layout.tsx)에 두는 것을 권장합니다. 그러면 페이지를 이동해도 Sidebar의 접힘 상태가 유지됩니다. Dashboard Layout에 두면 경로를 전환할 때마다 상태가 초기화됩니다.
모바일에서 Sidebar를 자동으로 닫으려면 어떻게 하나요?
모바일에서 Sidebar는 자동으로 Sheet 서랍 모드로 전환됩니다. 메뉴 항목을 클릭할 때 다음 코드를 호출하면 됩니다.

```tsx
const { setOpenMobile } = useSidebar();
&lt;SidebarMenuButton onClick={() =&gt; setOpenMobile(false)}&gt;
```

이렇게 하면 메뉴 항목을 클릭한 뒤 서랍이 자동으로 닫힙니다.
Sidebar 너비는 어떻게 커스터마이징하나요?
두 가지 방법이 있습니다.

1. CSS 변수 사용(권장):
```tsx
&lt;Sidebar style={{ "--sidebar-width": "280px" }} /&gt;
```

2. sidebar.tsx의 SIDEBAR_WIDTH 상수 수정

CSS 변수를 사용하면 Sidebar마다 서로 다른 너비를 지정할 수 있어 더 유연합니다.
다단계 메뉴는 어떻게 구현하나요?
Collapsible 컴포넌트로 SidebarMenuItem을 감쌉니다.

• CollapsibleTrigger에 1단계 메뉴 버튼 배치
• CollapsibleContent에 SidebarMenuSub와 2단계 메뉴 항목 배치
• ChevronDown 아이콘으로 펼침 상태 표시

전체 코드는 본문 3.5절에서 확인할 수 있습니다.
shadcn/ui Sidebar의 접근성은 어떤가요?
Radix UI를 기반으로 만들어져 키보드 탐색(Tab/방향키), ARIA 속성, 포커스 관리 등 완전한 접근성 지원을 기본 제공합니다. 아이콘만 사용하지 말고 아이콘과 텍스트가 함께 표시되도록 하면 됩니다.

7분 읽기 · 게시일: 2026년 3월 27일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog