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={...}
/>
처음엔 직관적이지만, 기능이 늘어날수록 문제가 생긴다.
옵션 폭발
기능이 늘어날수록 props가 수십 개로 불어나며, API가 이해하기 어려워진다.
커스터마이즈 한계
특정 기능만 바꾸고 싶어도 내부에 결합돼 있어 교체하기 어렵다.
테스트 복잡성
하나의 거대한 컴포넌트가 모든 상태를 다루다 보니, 테스트해야 할 경우의 수가 기하급수적으로 늘어난다.
확장 난이도
새로운 요구가 들어오면 props를 또 늘리거나 내부를 뜯어고쳐야 한다. 브레이킹 체인지도 잦아진다.
즉, 모놀리식 접근은 단순해 보이지만, 시간이 갈수록 유지보수 비용이 폭발한다.
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은 “부모는 맥락, 자식은 역할, 사용자는 조합”이라는 단순한 요약으로 끝나지 않는다. 실제로는 여섯 가지 핵심 원리가 얽혀 있다.
➡️ 결합도를 낮추고 확장 가능성을 높인다.
자식은 스타일이 아니라 역할을 정의한다.
Trigger: 열림/닫힘 진입점Content: 목록 컨테이너Item: 선택 가능한 항목Label, Group, Separator: 의미적 단위➡️ 자식이 수행하는 역할이 명확할수록 재사용과 교체가 쉬워진다.
모놀리식 컴포넌트는 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의 선언적 특성과 직결된다.
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
➡️ 데이터는 내려가고, 이벤트는 올라온다. 구조가 단순하니 예측 가능성이 확보된다.
➡️ 문제가 생겼을 때 어느 레이어를 봐야 하는지 명확하다.
부품을 교체·래핑해 새로운 변형을 쉽게 얻는다.
function CustomItem({ value, icon, children }) {
return (
<Select.Item value={value}>
<Icon name={icon} />
{children}
</Select.Item>
);
}
➡️ 상속이 아니라 합성으로 기능을 확장한다.
Material-UI 팀이 만든 Base UI는 Compound Pattern을 아예 설계 철학으로 삼은 라이브러리다.
Popover, Dialog, Menu 같은 컴포넌트를 완제품으로 주는 대신, 작은 부품 단위로 쪼개서 제공한다.
철학적 선언 — MUI 팀은 Base UI를 “unstyled, accessible building blocks”로 정의합니다. 완제품을 제공하는 게 아니라, 작은 블록을 조합하게 한다는 점에서 Compound Pattern 철학과 정확히 맞닿아 있습니다.
API 구조 — Popover, Menu 같은 컴포넌트는 Trigger, Content, Arrow 같은 하위 요소를 직접 조합해야 동작합니다. 즉, JSX 트리 자체가 API가 됩니다.
비교 우위 — Ant Design, Chakra 같은 라이브러리가 props 중심(monolithic) 접근을 택하는 것과 달리, Base UI는 조합 가능한 단위 제공에 집중합니다. Radix UI, Headless UI도 같은 계열이지만, Base UI는 Material 팀이 설계한 만큼 학습용·실무용 모두에 좋은 교본이 됩니다.
📖 참고 | MUI Base UI Overview
<Popover>
<Popover.Trigger>열기</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup>
<Popover.Arrow />
내용
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover>
➡️ 사용자는 Arrow를 빼거나 Portal을 생략하는 등 원하는 조합을 직접 만든다.
Base UI는 Compound Pattern을 패턴 적용 사례 수준이 아니라 제품 철학으로 끌어올린 대표적 사례다.
Compound Pattern을 함수형 사고로 모델링하면 이렇게 정리된다.
open, selected, items상태: Closed, Open(Idle), Open(Navigating)
전이:
Closed → Open(Idle) (Trigger 클릭)Open(Idle) → Open(Navigating) (키보드 이동)Open(Navigating) → Closed (선택/Enter)Compound Pattern은 Context 공유를 넘어서, UI를 상태 머신으로 모델링할 수 있는 발판이 된다.
Search, Group, Empty, Separator, Portal 등 상황별 조합value/onValueChange vs 내부 상태, 혼용 시 invariant 경고 필요aria-expanded, aria-selected 관리, 키보드 내비게이션 준수앗 그렇네 형님 😅
제가 마무리 부분 존댓말 섞어서 정리했네. 다시 반말톤으로 정리해서 보여줄게.
Compound Pattern은 단순한 UI 구조화 기법을 넘어, React의 핵심 철학과 실무적 효율성을 교차시키는 강력한 설계 패러다임이다.
결론적으로 Compound Pattern은 React의 합성 철학을 실무의 무기로 전환한다. 복잡한 UI 환경에서도 안정적이고 유연한 애플리케이션을 구축할 수 있는 길을 열어주며, 개발자에게는 단순한 기술을 넘어 철학을 실천으로 끌어내는 성장의 계기가 된다.
즉, Compound Pattern은 작은 단위를 조합해 큰 구조를 만들어가는 방식으로, React가 지향해 온 합성 중심의 사고방식을 가장 명확하게 보여주는 설계 원칙이다.
다음 글(2-3편)에서는 Compound Pattern을 더 밀어붙여, UI를 아예 제거하고 로직만 남기는 Headless 컴포넌트 설계를 살펴본다. 이는 디자인 시스템과 프레임워크를 넘나드는 확장성을 제공한다.