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.4Problem
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 failedAnalysis
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
- tags 활용:
tags: ['autodocs']를 사용하면 스토리에서 자동으로 문서를 생성할 수 있습니다 - argTypes 설정: 컴포넌트 props를 Storybook UI에서 제어할 수 있도록 argTypes를 정의해야 합니다
- play 함수: 인터랙션 테스트를 통해 사용자 상호작용을 시뮬레이션할 수 있습니다
- 시각적 테스트: Storybook과 Chromatic을 연동하면 시각적 회귀 테스트를 수행할 수 있습니다
- ** 접근성 테스트**: @storybook/addon-a11y를 사용하면 컴포넌트의 접근성을 점검할 수 있습니다
이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.