본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Frontend

Storybook: 컴포넌트를 상태별로 떼어 보는 도구

작성 2026. 10. 21.추가
목차9개 항목
출처와 참고

「Frontend_TIL: Storybook」 1~3편을 바탕으로, 문법은 Storybook 10 공식 문서로 확인해 재구성한 노트다.

Storybook이란

Storybook은 컴포넌트를 앱에서 떼어 내 상태별로 등록해 두고 따로 띄워 보는 UI 작업장(workshop)이다. 등록한 상태 하나를 스토리(story) 라고 한다. 버튼의 primary·disabled·loading처럼 앱에서 재현하려면 라우트를 옮기고 데이터를 꾸며야 하는 상태를, 스토리 목록에서 바로 고른다.

쓰임내용
개발컴포넌트 하나만 띄워 놓고 상태를 바꿔 가며 만든다. 컴포넌트 주도 개발(Component-Driven Development)의 기본 도구다
문서스토리와 props 표가 곧 컴포넌트 사용 설명서다. 디자이너·기획자와 같은 화면을 보고 이야기한다
테스트스토리를 렌더링 테스트·상호작용 테스트·시각 회귀·접근성 검사의 입력으로 그대로 쓴다
npm create storybook@latest      # 과거에는 npx sb init
npm run storybook                # 기본 6006 포트

설치하면 .storybook/main.ts(어떤 파일이 스토리인지, 프레임워크, 애드온)와 .storybook/preview.ts(모든 스토리에 적용할 전역 설정)가 생긴다.

스토리 쓰기: CSF3

스토리 파일은 CSF(Component Story Format) 라는 ES 모듈 규칙을 따른다. default export가 컴포넌트 단위 설정(meta)이고, 이름 붙은 export 하나하나가 스토리다. 컴포넌트 옆에 Button.stories.tsx로 둔다.

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite'
import { fn } from 'storybook/test'
import { Button } from './Button'
 
const meta = {
  title: 'Form/Button',            // 사이드바 경로. 생략하면 파일 위치로 자동 생성
  component: Button,
  args: { children: 'Button', onClick: fn() },   // 모든 스토리 공통 args
  argTypes: {
    color: { control: 'radio', options: ['blue', 'gray', 'orange'] },
  },
} satisfies Meta<typeof Button>
 
export default meta
type Story = StoryObj<typeof meta>
 
export const Blue: Story = { args: { color: 'blue' } }
export const Gray: Story = { args: { color: 'gray' } }
export const LongGray: Story = {
  name: 'Gray (긴 텍스트)',          // 사이드바에 보일 이름
  args: { ...Gray.args, children: '아주 긴 버튼 텍스트' },
}
요소역할
args스토리가 컴포넌트에 넘길 props. 스토리 → meta → 전역 순으로 가까운 쪽이 이긴다
argTypesargs를 어떻게 다룰지. 컨트롤 종류(radio·select·color…), 선택지, 설명
title사이드바 계층. Form/Button이면 Form 폴더 안의 Button
name스토리 표시 이름. export 이름은 짧게, 보이는 이름은 자세하게
fn()호출을 기록하는 스파이. Actions 패널에 이벤트가 찍히고 테스트에서 단언할 수 있다
  • 스토리가 객체라서 다른 스토리의 args를 펼쳐 재사용한다. 스토리를 다른 스토리 안에 컴포넌트로 넣어 조합할 수도 있다.
  • 과거 CSF2는 const Template = (args) => <Button {...args} />를 만들고 Template.bind({})로 스토리를 복제했으며, 표시 이름은 Blue.storyName으로 바꿨다. 그보다 앞에는 스토리를 JSX를 돌려주는 함수(export const Blue = () => <Button color="blue" />)로만 썼다. 지금도 함수 스토리는 render로 쓸 수 있지만 args를 거치지 않으면 컨트롤로 값을 바꿀 수 없다.

TypeScript

쓰는 법특징
const meta = { … } satisfies Meta<typeof Button> + StoryObj<typeof meta>권장. meta에 넣은 args가 타입에 반영되어, meta에서 채운 필수 props를 스토리마다 다시 쓰라고 요구하지 않는다(TS 4.9+)
const meta: Meta<typeof Button> = { … } + StoryObj<typeof Button>타입 주석 방식. meta의 args를 추론하지 못해 필수 props 오류가 날 수 있다

args가 컴포넌트 props와 맞지 않으면 타입 오류가 난다. 별도 설정 없이 프레임워크 패키지(@storybook/react-vite, @storybook/nextjs-vite 등)에서 타입을 가져온다.

설정의 세 층

같은 설정(args, decorators, parameters)을 세 곳에 둘 수 있고, 아래로 갈수록 범위가 좁고 우선한다.

.storybook/preview.ts   전역     모든 스토리
Button.stories.tsx      meta     이 컴포넌트의 모든 스토리
  export const Blue     스토리    이 스토리 하나

decorators

decorator는 스토리를 감싸는 함수다. 가운데 정렬 같은 Storybook 전용 배치, 테마·라우터·데이터 Provider처럼 컴포넌트가 앱에서 기대하는 바깥 환경을 넣는다.

// meta에 두면 이 컴포넌트의 스토리만, preview.ts에 두면 전부 감싼다
decorators: [(Story) => <div style={{ display: 'flex', justifyContent: 'center' }}><Story /></div>],

parameters

정렬·배경·뷰포트처럼 Storybook 기능에 주는 설정이다. 예를 들어 사이드바 정렬은 preview.ts에서 정한다.

// .storybook/preview.ts
const preview = {
  parameters: {
    layout: 'centered',                                  // 단순 가운데 정렬은 이것으로 충분
    options: { storySort: { method: 'alphabetical' } },  // 기본은 import 순서
  },
}
export default preview

화면을 살피는 도구

대부분의 기능은 애드온으로 붙는다. Storybook 9부터 자주 쓰는 도구는 코어에 들어가 따로 설치하지 않는다(과거에는 @storybook/addon-essentials 묶음을 설치했다). 문서(Docs)·접근성(a11y)·테스트(Vitest)는 별도 애드온이다.

도구하는 일
Controlsargs를 라디오·텍스트·색 선택 UI로 바꿔 즉석에서 상태를 바꾼다. args로 쓴 스토리에서만 동작한다
ActionsonClick 같은 콜백이 불렸는지, 어떤 인자로 불렸는지 로그로 보여 준다
Backgrounds배경색을 바꿔 라이트·다크에서 확인한다
Viewport기기 크기를 바꿔 반응형을 확인한다
Measure·Outline선택한 요소의 박스 모델(margin·padding·크기)과 모든 요소의 윤곽선을 보여 준다
Docs (@storybook/addon-docs)스토리·props 표·소스로 문서 페이지를 자동 생성한다. tags: ['autodocs']
a11y (@storybook/addon-a11y)스토리마다 axe 검사 결과를 보여 준다(접근성 평가)

스토리를 테스트로 쓰기

스토리는 이미 준비(Arrange)를 끝낸 테스트 케이스다. 그래서 같은 스토리를 여러 테스트의 입력으로 재사용한다.

테스트방법
렌더링(스모크)모든 스토리가 에러 없이 그려지는지. @storybook/addon-vitest가 스토리를 Vitest 테스트로 바꿔 실제 브라우저(Vitest 브라우저 모드)에서 돌린다. 과거의 test-runner는 지원 종료 예정이다
상호작용스토리에 play 함수를 두고 클릭·입력을 실행한 뒤 단언한다
시각 회귀스토리 스크린숏을 기준 이미지와 비교한다(Chromatic 등)
접근성a11y 애드온 검사를 테스트 실패로 연결한다
import { expect } from 'storybook/test'
 
export const Clicked: Story = {
  play: async ({ canvas, userEvent, args }) => {
    await userEvent.click(canvas.getByRole('button', { name: 'Button' }))
    await expect(args.onClick).toHaveBeenCalled()
  },
}

play 안의 쿼리는 Testing Library와 같다. 네트워크가 필요한 컴포넌트는 MSW 핸들러를 스토리에 붙여 응답을 고정한다(API 모킹).

정리

  • 스토리 = 컴포넌트 + 상태 하나. 앱을 돌아다니지 않고 상태별 UI를 바로 본다.
  • CSF3에서는 스토리가 args를 담은 객체다. meta·전역에 공통 args·decorators·parameters를 두고, 가까운 설정이 이긴다.
  • TypeScript는 satisfies Meta<typeof X> + StoryObj<typeof meta>로 쓴다.
  • Controls·Actions·Viewport 같은 도구는 Storybook 9부터 코어에 있다. Docs·a11y·Vitest는 애드온이다.
  • 스토리를 렌더링·상호작용·시각·접근성 테스트에 재사용한다. 디자인 시스템에서는 Storybook이 곧 컴포넌트 문서다(디자인 시스템).

관련 노트: 프론트엔드 테스트의 종류와 도구, 디자인 시스템, API 모킹: MSW와 json-server, 접근성 평가

원본 기록6

댓글

GitHub 계정으로 댓글을 남길 수 있어요.