
2편에서는 Storybook을 설치하고, CSF 구조와 첫 Story 작성법을 알아봤다. 이번 글에서는 Story를 감싸는 Decorator와 Story의 정적 메타데이터인 Parameter를 정리한다.
Decorator는 Story를 렌더링할 때 추가적인 마크업이나 컨텍스트로 감싸는 래퍼다.
공식 문서의 Decorators 페이지를 보면, 이런 설명이 나온다.
When writing stories, decorators are typically used to wrap stories with extra markup or context mocking.
왜 필요한지 예를 들어보자. 컴포넌트가 화면 가장자리에 딱 붙어서 렌더링되면 Storybook에서 보기 불편하다. 이럴 때 Decorator로 감싸서 여백을 줄 수 있다. 또는 ThemeProvider, Redux Store, React Router처럼 컨텍스트가 필요한 컴포넌트라면, Decorator에서 해당 Provider를 감싸주면 된다.
Decorator는 적용 범위에 따라 3가지 레벨로 나뉜다.
특정 Story 하나에만 적용된다.
import type { Meta, StoryObj } from '@storybook/nextjs';
import { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const WithMargin: Story = {
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
<Story />
</div>
),
],
};
WithMargin Story에만 3em의 마진이 적용된다. 다른 Story에는 영향을 주지 않는다.
해당 컴포넌트의 모든 Story에 적용된다. meta의 decorators 속성에 정의한다.
import type { Meta } from '@storybook/nextjs';
import { Button } from './Button';
const meta = {
component: Button,
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
<Story />
</div>
),
],
} satisfies Meta<typeof Button>;
export default meta;
이 파일에 있는 모든 Story(Primary, Secondary 등)에 마진이 적용된다.
모든 Story에 적용된다. .storybook/preview.ts 파일에 정의한다.
// .storybook/preview.tsx
import type { Preview } from '@storybook/nextjs';
const preview: Preview = {
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
<Story />
</div>
),
],
};
export default preview;
프로젝트의 모든 Story에 공통으로 적용된다. 전역 스타일이나 Provider를 설정할 때 주로 사용한다.
실무에서 가장 흔하게 쓰이는 Decorator 패턴은 Provider 감싸기다.
공식 문서의 Mocking providers 페이지에 이런 예제가 나온다. styled-components의 ThemeProvider를 모든 Story에 적용하는 경우다.
// .storybook/preview.tsx
import type { Preview } from '@storybook/nextjs';
import { ThemeProvider } from 'styled-components';
const preview: Preview = {
decorators: [
(Story) => (
<ThemeProvider theme="default">
<Story />
</ThemeProvider>
),
],
};
export default preview;
이렇게 하면 모든 Story가 ThemeProvider 안에서 렌더링되므로, 테마에 의존하는 컴포넌트들이 정상적으로 동작한다.
모든 Story에 같은 테마를 적용하는 게 아니라, Story마다 다른 테마를 쓰고 싶을 수도 있다. 이때 Decorator의 두 번째 인자인 context를 활용한다. context에서 parameters 값을 읽어 Provider에 전달하는 방식이다.
// .storybook/preview.tsx
import type { Preview } from '@storybook/nextjs';
import { ThemeProvider } from 'styled-components';
import * as themes from '../src/themes';
const preview: Preview = {
decorators: [
(Story, { parameters }) => {
const { theme = 'light' } = parameters;
return (
<ThemeProvider theme={themes[theme]}>
<Story />
</ThemeProvider>
);
},
],
};
export default preview;
이제 개별 Story에서 parameters.theme을 지정하면 해당 테마가 적용된다.
export const DarkMode: Story = {
parameters: {
theme: 'dark',
},
};
이 패턴은 Decorator를 한 번만 정의하고, Story마다 다른 설정을 parameters로 전달하는 구조다. Decorator와 Parameter가 함께 동작하는 대표적인 사례인데, Parameter에 대해서는 바로 아래에서 다룬다.
Parameter는 Story에 대한 정적 메타데이터다. 공식 문서의 Parameters 페이지에 이렇게 설명되어 있다.
Parameters are a set of static, named metadata about a story, typically used to control the behavior of Storybook features and addons.
args가 컴포넌트에 전달되는 props라면, parameters는 Storybook의 기능이나 애드온의 동작을 제어하는 설정값이다. 컴포넌트 자체에는 전달되지 않고, Storybook이 Story를 어떻게 렌더링하고 표시할지를 결정한다.
Decorator와 마찬가지로 Parameter도 3가지 레벨로 정의할 수 있다.
특정 Story 하나에만 적용된다.
import type { Meta, StoryObj } from '@storybook/nextjs';
import { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
parameters: {
backgrounds: {
options: {
red: { name: 'Red', value: '#f00' },
green: { name: 'Green', value: '#0f0' },
blue: { name: 'Blue', value: '#00f' },
},
},
},
};
Primary Story에서만 배경색 옵션이 빨강, 초록, 파랑으로 설정된다.
해당 컴포넌트의 모든 Story에 적용된다.
import type { Meta } from '@storybook/nextjs';
import { Button } from './Button';
const meta = {
component: Button,
parameters: {
backgrounds: {
options: {},
},
},
} satisfies Meta<typeof Button>;
export default meta;
모든 Story에 적용된다. .storybook/preview.ts에 정의한다.
// .storybook/preview.ts
import type { Preview } from '@storybook/nextjs';
const preview: Preview = {
parameters: {
backgrounds: {
options: {
light: { name: 'Light', value: '#fff' },
dark: { name: 'Dark', value: '#333' },
},
},
},
};
export default preview;
모든 Story에서 Light와 Dark 배경을 선택할 수 있게 된다.
더 구체적인 레벨이 상위 레벨을 덮어쓴다.
Story > Component > Global
Global에서 배경을 Light/Dark로 설정했더라도, 특정 Story에서 Red/Green/Blue로 재정의하면 그 Story에서는 후자가 적용된다.
Story가 Canvas에서 어떻게 배치될지를 결정한다.
const meta = {
component: Button,
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Button>;
centered — 가로세로 중앙 정렬fullscreen — Canvas 전체를 채움padded — 기본값, 여백 있음배경색 옵션을 설정한다. 위에서 다룬 예제 그대로다.
Controls 패널의 동작을 커스텀한다. 설치 시 기본으로 생성되는 설정이 있다.
// .storybook/preview.ts
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
이름에 "background"이나 "color"가 포함된 arg는 자동으로 색상 선택기로, "Date"로 끝나는 arg는 날짜 선택기로 표시된다.
| Decorator | Parameter | |
|---|---|---|
| 역할 | Story를 마크업/컨텍스트로 감싼다 | Story의 정적 메타데이터를 정의한다 |
| 대상 | 렌더링에 영향 | Storybook 기능/애드온에 영향 |
| 적용 레벨 | Story / Component / Global | Story / Component / Global |
| 예시 | ThemeProvider 감싸기, 마진 추가 | 배경색 옵션, 레이아웃, Controls 설정 |
둘 다 하위 레벨이 상위 레벨을 오버라이드하는 구조이고, .storybook/preview.ts에서 글로벌 설정을 할 수 있다는 공통점이 있다.
parameters 값을 읽으면, Story마다 다른 설정을 Provider에 전달할 수 있다.