[React] shadcn/ui
개요
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>
);
}