핵심 요약
shadcn/ui는 component "라이브러리"가 아니라 "코드 복사 패턴". Radix Primitives의 접근성 + Tailwind v4의 스타일링 + 자기 코드베이스로의 복사 = 가장 유연한 디자인 시스템 baseline.
- shadcn/ui v3 (2026-02): React 19 호환, Tailwind v4 기본
- Radix Primitives v2: 접근성·키보드·focus management
- CVA(class-variance-authority): variant 시스템
1. 시작
pnpm dlx shadcn@latest init
# 프로젝트 분석 → tailwind.config·components.json 생성
pnpm dlx shadcn@latest add button input form dialog dropdown-menu설치된 컴포넌트는 components/ui/ 에 코드 그대로 복사됨. 자기 프로젝트 코드처럼 자유 수정 가능.
2. 디자인 토큰 — Tailwind v4 통합
/* app/globals.css */
@import "tailwindcss";
@theme {
/* primitives */
--color-blue-500: oklch(0.6 0.2 240);
--color-blue-600: oklch(0.55 0.2 240);
/* semantic */
--color-background: var(--color-white);
--color-foreground: var(--color-neutral-950);
--color-primary: var(--color-blue-500);
--color-primary-foreground: var(--color-white);
--color-muted: var(--color-neutral-100);
--color-border: var(--color-neutral-200);
--color-ring: var(--color-blue-500);
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
}
@media (prefers-color-scheme: dark) {
@theme {
--color-background: var(--color-neutral-950);
--color-foreground: var(--color-neutral-50);
--color-muted: var(--color-neutral-900);
--color-border: var(--color-neutral-800);
}
}3. Button — variant 시스템
// components/ui/button.tsx
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-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-red-500 text-white hover:bg-red-600',
outline: 'border border-input bg-background hover:bg-muted',
ghost: 'hover:bg-muted',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
icon: 'h-10 w-10',
},
},
defaultVariants: { variant: 'default', size: 'md' },
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
)
}4. Form — react-hook-form + zod
// components/ui/form.tsx (shadcn 자동 설치)
// 사용 예시
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
function LoginForm() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '' },
})
function onSubmit(values) { ... }
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>이메일</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">로그인</Button>
</form>
</Form>
)
}5. Dialog — Radix 기반 접근성
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from '@/components/ui/dialog'
<Dialog>
<DialogTrigger asChild>
<Button>새 항목</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>항목 추가</DialogTitle>
</DialogHeader>
<form>...</form>
</DialogContent>
</Dialog>Radix가 ESC·focus trap·scroll lock·ARIA 모두 처리. 직접 구현하면 30~50줄.
6. Theme Provider — 다크 모드 토글
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
export function ThemeProvider({ children }) {
return (
<NextThemesProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</NextThemesProvider>
)
}
// 토글 버튼
import { useTheme } from 'next-themes'
function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} variant="ghost" size="icon">
<Sun className="dark:hidden" />
<Moon className="hidden dark:block" />
</Button>
)
}7. 접근성 체크리스트
- 모든 인터랙티브 요소 키보드 접근 가능
- focus visible (focus-visible:ring 사용)
- color contrast — 4.5:1 이상 (WCAG AA)
- 모달·드롭다운 trap focus
- aria-label·aria-describedby 적절히
- 스크린리더 테스트 (VoiceOver·NVDA)
8. 자주 쓰는 컴포넌트 셋업
# Form 관련
pnpm dlx shadcn@latest add form input label select textarea checkbox radio-group switch
# Layout
pnpm dlx shadcn@latest add card sheet dialog popover dropdown-menu
# Feedback
pnpm dlx shadcn@latest add toast alert progress skeleton
# Data
pnpm dlx shadcn@latest add table data-table tabs accordion9. CVA 컴포넌트 작성 패턴
shadcn에 없는 자체 컴포넌트도 CVA 패턴 따르기:
const cardVariants = cva(
'rounded-lg border bg-card text-card-foreground shadow-sm',
{
variants: {
padding: {
none: '',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
},
hoverable: {
true: 'transition-shadow hover:shadow-md cursor-pointer',
false: '',
},
},
defaultVariants: { padding: 'md', hoverable: false },
}
)10. 디자인 시스템 진화
- Phase 1 (1주): shadcn 기본 컴포넌트 + Tailwind 토큰
- Phase 2 (2~4주): 자체 브랜드 색·폰트·spacing 적용
- Phase 3 (1~3개월): 자체 컴포넌트 추가 (DataTable·Pricing·Empty State)
- Phase 4 (지속): 컴포넌트 라이브러리 npm 패키지화 (모노레포 packages/ui)
자주 묻는 질문
shadcn vs MUI·Chakra?
shadcn: 코드 복사, 자유 수정, 가벼움. MUI: 풍부한 기능, 무거움. 빠른 프로덕트는 shadcn.
접근성을 직접 신경 써야 하나?
Radix Primitives가 90% 처리. 단 본인 작성 컴포넌트는 별도 점검.

댓글 0