2-2편: Headless 컴포넌트 — 로직과 UI의 완전한 분리

JIIJIIJ·2025년 9월 25일
0

React

목록 보기
33/35
post-thumbnail

들어가며 — 합성 철학을 끝까지 밀어붙이면

앞에서 우리는 React가 왜 선언적이고, 왜 컴포넌트 기반을 택했는지 살펴봤다. 또 Compound Pattern을 통해 “UI를 작은 역할 단위로 쪼개 조합한다”는 합성 철학을 실무에 옮길 수 있음을 확인했다.

하지만 여기서 다시 근본적인 질문이 생긴다.

“UI는 본질일까? 아니면 그저 바뀌기 쉬운 껍데기에 불과할까?”

Headless 컴포넌트는 이 질문에 대한 하나의 대답이다. UI는 끊임없이 바뀌고, 로직은 오랫동안 살아남는다. 그렇다면 둘을 붙여놓을 게 아니라 완전히 분리해야 한다. 이것이 Headless 컴포넌트가 탄생한 이유이며, React가 주장해온 UI = f(state) 철학을 가장 극단적으로 실현하는 방식이다.

📖 참고 | React 공식 문서 – Thinking in React


문제 제기 — UI와 로직의 불균형

Compound Pattern으로 UI의 역할을 분리했지만, 여전히 로직과 UI가 하나의 컴포넌트에 묶여있다는 근본적인 문제는 남아있다. 실무에서 우리가 자주 겪는 불편함은 결국 이 UI와 로직의 결합 때문이다.

  1. 디자인 의존

    라이브러리에서 제공하는 UI가 팀의 디자인 시스템과 맞지 않으면, 끝없는 커스터마이징 전쟁이 시작된다.

  2. 로직 중복

    같은 동작(드롭다운 열림/닫힘, 선택 상태 관리, 키보드 내비게이션)을 여러 곳에서 반복해서 다시 구현한다.

  3. 수명 주기 불일치

    UI는 트렌드와 브랜드 리뉴얼에 따라 자주 바뀌지만, 로직은 몇 년 동안 변하지 않는다. 두 층위가 묶여 있으면 UI를 고칠 때마다 로직도 불필요하게 흔들린다.

즉, 시간이 지날수록 UI와 로직의 불균형은 유지보수 비용을 폭발적으로 늘린다.

📖 참고 | StackOverflow – Separation of concerns in frontend


해결책 — Headless가 제시하는 방향

UI와 로직의 결합으로 인해 발생하는 이 모든 문제를 해결하는 아이디어는 놀라울 정도로 단순하다. 로직을 UI로부터 완전히 해방시키는 것이다.

UI를 아예 떼어내고, 로직만 남기는 것.

Headless 컴포넌트는 “UI 없는 컴포넌트”다. 화면에 divbutton을 그리지 않는다. 대신 상태(state), 이벤트(event), 접근성(accessibility) 로직만 제공한다. UI는 개발자가 원하는 방식으로 직접 그린다.

이렇게 되면 UI는 자유롭게 갈아입힐 수 있는 껍데기가 되고, 로직은 오래 살아남는 핵심으로 남는다.

📖 참고 | Kent C. Dodds – Downshift 소개


철학적 배경 — 관심사 분리의 진화

Headless 컴포넌트가 로직과 UI를 분리하는 결정은 단순한 트렌드가 아니라, 소프트웨어 역사 속 '관심사 분리' 철학을 UI 계층까지 끌어내린 진화의 결과다.

  • 초창기 웹: HTML, CSS, JS가 한 파일에 섞여 있었다.
  • MVC(Model-View-Controller): 데이터(Model)와 뷰(View)를 분리하려는 첫 시도.
  • React: “UI는 상태의 함수”라는 철학을 내세워, 뷰를 예측 가능한 함수로 만들었다.
  • Compound Pattern: UI 내부의 작은 역할 단위까지 쪼개고 조합하는 합성 철학의 구체화.
  • Headless 컴포넌트: 여기서 더 나아가, UI를 완전히 없애고 로직만 남기는 극단적 형태.

즉, Headless는 단순한 패턴이 아니라 “관심사 분리”라는 오래된 소프트웨어 철학을 UI 설계에 끝까지 밀어붙인 결과물이다.

역사적으로도 이를 실험한 라이브러리들이 있었다. Downshift는 Select/Autocomplete에서 로직과 UI 분리를 가장 먼저 보여줬고, Reach UI, Radix UI, Headless UI (Tailwind Labs) 같은 라이브러리들이 이를 발전시켜왔다.

📖 참고 | Radix UI Docs, Headless UI Docs


해법 — Headless 컴포넌트의 구조

Headless 컴포넌트가 로직만 제공한다는 것을 이해했다면, 코드가 실제로 어떻게 구성되는지를 통해 Headless의 실체를 확인해 보자. Headless는 로직 = API, UI = 사용자 정의라는 원리로 움직인다.

예시: Headless Select

// 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 컴포넌트의 구조를 파악했다면, 이러한 완전한 분리가 실무에서 어떤 구체적인 이점을 가져다주는지 살펴보자.

1. 디자인 시스템 독립성

// Tailwind
<button className="px-4 py-2 bg-blue-500 text-white" {...getTriggerProps()}>
  {selected ?? "선택"}
</button>

// Material
<Button variant="outlined" {...getTriggerProps()}>
  {selected ?? "선택"}
</Button>

➡️ 로직은 동일, UI는 완전히 다르다.


2. 재사용 극대화

// 데스크탑 모달
<div className="desktop-modal" hidden={!isOpen}>{children}</div>

// 모바일 전체화면
<div className="mobile-fullscreen" hidden={!isOpen}>{children}</div>

➡️ 로직은 그대로 두고 UI만 교체한다.


3. 접근성 내장

<Menu>
  <Menu.Button>메뉴</Menu.Button>
  <Menu.Items>
    <Menu.Item>프로필</Menu.Item>
    <Menu.Item>로그아웃</Menu.Item>
  </Menu.Items>
</Menu>

➡️ 개발자는 스타일만 입히면 된다. 접근성은 이미 보장된다.


4. 테스트 단순화

test("아이템 선택 동작", () => {
  const { result } = renderHook(() => useSelect(["A", "B"]));
  act(() => result.current.select("B"));
  expect(result.current.selected).toBe("B");
});

➡️ DOM 없이도 로직만 검증 가능하다.


5. 실제 사례

  • Downshift: useCombobox 훅으로 Select/Autocomplete 로직 제공.
  • Headless UI (Tailwind Labs): Menu, Dialog, Popover 같은 핵심 UI 로직 제공.
  • Radix UI: 접근성 보장된 Headless 컴포넌트 세트.

➡️ 모두 공통적으로 “로직과 UI의 완전한 분리”를 철학으로 한다.

📖 참고 | Downshift Docs, Headless UI Docs, Radix UI Docs


정리 — 철학과 실무의 교차점

Headless 컴포넌트는 단순히 “스타일 없는 컴포넌트”가 아니다. 지금까지 살펴봤듯이, 이건 React가 강조해온 합성 철학의 궁극적 형태다.

  • 철학: UI = f(state)라는 공식을 끝까지 밀어붙여, UI는 언제든 교체 가능한 껍데기, 로직은 불변의 핵심으로 둔다.
  • 실무 가치: 디자인 유연성, 재사용성, 접근성, 테스트 용이성을 극대화한다.
  • 위상: Compound가 합성을 “역할 단위”로 구체화했다면, Headless는 합성을 “순수 로직” 차원으로 밀어붙인다.

즉, Headless는 React 개발자에게 단순한 도구가 아니라, 철학을 실천으로 끌어내는 성장의 계기다.

📖 참고 | Reactiflux – Jordan Walke Q&A


다음 편 예고

Headless 컴포넌트가 로직과 UI를 분리함으로써 얻는 자유와 안정성은 대규모 디자인 시스템 구축에서 그 진가를 발휘한다. 다음 글(2-4편)에서는 이 Headless 컴포넌트 위에서 디자인 시스템을 어떻게 구축할 수 있는지, 그리고 이 분리가 팀 생산성과 유지보수성에 어떤 변화를 주는지를 살펴본다.

profile
다크모드가 보기 좋아요

0개의 댓글