Y J S

Storybook์— ๋Œ€ํ•ด์„œ

Storybook์ด๋ž€?

Storybook์€ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ณ , ํ…Œ์ŠคํŠธํ•˜๊ณ , ๋ฌธ์„œํ™”ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์˜คํ”ˆ์†Œ์Šค ๋„๊ตฌ์ด๋‹ค. React, Vue, Angular ๋“ฑ ๋‹ค์–‘ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ง€์›ํ•˜๋ฉฐ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„๊ณผ ๋ถ„๋ฆฌํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘์—…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

Storybook์˜ ์ฃผ์š” ํŠน์ง•

  1. ์ปดํฌ๋„ŒํŠธ ๊ฒฉ๋ฆฌ ๊ฐœ๋ฐœ

    • ์•ฑ์˜ ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„๊ณผ ๋ถ„๋ฆฌํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐœ๋ฐœ
    • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ API ์—†์ด๋„ UI ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ
  2. ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ UI

    • ๋‹ค์–‘ํ•œ props์™€ ์ƒํƒœ๋ฅผ ์ฆ‰์‹œ ํ™•์ธ
    • Controls๋ฅผ ํ†ตํ•ด ์‹ค์‹œ๊ฐ„์œผ๋กœ props ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ
  3. ๋ฌธ์„œํ™”

    • ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ๋ฒ•๊ณผ ์˜ˆ์ œ๋ฅผ ์ž๋™ ์ƒ์„ฑ
    • Markdown ์ง€์›์œผ๋กœ ์ƒ์„ธํ•œ ๋ฌธ์„œ ์ž‘์„ฑ ๊ฐ€๋Šฅ
  4. ํ…Œ์ŠคํŠธ ์ง€์›

    • ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์™€ ์‹œ๊ฐ์  ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ์ง€์›
    • Accessibility ํ…Œ์ŠคํŠธ๋„ ๊ฐ€๋Šฅ
  5. ๋‹ค์–‘ํ•œ ๋ทฐํฌํŠธ

    • ๋‹ค์–‘ํ•œ ํ™”๋ฉด ํฌ๊ธฐ์—์„œ ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ
    • ๋ชจ๋ฐ”์ผ, ํƒœ๋ธ”๋ฆฟ, ๋ฐ์Šคํฌํ†ฑ ๋“ฑ ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

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

  1. @storybook/addon-essentials

    • Controls, Actions, Docs ๋“ฑ ํ•„์ˆ˜ ๊ธฐ๋Šฅ ํฌํ•จ
  2. @storybook/addon-a11y

    • ์ ‘๊ทผ์„ฑ ํ…Œ์ŠคํŠธ
  3. @storybook/addon-viewport

    • ๋‹ค์–‘ํ•œ ๋ทฐํฌํŠธ์—์„œ ํ…Œ์ŠคํŠธ
  4. @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

์ •์  ํŒŒ์ผ๋กœ ๋นŒ๋“œ๋˜์–ด ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ชจ๋ฒ” ์‚ฌ๋ก€

  1. ์ปดํฌ๋„ŒํŠธ๋ณ„ Story ํŒŒ์ผ ์ž‘์„ฑ

    • ๊ฐ ์ปดํฌ๋„ŒํŠธ๋งˆ๋‹ค Component.stories.tsx ํŒŒ์ผ ์ƒ์„ฑ
  2. ์˜๋ฏธ์žˆ๋Š” Story ์ด๋ฆ„ ์‚ฌ์šฉ

    • Primary, WithError, Loading ๋“ฑ ๋ช…ํ™•ํ•œ ์ด๋ฆ„ ์‚ฌ์šฉ
  3. Args ์žฌ์‚ฌ์šฉ

    • ๊ณตํ†ต args๋Š” meta์— ์ •์˜ํ•˜์—ฌ ์žฌ์‚ฌ์šฉ
  4. ๋ฌธ์„œํ™” ์œ ์ง€

    • ์ปดํฌ๋„ŒํŠธ ๋ณ€๊ฒฝ ์‹œ Story๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ
  5. ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€ ๋ฐ˜์˜

    • ์•ฑ์—์„œ ์‹ค์ œ๋กœ ์‚ฌ์šฉ๋˜๋Š” props ์กฐํ•ฉ์œผ๋กœ Story ์ž‘์„ฑ

์ฃผ์˜์‚ฌํ•ญ

  1. ์Šคํƒ€์ผ๋ง ์ด์Šˆ

    • Next.js์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋‚˜ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๊ธฐ๋Šฅ ์‚ฌ์šฉ ์‹œ ์„ค์ • ํ•„์š”
  2. ํ™˜๊ฒฝ ๋ณ€์ˆ˜

    • .env ํŒŒ์ผ์˜ ๋ณ€์ˆ˜๋Š” ๋ณ„๋„๋กœ ์„ค์ •ํ•ด์•ผ ํ•จ
  3. ์˜์กด์„ฑ ๊ด€๋ฆฌ

    • ํ”„๋กœ์ ํŠธ์˜ ์˜์กด์„ฑ๊ณผ Storybook์˜ ์˜์กด์„ฑ ๋ฒ„์ „ ์ถฉ๋Œ ์ฃผ์˜

๊ฒฐ๋ก 

Storybook์€ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ์— ํ•„์ˆ˜์ ์ธ ๋„๊ตฌ์ด๋‹ค. ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋ฉฐ, ์ž๋™ํ™”๋œ ๋ฌธ์„œํ™”๋ฅผ ํ†ตํ•ด ํŒ€ ๋‚ด ์†Œํ†ต์„ ์›ํ™œํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค. ํŠนํžˆ ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ด€๋ฆฌํ•  ๋•Œ ๋งค์šฐ ์œ ์šฉํ•˜๋‹ค.