React | Vue | Angular | Web Components | 그 외(More)
스토리북은 스토리가 렌더링되는 화면의 뷰포트(viewport)나 배경색(background)을 툴바(상단 메뉴)에서 쉽게 조작할 수 있는 기능을 기본으로 제공합니다. 이와 비슷한 원리로, 여러분도 직접 툴바에 버튼이나 메뉴를 만들어서 "전역 변수(globals)"라는 특별한 값들을 쥐락펴락할 수 있어요. 그리고 이 전역 변수값을 읽어와서 데코레이터(decorators)를 만들면, 스토리가 렌더링되는 방식을 내 마음대로 완벽하게 제어할 수 있게 됩니다.

스토리북에서 전역 변수(Globals)란, (특정 스토리에만 묶여 있는 게 아니라) 스토리가 렌더링될 때 영향을 미치는 "전역적인" 입력값을 뜻합니다. 스토리에 종속된 값이 아니기 때문에 스토리 함수의 args 매개변수로는 전달되지 않습니다. (대신 context.globals를 통해 접근할 수는 있어요.) 이 전역 변수들은 주로 모든 스토리에 공통으로 적용되는 '데코레이터' 안에서 핵심적인 역할을 합니다.
툴바 등에서 이 전역 변수값이 바뀌면, 스토리가 곧바로 다시 렌더링(re-render)되면서 데코레이터도 새로운 값을 받아 다시 실행됩니다. 전역 변수를 가장 쉽게 조작하는 방법은 바로 툴바에 전용 아이템(버튼/메뉴)을 하나 만들어 주는 거예요.
globalTypes와 toolbar 어노테이션 (Global types and the toolbar annotation)스토리북은 툴바 메뉴를 아주 쉽고 직관적으로 만들 수 있는 문법을 제공합니다. 프로젝트의 전역 설정 파일인 .storybook/preview.js|ts 안에 globalTypes를 정의하고 toolbar 어노테이션을 쓱 끼워 넣어주면 나만의 툴바가 완성됩니다.
예를 들어, 다국어 지원을 위해 화면 오른쪽 상단 툴바에 멋진 지구본 아이콘과 함께 언어(locale)를 선택할 수 있는 드롭다운 메뉴를 만들어 볼게요.
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Preview } from '@storybook/your-framework';
const preview: Preview = {
globalTypes: {
// 👇 우리가 만들 전역 변수의 이름입니다 (locale)
locale: {
description: 'Internationalization locale',
toolbar: {
// 툴바에 띄울 아이콘 모양
icon: 'globe',
// 드롭다운 메뉴에 들어갈 항목들 (배열 형태)
items: [
{ value: 'en', right: '🇺🇸', title: 'English' },
{ value: 'fr', right: '🇫🇷', title: 'Français' },
{ value: 'es', right: '🇪🇸', title: 'Español' },
{ value: 'zh', right: '🇨🇳', title: '中文' },
{ value: 'kr', right: '🇰🇷', title: '한국어' },
],
// 사용자가 선택한 값에 따라 툴바 버튼의 텍스트가 휙휙 바뀌게 할 건가요? (추천: true)
dynamicTitle: true,
},
},
},
initialGlobals: {
// 스토리북이 켜질 때 기본으로 선택되어 있을 언어(locale) 값입니다.
locale: 'en',
},
};
export default preview;
💡 팁: 예시 코드에 있는 icon 속성의 'globe' 모양은 스토리북의 @storybook/icons 패키지에서 가져온 겁니다. 툴바에 쓸 수 있는 수많은 아이콘 목록이 궁금하시다면 여기를 확인해 보세요!
위 코드처럼 메뉴 항목 설정에 right 속성을 추가하면 데코레이터와 연결되었을 때 드롭다운 메뉴 오른쪽 구석에 앙증맞은 국기 이모지를 띄울 수 있습니다.
toolbar 어노테이션 안에서 메뉴를 설정할 때 쓸 수 있는 옵션들은 다음과 같아요:
| 속성 (MenuItem) | 데이터 타입 (Type) | 설명 (Description) | 필수 여부 (Required) |
|---|---|---|---|
| value | String | 전역 변수(globals)에 실제로 저장될 메뉴 항목의 '값'입니다. | 예 (Yes) |
| title | String | 메뉴 항목에 표시될 메인 '텍스트'입니다. | 예 (Yes) |
| right | String | 메뉴 항목의 오른쪽 끝에 살짝 표시될 텍스트(주로 이모지나 단축키 등)입니다. | 아니오 (No) |
| icon | String | 이 항목이 선택되었을 때 툴바(상단)에 표시될 아이콘의 이름입니다. | 아니오 (No) |
전역 변수는 이름 그대로 '전역' 설정이므로 가급적 데코레이터 안에서 사용하여 모든 스토리에 일괄 적용되게 하는 것을 가장 권장합니다.
하지만, 가끔은 특정 스토리 하나에서만 그 전역 변수값을 쏙 뽑아서 써야 할 때가 있죠. 그럴 땐 렌더링 함수에 전달되는 context 객체 안에서 globals 값을 바로 꺼내 쓸 수 있습니다.
방금 만든 툴바 메뉴에서 Locale 값을 선택했을 때, 스토리가 그 값을 받아서 인사말을 다르게 렌더링하는 예시를 볼까요?
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
const meta = {
component: MyComponent, // (실제로는 보여주기용 텍스트만 렌더링할 거라 컴포넌트는 안 씁니다)
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
// 선택된 언어(locale)에 맞춰 인사말을 반환하는 간단한 함수입니다.
const getCaptionForLocale = (locale) => {
switch (locale) {
case 'es':
return 'Hola!';
case 'fr':
return 'Bonjour!';
case 'kr':
return '안녕하세요!';
case 'zh':
return '你好!';
default:
return 'Hello!';
}
};
export const StoryWithLocale: Story = {
// render 함수의 두 번째 인자인 context에서 globals.locale 값을 쏙 빼옵니다.
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
// 선택된 언어에 맞는 인사말을 화면에 렌더링합니다!
return <p>{caption}</p>;
},
};
스토리북 툴바에서 전역 변수(예: 테마, 언어 등) 값을 바꾸면, 그 상태로 이 스토리, 저 스토리를 넘나들어도 값이 그대로 유지됩니다. 참 편리하죠? 하지만 "이 컴포넌트의 이 스토리는 무조건 '어두운 테마(dark)' 환경에서만 테스트해야 해!"처럼 스토리가 특정한 환경(값)을 강제로 요구할 때가 있습니다.
이럴 땐 특정 스토리나 컴포넌트 전체에 globals 어노테이션을 달아서 값을 확정 지어버릴 수 있습니다. 이렇게 설정해 두면, 사용자가 툴바에서 뭘 선택했든 다 무시하고 그 스토리는 항상 지정된 전역 변수값으로만 렌더링됩니다. 게다가 사용자가 헷갈리지 않게 해당 스토리 화면에서는 그 전역 변수를 조작하던 툴바 메뉴가 아예 클릭할 수 없게(disabled) 잠겨버리죠.
버튼 컴포넌트의 OnDark 스토리를 볼 때만 배경(backgrounds)을 무조건 'dark'로 고정해 버리는 예시입니다:
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
const meta = {
component: Button,
globals: {
// 👇 이렇게 쓰면 Button 컴포넌트의 '모든' 스토리가 'gray' 배경으로 고정됩니다.
// backgrounds: { value: 'gray', grid: false },
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const OnDark: Story = {
globals: {
// 👇 이 스토리 하나만 콕 집어서 배경을 'dark'로 고정시킵니다!
backgrounds: { value: 'dark' },
},
};
이제 OnDark 스토리에 들어가면 배경색은 강제로 'dark'가 되고, 상단의 배경색 변경 툴바 메뉴는 회색으로 변하면서 비활성화됩니다. 마우스를 올려보면 "이 스토리는 배경색이 고정되어 있어서 변경할 수 없다"는 친절한 툴팁 설명이 나올 거예요.
💡 주의: 특정 스토리의 globals 값을 강제로 덮어씌우는 기능은 아주 유용하지만 꼭 필요할 때만 써야 합니다. 사용자가 스토리북 툴바를 이리저리 만지면서 컴포넌트의 다양한 상태 조합(전역 변수 변경 + args 조작 등)을 자유롭게 탐색해 볼 수 있어야 하는데, 값을 고정해 버리면 그런 자유로운 탐색 기회를 뺏어버리게 되니까요.
지금까지 전역 변수 하나를 만들고 사용하는 기본적인 방법을 알아봤습니다.
이제 좀 더 실전적인 예시를 볼까요? 예를 들어, 아까 만들었던 locale(언어) 전역 변수를 활용할 수 있게 데코레이터(decorator)를 하나 만들어서 preview.js|ts에 연결해 보겠습니다. 테마(Theme)를 관리하는 데코레이터 예시지만, 언어나 다른 전역 변수를 다루는 원리도 이와 똑같습니다.
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Preview } from '@storybook/your-framework';
// (예시로 styled-components의 ThemeProvider를 가져옵니다)
import { ThemeProvider } from 'styled-components';
// 프로젝트에서 만들어둔 진짜 테마 파일들을 불러옵니다.
import { MyThemes } from '../my-theme-folder/my-theme-file';
const preview: Preview = {
decorators: [
(Story, context) => {
// 툴바에서 사용자가 선택한 'theme' 글로벌 값을 가져와서 실제 테마 객체와 연결합니다.
const theme = MyThemes[context.globals.theme];
return (
// 스토리(컴포넌트) 전체를 선택된 테마로 감싸서(wrap) 렌더링합니다!
<<ThemeProvider theme={theme}>
<Story />
</ThemeProvider>
);
},
],
};
export default preview;
💡 다시 한번 강조하지만, 전역 변수는 말 그대로 전역(global)적인 녀석이라서 globalTypes와 initialGlobals 설정은 오직 전역 설정 파일인 .storybook/preview.js|ts에서만 세팅할 수 있습니다!
만약 여러분이 직접 새로운 애드온을 만들고 있는데, 버튼을 누르면 전역 변수값이 탁! 바뀌면서 스토리북 화면이 짠! 하고 새로고침되게 만들고 싶으신가요?
storybook/manager-api 모듈이 제공하는 useGlobals() 훅을 쓰면 됩니다. 이 훅에서 두 번째 값으로 뱉어내는 updateGlobals 함수를 사용하면 원하는 전역 변수값을 입맛대로 바꿀 수 있죠.
가장 대표적인 예로, 여러분만의 멋진 툴바 애드온(toolbar addon)을 만들어서, 툴바 버튼을 누를 때마다 전역 변수값이 바뀌면서 화면이 리렌더링되게 하는 코드를 짜보겠습니다:
// your-addon-register-file.js (애드온 등록 파일)
import React, { useCallback } from 'react';
import { OutlineIcon } from '@storybook/icons';
import { useGlobals } from 'storybook/manager-api';
import { addons } from 'storybook/preview-api';
import { ToggleButton } from 'storybook/internal/components';
import { FORCE_RE_RENDER } from 'storybook/internal/core-events';
const ExampleToolbar = (props) => {
// useGlobals() 훅으로 현재 전역 변수 상태와 업데이트 함수를 가져옵니다.
const [globals, updateGlobals] = useGlobals();
// 'my-param-key'라는 전역 변수값이 현재 true인지 확인합니다.
const isActive = globals['my-param-key'] || false;
// 툴바 버튼을 눌렀을 때 실행될 마법의 함수!
const refreshAndUpdateGlobal = () => {
// 1. 스토리북 전역 변수값을 반대로(true <-> false) 업데이트합니다.
updateGlobals({
['my-param-key']: !isActive,
});
// 2. 스토리북 애드온 API의 통신 채널에 "화면 다시 렌더링해!"라는 이벤트(FORCE_RE_RENDER)를 쏴줍니다.
addons.getChannel().emit(FORCE_RE_RENDER);
};
const toggleOutline = useCallback(() => refreshAndUpdateGlobal(), [isActive]);
return (
<ToggleButton
key="Example"
padding="small"
variant="ghost"
// 이 버튼이 눌린 상태인지 아닌지(active) 표시합니다.
pressed={isActive}
// 클릭 시 아까 만든 마법의 함수를 실행합니다.
onClick={toggleOutline}
ariaLabel="Addon feature"
tooltip="Toggle addon feature"
>
<OutlineIcon />
</ToggleButton>
);
};