이 글은 누구를 위한 것인가
- 컴포넌트가 많아졌는데 사용법을 찾으려면 코드를 직접 봐야 하는 팀
- Confluence나 Notion에 컴포넌트 문서를 쓰지만 코드와 항상 불일치하는 팀
- Storybook을 설치했지만 형식적인 스토리만 있고 실제로 활용되지 않는 팀
컴포넌트 문서가 실패하는 이유
대부분의 팀에서 컴포넌트 문서는 두 가지 형태로 실패한다.
외부 문서 (Confluence, Notion): 컴포넌트가 변경될 때 문서는 업데이트되지 않는다. 새로운 Props가 추가되거나 디자인이 바뀌어도 문서는 과거 상태에 머문다. 6개월 후면 아무도 믿지 않는다.
코드 주석: 코드 가까이 있어 동기화될 것 같지만, 비개발자(디자이너, PO)가 접근하기 어렵고 Props 설명 이상을 담기 힘들다.
해결책: 문서가 컴포넌트 코드와 같은 저장소에 있고, 실제로 렌더링된 컴포넌트를 보여주는 것이어야 한다.
1. Storybook 스토리 작성 원칙
모든 상태를 스토리로 만든다
컴포넌트의 모든 의미 있는 상태가 별도 스토리여야 한다.
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
docs: {
description: {
component: '주요 액션을 실행하는 기본 버튼 컴포넌트입니다. variant로 시각적 중요도를 표현합니다.',
},
},
},
argTypes: {
variant: {
description: '버튼의 시각적 스타일',
control: 'select',
options: ['primary', 'secondary', 'ghost', 'danger'],
},
size: {
description: '버튼 크기',
control: 'radio',
options: ['sm', 'md', 'lg'],
},
disabled: {
description: '비활성화 상태',
control: 'boolean',
},
loading: {
description: '로딩 중 상태 — 클릭 불가, 스피너 표시',
control: 'boolean',
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// 기본 상태
export const Primary: Story = {
args: { variant: 'primary', children: '확인' },
};
// 모든 variant 한눈에 보기
export const AllVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: '8px' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
</div>
),
};
// 상태별
export const Loading: Story = {
args: { variant: 'primary', children: '저장 중', loading: true },
};
export const Disabled: Story = {
args: { variant: 'primary', children: '비활성화', disabled: true },
};
// 실제 사용 시나리오
export const FormSubmit: Story = {
name: '폼 제출 시나리오',
render: () => {
const [loading, setLoading] = React.useState(false);
return (
<Button
variant="primary"
loading={loading}
onClick={() => {
setLoading(true);
setTimeout(() => setLoading(false), 2000);
}}
>
{loading ? '저장 중...' : '저장하기'}
</Button>
);
},
};
스토리 분류 체계
Components/
├─ Atoms/
│ ├─ Button
│ ├─ Input
│ └─ Badge
├─ Molecules/
│ ├─ FormField
│ ├─ SearchBar
│ └─ Card
├─ Organisms/
│ ├─ Header
│ ├─ ProductCard
│ └─ CheckoutForm
└─ Pages/
├─ HomePage
└─ ProductDetailPage
2. 문서 자동화
JSDoc + Controls로 Props 문서 자동화
interface ButtonProps {
/** 버튼의 시각적 스타일. Primary는 주요 액션, Danger는 파괴적 액션에 사용 */
variant: 'primary' | 'secondary' | 'ghost' | 'danger';
/** 버튼 크기. 기본값: 'md' */
size?: 'sm' | 'md' | 'lg';
/** 로딩 중 상태. true이면 스피너가 표시되고 클릭이 차단됨 */
loading?: boolean;
/** onClick 핸들러. disabled 또는 loading 상태에서는 호출되지 않음 */
onClick?: () => void;
}
JSDoc 주석이 Storybook ArgsTable에 자동으로 반영된다. 별도 Props 문서를 쓸 필요가 없다.
MDX로 사용 가이드 추가
스토리 파일과 별도로 사용 가이드 문서를 MDX로 작성한다.
{/* Button.mdx */}
import { Canvas, Meta, Controls } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';
<Meta of={ButtonStories} />
# Button
버튼은 사용자의 액션을 트리거하는 가장 기본적인 인터랙티브 요소입니다.
## 언제 어떤 variant를 쓸까요?
| Variant | 사용 시점 |
|---------|---------|
| Primary | 페이지당 1개. 가장 중요한 단일 액션 |
| Secondary | 보조 액션. Primary와 함께 사용 |
| Ghost | 덜 중요한 액션, 텍스트처럼 보이길 원할 때 |
| Danger | 삭제, 탈퇴 같은 되돌릴 수 없는 액션 |
## 라이브 예제
<Canvas of={ButtonStories.Primary} />
<Controls of={ButtonStories.Primary} />
## 하지 말아야 할 것
- 한 화면에 Primary 버튼 2개 이상 사용 금지
- 버튼 텍스트에 "클릭하세요", "눌러주세요" 사용 금지 — 동사로 시작 ("저장", "삭제", "구매")
- Loading 상태에서 추가 클릭 방지 처리 필수
3. 디자인-코드 검증 통합
Chromatic으로 시각적 회귀 테스트
Storybook의 모든 스토리를 스냅샷으로 저장하고, 코드 변경 시 시각적 차이를 자동으로 감지한다.
# .github/workflows/chromatic.yml
- name: Publish to Chromatic
uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true # 변경 있어도 빌드 실패 안 함 (수동 검토)
PR마다 Chromatic이 스크린샷 비교 결과를 PR 코멘트로 달아준다. 디자이너가 직접 코드 변경의 시각적 영향을 검토할 수 있다.
접근성 자동 테스트
// .storybook/preview.ts
import { withA11y } from '@storybook/addon-a11y';
export const decorators = [withA11y];
모든 스토리에 자동으로 axe 접근성 검사가 실행된다. 색상 대비 부족, aria 속성 누락, 키보드 접근성 문제를 스토리 개발 중에 발견할 수 있다.
4. 팀 운영 가이드라인
스토리 작성 기준 (Definition of Done)
컴포넌트 PR이 머지되려면 다음이 포함되어야 한다:
- Default 스토리 (Controls가 작동하는 기본 상태)
- 모든 variant/size 스토리
- Disabled, Loading, Error 상태 스토리 (해당하는 경우)
- 접근성 검사 통과
- ArgsTable Props 설명 완성
디자이너 참여 유도
Storybook을 배포해 디자이너가 URL로 직접 접근할 수 있게 한다. Chromatic 또는 Netlify에 자동 배포 설정으로 PR마다 프리뷰 URL이 생성되면 디자이너가 코드 없이 컴포넌트를 검토할 수 있다.
맺으며
Storybook이 살아있는 문서가 되려면 "스토리 추가가 의무"가 아니라 "컴포넌트 개발의 자연스러운 과정"이 되어야 한다. 스토리를 먼저 작성하고 컴포넌트를 구현하는 스토리 주도 개발(Story-Driven Development) 방식은 컴포넌트를 격리된 환경에서 개발하게 해 재사용성을 높이는 부수 효과도 있다.
처음에 모든 것을 완벽하게 문서화하려 하지 않는다. Default 스토리와 핵심 상태부터 시작하고, 실제 사용하면서 빠진 케이스를 추가하는 것이 지속 가능하다.