본문으로 건너뛰기 shadcn/ui 완벽 가이드 | Radix UI·Tailwind·복사 가능한 컴포넌트·커스터마이징

shadcn/ui 완벽 가이드 | Radix UI·Tailwind·복사 가능한 컴포넌트·커스터마이징

shadcn/ui 완벽 가이드 | Radix UI·Tailwind·복사 가능한 컴포넌트·커스터마이징

이 글의 핵심

shadcn/ui로 아름다운 UI를 빠르게 구축하는 완벽 가이드. 설치, 컴포넌트 추가, 커스터마이징, 테마, 다크모드까지 실전 예제로 정리. shadcn/ui·Radix UI·Tailwind CSS 중심으로 설명합니다.

이 글의 핵심

shadcn/ui로 아름다운 UI를 빠르게 구축하는 완벽 가이드입니다. 설치부터 컴포넌트 추가, 커스터마이징, 테마, 다크모드까지 실전 예제로 정리했습니다.

실무 경험 공유: 관리자 대시보드를 shadcn/ui로 구축하면서, UI 개발 시간을 70% 단축하고 일관된 디자인 시스템을 확립한 경험을 공유합니다.

들어가며: “UI 라이브러리가 불편해요”

실무에서 마주치는 문제들

커스터마이징이 어려워요

Material-UI는 스타일 오버라이드가 복잡합니다. shadcn/ui는 코드를 직접 수정합니다. 번들이 너무 커요

전체 라이브러리를 설치해야 합니다. shadcn/ui는 필요한 컴포넌트만 복사합니다. 디자인이 획일적이에요

모든 사이트가 비슷해 보입니다. shadcn/ui는 완전히 커스터마이징 가능합니다.

1. shadcn/ui란?

핵심 특징

shadcn/ui는 복사 가능한 컴포넌트 컬렉션입니다. 주요 장점

  • 복사 가능: npm 패키지가 아닌 코드 복사
  • 완전한 제어: 모든 코드를 직접 수정 가능
  • Radix UI 기반: 접근성 완벽 지원
  • Tailwind CSS: 유틸리티 우선 스타일링
  • TypeScript: 완벽한 타입 지원

2. 설치 및 설정

Next.js 프로젝트 생성

npx create-next-app@latest my-app --typescript --tailwind --eslint
cd my-app

shadcn/ui 초기화

npx shadcn-ui@latest init

설정 옵션

✔ Which style would you like to use? › Default
✔ Which color would you like to use as base color? › Slate
✔ Would you like to use CSS variables for colors? › yes

생성된 파일

my-app/
├── components/
│   └── ui/           # 컴포넌트가 여기에 추가됨
├── lib/
│   └── utils.ts      # 유틸리티 함수
├── tailwind.config.ts
└── components.json   # shadcn/ui 설정

3. 컴포넌트 추가

단일 컴포넌트

npx shadcn-ui@latest add button

여러 컴포넌트

npx shadcn-ui@latest add button card input label

모든 컴포넌트

npx shadcn-ui@latest add --all

4. 기본 컴포넌트 사용

Button

import { Button } from '@/components/ui/button';
export default function Page() {
  return (
    <div className="space-x-2">
      <Button>Default</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>
    </div>
  );
}

Input

import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
export default function Page() {
  return (
    <div className="space-y-2">
      <Label htmlFor="email">Email</Label>
      <Input id="email" type="email" placeholder="Email" />
    </div>
  );
}

Card

import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
export default function Page() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Card Title</CardTitle>
        <CardDescription>Card Description</CardDescription>
      </CardHeader>
      <CardContent>
        <p>Card Content</p>
      </CardContent>
      <CardFooter>
        <Button>Footer button</Button>
      </CardFooter>
    </Card>
  );
}

5. Form 구축

설치

npm install react-hook-form @hookform/resolvers zod
npx shadcn-ui@latest add form

Form 예제

'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
const formSchema = z.object({
  username: z.string().min(2, {
    message: 'Username must be at least 2 characters.',
  }),
  email: z.string().email({
    message: 'Please enter a valid email address.',
  }),
});
export function ProfileForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: ',
      email: ',
    },
  });
  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values);
  }
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="johndoe" {...field} />
              </FormControl>
              <FormDescription>
                This is your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input placeholder="john@example.com" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

6. 다크모드

설치

npm install next-themes

Provider 설정

// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  );
}
// app/layout.tsx
// 필요한 모듈 import
import { Providers } from './providers';
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

테마 토글

'use client';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
}

7. 커스터마이징

색상 변경

/* app/globals.css */
@layer base {
  :root {
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
  }
  .dark {
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
  }
}

컴포넌트 수정

// components/ui/button.tsx
// 이 파일을 직접 수정할 수 있습니다!
const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        // 새로운 variant 추가
        success: 'bg-green-500 text-white hover:bg-green-600',
      },
    },
  }
);

8. 실전 예제: 대시보드

// app/dashboard/page.tsx
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
export default function Dashboard() {
  return (
    <div className="container mx-auto p-6 space-y-6">
      <div className="flex justify-between items-center">
        <h1 className="text-3xl font-bold">Dashboard</h1>
        <Button>Add New</Button>
      </div>
      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">
              Total Revenue
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">$45,231.89</div>
            <p className="text-xs text-muted-foreground">
              +20.1% from last month
            </p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">
              Subscriptions
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">+2350</div>
            <p className="text-xs text-muted-foreground">
              +180.1% from last month
            </p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Sales</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">+12,234</div>
            <p className="text-xs text-muted-foreground">
              +19% from last month
            </p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">
              Active Now
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">+573</div>
            <p className="text-xs text-muted-foreground">
              +201 since last hour
            </p>
          </CardContent>
        </Card>
      </div>
      <Card>
        <CardHeader>
          <CardTitle>Recent Sales</CardTitle>
          <CardDescription>
            You made 265 sales this month.
          </CardDescription>
        </CardHeader>
        <CardContent>
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>Customer</TableHead>
                <TableHead>Email</TableHead>
                <TableHead>Amount</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              <TableRow>
                <TableCell>John Doe</TableCell>
                <TableCell>john@example.com</TableCell>
                <TableCell>$250.00</TableCell>
              </TableRow>
              <TableRow>
                <TableCell>Jane Smith</TableCell>
                <TableCell>jane@example.com</TableCell>
                <TableCell>$150.00</TableCell>
              </TableRow>
            </TableBody>
          </Table>
        </CardContent>
      </Card>
    </div>
  );
}

9. 고급 컴포넌트

Dialog

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
export function DialogDemo() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>Open Dialog</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure?</DialogTitle>
          <DialogDescription>
            This action cannot be undone.
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  );
}
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
export function DropdownMenuDemo() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">Open</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuLabel>My Account</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuItem>Profile</DropdownMenuItem>
        <DropdownMenuItem>Settings</DropdownMenuItem>
        <DropdownMenuItem>Logout</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

정리 및 체크리스트

핵심 요약

  • shadcn/ui: 복사 가능한 컴포넌트 컬렉션
  • 완전한 제어: 모든 코드를 직접 수정 가능
  • Radix UI: 접근성 완벽 지원
  • Tailwind CSS: 유틸리티 우선 스타일링
  • 다크모드: 내장 지원

구현 체크리스트

  • shadcn/ui 초기화
  • 필요한 컴포넌트 추가
  • 테마 설정
  • 다크모드 구현
  • Form 구축
  • 커스터마이징

같이 보면 좋은 글

  • Tailwind CSS 완벽 가이드
  • React 18 심화 가이드
  • Next.js 15 완벽 가이드

이 글에서 다루는 키워드

shadcn/ui, Radix UI, Tailwind CSS, React, UI Components, Design System

자주 묻는 질문 (FAQ)

Q. shadcn/ui vs Material-UI, 어떤 게 나은가요?

A. shadcn/ui는 더 가볍고 커스터마이징이 쉽습니다. Material-UI는 더 많은 컴포넌트를 제공하지만 무겁습니다.

Q. 왜 npm 패키지가 아닌가요?

A. 코드를 직접 소유하고 수정할 수 있게 하기 위함입니다. 의존성 지옥을 피할 수 있습니다.

Q. 접근성은 어떤가요?

A. Radix UI 기반이므로 접근성이 완벽합니다. ARIA 속성, 키보드 네비게이션 등을 모두 지원합니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, 많은 기업에서 프로덕션 환경에서 사용하고 있습니다. Vercel도 사용합니다.