refactor(topics): 멀티 에이전트용 지식 재편 — _Common(공통 기본기) + Domain_* 구조
에이전트 8종(대화형/프로그래머 C·S/디자이너/설계자/기획자/QA/PD/PM)에게 [공통 기본 능력 + 롤별 Specialty] 2층으로 지식을 주입하기 위한 재분류. 문서 내용·포맷은 무수정, 폴더 이동만 (6,372개 문서 수 보존 확인). - Topic_Programming → Domain_Programming (내부 구조 보존) - Topic_Graphic → Domain_Design - Topic_Business → Domain_Product - Topic_General → Domain_General - _Common 신설: Math(구 Topic_Math_Specialty), Reasoning(구 General/From_Thinking & Reasoning), Reasoning_Creativity(구 General/From_창의성), Communication(Poetic_Blog_Writing + From_writing) - 타 도메인의 From_* 폴더는 유지 (출처 표기일 뿐, 이미 도메인에 맞게 분류된 문서) - 빈 폴더 정리 (memory/procedures) - 에이전트→폴더 매핑은 workspace의 .astra/agent-knowledge-map.json (9개 에이전트) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
---
|
||||
id: wiki-2026-0508-compound-components
|
||||
title: Compound Components
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Compound Component Pattern, React Compound Components]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.95
|
||||
verification_status: applied
|
||||
tags: [react, patterns, components, composition, context]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react
|
||||
---
|
||||
|
||||
# Compound Components
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 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.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 problem solved
|
||||
- 단일 monolithic component 의 props explosion (`<Tabs items={...} renderTab={...} ...>`).
|
||||
- 매 caller 가 매 children 의 layout/order 를 control 의 X.
|
||||
- Slot composition 의 부재.
|
||||
|
||||
### 매 solution
|
||||
- Parent 가 Context 로 shared state 의 publish.
|
||||
- Children 이 Context 를 consume — explicit prop drilling 의 X.
|
||||
- Children 의 component 가 namespace pattern (`Tabs.Tab`) 또는 separate exports.
|
||||
|
||||
### 매 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).
|
||||
|
||||
### 매 응용
|
||||
1. Tabs, Accordion, Disclosure.
|
||||
2. Menu, Dropdown, Combobox.
|
||||
3. Dialog, Drawer, Popover.
|
||||
4. Form fields with label/error/description.
|
||||
5. DataTable column children.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Context-based Tabs
|
||||
```typescript
|
||||
import { createContext, useContext, useId, useState } from 'react';
|
||||
|
||||
type TabsContextValue = {
|
||||
activeId: string;
|
||||
setActiveId: (id: string) => void;
|
||||
baseId: string;
|
||||
};
|
||||
const TabsContext = createContext<TabsContextValue | null>(null);
|
||||
|
||||
function useTabs() {
|
||||
const ctx = useContext(TabsContext);
|
||||
if (!ctx) throw new Error('Tabs.* must be used inside <Tabs>');
|
||||
return ctx;
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Tabs.List = List;
|
||||
Tabs.Trigger = Trigger;
|
||||
Tabs.Panel = Panel;
|
||||
```
|
||||
|
||||
### 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>
|
||||
```
|
||||
|
||||
### Controlled variant
|
||||
```typescript
|
||||
type Props = {
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
### asChild slot pattern (Radix)
|
||||
```typescript
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// 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|Render Props]]
|
||||
- 응용: [[Component Library Architecture]] · [[Radix UI]] · [[shadcn-ui]]
|
||||
- Adjacent: [[A11y]] · [[Modern_Web_Rendering_and_Optimization|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