Storybook 알아보기
프론트엔드 테스트 도구 중 하나인 Storybook에 대해
테스팅
Storybook
Storybook은 리액트 컴포넌트를 앱의 전체적인 맥락(라우팅, API 통신 등)과 완전히 분리하여, 독립적인 UI 개발 환경에서 구축하고 시각적으로 테스트할 수 있게 해주는 도구이다.
단위 테스트가 컴포넌트의 로직을 분리하여 테스트한다면, Storybook은 컴포넌트의 시각적 결과물을 분리하여 개발하고 검토한다고 생각하면 쉽다.
npm create storybook@latest
프로젝트 구성을 자동으로 감지하여 필요한 패키지를 설치하고, .storybook 설정 폴더와 몇 가지 예시 *.stories.tsx 파일을 생성한다.
Story 작성
스토리는 컴포넌트의 특정 시각적 상태를 캡처한 것이다.
// 간단한 예시 버튼 컴포넌트
interface ButtonProps {
label: string;
primary?: boolean;
onClick?: () => void;
}
export const Button = ({ label, primary = false, ...props }: ButtonProps) => {
const mode = primary ? 'button--primary' : 'button--secondary';
return (
<button type="button" className={['button', mode].join(' ')} {...props}>
{label}
</button>
);
};
예를 들어 버튼 컴포넌트가 있다면, Primary 버튼, Disabled 버튼, Loading 버튼 등이 각각의 스토리가 된다.
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
// 1. Meta: 컴포넌트에 대한 메타데이터 (정보)
// - Storybook UI에서 어떻게 보일지 (제목, 컴포넌트 자체)를 정의합니다.
const meta: Meta<typeof Button> = {
title: 'Components/Button', // Storybook UI의 사이드바 경로
component: Button, // 스토리를 작성할 대상 컴포넌트
argTypes: {
// (선택) props의 타입을 Storybook이 추론/제어
onClick: { action: 'clicked' }, // 'Actions' 탭에서 클릭 이벤트를 로깅
},
};
export default meta;
// 2. StoryObj: 해당 컴포넌트의 특정 상태를 나타내는 스토리 객체
type Story = StoryObj<typeof meta>;
// 3. 스토리를 export
// - export된 객체 하나가 하나의 스토리가 됩니다.
// - args: 해당 스토리에 전달할 props입니다.
export const Primary: Story = {
args: {
primary: true,
label: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
label: 'Secondary Button',
},
};
스토리 파일은 컴포넌트 파일명 옆에 .stories.tsx 확장자로 생성한다.

작성하고 npm run storybook으로 실행하면 Storybook UI를 실행할 수 있다.
Storybook을 테스트에 활용하는 방법
시각적 회귀 테스트
- 목표: UI가 의도치 않게 변경되는 것(회귀)을 방지
- 방법:
Primary버튼의 CSS를 수정했다고 가정한다. 이 변경이Secondary버튼이나 다른 컴포넌트에 영향을 주지 않았는지 어떻게 확신할 수 있을까? - 연동: Chromatic 같은 도구를 Storybook에 연동
- 동작:
- Chromatic이 현재 모든 수토리의 스크린샷을 찍어 저장
- 코드를 수정한 후 PR을 올리면, CI(GitHub Actions 등)에서 CHromatic이 다시 스크린샷을 찍음
- 이전 스크린샷과 픽셀 단위로 비교하여 1px이라도 달라진 부분이 있다면 CI를 실패시키고 개발자에게 시각적 차이를 리포트
접근성 테스트
- 목표: 장애가 있는 사용자도 컴포넌트를 사용할 수 있는지 검사
- 방법:
@storybook/addon-a11y애드온을 설치 - 동작:
- Storybook UI에 'Accessibility' 탭이 추가됨
- 스토리를 볼 때 해당 애드온이
axe-core엔진을 실행하여, 접근성 관점에서 어떤 문제가 있는지 실시간으로 알려줌
인터랙션 테스트
- 목표: 컴포넌트의 단위 테스트와 통합 테스트를 Storybook 내에서 수행
- 방법: Storybook의
play함수와@storybook/test-runner를 사용 - 동작: 컴포넌트 스토리 파일을 수정
// ... (이전 코드)
import { userEvent, within } from '@storybook/testing-library';
import { expect } from '@storybook/jest'; // Vitest의 expect와 호환됨
export const Primary: Story = {
// ... (args)
// 'play' 함수: 스토리가 렌더링된 후 실행될 테스트 스크립트
play: async ({ canvasElement }) => {
// 캔버스(컴포넌트가 렌더링된 영역) 가져오기
const canvas = within(canvasElement);
// 1. RTL(Testing Library)처럼 DOM 요소 찾기
const button = canvas.getByRole('button', { name: /Primary Button/i });
// 2. DOM 요소 검증 (Vitest의 expect 사용)
await expect(button).toBeInTheDocument();
// 3. 사용자 상호작용 시뮬레이션
await userEvent.click(button);
// 4. (예시) 클릭 후 상태 변화 검증
// (이 예제에서는 'action'을 사용했지만, 실제로는 상태 변화를 검증)
},
};
- 실행:
- 브라우저: Storybook UI의 'Interactions' 탭에서
play함수가 실행되는 과정을 시각적으로 단계별로 보여줌 - CI (Headless):
npm run test-storybook명령어를 실행하면,@storybook/test-runner가 모든 스토리의play함수를 실제 브라우저(Headless) 환경에서 실행하여 Vitest처럼 테스트 리포트를 출력
- 브라우저: Storybook UI의 'Interactions' 탭에서
요약
정리하자면, Storybook은 단순히 컴포넌트를 모아보는 카탈로그가 아니라, 다음과 같은 역할을 하는 중심적인 테스트 허브이다.
- 개발: 컴포넌트를 독립적으로 시각화하고 개발 (기본 기능)
- 검증:
- 시각적: 변경 사항이 UI를 깨뜨리지 않았는지 (시각적 회귀)
- 접근성: 표준을 준수하는지
- 기능적: 사용자의 클릭/입력에 올바르게 반응하는지 (인터랙션)
RTL/Vitest를 사용한 테스트와 뭐가 다르냐 할 수 있다.
RTL/Vitest를 사용한 단위/통합 테스트가 기능/로직/데이터 흐름을 검증하며 코드가 올바르게 동작하는가의 관점이라면, Storybook은 시각적/접근성/인터랙션을 검증하며 컴포넌트가 올바르게 보이고 반응하는가의 관점으로 테스트할 수 있는 환경을 제공한다.
두 가지가 합쳐져야 비로소 견고한 프론트엔드 테스트가 완성된다.