앞에서 우리는 React가 왜 선언적이고, 왜 컴포넌트 기반을 택했는지 살펴봤다. 또 Compound Pattern을 통해 “UI를 작은 역할 단위로 쪼개 조합한다”는 합성 철학을 실무에 옮길 수 있음을 확인했다.
하지만 여기서 다시 근본적인 질문이 생긴다.
“UI는 본질일까? 아니면 그저 바뀌기 쉬운 껍데기에 불과할까?”
Headless 컴포넌트는 이 질문에 대한 하나의 대답이다. UI는 끊임없이 바뀌고, 로직은 오랫동안 살아남는다. 그렇다면 둘을 붙여놓을 게 아니라 완전히 분리해야 한다. 이것이 Headless 컴포넌트가 탄생한 이유이며, React가 주장해온 UI = f(state) 철학을 가장 극단적으로 실현하는 방식이다.
📖 참고 | React 공식 문서 – Thinking in React
Compound Pattern으로 UI의 역할을 분리했지만, 여전히 로직과 UI가 하나의 컴포넌트에 묶여있다는 근본적인 문제는 남아있다. 실무에서 우리가 자주 겪는 불편함은 결국 이 UI와 로직의 결합 때문이다.
디자인 의존
라이브러리에서 제공하는 UI가 팀의 디자인 시스템과 맞지 않으면, 끝없는 커스터마이징 전쟁이 시작된다.
로직 중복
같은 동작(드롭다운 열림/닫힘, 선택 상태 관리, 키보드 내비게이션)을 여러 곳에서 반복해서 다시 구현한다.
수명 주기 불일치
UI는 트렌드와 브랜드 리뉴얼에 따라 자주 바뀌지만, 로직은 몇 년 동안 변하지 않는다. 두 층위가 묶여 있으면 UI를 고칠 때마다 로직도 불필요하게 흔들린다.
즉, 시간이 지날수록 UI와 로직의 불균형은 유지보수 비용을 폭발적으로 늘린다.
📖 참고 | StackOverflow – Separation of concerns in frontend
UI와 로직의 결합으로 인해 발생하는 이 모든 문제를 해결하는 아이디어는 놀라울 정도로 단순하다. 로직을 UI로부터 완전히 해방시키는 것이다.
UI를 아예 떼어내고, 로직만 남기는 것.
Headless 컴포넌트는 “UI 없는 컴포넌트”다. 화면에 div나 button을 그리지 않는다. 대신 상태(state), 이벤트(event), 접근성(accessibility) 로직만 제공한다. UI는 개발자가 원하는 방식으로 직접 그린다.
이렇게 되면 UI는 자유롭게 갈아입힐 수 있는 껍데기가 되고, 로직은 오래 살아남는 핵심으로 남는다.
📖 참고 | Kent C. Dodds – Downshift 소개
Headless 컴포넌트가 로직과 UI를 분리하는 결정은 단순한 트렌드가 아니라, 소프트웨어 역사 속 '관심사 분리' 철학을 UI 계층까지 끌어내린 진화의 결과다.
즉, Headless는 단순한 패턴이 아니라 “관심사 분리”라는 오래된 소프트웨어 철학을 UI 설계에 끝까지 밀어붙인 결과물이다.
역사적으로도 이를 실험한 라이브러리들이 있었다. Downshift는 Select/Autocomplete에서 로직과 UI 분리를 가장 먼저 보여줬고, Reach UI, Radix UI, Headless UI (Tailwind Labs) 같은 라이브러리들이 이를 발전시켜왔다.
📖 참고 | Radix UI Docs, Headless UI Docs
Headless 컴포넌트가 로직만 제공한다는 것을 이해했다면, 코드가 실제로 어떻게 구성되는지를 통해 Headless의 실체를 확인해 보자. Headless는 로직 = API, UI = 사용자 정의라는 원리로 움직인다.
// Headless 훅에서 로직 제공
const { isOpen, selected, getTriggerProps, getItemProps } = useSelect(items);
// 개발자가 직접 UI 작성
<button {...getTriggerProps()}>
{selected ?? "선택하세요"}
</button>
<ul hidden={!isOpen}>
{items.map(item => (
<li key={item} {...getItemProps(item)}>
{item}
</li>
))}
</ul>
➡️ 여기서 Headless는 isOpen, selected, 이벤트 핸들러, 접근성 속성만 관리한다. UI는 전적으로 개발자에게 달려 있다.
📖 참고 | Downshift GitHub
Headless 컴포넌트의 구조를 파악했다면, 이러한 완전한 분리가 실무에서 어떤 구체적인 이점을 가져다주는지 살펴보자.
// Tailwind
<button className="px-4 py-2 bg-blue-500 text-white" {...getTriggerProps()}>
{selected ?? "선택"}
</button>
// Material
<Button variant="outlined" {...getTriggerProps()}>
{selected ?? "선택"}
</Button>
➡️ 로직은 동일, UI는 완전히 다르다.
// 데스크탑 모달
<div className="desktop-modal" hidden={!isOpen}>{children}</div>
// 모바일 전체화면
<div className="mobile-fullscreen" hidden={!isOpen}>{children}</div>
➡️ 로직은 그대로 두고 UI만 교체한다.
<Menu>
<Menu.Button>메뉴</Menu.Button>
<Menu.Items>
<Menu.Item>프로필</Menu.Item>
<Menu.Item>로그아웃</Menu.Item>
</Menu.Items>
</Menu>
➡️ 개발자는 스타일만 입히면 된다. 접근성은 이미 보장된다.
test("아이템 선택 동작", () => {
const { result } = renderHook(() => useSelect(["A", "B"]));
act(() => result.current.select("B"));
expect(result.current.selected).toBe("B");
});
➡️ DOM 없이도 로직만 검증 가능하다.
useCombobox 훅으로 Select/Autocomplete 로직 제공.Menu, Dialog, Popover 같은 핵심 UI 로직 제공.➡️ 모두 공통적으로 “로직과 UI의 완전한 분리”를 철학으로 한다.
📖 참고 | Downshift Docs, Headless UI Docs, Radix UI Docs
Headless 컴포넌트는 단순히 “스타일 없는 컴포넌트”가 아니다. 지금까지 살펴봤듯이, 이건 React가 강조해온 합성 철학의 궁극적 형태다.
즉, Headless는 React 개발자에게 단순한 도구가 아니라, 철학을 실천으로 끌어내는 성장의 계기다.
📖 참고 | Reactiflux – Jordan Walke Q&A
Headless 컴포넌트가 로직과 UI를 분리함으로써 얻는 자유와 안정성은 대규모 디자인 시스템 구축에서 그 진가를 발휘한다. 다음 글(2-4편)에서는 이 Headless 컴포넌트 위에서 디자인 시스템을 어떻게 구축할 수 있는지, 그리고 이 분리가 팀 생산성과 유지보수성에 어떤 변화를 주는지를 살펴본다.