2-1편 Compound Pattern — 조합형 컴포넌트 설계

JIIJIIJ·2025년 9월 23일
0

React

목록 보기
32/35
post-thumbnail

들어가며 — 2편 컴포넌트 철학을 실무로 끌어내기

2편에서 우리는 React가 컴포넌트 기반을 택한 이유를 살펴보았다. “UI를 기능 단위로 분할하고, 그 단위를 조합해 복잡성을 다룬다”는 철학은 React의 정체성 그 자체였다.

그런데 여기서 한 가지 질문이 생긴다.
“실무에서 합성(Composition)을 어떻게 설계로 구체화할 수 있을까?”

그 답 중 가장 대표적인 방식이 바로 Compound Pattern(조합형 컴포넌트 패턴)이다.
이 패턴은 단순한 코드 트릭이 아니라, React가 강조한 Composition over Inheritance 원칙을 UI 설계로 구체화한 것이다. 이번 글에서는 Compound Pattern을 철학과 실무 양쪽에서 살펴보고, 실제 사례(Base UI)를 통해 어떻게 구현되는지 탐구한다.


문제 제기 — 모놀리식 컴포넌트의 한계

복잡한 UI를 한 덩어리로 만들면 편해 보인다. 예를 들어 Select 컴포넌트를 생각해 보자.

<Select
  options={items}
  searchable
  clearable
  withGrouping
  withPortal
  withArrow
  onChange={...}
/>

처음엔 직관적이지만, 기능이 늘어날수록 문제가 생긴다.

  1. 옵션 폭발
    기능이 늘어날수록 props가 수십 개로 불어나며, API가 이해하기 어려워진다.

  2. 커스터마이즈 한계
    특정 기능만 바꾸고 싶어도 내부에 결합돼 있어 교체하기 어렵다.

  3. 테스트 복잡성
    하나의 거대한 컴포넌트가 모든 상태를 다루다 보니, 테스트해야 할 경우의 수가 기하급수적으로 늘어난다.

  4. 확장 난이도
    새로운 요구가 들어오면 props를 또 늘리거나 내부를 뜯어고쳐야 한다. 브레이킹 체인지도 잦아진다.

즉, 모놀리식 접근은 단순해 보이지만, 시간이 갈수록 유지보수 비용이 폭발한다.


철학적 배경 — 합성(Composition) vs 상속(Inheritance)

React 창시자들은 초기부터 합성(Composition)을 상속보다 우위에 두었다.
공식 문서에도 나와 있듯이:

“React는 Composition을 상속보다 권장합니다.”
React 공식 문서: Composition vs Inheritance

이 철학은 React 초창기부터 강조되었다.
Pete Hunt가 2013년 JSConf 발표에서 React를 처음 소개할 때도 다음과 같이 말했다:

“React는 작은 컴포넌트들을 조합해서 전체 UI를 구성합니다. Composition이 핵심 철학입니다.”
JSConf EU 2013 발표 영상

상속은 한 번의 설계로 다양한 경우를 포괄하려 하지만, UI는 조합 가능한 작은 단위들의 결합으로 더 잘 설명된다. Compound Pattern은 바로 이 철학을 설계 차원에서 구체화한 것이다.


해법 — Compound Pattern의 구조

Compound Pattern은 이렇게 작동한다:

  • 부모 컴포넌트: 상태와 행위를 관리하고, Context로 하위에 배포한다.
  • 자식 컴포넌트: 특정 역할만 수행한다. (Trigger, Content, Item 등)
  • 사용자: 필요한 자식만 조합해 UI를 완성한다.

하지만 Compound Pattern은 “부모는 맥락, 자식은 역할, 사용자는 조합”이라는 단순한 요약으로 끝나지 않는다. 실제로는 여섯 가지 핵심 원리가 얽혀 있다.

1. 맥락(Context) 기반 계약

  • 부모 컴포넌트가 상태와 동작의 단일 출처(Single Source of Truth)가 된다.
  • 이 상태는 Context를 통해 하위로 전파된다.
  • 자식은 props 대신 맥락을 구독하여 필요한 정보만 취한다.

➡️ 결합도를 낮추고 확장 가능성을 높인다.


2. 역할(Role) 중심의 하위 컴포넌트

자식은 스타일이 아니라 역할을 정의한다.

  • Trigger: 열림/닫힘 진입점
  • Content: 목록 컨테이너
  • Item: 선택 가능한 항목
  • Label, Group, Separator: 의미적 단위

➡️ 자식이 수행하는 역할이 명확할수록 재사용과 교체가 쉬워진다.


3. JSX 트리 자체가 API

모놀리식 컴포넌트는 props 조합이 API다.

<Select options={items} searchable clearable withArrow />

Compound Pattern은 JSX 구조 그 자체가 API다.

<Select>
  <Select.Trigger>열기</Select.Trigger>
  <Select.Content>
    <Select.Item value="A">A</Select.Item>
    <Select.Item value="B">B</Select.Item>
  </Select.Content>
</Select>

➡️ React의 선언적 특성과 직결된다.


4. 일방향 데이터 흐름

  • 부모 → 자식: 상태/맥락 전달
  • 자식 → 부모: 이벤트(Action) 발생
graph TD
  A[Select 부모] --> B[Trigger]
  A --> C[Content]
  C --> D[Item A]
  C --> E[Item B]
  
  B -- onClick --> A
  D -- onClick --> A
  E -- onClick --> A

➡️ 데이터는 내려가고, 이벤트는 올라온다. 구조가 단순하니 예측 가능성이 확보된다.


5. 책임의 경계 설정

  • 부모: 상태 관리, 전이 규칙, 접근성 제공
  • 자식: 역할 수행, UI 선언
  • 사용자: 원하는 조합 구성

➡️ 문제가 생겼을 때 어느 레이어를 봐야 하는지 명확하다.


6. 조합적 확장

부품을 교체·래핑해 새로운 변형을 쉽게 얻는다.

function CustomItem({ value, icon, children }) {
  return (
    <Select.Item value={value}>
      <Icon name={icon} />
      {children}
    </Select.Item>
  );
}

➡️ 상속이 아니라 합성으로 기능을 확장한다.


Base UI로 보는 Compound Pattern

Base UI의 철학

Material-UI 팀이 만든 Base UI는 Compound Pattern을 아예 설계 철학으로 삼은 라이브러리다.
Popover, Dialog, Menu 같은 컴포넌트를 완제품으로 주는 대신, 작은 부품 단위로 쪼개서 제공한다.

왜 Base UI인가?

  1. 철학적 선언 — MUI 팀은 Base UI를 “unstyled, accessible building blocks”로 정의합니다. 완제품을 제공하는 게 아니라, 작은 블록을 조합하게 한다는 점에서 Compound Pattern 철학과 정확히 맞닿아 있습니다.

  2. API 구조 — Popover, Menu 같은 컴포넌트는 Trigger, Content, Arrow 같은 하위 요소를 직접 조합해야 동작합니다. 즉, JSX 트리 자체가 API가 됩니다.

  3. 비교 우위 — Ant Design, Chakra 같은 라이브러리가 props 중심(monolithic) 접근을 택하는 것과 달리, Base UI는 조합 가능한 단위 제공에 집중합니다. Radix UI, Headless UI도 같은 계열이지만, Base UI는 Material 팀이 설계한 만큼 학습용·실무용 모두에 좋은 교본이 됩니다.

📖 참고 | MUI Base UI Overview

예시: Popover

<Popover>
  <Popover.Trigger>열기</Popover.Trigger>
  <Popover.Portal>
    <Popover.Positioner>
      <Popover.Popup>
        <Popover.Arrow />
        내용
      </Popover.Popup>
    </Popover.Positioner>
  </Popover.Portal>
</Popover>

분석

  • Trigger: 열림/닫힘 제어
  • Portal: DOM 계층 이동
  • Positioner: 위치 계산
  • Popup: 실제 콘텐츠
  • Arrow: 시각적 장식

➡️ 사용자는 Arrow를 빼거나 Portal을 생략하는 등 원하는 조합을 직접 만든다.

왜 중요한가?

  1. 조합성 — 같은 Popover도 팀 상황에 맞게 무한히 변형 가능
  2. 유연성 — Tailwind, styled-components, emotion 등 스타일링 무관하게 활용
  3. 예측 가능성 — 각 부품의 역할이 분명
  4. 확장성 — 새로운 기능이 필요하면 props 추가가 아니라 부품 추가로 대응

Base UI는 Compound Pattern을 패턴 적용 사례 수준이 아니라 제품 철학으로 끌어올린 대표적 사례다.


원리 모델 — 액션·계산·데이터, 상태 머신 관점

Compound Pattern을 함수형 사고로 모델링하면 이렇게 정리된다.

  • 데이터: open, selected, items
  • 계산: 포커스 이동, 활성 항목 결정
  • 액션: 클릭, 키보드 입력

상태 머신 관점

  • 상태: Closed, Open(Idle), Open(Navigating)

  • 전이:

    • ClosedOpen(Idle) (Trigger 클릭)
    • Open(Idle)Open(Navigating) (키보드 이동)
    • Open(Navigating)Closed (선택/Enter)

Compound Pattern은 Context 공유를 넘어서, UI를 상태 머신으로 모델링할 수 있는 발판이 된다.


확장 논의 — 실무 고려 사항

  1. 선택적 부품: Search, Group, Empty, Separator, Portal 등 상황별 조합
  2. 제어/비제어 전략: value/onValueChange vs 내부 상태, 혼용 시 invariant 경고 필요
  3. 접근성: aria-expanded, aria-selected 관리, 키보드 내비게이션 준수
  4. 테스트: 상태 머신 단위 → 부품 단위 → 조합 단위 E2E로 계층적 검증
  5. 성능: 많은 항목일 땐 가상 스크롤, 이벤트 위임, 포지셔닝 최적화

앗 그렇네 형님 😅
제가 마무리 부분 존댓말 섞어서 정리했네. 다시 반말톤으로 정리해서 보여줄게.


정리 — 철학과 실무의 교차점: Compound Pattern, React 본연의 가치를 구현하다

Compound Pattern은 단순한 UI 구조화 기법을 넘어, React의 핵심 철학과 실무적 효율성을 교차시키는 강력한 설계 패러다임이다.

  • 철학의 구현: React가 강조하는 Composition over Inheritance 원칙을 코드 레벨에서 가장 명확하게 드러낸다. 단일 컴포넌트에 모든 책임을 부여하는 모놀리식 접근 방식을 넘어서, UI를 유연하고 독립적인 부품들의 조합으로 바라보게 한다.
  • 실무적 가치: API 안정성, 높은 조합성, 유연성, 예측 가능한 동작, 확장성이라는 핵심 가치를 제공한다. Base UI처럼 이 패턴을 제품의 철학으로까지 끌어올린 사례는, Compound Pattern이 단순한 선택지가 아니라 견고한 UI를 위해 필수적인 원칙임을 보여준다.
  • 생산성과 협업: 각 부품의 역할이 명확해지고 책임의 경계가 뚜렷해지면서, 디버깅 동선이 단축되고 협업 효율성이 높아진다. 이는 개인 생산성뿐 아니라 대규모 팀에서도 UI의 일관성과 협업 속도를 유지하는 데 중요한 기반이 된다.

결론적으로 Compound Pattern은 React의 합성 철학을 실무의 무기로 전환한다. 복잡한 UI 환경에서도 안정적이고 유연한 애플리케이션을 구축할 수 있는 길을 열어주며, 개발자에게는 단순한 기술을 넘어 철학을 실천으로 끌어내는 성장의 계기가 된다.

즉, Compound Pattern은 작은 단위를 조합해 큰 구조를 만들어가는 방식으로, React가 지향해 온 합성 중심의 사고방식을 가장 명확하게 보여주는 설계 원칙이다.


다음 편 예고

다음 글(2-3편)에서는 Compound Pattern을 더 밀어붙여, UI를 아예 제거하고 로직만 남기는 Headless 컴포넌트 설계를 살펴본다. 이는 디자인 시스템과 프레임워크를 넘나드는 확장성을 제공한다.

profile
다크모드가 보기 좋아요

0개의 댓글