Base UI 리뷰: RadixUI와 비교
2025.06.21
얼마전 MUI에 기존 RadixUI팀원들이 합류하여 만든 Base UI가 출시되었습니다. BaseUI는 Radix, Material UI, Floating UI를 개발한 팀이 모여서 만든 프로젝트로, 접근성 높은 사용자 인터페이스를 구축하고 개발자 경험에 중점을 두고 개발했다고 합니다.
Headless한 컴포넌트를 제공하며, 예시에도 tailwind, CSS모듈, CSS-in-JS 등 다양한 스타일링 방식에 대한 예제를 보여줍니다. 이 라이브러리에 관심을 가진 이유는 기존에 RadixUI또한 이제껏 UI라이브러리중에 커스텀하기 가장 쉬웠기때문에 그보다 더 발전한 라이브러리가 되었을것이라 생각했습니다. 개발자들의 트위터를 보니 아직은 아니지만 animation 기능이 추가될 거라는 이야기가 있습니다.
framer-motion은 motion과 합쳐지고 shadcnUI기반의 MagicUI가 나오면서 애니메이션 라이브러리의 확장에 대해서도 매우 마음에 들었었는데, baseUI가 불러올 효과가 기대됩니다.
커스텀하기 좋은 Headless한 라이브러리 덕분에 ShadcnUI기반의 확장된 많은 UI라이브러리들이 생겨났고, cursor를 사용한 AI로 UI를 개발할떄 일관성 있는 개발을 했기 때문에, 이러한 UI 새로운 라이브러리들에 대한 소식은 매우 기대 됩니다.
-
Raidx의 유지보수가 아닌 새로운 UI를 만든 이유는? BaseUI는 이미 작년 24년도말쯤에 나온 이야기로, Radix를 개발하던 팀원중 한명인 Vlad Moroz의 트위터를 보면 BaseUI에 대한 이야기를 알 수 있습니다.
우선 Radix를 개발하는 WorkOS의 실제 비즈니스와 일치시키는것이 불가능했기 때문에, 팀원들이 떠나게 되었다고 말했습니다. 그렇게 일부 팀원가 MaterialUI, FloatingUI를 작업한 사람들과 함계 MUI의 지원을 받아 BaseUI를 만들고 있다고 합니다.
많은 개발자들이 ShadcnUI가 BaseUI를 대체하게 되는 것인지에 대해서 궁금증을 남겼고, 오늘 새벽에 Shadcn이 트위터에 이런 글을 남겼습니다.
"컴포넌트 라이브러리를 바꾸는건 지금 당장 할 수 있는 최악의 일이다." "그런데에 시간을 투자하지 말아라, Raidx의 업데이트되는 횟수가 줄고 있긴하지만, Radix에 어떤 버그가 있든 다른 곳에서 더 많은 버그를 발생 시킬 것이다." "컴포넌트 라이브러리는 안정적이여야하며, 위험을 감수하고 싶지 않은 부분이다."
# BaseUI VS RadixUI
Radix UI와 Base UI는 모두 접근성을 우선시하는 헤드리스 UI 라이브러리이지만, 내부 아키텍처와 구현 방식에서 차이점들이 보입니다.
1. 컴포지션 패턴
-
Radix UI:
asChild
+Slot
패턴 Radix UI는asChild
prop과Slot
컴포넌트를 사용하여 컴포지션을 구현합니다. 이는 직관적이고 배우기 쉬운 패턴이지만, 복잡한 컴포넌트 조합에서는 제한적일 수 있습니다.asChild
는 컴포넌트가 자식 요소를 대체하도록 하는 prop입니다.Slot
컴포넌트는 자식 요소의 props를 부모 컴포넌트에 전달하는 역할을 합니다.// 기본 사용법 <Dialog.Trigger asChild> <button>Open Dialog</button> </Dialog.Trigger> // 더 복잡한 사용법 <Dialog.Trigger asChild> <button className="custom-button"> <Icon /> Open Dialog </button> </Dialog.Trigger> // 중첩된 사용법 <Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger asChild> <button className="help-button"> Help </button> </Tooltip.Trigger> </Tooltip.Root> </Tooltip.Provider>
복잡한 컴포넌트 조합에서의 제한점:
asChild
는 단일 자식 요소만 지원하므로 여러 요소를 조합하기 어려움- 조건부 렌더링이나 동적 컴포넌트 변경이 복잡해짐
- props 전달이 단방향으로만 가능하여 세밀한 제어가 어려움
- 함수형 컴포넌트나 고차 컴포넌트와의 조합이 제한적
// 제한적인 예시 - 여러 조건부 요소 조합 // 이런 경우 asChild로는 구현하기 어려움 <Dialog.Trigger asChild> {isLink ? <a href={href}>Link</a> : <button>Button</button>} </Dialog.Trigger>
-
Base UI:
render
prop +useRender
패턴 Base UI는render
prop과useRender
훅을 사용하여 더 유연한 컴포지션을 제공합니다. 이 방식은 더 명시적이고 제어하기 쉬워서 복잡한 컴포넌트 조합에서 유리합니다.render
prop은 React 요소나 함수를 받아서 컴포넌트의 렌더링을 완전히 제어할 수 있게 해줍니다.// 기본 사용법 <Dialog.Trigger render={<button>Open Dialog</button>} /> // 함수형 render prop 사용법 <Dialog.Trigger render={(props, state) => ( <button {...props} className={state.disabled ? 'disabled' : 'enabled'} > Open Dialog </button> )} /> // 조건부 렌더링 <Dialog.Trigger render={isLink ? <a href={href}>Link</a> : <button>Button</button>} /> // 복잡한 컴포넌트 조합 <Dialog.Trigger render={ <Tooltip.Root> <Tooltip.Trigger asChild> <button className="custom-button"> <Icon /> Open Dialog </button> </Tooltip.Trigger> </Tooltip.Root> } />
BaseUI는 기존에 방식에 벗어나서, 완전한 렌더링 제어를 가능하게 하고 조건부 렌더링과 동적 컴포넌트 변경이 쉽도록 만들었습니다. 그리고 함수형 컴포넌트와 고차 컴포넌트와의 조합이 자유로워서 더 유연한 컴포지션이 가능합니다.
2. 렌더링 최적화 방식
-
Radix UI: 기본 React 최적화 Radix UI는 기본 React 렌더링 최적화에 의존합니다.
React.memo
와useCallback
을 통한 최적화를 사용하며, 컴포넌트별로 독립적인 최적화를 제공합니다.const DialogTrigger = React.forwardRef< DialogTriggerElement, DialogTriggerProps >((props: ScopedProps<DialogTriggerProps>, forwardedRef) => { const { __scopeDialog, ...triggerProps } = props; const context = useDialogContext(TRIGGER_NAME, __scopeDialog); const composedTriggerRef = useComposedRefs( forwardedRef, context.triggerRef ); return ( <Primitive.button type="button" aria-haspopup="dialog" aria-expanded={context.open} aria-controls={context.contentId} data-state={getState(context.open)} {...triggerProps} ref={composedTriggerRef} onClick={composeEventHandlers(props.onClick, context.onOpenToggle)} /> ); });
-
Base UI:
useRenderElement
고급 최적화 Base UI는useRenderElement
훅을 통한 고급 최적화를 제공합니다. 이는 상태 변경 시 필요한 부분만 리렌더링하여 더 세밀한 성능 제어가 가능하게 합니다.// Base UI의 useRenderElement 사용 예시 const element = useRenderElement("button", componentProps, { state, // 상태 변경 시에만 리렌더링 ref: [forwardedRef, buttonRef], // ref 병합 최적화 props: [buttonProps, elementProps], // props 병합 최적화 customStyleHookMapping, // 스타일 최적화 });
3. 컴포넌트 구조
-
Radix UI: 단일 컴포넌트 패턴 Radix UI는 단일 컴포넌트 패턴을 사용합니다. 이는 간단하고 직관적이지만, 세밀한 제어가 필요한 경우에는 제한적일 수 있습니다.
<Dialog.Root> <Dialog.Trigger /> <Dialog.Portal> <Dialog.Content> <Dialog.Title /> <Dialog.Description /> </Dialog.Content> </Dialog.Portal> </Dialog.Root>
-
Base UI: 세분화된 컴포넌트 패턴 Base UI는 더 세분화된 컴포넌트 패턴을 제공합니다. 이는 더 세밀한 제어와 커스터마이징이 가능하게 해줍니다.
<Dialog.Root> <Dialog.Trigger /> <Dialog.Portal> <Dialog.Positioner> <Dialog.Popup> <Dialog.Title /> <Dialog.Description /> <Dialog.Close /> </Dialog.Popup> </Dialog.Positioner> </Dialog.Portal> </Dialog.Root>
4. 상태 기반 스타일링
-
Radix UI: 기본 CSS 변수 Radix UI는 기본 CSS 변수를 사용합니다. 이는 간단하고 직관적이지만, 복잡한 상태에 따른 스타일링에는 제한적입니다.
/* Radix UI CSS 변수 */ .radix-dialog-content { --radix-dialog-content-transform-origin: center; --radix-dialog-content-animation-duration: 150ms; }
-
Base UI: 상태 기반 스타일 매핑 Base UI는 상태에 따른 스타일 매핑을 제공하여 더 세밀한 제어가 가능합니다. 이러한 상태 기반 스타일링은 복잡한 UI 상태를 더 체계적으로 관리할 수 있게 해줍니다.
// Base UI의 고급 상태 매핑 const customStyleHookMapping = { "data-state": (state) => (state.disabled ? "disabled" : "enabled"), "data-focused": (state) => state.focused, "data-pressed": (state) => state.pressed, };
결론
Radix UI는 단순하고 안정적인 API를 제공하여 학습 곡선이 낮고 검증된 아키텍처를 가지고 있습니다. 반면 Base UI는 좀더 세분화 되고 유연한 컴포넌트를 제공하여 복잡한 프로젝트에서 더 나은 성능과 유연성을 제공합니다.
아직 BaseUI의 경우 라이브러리의 종류가 많지않고, 안정적이지 않기떄문에 UI라이브러리를 교체하지는 않는게 좋습니다. 최근에 UI라이브러리는 매우 빠르게 개발자 친화적으로 진화 되어왓는데, BaseUI는 현재까지 나온 이러한 조건들을 충족시킨채 더욱 개선된 버전이 될 수 있을 것이라 생각합니다.