컴포넌트 명세
AI가 정확하게 해석하고 생성할 수 있는 컴포넌트 스키마 설계
컴포넌트 명세는 AI에게 컴포넌트 사용법의 청사진입니다. Props 스키마, 상태 모델, 합성 패턴을 명시적으로 정의해야 AI가 올바른 코드를 생성할 수 있습니다.
컴포넌트 명세의 구조
Props 스키마 설계
기본 원칙
TypeScript Interface 패턴
// components/button/types.ts
/**
* 버튼 컴포넌트의 시각적 변형
* @ai-hint variant는 용도에 따라 선택:
* - default: 일반 액션
* - destructive: 삭제/위험 액션
* - outline: 보조 액션
* - ghost: 최소 강조
* - link: 인라인 링크 스타일
*/
type ButtonVariant = 'default' | 'destructive' | 'outline' | 'ghost' | 'link'
/**
* 버튼 크기
* @ai-hint 컨텍스트에 따라 선택:
* - sm: 밀집된 UI, 테이블 내부
* - default: 일반적인 폼, 대화상자
* - lg: 히어로 섹션, 주요 CTA
* - icon: 아이콘 전용 (정사각형)
*/
type ButtonSize = 'default' | 'sm' | 'lg' | 'icon'
interface ButtonProps {
/** 버튼 텍스트 또는 내용 */
children: React.ReactNode
/** 시각적 스타일 변형 @default "default" */
variant?: ButtonVariant
/** 버튼 크기 @default "default" */
size?: ButtonSize
/** 비활성화 상태 */
disabled?: boolean
/** 로딩 상태 - true일 때 스피너 표시 및 클릭 비활성화 */
loading?: boolean
/** 전체 너비 확장 */
fullWidth?: boolean
/** 왼쪽 아이콘 */
leftIcon?: React.ReactNode
/** 오른쪽 아이콘 */
rightIcon?: React.ReactNode
/** 클릭 핸들러 */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
/** HTML button type @default "button" */
type?: 'button' | 'submit' | 'reset'
/** 접근성 레이블 (children이 아이콘만일 때 필수) */
'aria-label'?: string
}Discriminated Union 활용
복잡한 조건부 Props는 Discriminated Union으로 표현합니다.
// components/dialog/types.ts
/**
* 대화상자 - 확인 모드
* @ai-hint 사용자에게 확인/취소 선택을 요구할 때
*/
interface ConfirmDialogProps {
mode: 'confirm'
title: string
description: string
confirmLabel?: string
cancelLabel?: string
onConfirm: () => void
onCancel: () => void
/** 위험한 작업일 때 true */
destructive?: boolean
}
/**
* 대화상자 - 알림 모드
* @ai-hint 정보만 전달하고 닫기만 가능할 때
*/
interface AlertDialogProps {
mode: 'alert'
title: string
description: string
closeLabel?: string
onClose: () => void
}
/**
* 대화상자 - 커스텀 모드
* @ai-hint 복잡한 폼이나 커스텀 콘텐츠가 필요할 때
*/
interface CustomDialogProps {
mode: 'custom'
title: string
children: React.ReactNode
footer?: React.ReactNode
onClose: () => void
}
type DialogProps = ConfirmDialogProps | AlertDialogProps | CustomDialogProps상태 모델링
상태 머신으로 정의
// components/async-button/state.ts
type AsyncButtonState = 'idle' | 'loading' | 'success' | 'error' | 'disabled'
type AsyncButtonEvent =
| { type: 'SUBMIT' }
| { type: 'RESOLVE'; data?: unknown }
| { type: 'REJECT'; error: Error }
| { type: 'CANCEL' }
| { type: 'RESET' }
| { type: 'DISABLE' }
| { type: 'ENABLE' }
const asyncButtonMachine = {
initial: 'idle' as const,
states: {
idle: {
on: {
SUBMIT: 'loading',
DISABLE: 'disabled',
},
},
loading: {
on: {
RESOLVE: 'success',
REJECT: 'error',
CANCEL: 'idle',
},
},
success: {
on: {
RESET: 'idle',
},
},
error: {
on: {
SUBMIT: 'loading', // retry
RESET: 'idle',
},
},
disabled: {
on: {
ENABLE: 'idle',
},
},
},
}슬롯 기반 합성 패턴
Compound Component 패턴
// components/card/types.ts
interface CardContextValue {
variant: 'default' | 'outline' | 'elevated'
}
interface CardRootProps {
children: React.ReactNode
variant?: 'default' | 'outline' | 'elevated'
/** 클릭 가능한 카드 */
asChild?: boolean
}
interface CardHeaderProps {
children: React.ReactNode
/** 아이콘 또는 아바타 */
leading?: React.ReactNode
/** 액션 버튼 */
trailing?: React.ReactNode
}
interface CardTitleProps {
children: React.ReactNode
/** 제목 레벨 @default "h3" */
as?: 'h2' | 'h3' | 'h4'
}
interface CardDescriptionProps {
children: React.ReactNode
}
interface CardContentProps {
children: React.ReactNode
/** 패딩 제거 (이미지 등) */
noPadding?: boolean
}
interface CardFooterProps {
children: React.ReactNode
/** 정렬 @default "end" */
align?: 'start' | 'center' | 'end' | 'between'
}
// 사용 예시
/**
* @example
* <Card variant="outline">
* <Card.Header trailing={<IconButton icon={<MoreIcon />} />}>
* <Card.Title>제목</Card.Title>
* <Card.Description>설명</Card.Description>
* </Card.Header>
* <Card.Content>
* 본문 내용
* </Card.Content>
* <Card.Footer>
* <Button variant="ghost">취소</Button>
* <Button>확인</Button>
* </Card.Footer>
* </Card>
*/슬롯 스키마
// utils/slot-schema.ts
interface SlotDefinition {
/** 슬롯 이름 */
name: string
/** 필수 여부 */
required: boolean
/** 허용되는 컴포넌트 타입 */
allowedTypes: string[]
/** 최대 개수 (undefined = 무제한) */
maxCount?: number
/** 설명 */
description: string
}
const cardSlotSchema: SlotDefinition[] = [
{
name: 'header',
required: false,
allowedTypes: ['Card.Header'],
maxCount: 1,
description: '카드 상단 영역 (제목, 설명)',
},
{
name: 'content',
required: true,
allowedTypes: ['Card.Content'],
maxCount: 1,
description: '카드 본문 영역',
},
{
name: 'footer',
required: false,
allowedTypes: ['Card.Footer'],
maxCount: 1,
description: '카드 하단 영역 (액션 버튼)',
},
]컴포넌트 문서 템플릿
AI가 참조하기 좋은 구조화된 문서 포맷입니다.
# Button
## Overview
사용자 액션을 트리거하는 인터랙티브 요소.
## When to Use
- 폼 제출
- 대화상자 확인/취소
- 페이지 내 액션 트리거
## When NOT to Use
- 페이지 이동 → `<Link>` 사용
- 토글 상태 → `<Toggle>` 사용
## Variants
| Variant | 용도 | 예시 |
| ----------- | --------- | ------------ |
| default | 주요 액션 | 저장, 확인 |
| destructive | 위험 액션 | 삭제, 초기화 |
| outline | 보조 액션 | 취소, 이전 |
| ghost | 최소 강조 | 더보기, 설정 |
| link | 인라인 | 약관 보기 |
## Props Reference
<PropsTable component="Button" />
## Examples
### 기본 사용
\`\`\`tsx
<Button onClick={handleSave}>저장</Button>
\`\`\`
### 로딩 상태
\`\`\`tsx
<Button loading={isSubmitting} disabled={isSubmitting}>
{isSubmitting ? '저장 중...' : '저장'}
</Button>
\`\`\`
### 아이콘 버튼
\`\`\`tsx
<Button size="icon" aria-label="설정">
<SettingsIcon />
</Button>
\`\`\`
## Accessibility
- `aria-label`: 아이콘 전용 버튼에 필수
- `aria-disabled`: loading 상태에서 자동 적용
- 키보드: Enter/Space로 활성화
## Related Components
- `IconButton`: 아이콘 전용 버튼
- `ButtonGroup`: 버튼 그룹
- `Link`: 네비게이션용Spec as Data (기계가 읽는 컴포넌트 스펙)
MDX 문서만으로도 충분해 보이지만, AI/도구(MCP, 린터, 문서 생성기)가 일관되게 소비하려면 컴포넌트 스펙을 “문서”가 아니라 데이터로 관리하는 편이 강력합니다.
핵심 아이디어는 단순합니다:
- 단일 소스 of Truth:
ComponentMeta(TS/JSON)만 진실로 둔다. - 파생 산출물: MDX 문서, PropsTable, 검색 인덱스, MCP 리소스는 전부 meta에서 생성한다.
최소 스키마 (TypeScript)
// design-system/component-meta.ts
export type ComponentCategory =
| 'interactive'
| 'layout'
| 'form'
| 'navigation'
| 'feedback'
| 'data-display'
export interface PropMeta {
name: string
type: string
required: boolean
default?: string
description: string
/** @ai-hint 용도/선택 기준을 짧게. (문장 1개 권장) */
aiHint?: string
/** 예: { maxLength: 50 } / { oneOf: ['sm', 'md', 'lg'] } */
constraints?: Record<string, unknown>
}
export interface VariantMeta {
name: string
whenToUse: string
avoidFor?: string[]
tokens?: string[]
}
export interface ComponentMeta {
name: string
category: ComponentCategory
summary: string
props: PropMeta[]
variants?: VariantMeta[]
slots?: Array<{
name: string
required: boolean
allowedTypes: string[]
maxCount?: number
}>
accessibility: {
required: string[]
keyboard?: string[]
aria?: string[]
}
examples: Array<{
title: string
code: string
description?: string
}>
antiPatterns?: Array<{
title: string
why: string
instead: string
}>
related?: Array<{
name: string
relation: 'alternative' | 'composed-with' | 'specialized'
reason: string
}>
searchKeywords?: string[]
}예시: Button 메타데이터
// components/button/button.meta.ts
import type { ComponentMeta } from '../design-system/component-meta'
export const buttonMeta: ComponentMeta = {
name: 'Button',
category: 'interactive',
summary: '사용자 액션을 트리거하는 클릭 가능한 요소.',
props: [
{
name: 'variant',
type: "'default' | 'destructive' | 'outline' | 'ghost' | 'link'",
required: false,
default: 'default',
description: '시각적 스타일 변형',
aiHint: '화면에서 가장 중요한 액션이면 default, 되돌릴 수 없으면 destructive.',
},
{
name: 'size',
type: "'default' | 'sm' | 'lg' | 'icon'",
required: false,
default: 'default',
description: '버튼 크기',
aiHint: '툴바/밀집 UI는 sm, 주요 CTA는 lg를 우선 고려.',
},
{
name: 'loading',
type: 'boolean',
required: false,
default: 'false',
description: '로딩 상태 (스피너 표시 + 클릭 비활성화)',
},
],
variants: [
{
name: 'default',
whenToUse: '주요 액션/CTA',
avoidFor: ['위험 액션'],
tokens: ['colors.primary'],
},
{
name: 'destructive',
whenToUse: '삭제/초기화 등 위험 액션',
avoidFor: ['일반 에러 표시'],
tokens: ['colors.destructive'],
},
],
accessibility: {
required: ['아이콘-only 버튼은 aria-label 필수', 'loading 상태에서는 aria-busy=true 권장'],
keyboard: ['Enter/Space로 활성화'],
},
examples: [
{ title: 'Minimal', code: '<Button>저장</Button>' },
{ title: 'Submit', code: '<Button type="submit">저장</Button>' },
],
antiPatterns: [
{
title: '페이지 이동에 Button 사용',
why: '네비게이션은 Link가 시맨틱/접근성 측면에서 더 적절합니다.',
instead: '<Link href=\"/path\">자세히</Link>',
},
],
related: [{ name: 'Link', relation: 'alternative', reason: '페이지 네비게이션용' }],
searchKeywords: ['cta', 'submit', 'dialog-action'],
}검증 + 파생 산출물
이 구조를 쓰면 “문서-코드 드리프트”를 강하게 막을 수 있습니다.
meta검증: Zod/JSON Schema로 필수 필드/enum/제약을 빌드 타임에 검증- 문서 생성:
ComponentMeta→ MDX 섹션(요약/Props/Examples/A11y) 자동 생성 - MCP 리소스:
ComponentMeta→ds://components/button같은 JSON 리소스로 노출 - 린팅: “용도에 맞는 컴포넌트 선택/금지 패턴”을 룰로 만들어 자동 검출