--- id: react-headless-ui-patterns title: Headless UI — Radix / Headless UI / 자체 Build category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [react, headless, radix, accessibility, vibe-coding] tech_stack: { language: "TS / React", applicable_to: ["Frontend"] } applied_in: [] aliases: [Radix UI, Headless UI, shadcn/ui, render props, compound components] --- # Headless UI > 행동 + a11y 만 제공, **스타일은 너의 것**. Radix UI / Headless UI (Tailwind Labs) / Ariakit. shadcn/ui = Radix 위에 Tailwind. 직접 dropdown / dialog / combobox 만들지 말 것. ## 📖 핵심 개념 - Behavior: focus / keyboard / portal / aria. - Compound components: `...` 패턴. - Render prop / asChild: child element 가 행동 흡수. - Controlled vs uncontrolled. ## 💻 코드 패턴 ### Radix Dialog ```tsx import * as Dialog from '@radix-ui/react-dialog'; Confirm Are you sure?
``` 자동 처리: focus trap, ESC, click-outside, scroll lock, aria. ### Radix Dropdown ```tsx import * as Menu from '@radix-ui/react-dropdown-menu'; navigate('/profile')}>Profile Sign out More → ... ``` ### Headless UI Combobox (autocomplete) ```tsx import { Combobox, ComboboxInput, ComboboxOptions, ComboboxOption } from '@headlessui/react'; const [q, setQ] = useState(''); const filtered = q ? items.filter(i => i.name.includes(q)) : items; setQ(e.target.value)} displayValue={(i: Item) => i?.name} /> {filtered.map(i => {i.name})} ``` ### asChild 패턴 (Radix) ```tsx // 새 element 안 만들고 child 에 trigger props 합침 ``` 요구사항: child 가 ref 를 받아야 → forwardRef 컴포넌트 사용. ### Ariakit Toolbar (잘 쓰는 라이브러리) ```tsx import * as Ariakit from '@ariakit/react'; const toolbar = Ariakit.useToolbarStore(); Bold Italic ``` ### shadcn/ui (Radix + Tailwind, copy-paste) ```bash npx shadcn@latest add dialog # 컴포넌트 직접 코드베이스에 복사 — 너가 소유 + 수정 자유 ``` ### 컴파운드 패턴 자체 구현 ```tsx const TabsContext = createContext(null); function Tabs({ defaultValue, children }: ...) { const [value, setValue] = useState(defaultValue); return {children}; } function TabsTrigger({ value, children }: ...) { const ctx = useContext(TabsContext)!; return ; } function TabsContent({ value, children }: ...) { const ctx = useContext(TabsContext)!; return ctx.value === value ?
{children}
: null; } Tabs.Trigger = TabsTrigger; Tabs.Content = TabsContent; ``` ## 🤔 의사결정 기준 | 컴포넌트 | 추천 | |---|---| | Modal / Dialog | Radix Dialog | | Dropdown / Popover | Radix DropdownMenu / Popover | | Combobox / Listbox | Headless UI / Ariakit | | Tabs | Radix Tabs | | Toast | Sonner / Radix Toast | | Date picker | react-day-picker | | Drag & drop | dnd-kit | | 자체 디자인 시스템 | shadcn 카피 + 변경 | ## ❌ 안티패턴 - **자체 dialog (focus trap 없음)**: a11y / 키보드 깨짐. 라이브러리 사용. - **Portal 안 쓰면**: z-index / overflow 지옥. - **asChild 사용 시 child 가 forwardRef X**: ref 전달 안 됨. - **Animation 직접**: Radix `data-state` + Tailwind animate 가 깔끔. - **Controlled / uncontrolled 혼용**: 둘 중 하나. - **모든 컴포넌트 npm 라이브러리 의존**: 무거움. 자주 쓰는 건 자체. - **Radix UI css 미적용**: 기본 unstyled. Tailwind/CSS 직접. ## 🤖 LLM 활용 힌트 - Radix + Tailwind + shadcn/ui 표준 조합. - Combobox = Headless UI / Ariakit. - 자체 자체 일 때만 — focus trap / a11y 제대로 구현해야. ## 🔗 관련 문서 - [[Frontend_Tailwind_Architecture]] - [[Frontend_A11y_Testing]] - [[React_Component_Composition]]