Radix UI
Radix Primitives
접근성 우선 headless UI 컴포넌트 라이브러리.
Radix Primitives
접근성 우선 headless UI 컴포넌트 라이브러리.
Radix UI는 WorkOS에서 개발한 접근성(a11y) 중심의 headless React 컴포넌트 라이브러리입니다. Headless란 스타일이 없는 순수 기능과 로직만 제공한다는 의미로, 개발자가 완전한 디자인 자유도를 가지면서도 복잡한 접근성 요구사항을 자동으로 충족할 수 있습니다. WAI-ARIA 가이드라인을 완벽히 준수하며, 키보드 네비게이션과 포커스 관리가 내장되어 있습니다.
Radix Primitives는 Dialog, Dropdown Menu, Popover, Tooltip, Tabs, Accordion, Select 등 30개 이상의 저수준 UI 컴포넌트를 제공합니다. 각 컴포넌트는 Compound Component 패턴을 사용하여 Root, Trigger, Content 등 서브 컴포넌트로 구성됩니다. 이 패턴은 부모-자식 간 상태 공유를 Context API로 처리하여 개발자에게 유연한 구조를 제공합니다.
shadcn/ui는 Radix UI 위에 Tailwind CSS 스타일을 적용한 컴포넌트 컬렉션으로, 복사-붙여넣기 방식으로 프로젝트에 통합됩니다. 이는 패키지 의존성 없이 코드 소유권을 개발자에게 부여하는 새로운 접근법입니다. Radix + Tailwind 조합은 현대 React 프로젝트의 표준 UI 스택으로 자리잡았습니다.
Radix UI의 또 다른 특징은 비제어 컴포넌트(Uncontrolled)와 제어 컴포넌트(Controlled) 모두를 지원한다는 점입니다. 간단한 사용 시에는 기본 상태 관리를 사용하고, 복잡한 로직이 필요할 때는 외부 상태와 연동할 수 있습니다. 또한 Portal, asChild 패턴을 통해 DOM 구조 제어와 컴포넌트 합성이 가능합니다.
// Radix UI Dialog 컴포넌트 with Tailwind CSS
import * as Dialog from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
interface ModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: string;
children: React.ReactNode;
}
export function Modal({ open, onOpenChange, title, description, children }: ModalProps) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
{/* Overlay - 배경 어둡게 */}
<Dialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-fadeIn" />
{/* Content - 실제 모달 */}
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2
w-full max-w-md bg-white rounded-lg shadow-xl p-6
data-[state=open]:animate-contentShow
focus:outline-none">
{/* 닫기 버튼 */}
<Dialog.Close className="absolute right-4 top-4 rounded-sm opacity-70
hover:opacity-100 focus:outline-none focus:ring-2
focus:ring-slate-400 focus:ring-offset-2">
<X className="h-4 w-4" />
<span className="sr-only">닫기</span>
</Dialog.Close>
{/* 제목 - 스크린 리더가 읽음 */}
<Dialog.Title className="text-lg font-semibold text-gray-900">
{title}
</Dialog.Title>
{/* 설명 - 선택적 */}
{description && (
<Dialog.Description className="mt-2 text-sm text-gray-500">
{description}
</Dialog.Description>
)}
{/* 내용 */}
<div className="mt-4">
{children}
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
// 사용 예시
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>모달 열기</button>
<Modal
open={open}
onOpenChange={setOpen}
title="회원가입"
description="아래 정보를 입력해주세요."
>
<form className="space-y-4">
<input type="email" placeholder="이메일" className="w-full border p-2 rounded" />
<input type="password" placeholder="비밀번호" className="w-full border p-2 rounded" />
<button type="submit" className="w-full bg-blue-500 text-white p-2 rounded">
가입하기
</button>
</form>
</Modal>
</>
);
}
// Radix UI Dropdown Menu
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { ChevronRight, User, Settings, LogOut } from 'lucide-react';
export function UserMenu() {
return (
<DropdownMenu.Root>
{/* Trigger - 메뉴를 열 버튼 */}
<DropdownMenu.Trigger asChild>
<button className="flex items-center gap-2 px-3 py-2 rounded-lg
hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
<img src="/avatar.jpg" className="w-8 h-8 rounded-full" alt="프로필" />
<span>김철수</span>
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="min-w-[200px] bg-white rounded-lg shadow-lg border p-1
data-[state=open]:animate-slideDownAndFade"
sideOffset={5}
align="end"
>
{/* 일반 메뉴 아이템 */}
<DropdownMenu.Item className="flex items-center gap-2 px-3 py-2 rounded
cursor-pointer hover:bg-gray-100 focus:bg-gray-100 outline-none">
<User className="w-4 h-4" />
프로필
</DropdownMenu.Item>
{/* 서브 메뉴 */}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger className="flex items-center justify-between
px-3 py-2 rounded cursor-pointer hover:bg-gray-100 outline-none">
<div className="flex items-center gap-2">
<Settings className="w-4 h-4" />
설정
</div>
<ChevronRight className="w-4 h-4" />
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent className="min-w-[160px] bg-white
rounded-lg shadow-lg border p-1">
<DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer
hover:bg-gray-100 outline-none">
계정 설정
</DropdownMenu.Item>
<DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer
hover:bg-gray-100 outline-none">
알림 설정
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
{/* 구분선 */}
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
{/* 로그아웃 - 위험 동작 스타일 */}
<DropdownMenu.Item className="flex items-center gap-2 px-3 py-2 rounded
cursor-pointer text-red-600 hover:bg-red-50 outline-none">
<LogOut className="w-4 h-4" />
로그아웃
</DropdownMenu.Item>
{/* 화살표 */}
<DropdownMenu.Arrow className="fill-white" />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}