본문 바로가기
프론트엔드

Storybook - 컴포넌트 개발의 완전한 혁신

by 코딩의 세계 2026. 2. 24.

들어가며

당신이 리액트 컴포넌트를 개발할 때, 매번 전체 애플리케이션을 실행해서 그 컴포넌트를 테스트했던 경험이 있나요?

문제 상황:

Button 컴포넌트를 수정했다면:
1. npm start
2. 애플리케이션 대기 (3-5초)
3. 버튼이 있는 페이지로 이동
4. 버튼 상태 변경 (hover, disabled 등)
5. 수정이 필요하면 1번부터 반복...

→ 아주 비효율적입니다!

Storybook은 이 모든 번거로움을 제거합니다. 각 컴포넌트를 독립적으로 개발하고 테스트할 수 있는 환경을 제공합니다. 컴포넌트의 모든 상태(정상, 로딩, 에러 등)를 한눈에 볼 수 있고, 상호작용까지 테스트할 수 있습니다.

공식 사이트 가이드라인

https://storybook.js.org/tutorials/intro-to-storybook/react/ko/get-started/

 

리액트(React)를 위한 스토리북(Storybook) 튜토리얼

스토리북(Storybook)을 개발 환경에 설치해보세요

storybook.js.org

 


Storybook의 개념

Storybook이란?

Storybook = 컴포넌트 개발 환경 + 문서화 도구 + 테스트 플랫폼

특징:
1. 컴포넌트 독립 실행 환경
   - 전체 앱 없이 컴포넌트만 개발
   - Hot reload로 빠른 피드백

2. 모든 상태 시각화
   - 정상 상태
   - 로딩 상태
   - 에러 상태
   - 빈 상태
   - 모든 Props 조합

3. 상호작용 테스트
   - 클릭, 입력 등 사용자 상호작용
   - 액션 기록

4. 자동 문서화
   - PropTypes 자동 추출
   - 코드 예제 자동 생성

5. 협업 도구
   - 디자이너와 개발자 협업
   - 컴포넌트 리뷰

예시로 이해하기

일반 개발 흐름:
App.tsx → 여러 페이지 → 여러 컴포넌트 → Button
                                    ↑
                            여기에 도달하기까지
                            복잡한 네비게이션 필요

Storybook 흐름:
Storybook → Button.stories.tsx → 직접 Button 개발
                             ↑
                    즉시 컴포넌트 개발 가능!

Storybook 설치

1단계: 자동 설치

# 기존 React 프로젝트에 설치
npx storybook@latest init

# 또는 수동 설치
npm install -D storybook @storybook/react

2단계: 폴더 구조

my-app/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   └── Button.stories.tsx      # Storybook 파일
│   ├── App.tsx
│   └── index.tsx
├── .storybook/
│   ├── main.ts                     # Storybook 설정
│   └── preview.ts                  # 전역 설정
├── package.json
└── tsconfig.json

3단계: 실행

# Storybook 개발 서버 시작
npm run storybook

# localhost:6006 에서 확인

첫 Story 작성

기본 Story

// components/Button.tsx
import React from 'react';

interface ButtonProps {
  label: string;
  onClick?: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
}

export function Button({
  label,
  onClick,
  variant = 'primary',
  size = 'medium',
  disabled = false
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}
// components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

// 메타 정보 (Storybook에서 어떻게 표시할지)
const meta = {
  title: 'Components/Button',           // Storybook 사이드바 경로
  component: Button,                    // 컴포넌트
  parameters: {
    layout: 'centered'                  // 가운데 정렬
  },
  tags: ['autodocs']                    // 자동 문서 생성
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// Story 1: Primary 버튼
export const Primary: Story = {
  args: {
    label: 'Click Me',
    variant: 'primary'
  }
};

// Story 2: Secondary 버튼
export const Secondary: Story = {
  args: {
    label: 'Click Me',
    variant: 'secondary'
  }
};

// Story 3: Danger 버튼
export const Danger: Story = {
  args: {
    label: 'Delete',
    variant: 'danger'
  }
};

// Story 4: 비활성화 버튼
export const Disabled: Story = {
  args: {
    label: 'Disabled',
    disabled: true
  }
};

// Story 5: 작은 버튼
export const Small: Story = {
  args: {
    label: 'Small',
    size: 'small'
  }
};

// Story 6: 큰 버튼
export const Large: Story = {
  args: {
    label: 'Large',
    size: 'large'
  }
};

// Story 7: 클릭 이벤트 테스트
export const Interactive: Story = {
  args: {
    label: 'Click me!',
    onClick: () => alert('Button clicked!')
  }
};

Storybook에서 보이는 형태

Components
  └─ Button
      ├─ Primary       (파란 버튼)
      ├─ Secondary     (회색 버튼)
      ├─ Danger        (빨간 버튼)
      ├─ Disabled      (비활성화 상태)
      ├─ Small         (작은 크기)
      ├─ Large         (큰 크기)
      └─ Interactive   (클릭 가능)

각 Story를 클릭하면:
- 미리보기 영역에서 실시간으로 컴포넌트 렌더링
- Props 조정 가능
- 자동 생성된 문서
- 상호작용 가능

고급 기능

1. Args (Props 제어)

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta = {
  title: 'Components/Button',
  component: Button,
  
  // Props 정의 (Storybook에서 제어 가능)
  argTypes: {
    label: {
      control: 'text',                   // 텍스트 입력
      description: '버튼 텍스트'
    },
    variant: {
      control: 'select',                 // 드롭다운 선택
      options: ['primary', 'secondary', 'danger'],
      description: '버튼 스타일'
    },
    size: {
      control: 'select',
      options: ['small', 'medium', 'large'],
      description: '버튼 크기'
    },
    disabled: {
      control: 'boolean',                // 체크박스
      description: '비활성화 여부'
    },
    onClick: {
      action: 'clicked'                  // 클릭 감지
    }
  }
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// Default story - 모든 props 조정 가능
export const Default: Story = {
  args: {
    label: 'Button',
    variant: 'primary',
    size: 'medium',
    disabled: false
  }
};

2. Interactions (사용자 상호작용)

import { fn } from '@storybook/test';
import { userEvent, within, expect } from '@storybook/test';

export const WithInteraction: Story = {
  args: {
    label: 'Click Me',
    onClick: fn()                        // 클릭 추적
  },
  // 자동으로 실행되는 인터랙션 시나리오
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');
    
    // 1단계: 버튼 클릭
    await userEvent.click(button);
    
    // 2단계: onClick이 호출되었는지 확인
    expect(args.onClick).toHaveBeenCalled();
  }
};

3. Controls (Props 실시간 제어)

// Storybook에서 자동으로 제어 UI 생성
export const Default: Story = {
  args: {
    label: 'Button',
    variant: 'primary',
    size: 'medium'
  }
};

// Storybook UI에서:
// - label 입력창에서 텍스트 변경
// - variant 드롭다운에서 선택
// - size 라디오 버튼에서 선택
// → 즉시 미리보기 업데이트

4. Docs (자동 문서 생성)

const meta = {
  title: 'Components/Button',
  component: Button,
  
  parameters: {
    docs: {
      description: {
        component: '일반적인 버튼 컴포넌트입니다.'
      }
    }
  },
  
  argTypes: {
    label: {
      description: '버튼에 표시될 텍스트',
      table: {
        type: { summary: 'string' }
      }
    },
    variant: {
      description: '버튼의 시각적 스타일',
      table: {
        type: { summary: 'primary | secondary | danger' },
        defaultValue: { summary: 'primary' }
      }
    }
  }
} satisfies Meta<typeof Button>;

Storybook에서 생성되는 문서:

Button

일반적인 버튼 컴포넌트입니다.

Props:
- label (string): 버튼에 표시될 텍스트
- variant (primary | secondary | danger): 버튼의 시각적 스타일 (기본값: primary)
- size (small | medium | large): 버튼 크기 (기본값: medium)
- disabled (boolean): 비활성화 여부 (기본값: false)
- onClick (function): 클릭 핸들러

Examples:
<Button label="Click Me" variant="primary" />
<Button label="Delete" variant="danger" disabled />

실전 예제

Input 컴포넌트 Story

// components/Input.tsx
import React, { ChangeEvent } from 'react';

interface InputProps {
  value: string;
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
  placeholder?: string;
  type?: 'text' | 'email' | 'password';
  error?: string;
  disabled?: boolean;
}

export function Input({
  value,
  onChange,
  placeholder = 'Enter text...',
  type = 'text',
  error,
  disabled = false
}: InputProps) {
  return (
    <div className="input-wrapper">
      <input
        type={type}
        value={value}
        onChange={onChange}
        placeholder={placeholder}
        disabled={disabled}
        className={error ? 'input error' : 'input'}
      />
      {error && <span className="error-message">{error}</span>}
    </div>
  );
}
// components/Input.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { fn } from '@storybook/test';
import { Input } from './Input';

const meta = {
  title: 'Components/Input',
  component: Input,
  
  argTypes: {
    value: {
      control: 'text',
      description: '입력값'
    },
    placeholder: {
      control: 'text',
      description: '플레이스홀더 텍스트'
    },
    type: {
      control: 'select',
      options: ['text', 'email', 'password'],
      description: '입력 타입'
    },
    error: {
      control: 'text',
      description: '에러 메시지'
    },
    disabled: {
      control: 'boolean',
      description: '비활성화 여부'
    },
    onChange: {
      action: 'changed'
    }
  }
} satisfies Meta<typeof Input>;

export default meta;
type Story = StoryObj<typeof meta>;

// 기본 입력창
export const Default: Story = {
  args: {
    value: '',
    placeholder: 'Enter your name...',
    onChange: fn()
  }
};

// 이메일 입력
export const Email: Story = {
  args: {
    type: 'email',
    placeholder: 'Enter your email...',
    onChange: fn()
  }
};

// 비밀번호 입력
export const Password: Story = {
  args: {
    type: 'password',
    placeholder: 'Enter password...',
    onChange: fn()
  }
};

// 에러 상태
export const WithError: Story = {
  args: {
    value: 'invalid-email',
    error: 'Please enter a valid email',
    onChange: fn()
  }
};

// 비활성화 상태
export const Disabled: Story = {
  args: {
    value: 'Disabled input',
    disabled: true,
    onChange: fn()
  }
};

// 상호작용 포함 (값 변경 테스트)
export const Interactive: Story = {
  args: {
    value: '',
    onChange: fn()
  },
  render: (args) => {
    const [value, setValue] = React.useState('');
    
    return (
      <Input
        {...args}
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
          args.onChange(e);
        }}
      />
    );
  }
};

Card 컴포넌트 Story (복잡한 컴포넌트)

// components/Card.tsx
import React, { ReactNode } from 'react';

interface CardProps {
  title: string;
  description: string;
  image?: string;
  children?: ReactNode;
  onClick?: () => void;
  isLoading?: boolean;
  isError?: boolean;
}

export function Card({
  title,
  description,
  image,
  children,
  onClick,
  isLoading = false,
  isError = false
}: CardProps) {
  if (isLoading) {
    return (
      <div className="card loading">
        <div className="skeleton">로딩 중...</div>
      </div>
    );
  }

  if (isError) {
    return (
      <div className="card error">
        <p>에러가 발생했습니다.</p>
      </div>
    );
  }

  return (
    <div className="card" onClick={onClick}>
      {image && <img src={image} alt={title} className="card-image" />}
      <div className="card-content">
        <h2 className="card-title">{title}</h2>
        <p className="card-description">{description}</p>
        {children}
      </div>
    </div>
  );
}
// components/Card.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Card } from './Card';

const meta = {
  title: 'Components/Card',
  component: Card,
  parameters: {
    layout: 'padded'
  }
} satisfies Meta;

export default meta;
type Story = StoryObj;

// 기본 카드
export const Default: Story = {
  args: {
    title: 'Card Title',
    description: 'This is a card description'
  }
};

// 이미지가 있는 카드
export const WithImage: Story = {
  args: {
    title: 'Product Card',
    description: 'Amazing product',
    image: 'https://via.placeholder.com/300x200'
  }
};

// 자식 컴포넌트가 있는 카드
export const WithChildren: Story = {
  args: {
    title: 'Card with Action',
    description: 'Card with custom content'
  },
  render: (args) => (
    
      Learn More
    
  )
};

// 로딩 상태
export const Loading: Story = {
  args: {
    title: 'Loading Card',
    description: 'This will not show',
    isLoading: true
  }
};

// 에러 상태
export const Error: Story = {
  args: {
    title: 'Error Card',
    description: 'This will not show',
    isError: true
  }
};

// 모든 상태 한눈에 보기 (Grid 레이아웃)
export const AllStates: Story = {
  render: () => (
 
  )
};

Storybook 설정

.storybook/main.ts

import type { StorybookConfig } from '@storybook/react-webpack5';

const config: StorybookConfig = {
  // 자동 감지할 Story 파일 패턴
  stories: [
    '../src/**/*.stories.{js,jsx,ts,tsx}'
  ],

  // Storybook addons (추가 기능)
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-coverage',
    '@storybook/addon-a11y',  // 접근성 테스트
    '@storybook/addon-themes'  // 테마 전환
  ],

  // 프레임워크
  framework: {
    name: '@storybook/react-webpack5',
    options: {}
  },

  // 문서 자동 생성
  docs: {
    autodocs: 'tag'
  }
};

export default config;

.storybook/preview.ts

import type { Preview } from '@storybook/react';

const preview: Preview = {
  // 모든 Story에 적용되는 기본 파라미터
  parameters: {
    layout: 'centered',
    actions: { argTypesRegex: '^on[A-Z].*' }
  },

  // 전역 decorators (모든 Story 감싸기)
  decorators: [
    // Theme provider
    (Story) => (
      <div style={{ theme: 'light' }}>
        <Story />
      </div>
    ),
    
    // Redux provider 등
    (Story) => (
      <Provider store={store}>
        <Story />
      </Provider>
    )
  ]
};

export default preview;

프로덕션 레벨 설정

1. CSS 모듈 지원

// .storybook/main.ts 추가
const config: StorybookConfig = {
  // ...
  webpackFinal: async (config) => {
    config.module?.rules?.push({
      test: /\.module\.css$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: { modules: true }
        }
      ]
    });
    return config;
  }
};

2. TypeScript 지원

// tsconfig.json에 추가
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "types": ["@storybook/test"]
  },
  "include": [
    "src",
    ".storybook"
  ]
}

3. 접근성 테스트 (a11y)

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta = {
  title: 'Components/Button',
  component: Button,
  
  // a11y 테스트 설정
  parameters: {
    a11y: {
      config: {
        rules: [
          {
            id: 'color-contrast',
            enabled: true
          }
        ]
      }
    }
  }
} satisfies Meta<typeof Button>;

4. 시각적 회귀 테스트

import { expect, waitFor } from '@storybook/test';

export const WithVisualTest: Story = {
  args: {
    label: 'Button'
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');
    
    // 버튼이 렌더링될 때까지 대기
    await waitFor(() => {
      expect(button).toBeInTheDocument();
    });
  }
};

팀 협업

Storybook 공유

# 정적 Storybook 빌드
npm run build-storybook

# dist/storybook 디렉토리 생성 → 웹서버에 배포

디자이너와의 협업

// 컴포넌트와 디자인 시스템 일치시키기
const meta = {
  title: 'Design System/Colors',
  component: ColorPalette,
  
  parameters: {
    figma: 'https://www.figma.com/...',  // Figma 링크
    docs: {
      description: {
        component: '공식 디자인 시스템의 컬러입니다.'
      }
    }
  }
};

Storybook Commands

기본 명령어

# 개발 모드 (hot reload 지원)
npm run storybook

# 정적 빌드 생성
npm run build-storybook

# 원격 서버에서 실행
storybook dev -p 9009

# 프로덕션 모드 빌드
npm run build-storybook -- --configuration-mode=static

공식 자료

공식 사이트: https://storybook.js.org/
문서: https://storybook.js.org/docs/react/get-started/introduction
GitHub: https://github.com/storybookjs/storybook
커뮤니티: https://discord.gg/storybook

팀 협업 가이드

# Storybook 개발 규칙

## 컴포넌트 구조

모든 컴포넌트마다 Story 파일 작성 필수:

Button.tsx → 컴포넌트 Button.stories.tsx → Story (필수!) Button.test.tsx → 테스트 (선택) Button.module.css → 스타일

## Story 작성 체크리스트

- [ ] 기본 상태
- [ ] 모든 variant 표시
- [ ] 모든 크기 표시
- [ ] 에러/로딩 상태
- [ ] 비활성화 상태
- [ ] 상호작용 테스트

## argTypes 필수 정의

모든 props에 대해:
- description: 무엇인가?
- control: 어떻게 제어할 것인가?
- table: 타입과 기본값

## 성능 최적화

- Storybook 빌드 시간을 5초 이하로 유지
- 큰 컴포넌트는 분리하여 lazy load
- 불필요한 addons 제거

## 배포

- CI/CD에서 자동 빌드
- 프리뷰 URL 공유
- 모든 PR에 Storybook 링크 포함

## 검토 프로세스

1. 컴포넌트 구현
2. Story 파일 작성
3. PR에 Story 포함
4. 팀원 리뷰 (Storybook 확인)
5. 머지 전 Story 검증

## 문제 해결

**Q: Story가 로드되지 않음**
A: .storybook/main.ts의 stories 패턴 확인

**Q: 이미지가 안 보여요**
A: public 폴더에 이미지 저장하고 /image.png로 참조

**Q: Props가 제어되지 않음**
A: argTypes와 args를 모두 정의했는지 확인

**Q: 전역 스타일이 적용 안 됨**
A: .storybook/preview.ts에 import 추가

Storybook vs 다른 도구

기능 Storybook Styleguidist Ladle Bit

컴포넌트 개발 ✅ 최고 ✅ 좋음 ✅ 좋음 ✅ 좋음
문서화 ✅ 최고 ✅ 좋음 ❌ 약함 ✅ 좋음
테스트 ✅ 최고 ❌ 약함 ❌ 약함 ❌ 약함
커뮤니티 ✅ 최대 ✅ 중간 ❌ 작음 ✅ 중간
학습곡선 중간 낮음 낮음 높음
성능 중간 빠름 매우 빠름 중간

실제 사용 사례

1. 디자인 시스템 문서화

// 회사의 디자인 시스템을 Storybook으로 문서화
// Button, Card, Input, Modal 등 모든 컴포넌트
// → 디자이너와 개발자 간 일치 보장
// → 신규 입사자 온보딩 도구로 사용

2. 컴포넌트 라이브러리

// 오픈소스 UI 라이브러리
// Chakra UI, MUI, shadcn/ui 모두 Storybook 사용
// → 사용자가 모든 컴포넌트를 직접 테스트
// → 문서화와 테스트를 한 번에

3. 팀 협업

// PR에 Storybook 링크 포함
// → 코드 리뷰 전에 시각적으로 확인
// → UI/UX 팀이 변경 사항 검증
// → 커뮤니케이션 비용 75% 감소

결론

Storybook은:

개발 속도: 컴포넌트 개발 시간 50% 단축 ✅ 품질: 모든 상태를 테스트할 수 있음 ✅ 문서화: 자동으로 생성되는 문서 ✅ 협업: 디자이너, QA, 개발자 간 소통 개선 ✅ 재사용성: 컴포넌트의 모든 사용 예제 명시 ✅ 유지보수: 변경 사항 영향 범위 즉시 파악

특히 다음 경우에 필수입니다:

  • 컴포넌트 라이브러리 개발
  • 대규모 팀 협업
  • 디자인 시스템 구축
  • 복잡한 UI 컴포넌트

지금 바로 Storybook을 시작하세요!


https://www.youtube.com/playlist?list=PLwtj4R9Hld3GUdKDhl5YcXWRUNkrtIkX0

 

Storybook

 

www.youtube.com

(스토리북 유튜브 리스트)

'프론트엔드' 카테고리의 다른 글

동적 페이지 Title 변경 - React & Next.js  (0) 2026.03.03
Giscus란?  (0) 2026.02.06
MSW란?  (0) 2026.02.04
Deno란?  (0) 2026.02.03
FSD란?  (0) 2026.02.02