Storybook์ ๋ํด์
Storybook์ด๋?
Storybook์ UI ์ปดํฌ๋ํธ๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ๋ฐํ๊ณ , ํ ์คํธํ๊ณ , ๋ฌธ์ํํ ์ ์๊ฒ ํด์ฃผ๋ ์คํ์์ค ๋๊ตฌ์ด๋ค. React, Vue, Angular ๋ฑ ๋ค์ํ ํ๋ ์์ํฌ๋ฅผ ์ง์ํ๋ฉฐ, ์ ํ๋ฆฌ์ผ์ด์ ์ ๋๋จธ์ง ๋ถ๋ถ๊ณผ ๋ถ๋ฆฌํ์ฌ ์ปดํฌ๋ํธ๋ฅผ ์์ ํ ์ ์๊ฒ ํด์ค๋ค.
Storybook์ ์ฃผ์ ํน์ง
์ปดํฌ๋ํธ ๊ฒฉ๋ฆฌ ๊ฐ๋ฐ
- ์ฑ์ ๋๋จธ์ง ๋ถ๋ถ๊ณผ ๋ถ๋ฆฌํ์ฌ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ๋ฐ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ API ์์ด๋ UI ๊ฐ๋ฐ ๊ฐ๋ฅ
์ธํฐ๋ํฐ๋ธ UI
- ๋ค์ํ props์ ์ํ๋ฅผ ์ฆ์ ํ์ธ
- Controls๋ฅผ ํตํด ์ค์๊ฐ์ผ๋ก props ๋ณ๊ฒฝ ๊ฐ๋ฅ
๋ฌธ์ํ
- ์ปดํฌ๋ํธ ์ฌ์ฉ๋ฒ๊ณผ ์์ ๋ฅผ ์๋ ์์ฑ
- Markdown ์ง์์ผ๋ก ์์ธํ ๋ฌธ์ ์์ฑ ๊ฐ๋ฅ
ํ ์คํธ ์ง์
- ๋จ์ ํ ์คํธ์ ์๊ฐ์ ํ๊ท ํ ์คํธ ์ง์
- Accessibility ํ ์คํธ๋ ๊ฐ๋ฅ
๋ค์ํ ๋ทฐํฌํธ
- ๋ค์ํ ํ๋ฉด ํฌ๊ธฐ์์ ์ปดํฌ๋ํธ ํ ์คํธ
- ๋ชจ๋ฐ์ผ, ํ๋ธ๋ฆฟ, ๋ฐ์คํฌํฑ ๋ฑ ๋ค์ํ ํ๊ฒฝ ์๋ฎฌ๋ ์ด์
Storybook ์ค์น ๋ฐ ์ค์
Next.js ํ๋ก์ ํธ์ Storybook ์ค์น
npx storybook@latest init
์ด ๋ช ๋ น์ด๋ ์๋์ผ๋ก Storybook์ ์ค์ ํ๊ณ ํ์ํ ํจํค์ง๋ฅผ ์ค์นํ๋ค.
์๋ ์ค์น ๋ฐฉ๋ฒ
npm install --save-dev @storybook/react @storybook/react-webpack5
npx sb init
Story ์์ฑ ๋ฐฉ๋ฒ
๊ธฐ๋ณธ Story ๊ตฌ์กฐ
// Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
const meta = {
title: "Components/Button",
component: Button,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
backgroundColor: { control: "color" },
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
primary: true,
label: "Button",
},
};
export const Secondary: Story = {
args: {
label: "Button",
},
};
export const Large: Story = {
args: {
size: "large",
label: "Button",
},
};
export const Small: Story = {
args: {
size: "small",
label: "Button",
},
};
๋ค์ํ ์ํ ํํ
// ์ปดํฌ๋ํธ์ ๋ค์ํ ์ํ๋ฅผ Story๋ก ํํ
export const Loading: Story = {
args: {
loading: true,
label: "Loading...",
},
};
export const Disabled: Story = {
args: {
disabled: true,
label: "Disabled",
},
};
export const WithIcon: Story = {
args: {
icon: "plus",
label: "Add Item",
},
};
Args์ Controls ํ์ฉ
Storybook์ Controls๋ฅผ ํตํด ์ปดํฌ๋ํธ props๋ฅผ ์ค์๊ฐ์ผ๋ก ์กฐ์ํ ์ ์๋ค.
const meta = {
title: "Components/Card",
component: Card,
argTypes: {
title: {
control: "text",
description: "์นด๋์ ์ ๋ชฉ",
},
description: {
control: "text",
description: "์นด๋์ ์ค๋ช
",
},
size: {
control: "select",
options: ["small", "medium", "large"],
description: "์นด๋์ ํฌ๊ธฐ",
},
backgroundColor: {
control: "color",
description: "์นด๋์ ๋ฐฐ๊ฒฝ์",
},
},
} satisfies Meta<typeof Card>;
Parameters์ Decorators
Parameters
Storybook์ ์ค์ ๊ณผ ํ๊ฒฝ์ ์ ์ดํ ์ ์๋ค.
export default {
parameters: {
backgrounds: {
values: [
{ name: "light", value: "#ffffff" },
{ name: "dark", value: "#333333" },
],
},
viewport: {
viewports: {
mobile: {
name: "Mobile",
styles: {
width: "375px",
height: "667px",
},
},
},
},
},
};
Decorators
Story๋ฅผ ๊ฐ์ธ์ ์ถ๊ฐ ๊ธฐ๋ฅ์ ์ ๊ณตํ ์ ์๋ค.
// ํ
๋ง Provider๋ก ๊ฐ์ธ๊ธฐ
export default {
decorators: [
(Story) => (
<ThemeProvider theme={theme}>
<Story />
</ThemeProvider>
),
],
};
// ์คํ์ผ ์ ์ฉ
export default {
decorators: [
(Story) => (
<div style={{ padding: "20px" }}>
<Story />
</div>
),
],
};
Addons ํ์ฉ
Storybook์ ๋ค์ํ ์ ๋์จ์ ์ ๊ณตํ์ฌ ๊ธฐ๋ฅ์ ํ์ฅํ ์ ์๋ค.
์์ฃผ ์ฌ์ฉํ๋ Addons
@storybook/addon-essentials
- Controls, Actions, Docs ๋ฑ ํ์ ๊ธฐ๋ฅ ํฌํจ
@storybook/addon-a11y
- ์ ๊ทผ์ฑ ํ ์คํธ
@storybook/addon-viewport
- ๋ค์ํ ๋ทฐํฌํธ์์ ํ ์คํธ
@storybook/addon-backgrounds
- ๋ฐฐ๊ฒฝ์ ๋ณ๊ฒฝ
Addon ์ฌ์ฉ ์์
// .storybook/main.ts
export default {
addons: [
"@storybook/addon-essentials",
"@storybook/addon-a11y",
"@storybook/addon-viewport",
],
};
๋ฌธ์ํ (Docs)
Storybook์ ์๋์ผ๋ก ์ปดํฌ๋ํธ ๋ฌธ์๋ฅผ ์์ฑํ๋ค.
MDX๋ฅผ ํ์ฉํ ๋ฌธ์ ์์ฑ
// Button.mdx
import { Meta, Story, Canvas, Controls } from '@storybook/blocks';
import \* as ButtonStories from './Button.stories';
<Meta of={ButtonStories} />
# Button ์ปดํฌ๋ํธ
Button์ ์ฌ์ฉ์์ ์ก์
์ ํธ๋ฆฌ๊ฑฐํ๋ ๊ธฐ๋ณธ ์ปดํฌ๋ํธ์
๋๋ค.
## ์ฌ์ฉ๋ฒ
```tsx
import { Button } from "./Button";
<Button label="ํด๋ฆญ" primary />;
```
Props
| Props | Type | Default | Description |
|---|---|---|---|
| label | string | - | ๋ฒํผ์ ํ์๋ ํ ์คํธ |
| primary | boolean | false | ์ฃผ์ ๋ฒํผ ์คํ์ผ ์ ์ฉ |
| size | 'small' | 'medium' | 'large' | 'medium' | ๋ฒํผ ํฌ๊ธฐ |
ํ ์คํธ ํตํฉ
๋จ์ ํ ์คํธ
import { render, screen } from "@testing-library/react";
import { composeStories } from "@storybook/react";
import * as stories from "./Button.stories";
const { Primary } = composeStories(stories);
test("renders primary button", () => {
render(<Primary />);
expect(screen.getByRole("button")).toHaveTextContent("Button");
});
์๊ฐ์ ํ๊ท ํ ์คํธ
npm install --save-dev @storybook/test-runner
// package.json
{
"scripts": {
"test-storybook": "test-storybook"
}
}
Storybook ์คํ
๊ฐ๋ฐ ๋ชจ๋
npm run storybook
๊ธฐ๋ณธ์ ์ผ๋ก http://localhost:6006์์ ์คํ๋๋ค.
๋น๋
npm run build-storybook
์ ์ ํ์ผ๋ก ๋น๋๋์ด ๋ฐฐํฌํ ์ ์๋ค.
๋ชจ๋ฒ ์ฌ๋ก
์ปดํฌ๋ํธ๋ณ Story ํ์ผ ์์ฑ
- ๊ฐ ์ปดํฌ๋ํธ๋ง๋ค
Component.stories.tsxํ์ผ ์์ฑ
- ๊ฐ ์ปดํฌ๋ํธ๋ง๋ค
์๋ฏธ์๋ Story ์ด๋ฆ ์ฌ์ฉ
Primary,WithError,Loading๋ฑ ๋ช ํํ ์ด๋ฆ ์ฌ์ฉ
Args ์ฌ์ฌ์ฉ
- ๊ณตํต args๋ meta์ ์ ์ํ์ฌ ์ฌ์ฌ์ฉ
๋ฌธ์ํ ์ ์ง
- ์ปดํฌ๋ํธ ๋ณ๊ฒฝ ์ Story๋ ํจ๊ป ์ ๋ฐ์ดํธ
์ค์ ์ฌ์ฉ ์ฌ๋ก ๋ฐ์
- ์ฑ์์ ์ค์ ๋ก ์ฌ์ฉ๋๋ props ์กฐํฉ์ผ๋ก Story ์์ฑ
์ฃผ์์ฌํญ
์คํ์ผ๋ง ์ด์
- Next.js์ ์ ๋ ๊ฒฝ๋ก๋ ์ด๋ฏธ์ง ์ต์ ํ ๊ธฐ๋ฅ ์ฌ์ฉ ์ ์ค์ ํ์
ํ๊ฒฝ ๋ณ์
.envํ์ผ์ ๋ณ์๋ ๋ณ๋๋ก ์ค์ ํด์ผ ํจ
์์กด์ฑ ๊ด๋ฆฌ
- ํ๋ก์ ํธ์ ์์กด์ฑ๊ณผ Storybook์ ์์กด์ฑ ๋ฒ์ ์ถฉ๋ ์ฃผ์
๊ฒฐ๋ก
Storybook์ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ๊ฐ๋ฐ์ ํ์์ ์ธ ๋๊ตฌ์ด๋ค. ์ปดํฌ๋ํธ๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ๋ฐํ๊ณ ํ ์คํธํ ์ ์๊ฒ ํด์ฃผ๋ฉฐ, ์๋ํ๋ ๋ฌธ์ํ๋ฅผ ํตํด ํ ๋ด ์ํต์ ์ํํ๊ฒ ๋ง๋ ๋ค. ํนํ ๋๊ท๋ชจ ํ๋ก์ ํธ์์ ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ด๋ฆฌํ ๋ ๋งค์ฐ ์ ์ฉํ๋ค.