# shadcn/ui (Vercel 생태계)

> 복사해서 쓰는 모던 컴포넌트 라이브러리. Radix UI + Tailwind CSS v4 기반. (2026-10 갱신: OKLCH 토큰·`@theme inline`·차트/사이드바 토큰 반영)

## 핵심 특징
- 컴포넌트를 npm 설치가 아닌 **소스코드 직접 복사**
- Radix UI 헤드리스 + Tailwind CSS 스타일링
- Next.js, Vite, Remix 모두 지원
- 다크 모드 기본 지원
- CSS 변수 기반 테마 시스템

---

## 컬러 시스템 (Tailwind v4 + OKLCH, 2025~ 기준)

> 2025년부터 shadcn/ui CLI는 **Tailwind v4**를 기본으로 하고, 테마 변수는 HSL 채널값이 아니라 **완전한 `oklch()` 값**으로 생성한다.
> `tailwind.config.js` 대신 CSS의 `@theme inline`으로 토큰을 Tailwind 유틸리티에 연결한다. 기본 스타일은 `new-york`.

```css
/* app/globals.css */
@import "tailwindcss";
@import "tw-animate-css";          /* tailwindcss-animate 대체 */

@custom-variant dark (&:is(.dark *));

:root {
  --radius: 0.625rem;              /* 10px (v3 시절 0.5rem) */
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  /* v2에서 추가된 차트 / 사이드바 토큰 — Chart, Sidebar 컴포넌트 사용 시 필수 */
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);    /* 다크에서는 반투명 흰 보더 */
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
}

/* 토큰 → Tailwind 유틸리티 연결 (bg-background, text-muted-foreground ...) */
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  /* ... chart-2~5, sidebar-* 동일 패턴 */
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

@layer base {
  * { @apply border-border outline-ring/50; }
  body { @apply bg-background text-foreground; }
}
```

### v3(HSL) → v4(OKLCH) 마이그레이션 요점
- `hsl(var(--primary))` 래핑 제거 → 변수에 색 전체가 들어있으므로 `var(--primary)`로 바로 사용
- `tailwind.config.js`의 `theme.extend.colors` 삭제 → `@theme inline`으로 이동
- `tailwindcss-animate` → `tw-animate-css`
- `forwardRef` 제거(React 19), 각 컴포넌트에 `data-slot` 속성 부여 → 스타일 타깃팅에 활용
- 기본 스타일 `default`는 폐기, `new-york`만 유지
- 색 조정 시 OKLCH의 L(명도)만 바꾸면 채도·색상 유지된 단계 생성이 쉬움

### 기본값 그대로 쓰면 "AI 티"가 난다
shadcn 기본 테마(무채색 + Inter + 0.625rem)는 바이브 코딩 결과물의 가장 흔한 외형이다. 실제 프로젝트에서는 최소한 다음 3가지를 바꾼다 (`anti-slop.md` 참고).
1. `--primary`를 브랜드 색 1개로 (보라/인디고 기본값 지양)
2. 폰트를 Geist / Pretendard / SUIT 등으로 명시 지정
3. `--radius`를 프로젝트 성격에 맞게 (데이터 도구 0.25~0.375rem, 소비자 앱 0.75~1rem)

## 인기 테마 색상 옵션

```
Zinc (기본): 회색 기반, 세련됨
Slate:       블루그레이, 차분함
Stone:       웜 그레이, 따뜻함
Gray:        순수 그레이, 무난함
Neutral:     뉴트럴, 심플함
Red:         레드 포인트
Rose:        로즈 핑크
Orange:      오렌지 포인트
Green:       그린 포인트
Blue:        블루 포인트 (기업)
Violet:      바이올렛 포인트
```

## 타이포그래피 (Geist / Inter / Pretendard KR)

```
폰트 설치: next/font/google의 Geist / Geist Mono 권장 (create-next-app 기본값)
한글: Pretendard (next/font/local 또는 CDN). Inter 단독 사용은 AI 기본값처럼 보이므로 지양
기본 크기: 14px (text-sm) ~ 16px (text-base)

text-xs:   12px
text-sm:   14px
text-base: 16px
text-lg:   18px
text-xl:   20px
text-2xl:  24px
text-3xl:  30px
text-4xl:  36px
text-5xl:  48px
```

## 스페이싱 (Tailwind 기반)

```
p-1: 4px    p-2: 8px    p-3: 12px   p-4: 16px
p-5: 20px   p-6: 24px   p-8: 32px   p-10: 40px
p-12: 48px  p-16: 64px
```

## Border Radius

```
--radius: 0.625rem (10px) 기본 (Tailwind v4 / 2025~)

rounded-sm:  calc(var(--radius) - 4px)  → 6px
rounded-md:  calc(var(--radius) - 2px)  → 8px
rounded-lg:  var(--radius)              → 10px
rounded-xl:  calc(var(--radius) + 4px)  → 14px
rounded-full: 9999px
```

---

## 주요 컴포넌트 패턴

### 버튼 Variants
```
default:     bg=primary, text=primary-foreground, hover: opacity 90%
secondary:   bg=secondary, text=secondary-foreground
outline:     border=1px input, bg=background, hover: bg=accent
ghost:       bg=transparent, hover: bg=accent
link:        text=primary, underline on hover
destructive: bg=destructive, text=destructive-foreground
size sm:     h=36px, px=12, text-sm
size default:h=40px, px=16
size lg:     h=44px, px=32, text-base
size icon:   h=40px, w=40px
```

### 카드
```
<Card>           bg=card, text=card-foreground, radius=lg, border, shadow-sm
<CardHeader>     p=24, flex-col, space-y-1.5
<CardTitle>      text-2xl, font-semibold
<CardDescription> text-sm, text-muted-foreground
<CardContent>    p=24, pt=0
<CardFooter>     p=24, pt=0, flex items-center
```

### 입력 / 폼
```
Input:     h=40px, border=1px input, bg=background, radius=md, px=12
           focus: ring=2px ring-ring
Textarea:  min-h=80px, 동일 스타일
Label:     text-sm, font-medium, leading-none
FormItem:  space-y-2
```

### 테이블
```
Table:      w-full, caption-bottom, text-sm
TableHeader: bg=muted/50
TableRow:    border-b, hover:bg=muted/50, transition-colors
TableHead:   h=48px, px=16, text-left, font-medium, text-muted-foreground
TableCell:   px=16, py=12
```

### 다이얼로그
```
DialogOverlay: fixed inset-0, bg=black/80, backdrop-blur-sm
DialogContent: fixed center, bg=background, radius=lg, shadow-lg
              w=full, max-w=lg, p=24
DialogHeader:  flex-col, space-y-1.5
DialogTitle:   text-lg, font-semibold
DialogFooter:  flex justify-end, space-x-2, pt=4
```

### Badge / Chip
```
default:     bg=primary, text=primary-foreground
secondary:   bg=secondary
outline:     border=1px
destructive: bg=destructive
size:        px=10, py=2, text-xs, radius=full, font-semibold
```

### Toast (Sonner 권장)
```bash
npm install sonner
```
```jsx
import { toast } from 'sonner';
toast.success('성공!');
toast.error('오류 발생');
```

---

## 설치 및 설정

```bash
npx shadcn@latest init          # Tailwind v4 + new-york + OKLCH 자동 구성
npx shadcn@latest add button card input form table dialog
```

## Next.js 기본 설정

```tsx
// app/layout.tsx
import { Geist, Geist_Mono } from 'next/font/google'
import { cn } from '@/lib/utils'

const geist = Geist({ subsets: ['latin'], variable: '--font-sans' })
const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-mono' })
// 한글은 Pretendard를 globals.css에서 font-family 뒤에 이어 붙인다

export default function RootLayout({ children }) {
  return (
    <html lang="ko" suppressHydrationWarning>
      <body className={cn(geist.variable, geistMono.variable, 'font-sans min-h-screen bg-background antialiased')}>
        {children}
      </body>
    </html>
  )
}
```

## 아이콘
```bash
npm install lucide-react
```
```jsx
import { Plus, Trash2, Edit, ChevronRight } from 'lucide-react';
<Plus className="h-4 w-4" />
```

## 유틸리티 함수
```ts
// lib/utils.ts (자동 생성됨)
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
```

## 금지 사항
- 이모티콘 사용 금지 → lucide-react 사용
- 인라인 스타일 사용 금지 → Tailwind 클래스 사용
- 하드코딩 색상 금지 → CSS 변수 토큰 사용 (bg-primary, text-muted-foreground 등)
- 컴포넌트 직접 수정보다 → variants/cn() 패턴으로 확장
