React

[React] 헤드리스 컴포넌트란 무엇일까

joseph0926 2024. 11. 7. 18:56

최근에 회사에서 헤드리스 컴포넌트를 이용하여 공통 컴포넌트를 구현하였습니다.

이에 대한 정리를 겸해서 헤드리스 컴포넌트가 무엇인지, 그리고 사용하면 어떤 점이 좋은지 등을 알아보겠습니다.

 

헤드리스 컴포넌트란?

 

Completely unstyled, fully accessible UI components

유명한 헤드리스 컴포넌트 라이브러리 중 하나인 `HeadlessUI`의 소개글입니다.

여기서 설명하는 것을 보면 스타일이 없는, 완전히 접근 가능한(접근성이 좋은) UI 컴포넌트라고 설명하고 있습니다.

 

여기서 주목할 점은 두 가지입니다:

 

1. 스타일이 없는

2. 접근성이 좋은

 

만약 이게 일반적인 디자인 시스템 라이브러리였다면 1번의 문구를 자신들의 메인에 작성해놓지 않았을 것입니다.

하지만 헤드리스는 다릅니다. 스타일이 존재하지 않는다는 것은 헤드리스 컴포넌트의 기본이고 매우 중요한 부분입니다.

 

디자인 시스템 vs 헤드리스 컴포넌트

// Chakra UI Dropdown - 복잡한 커스터마이징

import { Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';

const CustomChakraMenu = chakra(Menu, {
  baseStyle: {
    // 복잡한 테마 오버라이딩이 필요
    list: {
      bg: 'custom.background',
      border: 'none',
      boxShadow: 'custom.shadow',
    },
    item: {
      _hover: {
        bg: 'custom.hover',
      },
    },
  },
});

// 사용
<CustomChakraMenu>
  <MenuButton>메뉴</MenuButton>
  <MenuList>
    <MenuItem>프로필</MenuItem>
    <MenuItem>설정</MenuItem>
  </MenuList>
</CustomChakraMenu>

 

// Radix UI Dropdown - 깔끔한 스타일링

import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import styled from 'styled-components';

const StyledTrigger = styled(DropdownMenu.Trigger)`
  background: white;
  border: 2px solid #eaeaea;
  border-radius: 4px;
  padding: 8px 12px;
`;

const StyledContent = styled(DropdownMenu.Content)`
  background: white;
  border-radius: 6px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
`;

// 사용
<DropdownMenu.Root>
  <StyledTrigger>메뉴</StyledTrigger>
  <StyledContent>
    <StyledItem>프로필</StyledItem>
    <StyledItem>설정</StyledItem>
  </StyledContent>
</DropdownMenu.Root>

 

접근성과 최적화

헤드리스 컴포넌트의 또 다른 큰 장점은 접근성과 최적화가 이미 잘 구현되어 있다는 점입니다.

 

접근성

예를 들어 모달 컴포넌트를 직접 구현한다고 가정해보겠습니다

 

// 기본 모달 - 접근성 이슈 존재
function BasicModal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        {children}
        <button onClick={onClose}>닫기</button>
      </div>
    </div>
  );
}
// Radix UI Dialog - 접근성 처리 완료
import * as Dialog from '@radix-ui/react-dialog';

function AccessibleModal({ children }) {
  return (
    <Dialog.Root>
      <Dialog.Trigger>모달 열기</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay />
        <Dialog.Content>
          {children}
          <Dialog.Close>닫기</Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

 

직접 구현한 모달의 경우 다음과 같은 접근성 이슈들이 있습니다

 

- 모달이 열렸을 때 배경 컨텐츠에 대한 포커스 트랩 처리가 없음

- 스크린 리더 사용자를 위한 적절한 ARIA 속성이 없음

- ESC 키를 눌렀을 때 모달이 닫히지 않음

- 모달이 열렸을 때 배경 컨텐츠가 여전히 스크린 리더에 읽힘

 

반면 Radix UI의 Dialog는 이러한 접근성 처리가 모두 되어있습니다

- `role="dialog"` 및 적절한 ARIA 속성 자동 적용

- 포커스 트랩 처리

- ESC 키로 닫기 기능

- 배경 컨텐츠에 대한 적절한 aria-hidden 처리

 

최적화

컴포넌트 최적화도 마찬가지입니다. 예를 들어 셀렉트 컴포넌트를 보겠습니다

 

// 최적화되지 않은 Select
function BasicSelect({ options, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="select">
      <button onClick={() => setIsOpen(!isOpen)}>
        {options.find(opt => opt.value === value)?.label}
      </button>
      {isOpen && (
        <div className="options">
          {options.map(option => (
            <div
              key={option.value}
              onClick={() => {
                onChange(option.value);
                setIsOpen(false);
              }}
            >
              {option.label}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
// Radix UI의 최적화된 Select
import * as Select from '@radix-ui/react-select';

function OptimizedSelect({ options }) {
  return (
    <Select.Root>
      <Select.Trigger>
        <Select.Value />
      </Select.Trigger>
      <Select.Portal>
        <Select.Content>
          <Select.Viewport>
            {options.map(option => (
              <Select.Item key={option.value} value={option.value}>
                <Select.ItemText>{option.label}</Select.ItemText>
              </Select.Item>
            ))}
          </Select.Viewport>
        </Select.Content>
      </Select.Portal>
    </Select.Root>
  );
}

 

직접 구현한 셀렉트의 경우 다음과 같은 최적화 이슈가 있습니다

 

- 옵션이 많을 경우 성능 저하 (가상화 처리 없음)

- 불필요한 리렌더링 발생

- 포지셔닝 계산 비효율

 

반면 Radix UI의 Select는 다음과 같은 최적화가 이미 되어있습니다

 

- 가상화된 뷰포트로 대량의 옵션도 효율적으로 처리

- 메모이제이션을 통한 불필요한 리렌더링 방지

- 효율적인 포지셔닝 계산 및 캐싱

 

이처럼 헤드리스 컴포넌트는 단순히 스타일만 제거된 것이 아닙니다. 오히려 스타일을 제외한 모든 복잡한 로직들이 최적화되어 있어 개발자가 디자인에만 집중할 수 있게 해줍니다.

 

헤드리스 컴포넌트 라이브러리 소개

마지막으로 널리 사용되는 헤드리스 컴포넌트 라이브러리들을 소개하겠습니다.

 

Radix UI

Radix UI는현재 가장 인기 있는 헤드리스 컴포넌트 라이브러리입니다.

 

주요 특징

- React 기반의 프리미티브 컴포넌트 제공

- 뛰어난 접근성과 키보드 인터랙션

- WAI-ARIA 명세 준수

- 타입스크립트로 작성되어 타입 안정성 보장

- 다양한 컴포넌트 (Dialog, Dropdown, Select, Tabs 등)

 

import * as Accordion from '@radix-ui/react-accordion';
import styled from 'styled-components';

const StyledAccordion = styled(Accordion.Root)`
  width: 300px;
`;

const StyledItem = styled(Accordion.Item)`
  margin-top: 1px;
`;

const StyledHeader = styled(Accordion.Header)`
  margin: 0;
`;

const StyledTrigger = styled(Accordion.Trigger)`
  padding: 16px;
  width: 100%;
  background: #fafafa;
`;

const StyledContent = styled(Accordion.Content)`
  padding: 16px;
  background: white;
`;

 

Headless UI

 

Headless UI는 Tailwind CSS 팀에서 만든 헤드리스 컴포넌트 라이브러리입니다.

 

주요 특징

- Tailwind CSS와의 완벽한 호환성

- React와 Vue 지원

- 간단하고 직관적인 API

- 필수적인 컴포넌트들 제공

 

import { Menu } from '@headlessui/react';

function MyMenu() {
  return (
    <Menu>
      <Menu.Button className='rounded-md bg-blue-500 px-4 py-2 text-white'>
        Options
      </Menu.Button>
      <Menu.Items className='absolute mt-2 rounded-md bg-white shadow-lg'>
        <Menu.Item>
          {({ active }) => (
            <a
              className={`${active ? 'bg-blue-500 text-white' : ''} px-4 py-2`}
            >
              Account
            </a>
          )}
        </Menu.Item>
        <Menu.Item>
          {({ active }) => (
            <a
              className={`${active ? 'bg-blue-500 text-white' : ''} px-4 py-2`}
            >
              Settings
            </a>
          )}
        </Menu.Item>
      </Menu.Items>
    </Menu>
  );
}

 

React Aria

 

React Aria는 Adobe에서 만든 헤드리스 컴포넌트 훅 라이브러리입니다.

 

주요 특징

- 훅 기반 API

- 국제화(i18n) 지원

- 모바일 터치 인터랙션 지원

- 광범위한 접근성 기능

 

import { useButton } from '@react-aria/button';

function Button(props) {
  const ref = useRef();
  const { buttonProps } = useButton(props, ref);

  return (
    <button
      {...buttonProps}
      ref={ref}
      className='rounded-md bg-blue-500 px-4 py-2 text-white'
    >
      {props.children}
    </button>
  );
}

 

각 라이브러리별 선택 기준

- Tailwind CSS를 사용하는 프로젝트라면 → Headless UI

- 접근성이 매우 중요한 프로젝트라면 → Radix UI

- 커스텀 컴포넌트 라이브러리를 만든다면 → React Aria