"이 버튼 hover 색상이 피그마랑 다른 것 같은데요."
디자이너한테 이 말을 들으면 나는 로컬에서 해당 페이지를 띄우고, 그 버튼이 있는 화면까지 이동하고, 로그인하고, 특정 상태를 만들어서 hover를 확인한다. 수정하고, 다시 확인하고, 디자이너한테 스크린샷을 찍어서 보낸다. 디자이너는 "아 근데 disabled 상태일 때는요?" 라고 묻는다. 또 그 상태를 만든다.
Storybook을 도입하고 나서 이 과정이 사라졌다. 디자이너가 직접 Storybook 페이지에 들어가서 버튼의 모든 상태를 확인한다. 수정하면 배포된 Storybook에 바로 반영된다. "확인해주세요"라는 메시지 대신 링크 하나를 던지면 끝이다.
설치와 기본 설정
Next.js 프로젝트 기준으로 설명한다.
npx storybook@latest init이 명령어가 프로젝트 구조를 분석해서 필요한 설정을 자동으로 해준다. Next.js를 감지하면 @storybook/nextjs 프레임워크를 설치한다.
.storybook/main.ts 파일이 생긴다.
import type { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
};
export default config;stories 경로를 프로젝트 구조에 맞게 조정하면 된다. 우리 팀은 컴포넌트 파일 옆에 스토리 파일을 둔다.
components/
├── Button/
│ ├── Button.tsx
│ ├── Button.stories.tsx
│ └── Button.test.tsx
첫 번째 스토리 작성하기
Button 컴포넌트의 스토리를 만들어보자.
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
isLoading: {
control: 'boolean',
},
disabled: {
control: 'boolean',
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: '확인',
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
children: '취소',
variant: 'secondary',
},
};
export const Loading: Story = {
args: {
children: '저장 중',
variant: 'primary',
isLoading: true,
},
};
export const Disabled: Story = {
args: {
children: '비활성화',
disabled: true,
},
};
export const AllVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};tags: ['autodocs']를 넣으면 Storybook이 자동으로 문서 페이지를 만들어준다. Props 테이블, 각 스토리의 코드 예시까지. 이게 디자이너에게 보여주기 좋다.
실전에서 유용한 패턴들
상태를 가진 컴포넌트
모달이나 드롭다운처럼 내부 상태가 있는 컴포넌트는 render 함수 안에서 상태를 관리한다.
export const ControlledModal: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={()=> setIsOpen(true)}>모달 열기</Button>
<Modal isOpen={isOpen} onClose={()=> setIsOpen(false)}>
<Modal.Header>알림</Modal.Header>
<Modal.Body>정말 삭제하시겠습니까?</Modal.Body>
<Modal.Footer>
<Button variant="ghost" onClick={()=> setIsOpen(false)}>
취소
</Button>
<Button variant="primary">삭제</Button>
</Modal.Footer>
</Modal>
</>
);
},
};API 호출이 필요한 컴포넌트
MSW(Mock Service Worker)를 붙여서 API 응답을 모킹한다.
import { http, HttpResponse } from 'msw';
export const WithData: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: '김개발', role: 'Frontend' },
{ id: 2, name: '이디자', role: 'Designer' },
]);
}),
],
},
},
};
export const WithError: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 });
}),
],
},
},
};
export const WithLoading: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/users', async () => {
await new Promise(resolve => setTimeout(resolve, 999999));
return HttpResponse.json([]);
}),
],
},
},
};이렇게 하면 성공, 실패, 로딩 세 가지 상태를 각각 스토리로 만들 수 있다. 에러 UI가 제대로 나오는지, 로딩 스켈레톤이 잘 보이는지 확인하기 위해 실제 서버 상태를 조작할 필요가 없다.
다크 모드 대응
Storybook에서 다크/라이트 모드를 전환하면서 확인하고 싶다면 decorator를 쓴다.
// .storybook/preview.tsx
const preview: Preview = {
globalTypes: {
theme: {
description: 'Global theme',
toolbar: {
title: 'Theme',
items: ['light', 'dark'],
dynamicTitle: true,
},
},
},
decorators: [
(Story, context) => {
const theme = context.globals.theme || 'light';
document.documentElement.setAttribute('data-theme', theme);
return <Story />;
},
],
};디자이너와의 협업 방식 변화
Storybook을 Chromatic이나 Vercel로 배포해두면, 디자이너가 직접 접속해서 컴포넌트를 확인할 수 있다. 우리 팀에서는 이런 흐름이 만들어졌다.
- 디자이너가 피그마에서 컴포넌트를 디자인한다.
- 개발자가 구현하고 스토리를 작성한다.
- PR을 올리면 Storybook 미리보기 링크가 자동으로 생긴다.
- 디자이너가 직접 Storybook에서 확인하고 피드백한다.
이전에는 "개발 서버 띄워서 직접 확인해줄 수 있어요?" 같은 요청이 있었다. 지금은 없다. PR 코멘트에 "Storybook에서 hover 상태 확인했는데, 피그마보다 transition이 빠른 것 같아요. 0.2s에서 0.3s로 바꿔주세요" 같은 구체적인 피드백이 온다.
이게 가능한 이유는 디자이너가 다른 페이지 맥락 없이 컴포넌트 하나만 볼 수 있기 때문이다. 전체 페이지를 탐색하면서 특정 상태를 만들 필요 없이, Controls 패널에서 props를 직접 조작하면서 모든 변형을 확인할 수 있다.
Storybook 도입할 때 주의할 점
처음부터 모든 컴포넌트에 스토리를 쓰려고 하면 안 된다. 그러면 스토리 작성 자체가 일이 되고, 아무도 하지 않게 된다. 우리 팀은 이렇게 기준을 잡았다.
- 공통 UI 컴포넌트(Button, Input, Modal 등)는 반드시 스토리를 작성한다.
- 페이지 컴포넌트나 비즈니스 로직이 무거운 컴포넌트는 선택적으로.
- 새로 만드는 컴포넌트는 스토리를 함께 작성한다. 기존 컴포넌트는 건드릴 때 추가한다.
완벽한 문서화보다 "쓸모있는 수준의 스토리"를 유지하는 게 중요하다. Controls에서 주요 props를 바꿔볼 수 있고, 주요 상태별 스토리가 있으면 그걸로 충분하다.
한 가지 더. Storybook은 "컴포넌트를 독립적으로 개발할 수 있게" 해준다고들 하는데, 진짜 가치는 그게 아니라 컴포넌트를 독립적으로 만들 수밖에 없게 강제한다는 거다. 스토리를 작성하려면 그 컴포넌트가 외부 의존성 없이 동작해야 한다. Props로 모든 걸 받아야 한다. 그 과정에서 자연스럽게 컴포넌트 설계가 나아진다. 처음에 스토리 작성이 어려우면, 그건 Storybook의 문제가 아니라 컴포넌트의 결합도가 높다는 신호다.
