🧩 Design System5. Composition (Slot·Polymorphism)📖 개요

05-composition — 컴포넌트 합성 패턴

이 챕터가 답하는 질문: 한 컴포넌트의 여러 부분에 어떻게 스타일을 분배하고, 다른 컴포넌트와 어떻게 조립하며, 동작과 외형을 어떻게 분리하는가? 한 줄 답 (Pyramid Top): 합성은 slot(부분) → polymorphism(태그) → compound(부모-자식) → headless(동작-외형 분리)의 4단 사다리다. 위로 갈수록 재사용의 단위가 커지고, props는 줄어든다.


Why — 왜 “합성”이 따로 챕터인가

4장 (04-recipes-variants)이 한 컴포넌트의 외형 변수를 다뤘다면, 5장은 그 다음 질문을 다룬다:

“Button은 variant로 풀린다. 그런데 Card에는 header, body, footer가 있다. 어떻게 variant를 분배할까? 그리고 Button을 <a>로도 <button>으로도 쓰고 싶다. 어떻게 태그를 바꿀까? Tabs는 List·Trigger·Panel이 서로 알아야 한다. 어떻게 상태를 공유할까? Radix Dialog는 동작은 완벽한데 디자인은 없다. 어떻게 스타일만 입힐까?”

이 4가지 질문은 모두 “하나의 컴포넌트가 여러 단위로 쪼개지거나, 여러 컴포넌트가 하나로 모일 때” 생긴다. 즉, *합성(composition)*의 문제다.


How — 4단 사다리

사다리풀려는 문제대표 기술챕터
① Slot recipe”한 컴포넌트 안에 부분이 여러 개”Panda sva, tailwind-variants slots01-slot-recipes
② Polymorphism”같은 컴포넌트, 다른 태그(<a> vs <button>)“Radix Slot + asChild02-polymorphism-and-aschild
③ Compound”부모와 자식이 같은 상태를 봐야 함”React Context + Component.Child03-compound-components
④ Headless”동작은 어렵고 디자인은 다양함”Radix Primitives, Headless UI, React Aria04-headless-ui-pattern
④’ Styling”headless 위에 우리 디자인 토큰”data-state + Panda _open / Tailwind data-[...]05-styling-third-party-components
⚠ Anti”props 30개 · 깊은 drilling · variant 폭발”(없음, 진단표)06-composition-anti-patterns

What — 챕터 인덱스

파일다루는 것분량
01-slot-recipesPanda sva slot recipe, tailwind-variants slots — root/label/icon에 variant 분배~13KB
02-polymorphism-and-aschildRadix Slot, asChild, polymorphic as prop의 안티패턴, cloneElement 기반~11KB
03-compound-components<Tabs><Tabs.List><Tabs.Trigger />...</Tabs>, Context로 상태 공유, dot notation~11KB
04-headless-ui-pattern”동작은 우리, 스타일은 너” 철학. Radix, Headless UI, React Aria, Ariakit, Reka UI, Melt UI 비교~11KB
05-styling-third-party-componentsdata attribute 기반 스타일링, data-state="open", Panda _open, Tailwind data-[state=open]:~11KB
06-composition-anti-patternsprops 폭발, prop drilling, variant 30개, slot 이름 불일치, asChild ref 누락~9KB

What-if — 합성을 잘못 다루면

  • slot 이름이 통일되지 않으면 — Card는 header/body/footer, Dialog는 title/content/actions, Toast는 icon/message/dismiss 각자 다르면 학습 비용이 폭증한다. → 도메인 전체에 공통 slot 어휘 규약을 둔다.
  • polymorphic as prop을 그대로 쓰면 — TypeScript 타입 폭발, forwardRef와 generic의 충돌. → asChild로 바꾼다.
  • compound component의 context를 export하지 않으면 — 외부에서 커스텀 자식을 만들 수 없다. → Context와 hook (useTabsContext)를 함께 export.
  • headless 라이브러리를 wrapping 없이 사용하면 — Radix를 직접 import하는 100개 파일이 생긴다. → 우리 디자인 시스템 패키지가 한 번 wrap하고 스타일 입힌다.

Insight — 왜 “합성”이 점점 더 중요해졌나

2013년의 React는 *“props만 받는 함수”*였다. 모든 것은 한 컴포넌트에 props로 들어갔다 — <Button color="blue" size="lg" rounded={true} icon="check" iconPosition="right" />. 그러다 보니 5년 만에 props 40개짜리 Button이 등장했다.

2018년 Ryan Florence와 Michael Jackson의 “compound components” 패턴, 그리고 2020년의 Radix Primitives는 그 흐름을 뒤집었다.

“props에 모든 것을 담지 말고, 자식 컴포넌트를 받아라.”

이 한 줄이 5장 전체의 정신이다. <Tabs.Trigger />Tabs라는 컨텍스트 안에서만 의미가 있고, 그 덕에 Tabs는 props 3~4개로 충분해진다. 디자인 시스템이 유연하면서도 학습 가능하려면 이 합성 어휘가 필수다.


요약

  • 합성은 slot → polymorphism → compound → headless의 4단 사다리.
  • 위로 갈수록 재사용 단위가 커지고 props는 줄어든다.
  • 안티패턴: props 30개, slot 이름 불일치, as prop 남용, headless를 직접 import.
  • 다음 챕터(06-theming)는 조립된 컴포넌트에 컨텍스트별 값을 어떻게 입히는가를 다룬다.