shadcn/ui 완벽 가이드 | Radix UI·Tailwind·복사 가능한 컴포넌트·커스터마이징·실전 활용
이 글의 핵심
shadcn/ui로 아름다운 UI를 구축하는 완벽 가이드입니다. Radix UI 기반, Tailwind 스타일링, 복사 가능한 컴포넌트, 커스터마이징까지 실전 예제로 정리했습니다.
실무 경험 공유: Material-UI에서 shadcn/ui로 전환하면서, 번들 크기가 80% 감소하고 커스터마이징이 자유로워진 경험을 공유합니다.
들어가며: “UI 라이브러리가 무거워요”
실무 문제 시나리오
시나리오 1: 번들 크기가 너무 커요
Material-UI는 무겁습니다. shadcn/ui는 필요한 것만 복사합니다.
시나리오 2: 커스터마이징이 어려워요
기존 라이브러리는 제한적입니다. shadcn/ui는 코드를 직접 수정할 수 있습니다.
시나리오 3: 디자인 시스템이 필요해요
처음부터 만들기 어렵습니다. shadcn/ui는 기본 제공합니다.
1. shadcn/ui란?
핵심 특징
shadcn/ui는 복사 가능한 컴포넌트 컬렉션입니다.
주요 장점:
- 복사 가능: npm 패키지가 아님
- Radix UI: 접근성 완벽
- Tailwind: 스타일링 자유
- 커스터마이징: 코드 직접 수정
- TypeScript: 완벽한 지원
2. 설치 및 설정
프로젝트 초기화
npx shadcn-ui@latest init
설정 질문:
- TypeScript: Yes
- Style: Default
- Base color: Slate
- CSS variables: Yes
생성된 파일
my-app/
├── components/
│ └── ui/
├── lib/
│ └── utils.ts
├── tailwind.config.js
└── components.json
3. 컴포넌트 추가
Button
npx shadcn-ui@latest add button
// app/page.tsx
import { Button } from '@/components/ui/button';
export default function Home() {
return (
<div>
<Button>Click me</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
</div>
);
}
Card
npx shadcn-ui@latest add card
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
export default function UserCard() {
return (
<Card>
<CardHeader>
<CardTitle>John Doe</CardTitle>
<CardDescription>Software Engineer</CardDescription>
</CardHeader>
<CardContent>
<p>Email: john@example.com</p>
</CardContent>
<CardFooter>
<Button>View Profile</Button>
</CardFooter>
</Card>
);
}
4. Form 컴포넌트
Form + Zod
npx shadcn-ui@latest add form input label
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
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({
email: z.string().email(),
password: z.string().min(8),
});
export default function LoginForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="[email protected]" {...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">Submit</Button>
</form>
</Form>
);
}
5. Dialog
npx shadcn-ui@latest add dialog
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
export default function UserDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>User Details</DialogTitle>
<DialogDescription>
View and edit user information.
</DialogDescription>
</DialogHeader>
<div>
{/* Content */}
</div>
</DialogContent>
</Dialog>
);
}
6. Table
npx shadcn-ui@latest add table
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
export default function UsersTable({ users }) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
7. 커스터마이징
색상 변경
/* app/globals.css */
@layer base {
:root {
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
}
}
컴포넌트 수정
// components/ui/button.tsx
// 직접 수정 가능!
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
// 원하는 대로 수정
return <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
}
);
정리 및 체크리스트
핵심 요약
- shadcn/ui: 복사 가능한 컴포넌트
- Radix UI: 접근성 완벽
- Tailwind: 스타일링 자유
- 커스터마이징: 코드 직접 수정
- TypeScript: 완벽한 지원
- 작은 번들: 필요한 것만
구현 체크리스트
- shadcn/ui 초기화
- 컴포넌트 추가
- Form 구현
- Dialog 구현
- Table 구현
- 색상 커스터마이징
- 컴포넌트 수정
같이 보면 좋은 글
- Tailwind CSS 완벽 가이드
- Storybook 완벽 가이드
- Radix UI 가이드
이 글에서 다루는 키워드
shadcn/ui, Radix UI, Tailwind, UI Components, React, Design System, Frontend
자주 묻는 질문 (FAQ)
Q. Material-UI와 비교하면 어떤가요?
A. shadcn/ui가 더 가볍고 커스터마이징이 자유롭습니다. Material-UI는 더 많은 컴포넌트를 제공합니다.
Q. npm 패키지가 아닌가요?
A. 네, 코드를 직접 복사해서 사용합니다. 이게 장점입니다.
Q. 다크 모드를 지원하나요?
A. 네, next-themes와 함께 사용하면 간단히 구현할 수 있습니다.
Q. 프로덕션에서 사용해도 되나요?
A. 네, 많은 프로젝트에서 안정적으로 사용하고 있습니다.