입력
Button, Input, Textarea, Select, Checkbox, Radio Group, Switch, Slider, Toggle, Combobox, Date Picker, Input OTP, Form
“This is not a component library. It’s how you build your component library.”
공식 문서의 첫 문장이다. 마케팅 카피가 아니라 기술적 사실의 서술이다.
npm install shadcn-ui 같은 건 없다. 설치할 패키지가 없다pnpm dlx shadcn@latest add button✔ Checking registry.✔ Installing dependencies.✔ Created 1 file: - components/ui/button.tsx ← 내 레포에 생긴 파일node_modules에는 프리미티브 패키지만 들어간다components/ui/button.tsx는 내 코드다. git에 커밋된다| 전통적 라이브러리 | shadcn/ui | |
|---|---|---|
| 설치 위치 | node_modules |
내 레포 |
| 수정 방법 | props, theme override, !important |
파일을 연다 |
| 버전 업 | npm update → 전부 바뀜 |
내가 원할 때 파일 단위로 |
| 스타일 커스터마이징 | 라이브러리가 허락한 범위 | 제한 없음 |
| 번들 크기 | 안 쓰는 것도 포함될 수 있음 | 쓰는 것만 |
| 초기 속도 | 빠름 | 빠름 |
| 장기 유지보수 | 라이브러리에 종속 | 내 책임 |
마지막 줄이 트레이드오프의 핵심이다. 자유를 얻고 책임을 진다.
정당한 질문이다. 실제로 초기엔 복붙과 비슷했다. 지금은 다르다.
add dialog를 하면 필요한 button도 함께 들어온다globals.css에 자동 추가components.json의 alias에 따라 import 경로가 조정된다shadcn/ui 컴포넌트는 스타일만 있는 게 아니다. 동작과 접근성은 프리미티브가 담당한다.
flowchart TB
A["내 앱<br/>Button variant='outline'"] --> B["components/ui/button.tsx<br/>= shadcn 이 복사해 준 코드<br/>Tailwind 클래스 + cva"]
B --> C["Base UI (또는 Radix / React Aria)<br/>포커스 트랩 · 키보드 · ARIA · 포지셔닝"]
C --> D["브라우저 DOM"]
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class B warn
class C key
class A,D mute
shadcn/ui가 소유권을 주는 건 위쪽 두 층뿐이다. 프리미티브는 여전히 npm 패키지고, 그건 오히려 다행이다 — 포커스 트랩과 ARIA를 직접 유지보수하고 싶은 사람은 없다. (18장)
오래된 자료는 전부 Radix UI를 전제한다. 지금 init하면 Base UI가 기본이다.
shadcn/create로 만든 프로젝트에서 Base UI 선택이 Radix의 2배였던 것이 배경pnpm dlx shadcn@latest init # Base UI (기본)pnpm dlx shadcn@latest init -b radix # Radix로 고정| Radix | Base UI | |
|---|---|---|
| 합성 프롭 | asChild |
render |
| 위치 계산 | Content가 직접 |
Positioner로 분리 |
| 패키지 | 통합 radix-ui |
@base-ui-components/react |
<Tooltip.Trigger asChild> <button>도움말</button></Tooltip.Trigger>내부적으로 Slot 컴포넌트가 props를 병합한다.
그래서 “왜 내 onClick이 안 먹지” 같은 디버깅이 어려웠다.
<Tooltip.Trigger render={<button>도움말</button>} />병합 지점이 눈에 보인다. 더 명시적이다.
입력
Button, Input, Textarea, Select, Checkbox, Radio Group, Switch, Slider, Toggle, Combobox, Date Picker, Input OTP, Form
표시
Card, Badge, Avatar, Table, Data Table, Separator, Skeleton, Progress, Chart, Typeset
오버레이
Dialog, Sheet, Drawer, Popover, Tooltip, Dropdown Menu, Context Menu, Command, Alert Dialog, Hover Card
구조 · 피드백
Tabs, Accordion, Collapsible, Sidebar, Navigation Menu, Breadcrumb, Pagination, Resizable, Scroll Area, Carousel · Alert, Toast, Sonner
70개가 넘는다. 그 밖에 Blocks(로그인 화면, 대시보드 등 완성된 조합)와 Charts(Recharts 래퍼)도 제공된다.
얻는 것
디자인 변경에 제약이 없다. 코드를 읽고 이해할 수 있다. 필요 없는 코드는 지운다. 라이브러리 업그레이드 지옥이 없다. 팀 컴포넌트로 자연스럽게 확장.
지는 책임
버그 수정이 자동으로 안 온다. 접근성 개선도 자동이 아니다. 팀원이 제각각 고치면 일관성이 무너진다. 어떤 파일을 수정했는지 추적해야 한다. 신규 입사자에게 “우리 규칙”을 알려줘야 한다.
반대로 디자이너가 있고 디자인이 계속 진화하는 제품이라면 이 모델이 압도적으로 유리하다.
node_modules가 아니다asChild → render