컴포넌트 문서화 전략 — Storybook으로 디자인 시스템 살아있게 만들기

디자인 시스템

Storybook컴포넌트 문서화디자인 시스템협업

이 글은 누구를 위한 것인가

  • 컴포넌트가 많아졌는데 사용법을 찾으려면 코드를 직접 봐야 하는 팀
  • 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 스토리와 핵심 상태부터 시작하고, 실제 사용하면서 빠진 케이스를 추가하는 것이 지속 가능하다.