devops2025-02-15·9 min·204/348

Storybook 컴포넌트 테스팅: UI 컴포넌트 격리 테스트

Storybook을 활용한 UI 컴포넌트 테스팅과 시각적 테스트 방법을 설명합니다.

Storybook 컴포넌트 테스팅

Introduction

Storybook은 UI 컴포넌트를 격리된 환경에서 개발하고 테스트할 수 있는 도구입니다. 컴포넌트의 다양한 상태를 시각적으로 확인하고 테스트할 수 있어 UI 개발 생산성을 크게 향상시킵니다. 이 글에서는 Storybook 설정부터 컴포넌트 테스팅까지 전체 과정을 다루겠습니다.

Environment

// package.json
{
  "name": "my-project",
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "test-storybook": "test-storybook"
  },
  "devDependencies": {
    "@storybook/react": "^7.6.0",
    "@storybook/react-vite": "^7.6.0",
    "@storybook/addon-essentials": "^7.6.0",
    "@storybook/addon-interactions": "^7.6.0",
    "@storybook/addon-a11y": "^7.6.0",
    "@storybook/test": "^7.6.0",
    "storybook": "^7.6.0"
  }
}
npx storybook --version
# Storybook 7.6.4

Problem

Storybook 설정 시 발생하는 문제들:

// .storybook/main.js
module.exports = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions'
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {}
  }
};

// 문제점:
// 1. Vite 설정 충돌
// 2. 스토리 로딩 실패
// 3. 인터랙션 테스트 미작동
# Storybook 실행 시 에러
npx storybook dev -p 6006

# 에러:
# ERR! Error: Cannot find module './stories/Button.stories.tsx'
# ERR!     at storybook dev failed

Analysis

Storybook 설정 구조를 분석했습니다:

# Storybook 폴더 구조
.storybook/
├── main.js
├── preview.js
└── manager.js

# 스토리 파일 구조
src/
├── components/
│   └── Button/
│       ├── Button.tsx
│       ├── Button.test.tsx
│       └── Button.stories.tsx
// .storybook/main.js 분석
const main = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y'
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {}
  },
  docs: {
    autodocs: 'tag'
  }
};

export default main;

Solution

1단계: Storybook 설정 구성

// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    '@storybook/addon-designs'
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {}
  },
  docs: {
    autodocs: 'tag',
    defaultName: 'Documentation'
  },
  typescript: {
    reactDocgen: 'react-docgen-typescript'
  }
};

export default config;

2단계: preview 설정

// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import '../src/styles/globals.css';

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i
      }
    },
    viewport: {
      viewports: {
        mobile: {
          name: 'Mobile',
          styles: { width: '375px', height: '667px' }
        },
        tablet: {
          name: 'Tablet',
          styles: { width: '768px', height: '1024px' }
        },
        desktop: {
          name: 'Desktop',
          styles: { width: '1280px', height: '720px' }
        }
      }
    }
  }
};

export default preview;

3단계: 스토리 작성

// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';

const meta = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    backgroundColor: { control: 'color' },
    size: {
      control: { type: 'select' },
      options: ['small', 'medium', 'large']
    }
  },
  args: {
    onClick: fn()
  }
} satisfies Meta;

export default meta;
type Story = StoryObj;

export const Primary: Story = {
  args: {
    primary: true,
    label: 'Button',
    backgroundColor: '#1ea7fd'
  }
};

export const Secondary: Story = {
  args: {
    label: 'Secondary'
  }
};

export const Large: Story = {
  args: {
    size: 'large',
    label: 'Large Button'
  }
};

export const Small: Story = {
  args: {
    size: 'small',
    label: 'Small Button'
  }
};

4단계: 인터랙션 테스트

// src/components/Button/Button.stories.tsx (인터랙션 테스트 추가)
import { expect, userEvent, within } from '@storybook/test';

export const ClickTest: Story = {
  args: {
    label: 'Click me'
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');
    
    await userEvent.click(button);
    
    await expect(args.onClick).toHaveBeenCalled();
    await expect(button).toHaveTextContent('Click me');
  }
};

export const FormSubmission: Story = {
  render: () => (
    
), play: async ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByTestId('input'); const button = canvas.getByRole('button'); await userEvent.type(input, 'Hello World'); await userEvent.click(button); await expect(input).toHaveValue('Hello World'); } };

Lessons Learned

  1. tags 활용: tags: ['autodocs']를 사용하면 스토리에서 자동으로 문서를 생성할 수 있습니다
  2. argTypes 설정: 컴포넌트 props를 Storybook UI에서 제어할 수 있도록 argTypes를 정의해야 합니다
  3. play 함수: 인터랙션 테스트를 통해 사용자 상호작용을 시뮬레이션할 수 있습니다
  4. 시각적 테스트: Storybook과 Chromatic을 연동하면 시각적 회귀 테스트를 수행할 수 있습니다
  5. ** 접근성 테스트**: @storybook/addon-a11y를 사용하면 컴포넌트의 접근성을 점검할 수 있습니다

이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.