[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -2,159 +2,260 @@
|
||||
id: wiki-2026-0508-compound-components
|
||||
title: Compound Components
|
||||
category: 10_Wiki/Topics
|
||||
status: needs_review
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: []
|
||||
aliases: [Compound Component Pattern, React Compound Components]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.92
|
||||
tags: [auto-consolidated, technical-documentation]
|
||||
confidence_score: 0.95
|
||||
verification_status: applied
|
||||
tags: [react, patterns, components, composition, context]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-08
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
inferred_by: Claude Opus 4.7 (auto-normalize 2026-05-08)
|
||||
tech_stack:
|
||||
language: unspecified
|
||||
framework: unspecified
|
||||
language: typescript
|
||||
framework: react
|
||||
---
|
||||
|
||||
# [[Compound Components|Compound Components]]
|
||||
# Compound Components
|
||||
|
||||
## 📌 한 줄 통찰 (The Karpathy Summary)
|
||||
컴파운드 컴포넌트(Compound Components) 패턴은 React에서 부모 컴포넌트와 자식 컴포넌트들이 암묵적인 상태와 동작을 공유하며 하나의 응집된 단위로 함께 작동하도록 하는 설계 패턴이다.[1, 2] 이 패턴을 사용하면 수많은 prop을 전달해야 하는 문제를 피하고, 개발자가 네이티브 HTML 요소를 사용하듯 유연하게 UI를 구성(compose)할 수 있다.[1, 3] 마치 레고 블록처럼 부모 컴포넌트가 기본 구조와 규칙을 제공하고 자식 컴포넌트들을 자유롭게 조립하여 확장 가능한 사용자 인터페이스를 구축할 수 있게 해준다.[4]
|
||||
## 매 한 줄
|
||||
> **"매 parent 가 매 child 들과 implicit state share — 매 expressive composable API."**. Compound Component Pattern은 React (그리고 Vue/Solid) 에서 `<Tabs><Tab/><Tab.Panel/></Tabs>` 류의 declarative API 를 만드는 표준. Radix UI, Headless UI, Ariakit, shadcn/ui 의 dominant pattern. 2026년 React 19 + Server Components 시대에도 그대로 valid.
|
||||
|
||||
## 📖 구조화된 지식 (Synthesized Content)
|
||||
* **설계 철학 및 멘탈 모델의 전환**
|
||||
* 기존의 Prop 기반(Prop-Driven) API는 요구사항(레이아웃 변경, 조건부 동작 등)이 추가될 때마다 Prop이 폭발적으로 증가하여 유지보수가 어렵고 컴포넌트 내부가 파악하기 힘든 '블랙박스'가 되는 문제가 있습니다 [5-7].
|
||||
* 합성 컴포넌트는 이를 '구성 중심(Composition-Driven)' 모델로 전환하여, 컴포넌트는 상태와 규칙만 관리하고 레이아웃과 구조의 결정권은 이를 사용하는 소비자(Consumer)에게 일임합니다 [7, 8].
|
||||
## 매 핵심
|
||||
|
||||
* **[[React Context|React Context]]를 활용한 암시적 상태 공유**
|
||||
* 이 패턴의 핵심 기술은 React Context를 내부 상태 관리의 '계약(Contract)'으로 사용하는 것입니다 [9]. 부모(Root) 컴포넌트가 Context를 통해 상태를 제공하고, 하위 컴포넌트들은 [[Prop Drilling|Prop Drilling]] 없이 필요한 상태만 구독하여 동작합니다 [1, 10].
|
||||
* 내부 구현이 추상화되어 있으므로, 소비자는 내부가 어떻게 작동하는지 몰라도 하위 컴포넌트들을 자유롭게 조립할 수 있습니다 [9].
|
||||
### 매 problem solved
|
||||
- 단일 monolithic component 의 props explosion (`<Tabs items={...} renderTab={...} ...>`).
|
||||
- 매 caller 가 매 children 의 layout/order 를 control 의 X.
|
||||
- Slot composition 의 부재.
|
||||
|
||||
* **주요 장점**
|
||||
* **뛰어난 유연성과 가독성:** 특정 기능을 쉽게 포함하거나 제외할 수 있으며, 개발자는 하위 요소의 렌더링 순서와 구조를 자유롭게 재배치할 수 있습니다 [4, 7, 8].
|
||||
* **접근성(A11y) 자동화:** 컴포넌트 내부 Context에서 상태와 ID를 제어하므로, `aria-controls`나 `aria-labelledby` 같은 접근성 속성을 소비자가 수동으로 연결할 필요 없이 자동으로 처리할 수 있습니다 [11].
|
||||
### 매 solution
|
||||
- Parent 가 Context 로 shared state 의 publish.
|
||||
- Children 이 Context 를 consume — explicit prop drilling 의 X.
|
||||
- Children 의 component 가 namespace pattern (`Tabs.Tab`) 또는 separate exports.
|
||||
|
||||
* **성능 최적화 기법**
|
||||
* 복잡한 시스템이나 대규모 렌더링이 필요한 경우, 불필요한 리렌더링을 방지하기 위해 빈번하게 변경되는 상태와 정적인 구성을 각각 다른 Context로 분리(Split Contexts)하여 최적화할 수 있습니다 [12, 13].
|
||||
* 필요한 곳에만 하위 컴포넌트를 전략적으로 메모이제이션(Memoization)하여 성능을 유지합니다 [14].
|
||||
### 매 patterns
|
||||
- **React Context based** (most common 2026).
|
||||
- **`React.Children` cloning** (legacy, fragile with refs/server components).
|
||||
- **State reducer** (advanced — caller customizes reducer).
|
||||
- **`asChild` slot** (Radix — wraps user element).
|
||||
|
||||
* **사용 시 주의사항 및 한계**
|
||||
* 패턴을 구현하기 위해 초기에 작성해야 할 코드가 많아지고, Context 기반 렌더링 비용이 발생하며, 디버깅이 다소 까다로워질 수 있습니다 [11].
|
||||
* 버튼, 배지, 아바타, 아이콘처럼 구조가 단일하고 레이아웃이 고정된 컴포넌트에는 불필요한 추상화(Overusing)가 될 수 있으므로 피해야 합니다 [15, 16]. 탭, 아코디언, 모달, 드롭다운과 같이 레이아웃과 조립 방식이 다양한 복잡한 UI에 가장 적합합니다 [17-19].
|
||||
### 매 응용
|
||||
1. Tabs, Accordion, Disclosure.
|
||||
2. Menu, Dropdown, Combobox.
|
||||
3. Dialog, Drawer, Popover.
|
||||
4. Form fields with label/error/description.
|
||||
5. DataTable column children.
|
||||
|
||||
---
|
||||
## 💻 패턴
|
||||
|
||||
- **작동 원리와 개념**: 컴파운드 컴포넌트 패턴은 부모 컴포넌트를 여러 개의 작은 자식 컴포넌트로 쪼갠 후, 이들 간의 상호작용을 props나 Context API 등의 상태 관리 기법으로 통제하는 원리에 기반한다.[2] 부모 컴포넌트는 전체적인 상태를 관리하고 유연한 자식 컴포넌트(예: `<Modal.Header>`, `<Modal.Body>`, `<Modal.Footer>`)를 노출하여 사용자가 자연스럽게 UI를 조립할 수 있게 돕는다.[1]
|
||||
- **Props 전달의 한계 극복(Prop Soup 해결)**: 전통적인 컴포넌트 방식은 타이틀, 내용, 버튼 등 UI의 다양한 요소를 구현하기 위해 지나치게 많은 prop을 전달해야 하는 'Prop Soup' 문제를 유발하며, 이는 유지보수성과 재사용성을 떨어뜨린다.[3, 5] 컴파운드 패턴은 React의 특수한 `children` prop을 활용해 어떠한 HTML 구조나 JSX도 유연하게 수용함으로써 이러한 경직성을 해결한다.[6]
|
||||
- **서브컴포넌트의 캡슐화와 종속성**: 이 패턴에서 생성되는 서브컴포넌트들은 오직 부모 컴포넌트의 컨텍스트 내부에서만 의미를 가진다.[7] 부모 컴포넌트의 범위를 벗어나 독립적으로 존재하거나 사용되지 않는 헬퍼(helper) 컴포넌트로 설계되어 우발적인 오용을 방지하고 코드의 발견성을 높인다.[7]
|
||||
- **주요 적용 대상**: 이 패턴은 드롭다운, 모달, 탭, 테이블 등 자식 컴포넌트가 부모 컴포넌트의 로직에 의존하면서도 다양한 형태의 렌더링이 필요한 복잡한 UI 요소를 개발할 때 빛을 발한다.[2, 8, 9] ShadCN, Material UI, Radix UI와 같은 유명한 디자인 시스템 및 컴포넌트 라이브러리들이 이 패턴을 채택하고 있다.[8]
|
||||
### Context-based Tabs
|
||||
```typescript
|
||||
import { createContext, useContext, useId, useState } from 'react';
|
||||
|
||||
## ⚠️ 모순 및 업데이트 (Contradictions & Updates)
|
||||
컴파운드 컴포넌트 패턴은 직관적이고 커스터마이징하기 쉬운 복잡한 API를 구축하는 데 훌륭한 장점을 제공하지만, 몇 가지 명확한 단점과 제약 사항이 존재한다.[10]
|
||||
- **상태 관리의 복잡성 증가**: 여러 컴포넌트가 상태를 공유해야 하므로 내부적인 상태 처리 로직이 단일 컴포넌트 구조보다 다소 복잡해질 수 있다.[11]
|
||||
- **서브컴포넌트의 오용 위험**: 서브컴포넌트는 부모 컴포넌트와 의미론적(semantically)으로 연결되어야 하므로 무작위로 부착해서는 안 된다.[12] 특히 서브컴포넌트만을 개별적으로 재수출(re-export)하는 것을 피해야 한다.[12] 만약 부모 컴포넌트의 컨텍스트 내에서 변경 사항이 생겼을 때, 개별 서브컴포넌트만 사용하는 소비자(consumer)가 이를 인지하지 못하면 치명적인 오류가 발생할 수 있다.[12]
|
||||
- **과도한 패턴 적용(Over-engineering) 경계**: 애플리케이션의 모든 컴포넌트를 컴파운드 컴포넌트 패턴으로 만들려고 시도해서는 안 된다.[12] 자식 컴포넌트의 렌더링 구조가 중요하고 소비자에게 유연성을 제공해야 하는 상황에서만 선택적으로 적용하는 것이 권장된다.[12]
|
||||
type TabsContextValue = {
|
||||
activeId: string;
|
||||
setActiveId: (id: string) => void;
|
||||
baseId: string;
|
||||
};
|
||||
const TabsContext = createContext<TabsContextValue | null>(null);
|
||||
|
||||
## 🔗 지식 연결 (Graph)
|
||||
- **Related Topics:** [[Render Props|Render Props]], Headless Components, Context API, [[Atomic Design|Atomic Design]]
|
||||
- **Projects/Contexts:** [[Radix UI|Radix UI]], Headless UI, [[MUI|MUI]]
|
||||
- **Contradictions/Notes:** 합성 컴포넌트는 매우 유연하고 강력한 패턴이지만, 하위 컴포넌트의 구성을 소비자가 자유롭게 바꿀 수 있기 때문에 의도치 않게 접근성이나 일관된 UX를 해칠 수 있습니다. 따라서 디자인 시스템에서는 안전한 조합의 경계(Safe composition [[Boundaries|Boundaries]])를 정의하고 문서화하는 것이 필수적입니다 [15, 17]. 또한 단순하고 고정된 레이아웃을 가진 컴포넌트에서는 일반적인 Prop 기반 접근법이 훨씬 간단하고 안전합니다 [16].
|
||||
function useTabs() {
|
||||
const ctx = useContext(TabsContext);
|
||||
if (!ctx) throw new Error('Tabs.* must be used inside <Tabs>');
|
||||
return ctx;
|
||||
}
|
||||
|
||||
---
|
||||
*Last updated: 2026-04-26*
|
||||
export function Tabs({
|
||||
defaultValue,
|
||||
children,
|
||||
}: { defaultValue: string; children: React.ReactNode }) {
|
||||
const [activeId, setActiveId] = useState(defaultValue);
|
||||
const baseId = useId();
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeId, setActiveId, baseId }}>
|
||||
<div className="tabs">{children}</div>
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
---
|
||||
function List({ children }: { children: React.ReactNode }) {
|
||||
return <div role="tablist" className="flex gap-2">{children}</div>;
|
||||
}
|
||||
|
||||
### Related Concepts
|
||||
function Trigger({ value, children }: { value: string; children: React.ReactNode }) {
|
||||
const { activeId, setActiveId, baseId } = useTabs();
|
||||
const selected = activeId === value;
|
||||
return (
|
||||
<button
|
||||
role="tab"
|
||||
id={`${baseId}-trigger-${value}`}
|
||||
aria-selected={selected}
|
||||
aria-controls={`${baseId}-panel-${value}`}
|
||||
tabIndex={selected ? 0 : -1}
|
||||
onClick={() => setActiveId(value)}
|
||||
className={selected ? 'tab tab-active' : 'tab'}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
#### [아키텍처 및 UI 설계 패턴]
|
||||
- [[Container and Presentational Pattern]]
|
||||
- 연결 이유: 컴파운드 컴포넌트가 UI 요소들을 논리적 그룹으로 묶는 패턴이라면, 컨테이너/프레젠테이셔널 패턴 역시 로직(상태 관리, 데이터 패칭)과 표현(UI 렌더링)을 분리하여 코드의 재사용성을 높이는 대표적인 React UI 설계 패턴이기 때문이다.[13-15]
|
||||
- 이 개념을 통해 더 깊게 이해할 수 있는 부분: 관심사 분리(Separation of Concerns) 원칙을 적용하여 컴포넌트의 책임을 어떻게 나눌 것인가에 대한 거시적인 설계 전략.
|
||||
function Panel({ value, children }: { value: string; children: React.ReactNode }) {
|
||||
const { activeId, baseId } = useTabs();
|
||||
if (activeId !== value) return null;
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
id={`${baseId}-panel-${value}`}
|
||||
aria-labelledby={`${baseId}-trigger-${value}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
- [[Render Props]]
|
||||
- 연결 이유: 컴파운드 컴포넌트와 마찬가지로 부모 컴포넌트가 내부 상태를 자식에게 전달하되, 렌더링 구조의 통제권을 컴포넌트를 소비하는 측에 위임하여 동적인 렌더링을 가능하게 하는 유연한 UI 설계 패턴이기 때문이다.[16, 17]
|
||||
- 이 개념을 통해 더 깊게 이해할 수 있는 부분: React에서 컴포넌트 간 상태와 로직을 공유하고 UI 구조 결정을 소비자에게 이관하는 다양한 접근 방식과 그 유연성.
|
||||
|
||||
#### [상태 및 데이터 관리 도구]
|
||||
- [[Context API]]
|
||||
- 연결 이유: 컴파운드 컴포넌트 구조 내에서 부모 컴포넌트가 자식 컴포넌트들과 상태를 공유하고 관리할 때, 복잡한 Prop Drilling을 방지하기 위해 Context API가 내부적으로 널리 사용되기 때문이다.[2, 18, 19]
|
||||
- 이 개념을 통해 더 깊게 이해할 수 있는 부분: 컴포넌트 트리 내에서 명시적인 prop 전달 없이 데이터를 효율적으로 공유하고 컴포넌트 그룹 내의 상태를 캡슐화하는 원리.
|
||||
|
||||
### Deeper Research Questions
|
||||
- 컴파운드 컴포넌트 패턴과 Render Props 패턴은 각각 어떠한 요구사항이 있을 때 도입하는 것이 더 적합하며, 두 패턴의 장점을 결합하여 사용할 수 있는 방법은 무엇인가?
|
||||
- Context API를 활용해 컴파운드 컴포넌트의 상태를 공유할 때 발생할 수 있는 자식 컴포넌트들의 불필요한 리렌더링(re-rendering) 문제는 어떠한 방식으로 최적화할 수 있는가?
|
||||
- ShadCN이나 Material UI 같은 대규모 디자인 시스템 라이브러리는 컴파운드 컴포넌트 패턴을 적용하면서 어떻게 내부 상태의 복잡성을 관리하고 API의 일관성을 유지하고 있는가?
|
||||
- React Server Components (RSC)의 도입으로 인해, 브라우저의 상태 및 상호작용에 의존하는 컴파운드 컴포넌트 패턴의 구현 방식에는 어떠한 구조적 변화나 제약이 따르는가?
|
||||
- 컴파운드 패턴의 서브컴포넌트를 부모와 분리하여 내보내지 말라는 원칙이, 대규모 프로젝트의 리팩토링이나 버전 업데이트 과정에서 구체적으로 어떤 안티패턴과 런타임 오류를 방지하는가?
|
||||
|
||||
### Practical Application Contexts
|
||||
- **Implementation:** React의 `children` prop이나 Context API를 사용하여 `Accordion`, `AccordionItem`처럼 부모 컴포넌트가 상태(예: `isOpen`)를 관리하고 자식 컴포넌트들이 이를 참조하여 동작을 결정하는 형태로 코드를 작성한다.[20, 21]
|
||||
- **System Design:** 조직 내에서 공통적으로 사용할 UI 컴포넌트 라이브러리나 디자인 시스템을 구축할 때, 사용자가 다양한 레이아웃 요구사항에 맞춰 컴포넌트를 조합할 수 있도록 유연한 설계를 제공하는 데 핵심적으로 사용된다.[8]
|
||||
- **Operation / Maintenance:** 수많은 기능을 하나의 컴포넌트에 몰아넣어 발생하는 과도한 prop 추가(Prop Soup)를 방지하므로, 새로운 UI 레이아웃이 필요할 때 기존 컴포넌트 내부 로직을 수정할 필요 없이 외부에서 렌더링 순서만 변경하여 유지보수 비용을 낮춘다.[5, 6, 22]
|
||||
- **Learning Path:** React에서 기본 함수형 컴포넌트 생성과 훅(`useState`, `useEffect`) 사용법을 숙지한 후, 재사용 가능하고 확장성 있는 고급 컴포넌트 아키텍처를 설계하는 단계에서 필수적으로 학습한다.[1, 23, 24]
|
||||
- **My Project Relevance:** 여러 페이지에서 모양은 다르지만 동일한 동작(열림/닫힘)을 수행하는 드롭다운, 모달, 아코디언 위젯을 개발해야 할 때, 이 패턴을 도입하여 하나의 강력한 컴포넌트로 모든 요구사항을 처리할 수 있다.[9, 20, 25, 26]
|
||||
|
||||
### Adjacent Topics
|
||||
- [[Custom Hooks]]
|
||||
- 확장 방향: 컴파운드 컴포넌트가 주로 UI의 조립과 유연한 렌더링 구조를 담당한다면, 복잡한 비즈니스 로직이나 API 호출 등은 Custom Hooks로 분리 추출하여 두 설계 패턴을 결합하는 클린 코드 전략으로 지식을 확장할 수 있다.[10, 27]
|
||||
- [[Higher-Order Components (HOCs)]]
|
||||
- 확장 방향: 로직을 재사용하는 또 다른 고급 React 패턴인 HOC와 비교하여, 컴파운드 컴포넌트가 해결하지 못하는 횡단 관심사(Cross-cutting Concerns, 예: 인증, 로깅) 처리에는 HOC가 어떻게 적용되는지 학습 범위를 넓힐 수 있다.[16, 28, 29]
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-02*
|
||||
|
||||
|
||||
## 📌Brief 단기 요약
|
||||
합성 컴포넌트(Compound Components)는 여러 개의 연관된 하위 컴포넌트들이 암시적으로 상태를 공유하며 하나의 응집력 있는 단위로 동작하도록 설계하는 React 컴포넌트 패턴입니다 [1, 2]. 단일 컴포넌트에 수십 개의 Prop을 밀어 넣어 비대해지는 것을 방지하고, 기능과 책임을 여러 컴포넌트에 분산시킵니다 [3, 4]. 이는 HTML의 `<select>`와 `<option>` 태그처럼 직관적이고 선언적인 API를 형성하여 뛰어난 유연성과 재사용성을 제공합니다 [1, 4].
|
||||
|
||||
## 🤖 LLM 활용 힌트 (How to Use This Knowledge)
|
||||
|
||||
**언제 이 지식을 쓰는가:**
|
||||
- *(TODO)*
|
||||
|
||||
**언제 쓰면 안 되는가:**
|
||||
- *(TODO)*
|
||||
|
||||
## 🧪 검증 상태 (Validation)
|
||||
|
||||
- **정보 상태:** needs_review
|
||||
- **출처 신뢰도:** A
|
||||
- **검토 이유:** *(P-Reinforce Phase 1 자동 정규화. 본문 검증 필요.)*
|
||||
|
||||
## 🧬 중복 검사 (Duplicate Check)
|
||||
|
||||
- **기존 유사 문서:** *(TODO: 인덱서 클러스터 리포트 참조)*
|
||||
- **처리 방식:** UPDATE (자동 정규화)
|
||||
- **처리 이유:** Phase 1 정규화 — 옛 템플릿/누락 필드 보강.
|
||||
|
||||
## 🕓 변경 이력 (Changelog)
|
||||
|
||||
| 날짜 | 변경 내용 | 처리 방식 | 신뢰도 |
|
||||
|------|-----------|-----------|--------|
|
||||
| 2026-05-08 | P-Reinforce Phase 1 정규화 (frontmatter + 헤더 표준화) | UPDATE | A |
|
||||
|
||||
## 💻 코드 패턴 (Code Patterns)
|
||||
|
||||
**패턴 1:** *(TODO: 이 프로젝트 컨벤션 반영한 구조 스켈레톤)*
|
||||
|
||||
```text
|
||||
# TODO
|
||||
Tabs.List = List;
|
||||
Tabs.Trigger = Trigger;
|
||||
Tabs.Panel = Panel;
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준 (Decision Criteria)
|
||||
### Usage
|
||||
```tsx
|
||||
<Tabs defaultValue="overview">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="overview"><OverviewView /></Tabs.Panel>
|
||||
<Tabs.Panel value="settings"><SettingsView /></Tabs.Panel>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
**선택 A를 써야 할 때:**
|
||||
- *(TODO)*
|
||||
### Controlled variant
|
||||
```typescript
|
||||
type Props = {
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
**선택 B를 써야 할 때:**
|
||||
- *(TODO)*
|
||||
export function Tabs({ value, defaultValue, onValueChange, children }: Props) {
|
||||
const [internal, setInternal] = useState(defaultValue ?? '');
|
||||
const isControlled = value !== undefined;
|
||||
const activeId = isControlled ? value : internal;
|
||||
const setActiveId = (next: string) => {
|
||||
if (!isControlled) setInternal(next);
|
||||
onValueChange?.(next);
|
||||
};
|
||||
// ...rest
|
||||
}
|
||||
```
|
||||
|
||||
**기본값:**
|
||||
> *(TODO)*
|
||||
### asChild slot pattern (Radix)
|
||||
```typescript
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
|
||||
## ❌ 안티패턴 (Anti-Patterns)
|
||||
function Trigger({ asChild, children, value }: {
|
||||
asChild?: boolean;
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { setActiveId } = useTabs();
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp onClick={() => setActiveId(value)} role="tab">
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
|
||||
- **[안티패턴]:** *(TODO: 무엇을 하면 안 되는가 + 이유 + 대신 무엇을)*
|
||||
// Usage: customize button as anchor
|
||||
<Tabs.Trigger value="x" asChild>
|
||||
<a href="#x">Section X</a>
|
||||
</Tabs.Trigger>
|
||||
```
|
||||
|
||||
### Form field compound
|
||||
```typescript
|
||||
const FieldContext = createContext<{ id: string; errorId: string } | null>(null);
|
||||
|
||||
export function Field({ children }: { children: React.ReactNode }) {
|
||||
const id = useId();
|
||||
return (
|
||||
<FieldContext.Provider value={{ id, errorId: `${id}-error` }}>
|
||||
<div className="field">{children}</div>
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Label({ children }: { children: React.ReactNode }) {
|
||||
const { id } = useContext(FieldContext)!;
|
||||
return <label htmlFor={id}>{children}</label>;
|
||||
}
|
||||
|
||||
function Input(props: React.InputHTMLAttributes<HTMLInputElement>) {
|
||||
const { id, errorId } = useContext(FieldContext)!;
|
||||
return <input id={id} aria-describedby={errorId} {...props} />;
|
||||
}
|
||||
|
||||
function Error({ children }: { children: React.ReactNode }) {
|
||||
const { errorId } = useContext(FieldContext)!;
|
||||
return <span id={errorId} role="alert">{children}</span>;
|
||||
}
|
||||
|
||||
Field.Label = Label;
|
||||
Field.Input = Input;
|
||||
Field.Error = Error;
|
||||
```
|
||||
|
||||
### Server Components note
|
||||
```typescript
|
||||
// Compound components 의 Context — Client Component only.
|
||||
// 매 Server Component layout 에서:
|
||||
// <Tabs> ← Client Component (provides context)
|
||||
// <Tabs.Panel value="x"> ← Client (consumes context)
|
||||
// <ServerOnlyData /> ← can be Server Component child
|
||||
// </Tabs.Panel>
|
||||
// </Tabs>
|
||||
'use client';
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Standard widgets (tabs, menu) | Context-based compound |
|
||||
| Wrap user element | asChild + Slot |
|
||||
| Caller controls state | Controlled variant |
|
||||
| Behavior-only library | Headless compound (Radix style) |
|
||||
| Server Components mixed | 'use client' on the parent + child triggers |
|
||||
|
||||
**기본값**: Context-based, namespace exports (`Tabs.Trigger`), controlled+uncontrolled support, `useId` for a11y, `useContext` 로 misuse 방지.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Component-Based Architecture (CBA)]] · [[React Patterns]]
|
||||
- 변형: [[Headless UI]] · [[Render Props]] · [[Slots]]
|
||||
- 응용: [[Component Library Architecture]] · [[Radix UI]] · [[shadcn-ui]]
|
||||
- Adjacent: [[React Context]] · [[A11y]] · [[Server Components]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: building reusable widgets (tabs/menu/dialog), design system primitives, replacing prop-explosion components, headless library.
|
||||
**언제 X**: 1-shot static UI, simple presentational components, server-only components (Context 의 X).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`React.Children` cloning**: brittle with refs, breaks with fragments — Context 가 표준.
|
||||
- **Missing context throw**: child used standalone gives undefined error — explicit throw with helpful message.
|
||||
- **Over-namespacing**: 5+ children types — split into multiple components.
|
||||
- **Forgetting a11y**: tabs without role/aria-* — Radix-style attribute mapping 필수.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Kent C. Dodds compound components / Radix UI source / React 19 docs / WAI-ARIA 1.2).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Context-based Tabs + asChild + form field 패턴 |
|
||||
|
||||
Reference in New Issue
Block a user