본문으로 건너뛰기
Previous
Next
shadcn/ui 완벽 가이드 — 복사해서 쓰는 컴포넌트의 새 표준, 커스터마이징 자유로운 UI 키트

shadcn/ui 완벽 가이드 — 복사해서 쓰는 컴포넌트의 새 표준, 커스터마이징 자유로운 UI 키트

shadcn/ui 완벽 가이드 — 복사해서 쓰는 컴포넌트의 새 표준, 커스터마이징 자유로운 UI 키트

이 글의 핵심

shadcn/ui는 "라이브러리를 설치"하는 대신 "CLI로 컴포넌트 소스 코드를 복사해 오는" 새로운 배포 모델의 React 컴포넌트 키트입니다. Radix UI의 접근성 프리미티브 + Tailwind CSS의 스타일 + 프로젝트 안에 소스가 들어오는 자유로운 커스터마이징 + 공식 CLI로 2023년 이후 React 생태계 UI의 사실상 표준이 되었습니다. Vercel v0·Cal.com·Taxonomy 등 유명 프로젝트가 기반으로 사용합니다.

설치 (Next.js 15 기준)

pnpm dlx shadcn@latest init
# 질문에 답: TS, app/ dir, Tailwind v4, components/ui 위치, CSS variables for theme, etc.

생성/변경 파일:

  • components.json — shadcn CLI 설정
  • lib/utils.tscn() 유틸
  • app/globals.css — Tailwind + 디자인 토큰
  • tailwind.config.ts (v4에선 대부분 필요 없지만 호환성)

첫 컴포넌트

pnpm dlx shadcn@latest add button dialog input label

components/ui/button.tsx·dialog.tsx·input.tsx·label.tsx 생성. 이후 import { Button } from "@/components/ui/button" 로 사용.

import { Button } from "@/components/ui/button"

export default function Page() {
  return (
    <div className="p-8 space-x-2">
      <Button>Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Delete</Button>
      <Button size="lg">Large</Button>
      <Button size="icon"><SunIcon /></Button>
    </div>
  )
}

테마 (CSS 변수)

/* app/globals.css */
@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:where(.dark, .dark *));

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --accent: oklch(0.97 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --radius: 0.625rem;
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.985 0 0);
  --primary-foreground: oklch(0.205 0 0);
  /* ... */
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  /* ... */
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

이 변수들이 bg-primary·text-primary-foreground·border-input·rounded-lg 같은 Tailwind 클래스를 만듭니다.

색 테마 바꾸기

한 번에 바꾸기:

pnpm dlx shadcn@latest add theme zinc     # zinc 기반 전체 팔레트

또는 공식 테마 에디터(UI)에서 OKLCH 값 뽑아 globals.css에 붙여넣기.

다크 모드

pnpm dlx shadcn@latest add theme-provider mode-toggle
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}
import { ModeToggle } from "@/components/mode-toggle"

<header><ModeToggle /></header>

시스템 기본 + 수동 토글 + 로컬스토리지 저장이 자동 처리.

핵심 컴포넌트 카탈로그

Dialog

import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"

<Dialog>
  <DialogTrigger asChild><Button>Open</Button></DialogTrigger>
  <DialogContent>
    <DialogHeader><DialogTitle>Confirm</DialogTitle></DialogHeader>
    <p>Are you sure?</p>
  </DialogContent>
</Dialog>

Radix 기반으로 포커스 트랩·ESC·ARIA 모두 처리됨.

Form + react-hook-form + zod

pnpm dlx shadcn@latest add form input label button
"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})
type FormValues = z.infer<typeof schema>

export function LoginForm() {
  const form = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: { email: "", password: "" },
  })

  function onSubmit(values: FormValues) {
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 max-w-sm">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl><Input type="email" {...field} /></FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl><Input type="password" {...field} /></FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Sign in</Button>
      </form>
    </Form>
  )
}

Data Table (TanStack Table)

pnpm dlx shadcn@latest add table
pnpm add @tanstack/react-table
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"

type User = { id: number; name: string; email: string }
const columns: ColumnDef<User>[] = [
  { accessorKey: "id", header: "ID" },
  { accessorKey: "name", header: "Name" },
  { accessorKey: "email", header: "Email" },
]

export function UsersTable({ data }: { data: User[] }) {
  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })

  return (
    <Table>
      <TableHeader>
        {table.getHeaderGroups().map((hg) => (
          <TableRow key={hg.id}>
            {hg.headers.map((h) => (
              <TableHead key={h.id}>{flexRender(h.column.columnDef.header, h.getContext())}</TableHead>
            ))}
          </TableRow>
        ))}
      </TableHeader>
      <TableBody>
        {table.getRowModel().rows.map((row) => (
          <TableRow key={row.id}>
            {row.getVisibleCells().map((cell) => (
              <TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Toast (sonner)

pnpm dlx shadcn@latest add sonner
import { Toaster, toast } from "sonner"

<Toaster richColors />
<Button onClick={() => toast.success("Saved")}>Save</Button>

Command (Command Palette)

pnpm dlx shadcn@latest add command

Cmd+K 팔레트를 10분 안에 구축 가능. Vercel·Linear 스타일 UI.

CLI 고급

업데이트 diff 확인

pnpm dlx shadcn@latest diff button
# 내 프로젝트의 button.tsx와 최신 레지스트리 비교

커스텀 Registry

// components.json (v2)
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "tailwind": { "css": "app/globals.css" },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui"
  },
  "registries": {
    "@my-team": "https://ui.my-team.com/r/{name}.json"
  }
}
pnpm dlx shadcn@latest add @my-team/company-button

사내 공용 컴포넌트도 동일한 CLI로 배포할 수 있어 디자인 시스템 구축이 쉽습니다.

커스터마이징 패턴

cva (class variance authority)로 variant 정의

import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "size-9",
      },
    },
    defaultVariants: { variant: "default", size: "default" },
  },
)

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return <button className={cn(buttonVariants({ variant, size, className }))} {...props} />
}

새 variant 추가가 타입 안전하게 한 줄.

브랜딩 맞춤

  • --primary·--radius·--font-sans만 바꿔도 전체 키트 룩이 변경
  • 로고·아이콘은 Lucide(lucide-react)를 기본으로 사용

v0과의 연계

Vercel v0(AI UI 생성기)가 출력하는 코드가 shadcn 규약입니다. v0 → npx shadcn@latest add <url>로 그대로 프로젝트에 병합. AI 출력 결과물을 즉시 합법적으로 도입 가능한 유일한 UI 키트.

실전 앱 구성 예시

Admin Dashboard

<SidebarProvider>
  <AppSidebar />
  <SidebarInset>
    <Header />
    <main className="p-6 space-y-6">
      <StatsCards />
      <DataTable />
    </main>
  </SidebarInset>
</SidebarProvider>

sidebar·card·skeleton·avatar·dropdown-menu·data-table만 있으면 어드민의 80%가 해결됩니다.

SaaS 마케팅

  • navigation-menu·hover-card
  • hero + marquee (v0에서 생성)
  • accordion(FAQ)·tabs(기능 비교)·pricing-table

접근성

Radix 기반이라 기본:

  • 키보드 네비게이션
  • 포커스 트랩
  • ARIA roles/attributes 적절
  • 축소 모션 선호 존중

추가 작업 없이 AAA에 가까운 접근성이 제공됩니다.

트러블슈팅

CSS 변수가 적용 안 됨

  • @theme inline에 매핑 누락
  • Tailwind v4 플러그인이 로드됐는지
  • dark 클래스가 html에 붙는지

컴포넌트 스타일이 다른 요소와 충돌

  • Tailwind Preflight이 필요 → @import "tailwindcss"가 entry에
  • 기존 CSS 리셋 중복 여부 확인

size prop 커스터마이징 어려움

  • button.tsxbuttonVariants를 직접 수정

Dialog가 스크롤을 막음

  • body scroll lock 기본 동작. 필요 시 modal={false}

번들 크기

  • 미사용 컴포넌트는 import하지 않으면 빌드에 포함되지 않음
  • Radix는 primitive별 패키지라 tree-shaken

체크리스트

  • shadcn init으로 토큰·디렉터리 세팅
  • 색·radius·폰트를 CSS 변수로 관리
  • theme-provider + mode-toggle로 다크 모드
  • Form·Data Table·Dialog·Toast 핵심 컴포넌트 구비
  • cva로 variant 체계 관리
  • 사내 registry로 공용 컴포넌트 배포
  • shadcn diff로 주기 업데이트 확인
  • v0 등 AI 도구 결과를 CLI로 통합

마무리

shadcn/ui는 “UI 라이브러리의 패러다임”을 바꾼 프로젝트입니다. 소스 코드를 내 리포지토리에 두는 모델 덕분에 깊은 커스터마이징이 자유롭고, Radix + Tailwind라는 표준 조합이 학습 투자를 오래 보존해줍니다. 2026년 현재 Vercel v0·Cal.com·Dub·Midday·TanStack 공식 예제 대부분이 shadcn 기반이며, 사실상의 React UI 표준으로 자리잡았습니다. 스타트업·사이드 프로젝트부터 엔터프라이즈 디자인 시스템의 씨앗까지 커버하는 드문 범용성을 가지니, 새 React 프로젝트를 시작한다면 첫 선택지로 강력히 추천합니다.

관련 글

  • Tailwind CSS 4 완벽 가이드
  • Radix UI 완벽 가이드
  • 디자인 시스템 완벽 가이드
  • React 컴포넌트 패턴

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. shadcn/ui 완벽 가이드. Radix + Tailwind v4 기반으로 “패키지가 아닌 소스 코드를 복사”해서 쓰는 새로운 컴포넌트 배포 모델. Next.js·Vite·Astro 설치·테마·다크모드·커스터마이징… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

shadcn/ui, React, Tailwind CSS, Radix UI, Design System, Component Library, Next.js 등으로 검색하시면 이 글이 도움이 됩니다.