2 분 소요

개요

shadcn/ui는 Radix UI와 Tailwind CSS를 기반으로 만들어진 UI 컴포넌트 모음입니다. npm 패키지가 아니라 소스 코드를 프로젝트에 직접 복사하는 방식이라, 컴포넌트를 자유롭게 수정할 수 있습니다.


설치 및 초기화

Tailwind CSS가 설치된 프로젝트에서 시작합니다.

npx shadcn@latest init

초기화 중 질문이 나오면 프로젝트에 맞게 선택합니다.

# tailwind.config.ts, components.json, utils 파일 등이 생성됩니다


컴포넌트 추가

필요한 컴포넌트만 골라서 추가합니다.

npx shadcn@latest add button
npx shadcn@latest add input card badge
npx shadcn@latest add dialog
npx shadcn@latest add form      # React Hook Form 통합 포함

추가된 파일은 src/components/ui/ 폴더에 저장됩니다.


Button

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

function App() {
  return (
    <div className="flex gap-2">
      <Button>기본</Button>
      <Button variant="outline">아웃라인</Button>
      <Button variant="ghost">고스트</Button>
      <Button variant="destructive">삭제</Button>
      <Button size="sm">작게</Button>
      <Button size="lg">크게</Button>
      <Button disabled>비활성</Button>
    </div>
  );
}


Input, Label

import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

function Form() {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="email">이메일</Label>
      <Input id="email" type="email" placeholder="example@email.com" />
    </div>
  );
}


Card

import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";

function ProductCard() {
  return (
    <Card className="w-80">
      <CardHeader>
        <CardTitle>React 기초 강의</CardTitle>
        <CardDescription>제로베이스 초보자를 위한 강의</CardDescription>
      </CardHeader>
      <CardContent>
        <p>총 22개 강의, 약 10시간 분량</p>
      </CardContent>
      <CardFooter>
        <Button className="w-full">수강 신청</Button>
      </CardFooter>
    </Card>
  );
}


Badge

import { Badge } from "@/components/ui/badge";

function TagList() {
  return (
    <div className="flex gap-2">
      <Badge>React</Badge>
      <Badge variant="secondary">TypeScript</Badge>
      <Badge variant="outline">Vite</Badge>
      <Badge variant="destructive">삭제됨</Badge>
    </div>
  );
}


Dialog (모달)

npx shadcn@latest add dialog
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
  DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

function DeleteConfirm({ onConfirm }: { onConfirm: () => void }) {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="destructive">삭제</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>정말 삭제할까요?</DialogTitle>
          <DialogDescription>
            이 작업은 취소할 수 없습니다.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <Button variant="outline">취소</Button>
          <Button variant="destructive" onClick={onConfirm}>삭제</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}


Form + React Hook Form + Zod 통합

npx shadcn@latest add form input label button
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
  Form, FormControl, FormField, FormItem, FormLabel, 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, "비밀번호는 8자 이상이어야 합니다"),
});

type FormData = z.infer<typeof schema>;

function LoginForm() {
  const form = useForm<FormData>({ resolver: zodResolver(schema) });

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit((data) => console.log(data))} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>이메일</FormLabel>
              <FormControl>
                <Input placeholder="example@email.com" {...field} />
              </FormControl>
              <FormMessage />  {/* 오류 메시지 자동 표시 */}
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>비밀번호</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit" className="w-full">로그인</Button>
      </form>
    </Form>
  );
}


관련 링크