[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
---
|
||||
id: frontend-shadcn-radix-patterns
|
||||
title: shadcn/ui / Radix / Headless component patterns
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [frontend, ui, vibe-coding]
|
||||
tech_stack: { language: "TS / React", applicable_to: ["Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [shadcn/ui, Radix, headless component, accessible component, Aria-Kit, Ark UI]
|
||||
---
|
||||
|
||||
# shadcn/ui / Radix Patterns
|
||||
|
||||
> "Component library install" → "Component code copy". **Radix (a11y primitive) + Tailwind = shadcn/ui**. Customizable. Modern React 의 default.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Headless: behavior + a11y, style 없음.
|
||||
- shadcn: copy code (npm install X).
|
||||
- Radix / Aria / Ark = primitive.
|
||||
- Tailwind = style.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### shadcn/ui setup
|
||||
```bash
|
||||
npx shadcn@latest init
|
||||
npx shadcn@latest add button dialog dropdown-menu
|
||||
```
|
||||
|
||||
→ Code 가 `components/ui/` 에 추가. Yours 가 됨.
|
||||
|
||||
### Generated button
|
||||
```tsx
|
||||
// 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 text-sm font-medium ...',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground',
|
||||
outline: 'border border-input bg-background',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: 'default', size: 'default' },
|
||||
}
|
||||
);
|
||||
|
||||
export const Button = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<button ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props} />
|
||||
));
|
||||
```
|
||||
|
||||
### CVA (class-variance-authority)
|
||||
```ts
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
const styles = cva('base-class', {
|
||||
variants: {
|
||||
color: { red: 'bg-red-500', blue: 'bg-blue-500' },
|
||||
size: { sm: 'h-8', md: 'h-10', lg: 'h-12' },
|
||||
},
|
||||
compoundVariants: [
|
||||
{ color: 'red', size: 'lg', class: 'shadow-lg' },
|
||||
],
|
||||
defaultVariants: { color: 'blue', size: 'md' },
|
||||
});
|
||||
|
||||
styles({ color: 'red' });
|
||||
// 'base-class bg-red-500 h-10'
|
||||
```
|
||||
|
||||
### Radix Dialog
|
||||
```tsx
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<button>Open</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
|
||||
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded p-6">
|
||||
<Dialog.Title>Title</Dialog.Title>
|
||||
<Dialog.Description>Description</Dialog.Description>
|
||||
<Dialog.Close>Close</Dialog.Close>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
```
|
||||
|
||||
→ A11y (focus trap, escape, aria) 자동.
|
||||
|
||||
### asChild pattern
|
||||
```tsx
|
||||
// 자기 component 사용
|
||||
<Dialog.Trigger asChild>
|
||||
<Button variant="outline">Open</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
// → Trigger 가 button render X. 자식 의 button 에 props 머지.
|
||||
```
|
||||
|
||||
→ Style + a11y 공유 — wrapper 없음.
|
||||
|
||||
### Radix primitives list
|
||||
```
|
||||
- Accordion / Alert Dialog / Avatar / Checkbox
|
||||
- Collapsible / Context Menu / Dialog / Dropdown Menu
|
||||
- Hover Card / Label / Menubar / Navigation Menu
|
||||
- Popover / Progress / Radio Group / Scroll Area
|
||||
- Select / Separator / Slider / Switch / Tabs
|
||||
- Toast / Toggle / Tooltip
|
||||
```
|
||||
|
||||
→ A11y + keyboard 자동.
|
||||
|
||||
### Headless UI (Tailwind 의)
|
||||
```tsx
|
||||
import { Menu } from '@headlessui/react';
|
||||
|
||||
<Menu>
|
||||
<Menu.Button>Options</Menu.Button>
|
||||
<Menu.Items>
|
||||
<Menu.Item>{({ active }) => <a className={active ? 'bg-blue-500' : ''}>Profile</a>}</Menu.Item>
|
||||
<Menu.Item>...</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
### Ark UI (modern, multi-framework)
|
||||
```tsx
|
||||
import { Dialog } from '@ark-ui/react';
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>Open</Dialog.Trigger>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Positioner>
|
||||
<Dialog.Content>...</Dialog.Content>
|
||||
</Dialog.Positioner>
|
||||
</Dialog.Root>
|
||||
```
|
||||
|
||||
→ React + Vue + Solid + Svelte 같은 API.
|
||||
|
||||
### React Aria
|
||||
```tsx
|
||||
import { useButton } from '@react-aria/button';
|
||||
|
||||
function Button(props) {
|
||||
const ref = useRef();
|
||||
const { buttonProps } = useButton(props, ref);
|
||||
return <button {...buttonProps} ref={ref}>{props.children}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
→ Adobe 의 a11y primitive.
|
||||
|
||||
### Tailwind Variants (alternative)
|
||||
```ts
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
const button = tv({
|
||||
base: 'rounded px-4 py-2',
|
||||
variants: {
|
||||
color: { red: 'bg-red-500', blue: 'bg-blue-500' },
|
||||
},
|
||||
});
|
||||
|
||||
<button className={button({ color: 'red' })} />
|
||||
```
|
||||
|
||||
→ CVA 와 비슷.
|
||||
|
||||
### Form (react-hook-form + Zod + shadcn)
|
||||
```tsx
|
||||
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) });
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
function LoginForm() {
|
||||
const form = useForm<FormData>({ resolver: zodResolver(schema) });
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl><Input {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
→ shadcn 의 Form / FormField 가 wrapper.
|
||||
|
||||
### Theme (CSS variables)
|
||||
```css
|
||||
/* globals.css */
|
||||
:root {
|
||||
--primary: 222 47% 11%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222 47% 11%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222 47% 11%;
|
||||
--background: 222 47% 11%;
|
||||
--foreground: 210 40% 98%;
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// tailwind.config.js
|
||||
{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'hsl(var(--primary))',
|
||||
background: 'hsl(var(--background))',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
→ Light / dark 가 CSS var 만.
|
||||
|
||||
### Dark mode
|
||||
```tsx
|
||||
import { ThemeProvider, useTheme } from 'next-themes';
|
||||
|
||||
<ThemeProvider attribute="class">
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
|
||||
function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
return <Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>Toggle</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
### MUI / Chakra / Mantine 비교
|
||||
```
|
||||
MUI:
|
||||
- Material Design
|
||||
- 큰 ecosystem
|
||||
- Bundle 큰
|
||||
- 변경 어려움 (theme 만)
|
||||
|
||||
Chakra:
|
||||
- Style props (sx={{...}})
|
||||
- 작은 bundle
|
||||
- 합리
|
||||
|
||||
Mantine:
|
||||
- 깊은 component
|
||||
- 좋은 hook
|
||||
- TypeScript 친화
|
||||
|
||||
shadcn / Radix:
|
||||
- Code own
|
||||
- 변경 자유
|
||||
- 작은 bundle
|
||||
- 모던 default
|
||||
|
||||
→ Modern React = shadcn 가 default.
|
||||
```
|
||||
|
||||
### Bundle size
|
||||
```
|
||||
shadcn (Radix + Tailwind): only what 사용.
|
||||
MUI: full library (3+ MB).
|
||||
|
||||
→ shadcn 가 더 작음.
|
||||
```
|
||||
|
||||
### Server component (RSC) 친화
|
||||
```tsx
|
||||
// shadcn primitives 가 'use client' 명시.
|
||||
// Server component 가 import 가능 (자동 client).
|
||||
```
|
||||
|
||||
### Storybook
|
||||
```tsx
|
||||
// Button.stories.tsx
|
||||
export default { component: Button };
|
||||
export const Primary = { args: { variant: 'default' } };
|
||||
export const Destructive = { args: { variant: 'destructive' } };
|
||||
```
|
||||
|
||||
→ shadcn 의 component 가 component, storybook 친화.
|
||||
|
||||
### A11y testing
|
||||
```ts
|
||||
import { axe } from 'jest-axe';
|
||||
|
||||
test('button is accessible', async () => {
|
||||
const { container } = render(<Button>Click</Button>);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
```
|
||||
|
||||
### Custom variant
|
||||
```tsx
|
||||
const buttonVariants = cva('...', {
|
||||
variants: {
|
||||
variant: {
|
||||
// ... existing
|
||||
success: 'bg-green-500 text-white hover:bg-green-600', // 추가
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
→ 기존 file 수정. Library upgrade 영향 X (own code).
|
||||
|
||||
### Update strategy
|
||||
```bash
|
||||
# shadcn 의 새 version
|
||||
npx shadcn@latest diff button
|
||||
# → 변경점 보임
|
||||
|
||||
# Manual merge
|
||||
```
|
||||
|
||||
→ Library upgrade 가 자동 X — but freedom.
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| Modern React | shadcn/ui + Radix |
|
||||
| 빠른 prototype | shadcn (모든 거 즉시) |
|
||||
| 깊이 customize | Radix only |
|
||||
| 작은 bundle | Headless UI / Radix |
|
||||
| Multi-framework | Ark UI |
|
||||
| Material Design | MUI |
|
||||
| 폼 | shadcn Form + RHF + Zod |
|
||||
| Style props | Chakra / Mantine |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **MUI + Tailwind**: 2 css system 충돌.
|
||||
- **Radix 없이 dialog 작성**: a11y / focus / esc 깨짐.
|
||||
- **shadcn 가 그대로 + customize 안**: own code 의 가치 X.
|
||||
- **CSS variable 없이 dark mode**: class name 폭발.
|
||||
- **A11y test 없음**: keyboard / screen reader 깨짐.
|
||||
- **Bundle analyze 없음**: 큰 component lib 잠입.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- shadcn/ui = "code 가 own".
|
||||
- Radix = a11y primitive.
|
||||
- CVA / Tailwind Variants 가 variant 패턴.
|
||||
- React Aria / Headless UI / Ark UI = alternative.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[React_Headless_UI_Patterns]]
|
||||
- [[Frontend_Tailwind_Architecture]]
|
||||
- [[Frontend_Design_Tokens]]
|
||||
Reference in New Issue
Block a user