본문 바로가기
✨FRONTEND/📍React

UI 컴포넌트 라이브러리 비교(feat. Ant Design, HeadlessUI)

by 짱돌보리 2025. 6. 1.
728x90

UI 컴포넌트 라이브러리 비교

[🔥React 뽀개기] - 컴파운드 컴포넌트 + Headless UI 톺아보기

 

컴파운드 컴포넌트 + Headless UI 톺아보기

컴파운드 컴포넌트 + Headless UI 톺아보기Ant Design을 사용하면서 문득 이런 생각이 들었다.“이 구조… 혹시 컴파운드 컴포넌트 패턴인가?”꼬꼬무로 컴파운드 컴포넌트에 대해 알다보니 자연스럽

bori-note.tistory.com

 

지난글에서 컴파운드 컴포넌트 패턴에 대해 정리했었는데, 이번에는 그와 자주 비교되는 방식인 Headless UI 스타일 컴포넌트를 직접 구현해보며 차이를 느껴봤다. 두 패턴 모두 재사용성과 유연성 측면에서 장점이 있지만, 실제로 써보니 구현 방식이나 사용하는 입장에서 느껴지는 차이가 꽤 뚜렷했다. 이번 글에서는 간단한 UI를 만들면서 두 방식의 구조와 사용성을 비교해보고, 어떤 상황에 어떤 방식을 쓰는 게 더 나을지 내 기준에서 정리해보려고 한다.

✅ Ant Design

Components Overview - Ant Design

 

Components Overview - Ant Design

An enterprise-class UI design language and React UI library with a set of high-quality React components, one of best React UI library for enterprises

ant.design

 

Ant Design의 Select 컴포넌트가 예전에는 아래처럼 자식 컴포넌트를 직접 넣는 ‘컴파운드 스타일’을 썼다.

// 🙅‍♀️ v5.11.0 미만에서 사용되던 방식 (컴파운드 컴포넌트 스타일)
const { Option } = Select

const CompoundStyleSelect = () => {
  return (
    <Select style={{ width: '200px' }}>
      <Option value="apple">🍎 Apple</Option>
      <Option value="banana">🍌 Banana</Option>
    </Select>
  )
}

 

근데 최근 v5.11.0 이후부터는 이런 방식이 잘 렌더링되지도 않고, 아예 공식문서에서 options prop 기반 사용을 권장하고 있다.

// ✅ v5.11.0 이상에서 권장되는 방식 (options prop 사용)
import { Select } from 'antd'

const DataBasedSelect = () => {
  return (
    <Select
      style={{ width: '200px' }}
      options={[
        {
          value: 'apple',
          label: '🍎 Apple',
        },
        {
          value: 'banana',
          label: '🍌 Banana',
        },
      ]}
    />
  )
}

 

현재 내 ant design 버전이 5.11.0 이상이라 예전의 컴파운드 스타일 렌더링이 잘 안 되는 것을 볼 수 있다.

 

공식문서에 prop을 활용한 데이터 기반 방식을 권장한다고 나와있음.

✅ Basic Compound Component

그럼 반대로, 직접 Toggle 컴파운드 컴포넌트를 만들어보자!

<Toggle>
  <ToggleButton />
  <ToggleOn>✅ 토글 ON 상태입니다.</ToggleOn>
  <ToggleOff>❌ 토글 OFF 상태입니다.</ToggleOff>
</Toggle>
  • 상태는 Toggle에서 관리 (useState, Context로 제공)
  • 자식 컴포넌트는 이 상태를 구독해서 필요한 UI만 렌더링

이 구조 자체가 흔히 말하는 컴파운드 컴포넌트 패턴이다. 각 컴포넌트가 Context를 공유하고, 조합해서 원하는 UI를 구성할 수 있음!

  1. ToggleContext 생성
const ToggleContext = createContext<ToggleContextType | undefined>(undefined)
  1. Toggle (부모 컴포넌트)
export function Toggle({ children }: { children: ReactNode }) {
  const [on, setOn] = useState(false)
  const toggle = () => setOn((prev) => !prev)

  return (
    <ToggleContext.Provider value={{ on, toggle }}>
      <div>{children}</div>
    </ToggleContext.Provider>
  )
}
  • 토글의 상태(on)와 토글을 변경하는 함수(toggle)를 제공
  • 하위 컴포넌트는 이 값을 context로부터 받아 사용함
  1. useToggleContext (공통 훅)
const useToggleContext = () => {
  const context = useContext(ToggleContext)
  if (!context)
    throw new Error('Toggle compound components must be used within <Toggle>')
  return context
}
  • 각 자식 컴포넌트에서 안전하게 context를 접근하기 위한 custom hook
  1. ToggleButton
export function ToggleButton() {
  const { toggle, on } = useToggleContext()
  return (
    <button onClick={toggle}>
      {on ? '끄기' : '켜기'}
    </button>
  )
}
  • 버튼 클릭 시 상태를 변경 (toggle)
  • 현재 상태에 따라 버튼 텍스트도 달라짐
  1. ToggleOn, ToggleOff
export function ToggleOn({ children }: { children: ReactNode }) {
  const { on } = useToggleContext()
  return on ? <div>{children}</div> : null
}

export function ToggleOff({ children }: { children: ReactNode }) {
  const { on } = useToggleContext()
  return !on ? <div>{children}</div> : null
}
  • ToggleOn: 상태가 on === true일 때만 표시됨
  • ToggleOff: 상태가 on === false일 때만 표시됨

Toggle이 상태를 제공하고
ToggleButton, ToggleOn, ToggleOff는 그 상태를 구독해서 반응

 

 

 

위에서 내가 만든 토글 컴포넌트는 헤드리스 UI + 컴파운드 컴포넌트 패턴이 결합된 형태다.

  • Toggle 컴포넌트는 상태(on)와 상태를 바꾸는 함수(toggle)만 관리하고,
  • UI를 담당하는 자식 컴포넌트(ToggleButton, ToggleOn, ToggleOff)는 이 상태를 받아서 각자 UI를 렌더링하지만,
  • 여기서 스타일은 ToggleButton에만 들어있고, 나머지는 UI 최소화되어 있어
  • 사실상 '스타일은 있지만 기능은 완전 분리'된 헤드리스 UI 스타일에 가깝다.

만약 완전한 헤드리스 UI로 만들고 싶다면?

  • ToggleButton 에 스타일을 빼고
  • UI 꾸미는 부분은 사용자(컴포넌트 사용하는 쪽)에게 완전히 위임하면 된다.

HeadlessToggleButton을 렌더 프롭(render prop) 방식으로 변경

  • (on) => ReactNode형태의 함수를 children으로 받아서, 사용자가 직접 버튼 구조와 스타일을 제어할 수 있도록 수정함.
export function HeadlessToggleButton({
  children,
}: {
  children: (on: boolean) => React.ReactNode
}) {
  const { toggle, on } = useToggleContext()
  return <button onClick={toggle}>{children(on)}</button>
}
  • 아래처럼 사용자 입장에서 버튼의 스타일을 원하는 대로 꾸밀 수 있음:
<HeadlessToggleButton>
  {(on) => (
    <button className="rounded-2xl bg-red-200 px-4 py-2">
      {on ? '끄기 ❌' : '켜기 ✅'}
    </button>
  )}
</HeadlessToggleButton>

 

✅ Headless UI 라이브러리

"복잡한 로직과 접근성은 라이브러리가 대신 처리할게.
디자인은 너가 원하는 대로 꾸며줘!"

 

Headless UI는 Tailwind Labs에서 만든 스타일 없는 접근성 보장 UI 컴포넌트다.

Headless UI

 

Headless UI

Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.

headlessui.com

 

<Switch
  checked={enabled}
  onChange={setEnabled}
  className={`${
    enabled ? 'bg-blue-600' : 'bg-gray-300'
  } relative inline-flex h-6 w-11 items-center rounded-full`}
>
  <span
    className={`${
      enabled ? 'translate-x-6' : 'translate-x-1'
    } inline-block h-4 w-4 transform rounded-full bg-white transition`}
  />
</Switch>
  • <Switch>는 스위치 기능만 제공하고, 버튼 스타일은 직접 꾸며야 함.

✅ MUI

  • MUI는 기본적으로 완성된 스타일과 기능을 제공하는 컴포넌트 라이브러리다.
<FormControlLabel
  control={<Switch checked={checked} onChange={handleChange} />}
  label={checked ? '켜짐 ✅' : '꺼짐 ❌'}
/>

 

 

예전엔 그냥 UI 잘 나오는 게 최고라고 생각했는데,
직접 만들다 보니 컴포넌트 구조, 상태 관리, 사용자 자유도… 생각할 게 훨씬 많아졌다.

 

앞으론 UI 라이브러리 쓸 땐 "내가 원하는 구조에 맞는지" 먼저 따져보고,
직접 만들 땐 "얼마나 유연하게 쓸 수 있게 할 건지" 고민해보는 습관을 들이려고 한다.🙂‍↕️🙂‍↕️