Loading...
Loading...
Storybook 스토리 작성 및 CSF 3.0 베스트 프랙티스 스킬. 다음 상황에서 사용: (1) 새 스토리 파일(.stories.tsx, .stories.ts) 작성 시, (2) 기존 스토리 수정 시, (3) Args, Decorators, Parameters 설정 시, (4) Storybook 설정 파일(.storybook/) 작업 시, (5) 'story', 'stories', 'storybook', 'CSF' 키워드가 포함된 작업 시
npx skill4agent add pedronauck/skills storybook// ❌ CSF 2.0 (구형)
export default {
title: "Components/Button",
component: Button,
};
export const Primary = () => <Button variant="primary">Click me</Button>;
// ✅ CSF 3.0 (권장)
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
const meta = {
component: Button,
tags: ["autodocs"], // 자동 문서 생성
args: {
variant: "primary",
children: "Click me",
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {};
export const Secondary: Story = {
args: {
variant: "secondary",
},
};argsargTypes.defaultValueargs// ❌ 하드코딩된 Props
export const Disabled: Story = {
render: () => <Button disabled>Disabled</Button>,
};
// ❌ 여러 스토리에서 같은 args 중복
export const Primary: Story = {
args: { children: "Click me", variant: "primary" },
};
export const Secondary: Story = {
args: { children: "Click me", variant: "secondary" },
};
// ✅ Meta에서 공통 args 선언, 스토리에서 차이점만 오버라이드
const meta = {
component: Button,
args: {
children: "Click me",
variant: "primary",
},
} satisfies Meta<typeof Button>;
export const Primary: Story = {};
export const Secondary: Story = {
args: { variant: "secondary" },
};
export const Disabled: Story = {
args: { disabled: true },
};titletitle// ❌ title 직접 명시 — 타입 안전하지 않고 싱크 깨짐 위험
const meta = {
title: "Components/Button",
component: Button,
} satisfies Meta<typeof Button>;
// ✅ title 생략 — 파일 경로에서 자동 추론
const meta = {
component: Button,
} satisfies Meta<typeof Button>;satisfies// ❌ 타입 추론 불가
const meta: Meta<typeof Button> = {
component: Button,
};
// ✅ 타입 체크와 추론 모두 가능
const meta = {
component: Button,
args: {
size: "md",
variant: "primary",
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;// 개별 스토리에 Decorator 적용
export const WithTheme: Story = {
decorators: [
Story => (
<ThemeProvider theme="dark">
<Story />
</ThemeProvider>
),
],
};
// 모든 스토리에 Decorator 적용
const meta = {
component: Button,
decorators: [
Story => (
<div style={{ padding: "3rem" }}>
<Story />
</div>
),
],
} satisfies Meta<typeof Button>;const meta = {
component: Button,
parameters: {
layout: "centered", // 스토리를 중앙 정렬
backgrounds: {
default: "light",
values: [
{ name: "light", value: "#ffffff" },
{ name: "dark", value: "#000000" },
],
},
},
} satisfies Meta<typeof Button>;
// 개별 스토리에서 오버라이드
export const OnDark: Story = {
parameters: {
backgrounds: { default: "dark" },
},
};ReactNodecontrol: 'text'control: false// ❌ 불필요한 argType 수동 지정 — 타입 변경 시 싱크 깨짐
const meta = {
component: Button,
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "tertiary"],
},
size: {
control: "radio",
options: ["sm", "md", "lg"],
},
disabled: {
control: "boolean",
},
},
} satisfies Meta<typeof Button>;
// ✅ 자동 추론에 맡기고, 필요한 경우만 수동 지정
const meta = {
component: Button,
argTypes: {
// ReactNode 타입이지만 텍스트 입력이 필요한 경우
children: { control: "text" },
},
} satisfies Meta<typeof Button>;
// ✅ 특정 스토리에서 prop을 고정할 때 — control: false
export const Horizontal: Story = {
args: { orientation: "horizontal" },
argTypes: {
orientation: { control: false }, // 이 스토리에서는 항상 horizontal
},
};import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
// 1. Meta 정의 — title 생략, 공통 args 선언, argTypes는 자동 추론에 위임
const meta = {
component: Button,
tags: ["autodocs"],
parameters: {
layout: "centered",
},
args: {
children: "Button",
size: "md",
variant: "primary",
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// 2. 기본 스토리 — Meta args를 그대로 사용
export const Primary: Story = {};
// 3. 변형 스토리들 — 차이점만 오버라이드
export const Secondary: Story = {
args: {
variant: "secondary",
},
};
export const Disabled: Story = {
args: {
disabled: true,
},
};
// 4. prop 고정이 필요한 스토리 — control: false 사용
export const Horizontal: Story = {
args: { orientation: "horizontal" },
argTypes: {
orientation: { control: false },
},
};
// 5. 복잡한 상태나 컨텍스트가 필요한 경우
export const WithCustomTheme: Story = {
decorators: [
Story => (
<ThemeProvider theme="custom">
<Story />
</ThemeProvider>
),
],
};원칙: 대부분의 argType은 Storybook이 컴포넌트 타입에서 자동 추론. 아래는 자동 추론이 부적절할 때만 사용.기본값은가 아닌argTypes.defaultValue에서 선언.args
argTypes: {
// ReactNode 타입이지만 텍스트 입력이 필요할 때
children: { control: 'text' },
// Range slider (자동 추론이 적절하지 않을 때)
opacity: {
control: { type: 'range', min: 0, max: 1, step: 0.1 },
},
// Action logger (이벤트 핸들러)
onClick: { action: 'clicked' },
// Control 비활성화 (특정 스토리에서 prop 고정)
orientation: { control: false },
}parameters: {
// 레이아웃 설정
layout: 'centered' | 'fullscreen' | 'padded',
// 배경 설정
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
],
},
// Actions 패널 설정
actions: {
argTypesRegex: '^on[A-Z].*', // on으로 시작하는 Props 자동 감지
},
// Docs 설정
docs: {
description: {
component: '버튼 컴포넌트 상세 설명',
},
},
}// 1. 스타일 래퍼
(Story) => (
<div style={{ padding: '3rem' }}>
<Story />
</div>
)
// 2. Theme Provider
(Story) => (
<ThemeProvider theme="dark">
<Story />
</ThemeProvider>
)
// 3. Router Provider (React Router 사용 시)
(Story) => (
<MemoryRouter initialEntries={['/']}>
<Story />
</MemoryRouter>
)
// 4. 다국어 Provider
(Story) => (
<I18nProvider locale="ko">
<Story />
</I18nProvider>
)
// 5. 전역 상태 Provider
(Story) => (
<Provider store={mockStore}>
<Story />
</Provider>
)Component.tsx # 컴포넌트 구현
Component.stories.tsx # 스토리 파일 (같은 디렉토리)
Component.test.tsx # 테스트 파일// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(ts|tsx)"],
addons: [
"@storybook/addon-essentials", // Controls, Actions, Docs 등
"@storybook/addon-interactions", // Play functions
],
framework: {
name: "@storybook/react-vite",
options: {},
},
};
export default config;// .storybook/preview.ts
import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
// 모든 스토리에 적용될 전역 Decorators
decorators: [
(Story) => (
<div style={{ fontFamily: 'Arial, sans-serif' }}>
<Story />
</div>
),
],
};
export default preview;