스토리북 치트시트
Storybook - Build UIs in Isolation
스토리북은 UI 컴포넌트와 페이지를 격리된 환경에서 구축하기 위한 프론트엔드 워크샵입니다. 전체 앱을 실행하지 않고도 접근하기 어려운 상태와 엣지 케이스를 개발하고 공유할 수 있도록 도와줍니다. 수천 개의 팀이 UI 개발, 테스트, 문서화를 위해 이를 사용하고 있습니다.
[No text to translate]목차
The remaining sections (4-20) would need their specific content to be translated. Would you like me to continue with the translations for those sections?```bash
Initialize Storybook in existing project
npx storybook@latest init
Create new project with Storybook
npx create-react-app my-app cd my-app npx storybook@latest init
Start Storybook
npm run storybook
### Manual Installation
```bash
# Install Storybook CLI
npm install -g @storybook/cli
# Initialize in project
sb init
# Or specify framework
sb init --type react
sb init --type vue3
sb init --type angular
Framework-Specific Setup
# React
npx storybook@latest init --type react
# Vue 3
npx storybook@latest init --type vue3
# Angular
npx storybook@latest init --type angular
# Svelte
npx storybook@latest init --type svelte
# Web Components
npx storybook@latest init --type web-components
# HTML
npx storybook@latest init --type html
Project Structure
my-app/
├── .storybook/
│ ├── main.js # Main configuration
│ └── preview.js # Global settings
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.js
│ │ │ ├── Button.css
│ │ │ └── Button.stories.js
│ │ └── Header/
│ │ ├── Header.js
│ │ └── Header.stories.js
│ └── stories/ # Example stories
├── package.json
└── README.md
Getting Started
Basic Story
// src/components/Button/Button.stories.js
import { Button } from './Button';
export default {
title: 'Example/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
backgroundColor: { control: 'color' },
},
};
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
label: 'Button',
},
};
export const Large = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small = {
args: {
size: 'small',
label: 'Button',
},
};
Component Example
// src/components/Button/Button.js
import React from 'react';
import PropTypes from 'prop-types';
import './button.css';
export const Button = ({ primary = false, backgroundColor = null, size = 'medium', label, ...props }) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={{ backgroundColor }}
{...props}
>
{label}
</button>
);
};
Button.propTypes = {
primary: PropTypes.bool,
backgroundColor: PropTypes.string,
size: PropTypes.oneOf(['small', 'medium', 'large']),
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
Button.defaultProps = {
primary: false,
onClick: undefined,
};
Running Storybook
# Start development server
npm run storybook
# Build static Storybook
npm run build-storybook
# Serve built Storybook
npx http-server storybook-static
# Start with specific port
npm run storybook -- --port 9001
# Start in quiet mode
npm run storybook -- --quiet
Writing Stories
Story Structure
// Component.stories.js
import { Component } from './Component';
// Default export - story metadata
export default {
title: 'Path/To/Component',
component: Component,
parameters: {
// Story-level parameters
},
argTypes: {
// Control definitions
},
args: {
// Default args for all stories
},
};
// Named exports - individual stories
export const Default = {};
export const WithProps = {
args: {
prop1: 'value1',
prop2: 'value2',
},
};
export const CustomStory = {
render: (args) => <Component {...args} />,
args: {
// Story-specific args
},
};
Story Naming
// Organize stories with hierarchy
export default {
title: 'Design System/Components/Button',
// Creates: Design System > Components > Button
};
export default {
title: 'Pages/Dashboard/UserProfile',
// Creates: Pages > Dashboard > UserProfile
};
export default {
title: 'Example/Button',
// Creates: Example > Button
};
Multiple Components
// stories/Forms.stories.js
import { Input } from '../components/Input';
import { Button } from '../components/Button';
import { Form } from '../components/Form';
export default {
title: 'Forms/LoginForm',
};
export const LoginForm = () => (
<Form>
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
<Button primary>Login</Button>
</Form>
);
export const SignupForm = () => (
<Form>
<Input placeholder="Full Name" />
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
<Input type="password" placeholder="Confirm Password" />
<Button primary>Sign Up</Button>
</Form>
);
Story Parameters
export default {
title: 'Example/Button',
component: Button,
parameters: {
// Layout
layout: 'centered', // 'centered' | 'fullscreen' | 'padded'
// Backgrounds
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
],
},
// Viewport
viewport: {
defaultViewport: 'mobile1',
},
// Documentation
docs: {
description: {
component: 'Button component for user interactions',
},
},
},
};
Story Formats
CSF 3.0 (Current)
// Modern format with object syntax
export default {
title: 'Example/Button',
component: Button,
};
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
primary: false,
label: 'Button',
},
};
CSF 2.0 (Legacy)
// Function-based format
export default {
title: 'Example/Button',
component: Button,
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
primary: false,
label: 'Button',
};
Custom Render Functions
export const CustomRender = {
render: (args) => {
const [count, setCount] = React.useState(0);
return (
<div>
<p>Count: {count}</p>
<Button
{...args}
onClick={() => setCount(count + 1)}
/>
</div>
);
},
args: {
label: 'Increment',
},
};
Play Functions
import { userEvent, within } from '@storybook/testing-library';
export const InteractiveExample = {
args: {
label: 'Click me',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
// Simulate user interaction
await userEvent.click(button);
// Add assertions if needed
// expect(button).toHaveClass('clicked');
},
};
Controls & Actions
ArgTypes Configuration
export default {
title: 'Example/Button',
component: Button,
argTypes: {
// Text input
label: {
control: 'text',
description: 'Button label text',
},
// Boolean
primary: {
control: 'boolean',
description: 'Primary button style',
},
// Select dropdown
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Button size',
},
// Radio buttons
variant: {
control: 'radio',
options: ['primary', 'secondary', 'danger'],
},
// Color picker
backgroundColor: {
control: 'color',
},
// Number input
borderRadius: {
control: { type: 'number', min: 0, max: 50, step: 1 },
},
// Range slider
opacity: {
control: { type: 'range', min: 0, max: 1, step: 0.1 },
},
// Object editor
style: {
control: 'object',
},
// Array editor
items: {
control: 'object',
},
// Date picker
createdAt: {
control: 'date',
},
// Disable control
onClick: {
action: 'clicked',
control: false,
},
},
};
Actions
import { action } from '@storybook/addon-actions';
export default {
title: 'Example/Button',
component: Button,
argTypes: {
onClick: { action: 'clicked' },
onMouseEnter: { action: 'mouse-enter' },
onMouseLeave: { action: 'mouse-leave' },
},
};
// Or in individual stories
export const WithActions = {
args: {
label: 'Button',
onClick: action('button-click'),
onMouseEnter: action('mouse-enter'),
},
};
// Multiple actions
export const MultipleActions = {
args: {
label: 'Multi Action Button',
onClick: (...args) => {
action('primary-click')(...args);
action('analytics-track')('button-clicked');
},
},
};
Advanced Controls
export default {
title: 'Example/Form',
component: Form,
argTypes: {
// Conditional controls
showAdvanced: {
control: 'boolean',
},
advancedOptions: {
control: 'object',
if: { arg: 'showAdvanced', truthy: true },
},
// Custom control
theme: {
control: {
type: 'select',
labels: {
light: 'Light Theme',
dark: 'Dark Theme',
auto: 'Auto (System)',
},
},
options: ['light', 'dark', 'auto'],
},
// Multi-select
features: {
control: 'multi-select',
options: ['feature1', 'feature2', 'feature3'],
},
},
};
애드온
필수 애드온
// .storybook/main.js
module.exports = {
addons: [
'@storybook/addon-essentials', // Includes most common addons
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-design-tokens',
],
};
// Individual addons (if not using essentials)
module.exports = {
addons: [
'@storybook/addon-controls',
'@storybook/addon-actions',
'@storybook/addon-viewport',
'@storybook/addon-backgrounds',
'@storybook/addon-docs',
'@storybook/addon-toolbars',
],
};
접근성 애드온
// .storybook/preview.js
export const parameters = {
a11y: {
// Optional selector to inspect
element: '#storybook-root',
config: {
rules: [
{
// Disable specific rules
id: 'color-contrast',
enabled: false,
},
],
},
// Show violations in action panel
manual: true,
},
};
// In stories
export const AccessibleButton = {
args: {
label: 'Accessible Button',
},
parameters: {
a11y: {
// Story-specific a11y config
config: {
rules: [
{ id: 'button-name', enabled: true },
],
},
},
},
};
뷰포트 애드온
// .storybook/preview.js
export const parameters = {
viewport: {
viewports: {
mobile: {
name: 'Mobile',
styles: {
width: '375px',
height: '667px',
},
},
tablet: {
name: 'Tablet',
styles: {
width: '768px',
height: '1024px',
},
},
desktop: {
name: 'Desktop',
styles: {
width: '1024px',
height: '768px',
},
},
},
defaultViewport: 'mobile',
},
};
디자인 토큰 애드온
// design-tokens.json
{
"colors": {
"primary": "#007bff",
"secondary": "#6c757d",
"success": "#28a745",
"danger": "#dc3545"
},
"spacing": {
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "32px"
},
"typography": {
"fontFamily": {
"primary": "Inter, sans-serif",
"mono": "Monaco, monospace"
},
"fontSize": {
"sm": "14px",
"md": "16px",
"lg": "18px",
"xl": "24px"
}
}
}
// .storybook/main.js
module.exports = {
addons: [
{
name: '@storybook/addon-design-tokens',
options: {
designTokenGlob: '**/design-tokens.json',
},
},
],
};
커스텀 애드온
// .storybook/addons/theme-switcher/register.js
import { addons, types } from '@storybook/addons';
import { ADDON_ID, TOOL_ID } from './constants';
import { Tool } from './Tool';
addons.register(ADDON_ID, () => {
addons.add(TOOL_ID, {
type: types.TOOL,
title: 'Theme Switcher',
render: Tool,
});
});
// .storybook/addons/theme-switcher/Tool.js
import React from 'react';
import { useGlobals } from '@storybook/api';
import { IconButton } from '@storybook/components';
export const Tool = () => {
const [globals, updateGlobals] = useGlobals();
const theme = globals.theme || 'light';
const toggleTheme = () => {
updateGlobals({
theme: theme === 'light' ? 'dark' : 'light',
});
};
return (
<IconButton
key="theme-switcher"
title="Toggle theme"
onClick={toggleTheme}
>
{theme === 'light' ? '🌙' : '☀️'}
</IconButton>
);
};
구성
기본 구성
// .storybook/main.js
module.exports = {
// Stories location
stories: [
'../src/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)',
],
// Addons
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
// Framework
framework: {
name: '@storybook/react-vite',
options: {},
},
// Features
features: {
buildStoriesJson: true,
storyStoreV7: true,
},
// TypeScript
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
// Webpack customization
webpackFinal: async (config) => {
// Add custom webpack config
config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
});
return config;
},
// Vite customization
viteFinal: async (config) => {
// Customize Vite config
config.define = {
...config.define,
global: 'globalThis',
};
return config;
},
// Documentation
docs: {
autodocs: 'tag',
},
// Static directories
staticDirs: ['../public'],
};
미리보기 구성
// .storybook/preview.js
import '../src/index.css'; // Global styles
export const parameters = {
// Actions
actions: { argTypesRegex: '^on[A-Z].*' },
// Controls
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
expanded: true,
sort: 'requiredFirst',
},
// Layout
layout: 'centered',
// Backgrounds
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
{ name: 'gray', value: '#f8f9fa' },
],
},
// Viewport
viewport: {
viewports: {
mobile1: {
name: 'Small mobile',
styles: { width: '320px', height: '568px' },
},
mobile2: {
name: 'Large mobile',
styles: { width: '414px', height: '896px' },
},
tablet: {
name: 'Tablet',
styles: { width: '768px', height: '1024px' },
},
},
},
// Documentation
docs: {
theme: themes.light,
source: {
state: 'open',
},
},
};
// Global decorators
export const decorators = [
(Story) => (
<div style={{ margin: '3em', fontFamily: 'Inter, sans-serif' }}>
<Story />
</div>
),
];
// Global args
export const args = {
// Default args for all stories
};
// Global arg types
export const argTypes = {
// Global arg types
};
환경 변수
// .storybook/main.js
module.exports = {
env: (config) => ({
...config,
API_URL: 'https://api.example.com',
FEATURE_FLAG: true,
}),
webpackFinal: async (config) => {
config.plugins.push(
new webpack.DefinePlugin({
'process.env.STORYBOOK': JSON.stringify(true),
})
);
return config;
},
};
// In components
const apiUrl = process.env.STORYBOOK
? 'https://api-mock.example.com'
: process.env.API_URL;
테마
커스텀 테마
// .storybook/theme.js
import { create } from '@storybook/theming';
export default create({
base: 'light', // 'light' | 'dark'
// Brand
brandTitle: 'My Company Storybook',
brandUrl: 'https://example.com',
brandImage: 'https://example.com/logo.svg',
brandTarget: '_self',
// Colors
colorPrimary: '#FF4785',
colorSecondary: '#029CFD',
// UI
appBg: '#F6F9FC',
appContentBg: '#FFFFFF',
appBorderColor: '#E6ECF0',
appBorderRadius: 4,
// Typography
fontBase: '"Inter", sans-serif',
fontCode: 'Monaco, monospace',
// Text colors
textColor: '#2E3438',
textInverseColor: '#FFFFFF',
textMutedColor: '#798186',
// Toolbar
barTextColor: '#798186',
barSelectedColor: '#029CFD',
barBg: '#FFFFFF',
// Form
inputBg: '#FFFFFF',
inputBorder: '#E6ECF0',
inputTextColor: '#2E3438',
inputBorderRadius: 4,
});
// .storybook/manager.js
import { addons } from '@storybook/addons';
import theme from './theme';
addons.setConfig({
theme,
});
다크 테마
// .storybook/dark-theme.js
import { create } from '@storybook/theming';
export default create({
base: 'dark',
brandTitle: 'My Company Storybook',
brandUrl: 'https://example.com',
brandImage: 'https://example.com/logo-white.svg',
colorPrimary: '#FF4785',
colorSecondary: '#029CFD',
appBg: '#1A1A1A',
appContentBg: '#2D2D2D',
appBorderColor: '#404040',
textColor: '#FFFFFF',
textInverseColor: '#1A1A1A',
textMutedColor: '#CCCCCC',
barTextColor: '#CCCCCC',
barSelectedColor: '#029CFD',
barBg: '#2D2D2D',
inputBg: '#404040',
inputBorder: '#666666',
inputTextColor: '#FFFFFF',
});
테마 전환
// .storybook/preview.js
import { themes } from '@storybook/theming';
export const parameters = {
docs: {
theme: themes.light,
},
darkMode: {
// Override the default dark theme
dark: { ...themes.dark, appBg: 'black' },
// Override the default light theme
light: { ...themes.normal, appBg: 'white' },
// Set the initial theme
current: 'light',
// Disable the addon for specific stories
stylePreview: true,
},
};
// Global decorator for theme switching
export const decorators = [
(Story, context) => {
const theme = context.globals.theme || 'light';
return (
<div className={`theme-${theme}`}>
<Story />
</div>
);
},
];
// Global types for theme switcher
export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'circlehollow',
items: [
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' },
],
showName: true,
},
},
};
테스팅
시각적 테스팅
// .storybook/test-runner.js
const { getStoryContext } = require('@storybook/test-runner');
module.exports = {
async postRender(page, context) {
const storyContext = await getStoryContext(page, context);
// Skip visual tests for specific stories
if (storyContext.parameters?.skipVisualTest) {
return;
}
// Take screenshot
const image = await page.screenshot();
expect(image).toMatchImageSnapshot({
customSnapshotIdentifier: context.id,
});
},
};
// In stories
export const VisualTest = {
args: {
label: 'Visual Test Button',
},
parameters: {
// Skip this story in visual tests
skipVisualTest: true,
},
};
상호작용 테스팅
// Button.stories.js
import { userEvent, within, expect } from '@storybook/test';
export const InteractionTest = {
args: {
label: 'Click me',
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// Find elements
const button = canvas.getByRole('button', { name: /click me/i });
// Simulate interactions
await userEvent.click(button);
// Assertions
await expect(args.onClick).toHaveBeenCalled();
// Check DOM changes
await expect(button).toHaveClass('clicked');
// Wait for async operations
await canvas.findByText('Success!');
},
};
export const FormInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Fill form
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/password/i);
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.type(emailInput, 'user@example.com');
await userEvent.type(passwordInput, 'password123');
await userEvent.click(submitButton);
// Check results
await expect(canvas.getByText(/success/i)).toBeInTheDocument();
},
};
스토리를 사용한 단위 테스팅
// Button.test.js
import { render, screen } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories';
const { Primary, Secondary, Large } = composeStories(stories);
describe('Button', () => {
test('renders primary button', () => {
render(<Primary />);
expect(screen.getByRole('button')).toHaveClass('storybook-button--primary');
});
test('renders secondary button', () => {
render(<Secondary />);
expect(screen.getByRole('button')).not.toHaveClass('storybook-button--primary');
});
test('renders large button', () => {
render(<Large />);
expect(screen.getByRole('button')).toHaveClass('storybook-button--large');
});
});
접근성 테스팅
// a11y.test.js
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories';
expect.extend(toHaveNoViolations);
const { Primary, Secondary } = composeStories(stories);
describe('Button Accessibility', () => {
test('Primary button should not have accessibility violations', async () => {
const { container } = render(<Primary />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('Secondary button should not have accessibility violations', async () => {
const { container } = render(<Secondary />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
문서화
자동 문서화
// Component with JSDoc
/**
* Primary UI component for user interaction
*/
export const Button = ({
/**
* Is this the principal call to action on the page?
*/
primary = false,
/**
* What background color to use
*/
backgroundColor,
/**
* How large should the button be?
*/
size = 'medium',
/**
* Button contents
*/
label,
/**
* Optional click handler
*/
onClick,
...props
}) => {
// Component implementation
};
// Stories with auto-generated docs
export default {
title: 'Example/Button',
component: Button,
tags: ['autodocs'], // Enable auto-documentation
parameters: {
docs: {
description: {
component: 'Button component for user interactions. Supports multiple sizes and variants.',
},
},
},
};
커스텀 문서화
// Button.stories.js
export default {
title: 'Example/Button',
component: Button,
parameters: {
docs: {
description: {
component: `
# Button Component
The Button component is a fundamental UI element used throughout the application.
## Usage
\`\`\`jsx
import { Button } from './Button';
<Button primary onClick={handleClick}>
Click me
</Button>
\`\`\`
## Design Guidelines
- Use primary buttons for main actions
- Use secondary buttons for supporting actions
- Ensure sufficient color contrast for accessibility
`,
},
},
},
};
export const Primary = {
args: {
primary: true,
label: 'Button',
},
parameters: {
docs: {
description: {
story: 'Primary buttons are used for the main call-to-action on a page.',
},
},
},
};
MDX 문서화
<!-- Button.stories.mdx -->
import { Meta, Story, Canvas, ArgsTable, Description } from '@storybook/addon-docs';
import { Button } from './Button';
<Meta title="Example/Button" component={Button} />
# Button
<Description of={Button} />
## Examples
### Primary Button
<Canvas>
<Story name="Primary" args={{ primary: true, label: 'Button' }}>
{(args) => <Button {...args} />}
</Story>
</Canvas>
### Secondary Button
<Canvas>
<Story name="Secondary" args={{ label: 'Button' }}>
{(args) => <Button {...args} />}
</Story>
</Canvas>
## Props
<ArgsTable of={Button} />
## Usage Guidelines
- Use primary buttons sparingly, typically one per page
- Secondary buttons can be used multiple times
- Always provide meaningful labels
- Consider accessibility when choosing colors
## Code Example
```jsx
import { Button } from './Button';
function MyComponent() {
return (
<div>
<Button primary onClick={() => alert('기본 버튼 클릭!')}>
기본 작업
</Button>
<Button onClick={() => alert('보조 버튼 클릭!')}>
보조 작업
</Button>
</div>
);
}
### 문서화 페이지
Would you like me to fill in the empty sections or provide more details about the translation?```javascript
// .storybook/main.js
module.exports = {
stories: [
'../src/**/*.stories.@(js|jsx|ts|tsx)',
'../docs/**/*.stories.mdx', // Documentation stories
],
};
// docs/Introduction.stories.mdx
import { Meta } from '@storybook/addon-docs';
<Meta title="Introduction" />
# Design System
Welcome to our design system documentation.
## Getting Started
This Storybook contains all the components, patterns, and guidelines for our design system.
### Installation
```bash
npm install @company/design-system
Usage
import { Button, Input, Card } from '@company/design-system';
Principles
- Consistency - Maintain visual and functional consistency
- Accessibility - Ensure all components are accessible
- Performance - Optimize for speed and efficiency
- Flexibility - Support customization and theming
```bash
# Build Storybook for deployment
npm run build-storybook
# Output directory
ls storybook-static/
# Serve locally
npx http-server storybook-static
```### 정적 빌드
```bash
# netlify.toml
[build]
command = "npm run build-storybook"
publish = "storybook-static"
[build.environment]
NODE_VERSION = "16"
# Deploy
npm install -g netlify-cli
netlify deploy --prod --dir=storybook-static
```### Netlify 배포
```json
// vercel.json
{
"buildCommand": "npm run build-storybook",
"outputDirectory": "storybook-static",
"framework": null
}
```### Vercel 배포
```yaml
# .github/workflows/storybook.yml
name: Build and Deploy Storybook
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Storybook
run: npm run build-storybook
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./storybook-static
```### GitHub Pages
```dockerfile
# Dockerfile
FROM node:16-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build-storybook
FROM nginx:alpine
COPY --from=builder /app/storybook-static /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```### Docker 배포
```bash
# Build and deploy to S3
npm run build-storybook
aws s3 sync storybook-static/ s3://my-storybook-bucket --delete
aws cloudfront create-invalidation --distribution-id ABCD1234 --paths "/*"
```### AWS S3 + CloudFront
```javascript
// .storybook/main.js
const path = require('path');
module.exports = {
webpackFinal: async (config) => {
// Add alias
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, '../src'),
'@components': path.resolve(__dirname, '../src/components'),
};
// Add custom loader
config.module.rules.push({
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
],
});
// Add plugin
config.plugins.push(
new webpack.DefinePlugin({
__VERSION__: JSON.stringify(process.env.npm_package_version),
})
);
return config;
},
};
```## 고급 기능
```javascript
// .storybook/main.js
module.exports = {
babel: async (options) => ({
...options,
plugins: [
...options.plugins,
['babel-plugin-styled-components', { displayName: true }],
],
}),
};
```### 사용자 정의 Webpack 구성
```javascript
// .storybook/main.js
const isDevelopment = process.env.NODE_ENV === 'development';
module.exports = {
addons: [
'@storybook/addon-essentials',
...(isDevelopment ? ['@storybook/addon-a11y'] : []),
],
features: {
storyStoreV7: !isDevelopment, // Disable in development for faster builds
},
webpackFinal: async (config) => {
if (isDevelopment) {
// Development-specific webpack config
config.optimization.minimize = false;
}
return config;
},
};
```### 사용자 정의 Babel 구성
```javascript
// .storybook/manager.js
import { addons } from '@storybook/addons';
addons.setConfig({
// Show/hide panels
showPanel: true,
panelPosition: 'bottom', // 'bottom' | 'right'
// Sidebar
showNav: true,
showToolbar: true,
// Initial active panel
selectedPanel: 'storybook/controls/panel',
// Sidebar tree expansion
initialActive: 'sidebar',
sidebar: {
showRoots: false,
collapsedRoots: ['other'],
},
// Toolbar
toolbar: {
title: { hidden: false },
zoom: { hidden: false },
eject: { hidden: false },
copy: { hidden: false },
fullscreen: { hidden: false },
},
});
```### 환경별 구성
```javascript
// .storybook/main.js
module.exports = {
features: {
storyStoreV7: true, // Enable lazy loading
},
stories: [
{
directory: '../src/components',
files: '**/*.stories.*',
titlePrefix: 'Components',
},
{
directory: '../src/pages',
files: '**/*.stories.*',
titlePrefix: 'Pages',
},
],
};
```### 사용자 정의 관리자 UI
```javascript
// .storybook/main.js
module.exports = {
webpackFinal: async (config) => {
// Code splitting
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
};
// Tree shaking
config.optimization.usedExports = true;
config.optimization.sideEffects = false;
return config;
},
};
```## 성능
```javascript
// .storybook/preview.js
export const parameters = {
// Reduce memory usage
options: {
storySort: {
method: 'alphabetical',
order: ['Introduction', 'Components', 'Pages'],
locales: 'en-US',
},
},
// Disable source code addon for better performance
docs: {
source: {
state: 'closed',
},
},
};
// Cleanup decorators
export const decorators = [
(Story, context) => {
// Cleanup on story change
React.useEffect(() => {
return () => {
// Cleanup logic
};
}, [context.id]);
return <Story />;
},
];
```### 지연 로딩
```javascript
// Figma integration
export default {
title: 'Example/Button',
component: Button,
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/ABC123/Design-System?node-id=123%3A456',
},
},
};
// Sketch integration
export const SketchExample = {
parameters: {
design: {
type: 'sketch',
url: 'https://sketch.cloud/s/abc123',
},
},
};
// Adobe XD integration
export const XDExample = {
parameters: {
design: {
type: 'adobe-xd',
url: 'https://xd.adobe.com/view/abc123',
},
},
};
```### 번들 최적화
```javascript
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
testPathIgnorePatterns: ['/node_modules/', '/storybook-static/'],
// Test stories
testMatch: [
'<rootDir>/src/**/__tests__/**/*.(js|jsx|ts|tsx)',
'<rootDir>/src/**/?(*.)(spec|test).(js|jsx|ts|tsx)',
'<rootDir>/src/**/*.stories.test.(js|jsx|ts|tsx)',
],
};
// Chromatic integration
// .github/workflows/chromatic.yml
name: 'Chromatic'
on: push
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: yarn
- uses: chromaui/action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
```### 메모리 최적화
```yaml
# .github/workflows/storybook-tests.yml
name: Storybook Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- run: npm ci
- run: npm run build-storybook --quiet
- run: npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on http://127.0.0.1:6006 && npm run test-storybook"
```## 통합
```bash
# Port already in use
npm run storybook -- --port 9001
# Clear cache
rm -rf node_modules/.cache/storybook
# Webpack issues
npm run storybook -- --no-manager-cache
# TypeScript issues
npm run storybook -- --no-dll
# Memory issues
NODE_OPTIONS="--max-old-space-size=4096" npm run storybook
```### 디자인 도구 통합
```bash
# Enable debug logging
DEBUG=storybook:* npm run storybook
# Webpack debug
npm run storybook -- --debug-webpack
# Verbose output
npm run storybook -- --verbose
성능 문제
// .storybook/main.js
module.exports = {
// Disable source maps in development
webpackFinal: async (config) => {
if (process.env.NODE_ENV === 'development') {
config.devtool = false;
}
return config;
},
// Reduce bundle size
features: {
storyStoreV7: true,
buildStoriesJson: true,
},
};
빌드 문제
# Clear all caches
rm -rf node_modules/.cache
rm -rf storybook-static
npm run build-storybook
# Check for conflicting dependencies
npm ls
# Update Storybook
npx storybook@latest upgrade
모범 사례
스토리 구성
- 계층적 구조: 일관된 이름 지정 패턴 사용
- 논리적 그룹화: 관련 컴포넌트를 함께 그룹화
- 명확한 이름 지정: 설명적인 스토리 이름 사용
- 문서화: 포괄적인 문서 포함
컴포넌트 개발
- 격리: 컴포넌트를 격리하여 개발
- Props 인터페이스: 명확하고 일관된 prop 인터페이스 설계
- 기본값: 합리적인 기본값 제공
- 오류 처리: 엣지 케이스를 우아하게 처리
테스트 전략
- 시각적 테스트: 시각적 회귀 테스트 사용
- 상호작용 테스트: 사용자 상호작용 테스트
- 접근성 테스트: 컴포넌트의 접근성 보장
- 단위 테스트: 컴포넌트 로직 테스트
성능 최적화
- 지연 로딩: 스토리 스토어 v7을 사용한 지연 로딩
- 번들 분할: webpack 구성 최적화
- 메모리 관리: 리소스 적절히 정리
- 빌드 최적화: 빌드 시간 최소화
팀 협업
- 디자인 시스템: 일관된 디자인 시스템 유지
- 문서화: 문서를 최신 상태로 유지
- 코드 리뷰: 스토리와 컴포넌트 검토
- 자동화: 테스트 및 배포 자동화
요약
Storybook은 다음을 가능하게 하는 현대 프론트엔드 개발의 필수 도구입니다:
- 컴포넌트 격리: UI 컴포넌트를 독립적으로 개발 및 테스트
- 시각적 문서화: 디자인 시스템을 위한 라이브 문서 생성
- 대화형 개발: 실시간 피드백으로 컴포넌트 빌드
- 테스트 통합: 시각적, 상호작용, 접근성 테스트
- 팀 협업: 팀 간 컴포넌트 공유
- 디자인 시스템 관리: 일관된 UI 패턴 유지
주요 이점으로는 더 빠른 개발 주기, 더 나은 컴포넌트 품질, 향상된 팀 협업, 포괄적인 문서화가 있습니다. Storybook은 모든 주요 프론트엔드 프레임워크를 지원하고 애드온 및 구성을 통해 광범위한 맞춤 설정 옵션을 제공합니다.