Storybook 작성

정해준·2024년 10월 4일
post-thumbnail
# 스토리북 스토리북은 UI 컴포넌트와 페이지를 독릭접으로 구축하기 위한 프론트엔드 워크햡이라고 공식문서에서는 설명하고 있습니다. [스토리북 공식문서](https://storybook.js.org/)

위의 설명처럼 ui 컴포넌트와 페이지를 구축하기 위한 것으로 개발을 하면서 모든 컴포너틑 확인하기 힘들기 때문에 하나의 페이지로 작성하여 개발한 컴포넌트를 보기 편하게 만듭니다.

스토리북 세팅

저는 스토리북 세팅을 nextjs환경에서 진행하였습니다.

우선 next를 먼저 세팅을 하고 스토리북을 설정하였습니다.

# Next.js setting
npx create-next-app@latest 프로젝트명

cd 프로젝트명

# Storybook setting
npx storybook@latest init

위에처럼 next를 다운로드하고 next를 다운한 폴더로 넘어가서 스토리북 설정을 하면 자동으로 프레임워크인 next를 감지하여 그거에 맞게 스토리북을 설정해줍니다.

npm run storybook

# or
yarn storybook

사용하시는 거에 맞게 사용하시면 스토리북페이지에 자동으로 작성되어 있는 스토리북 페이지를 볼 수 있습니다.

스토리북을 설정하고 나면
.storybook이라는 폴더가 생기는데 그 폴더 안에 main.ts 및 preview.ts라는 파일이 있습니다.

// main.ts
import type { StorybookConfig } from "@storybook/nextjs";

const config: StorybookConfig = {
  stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@chromatic-com/storybook",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/nextjs",
    options: {},
  },
};
export default config;

// preview.ts
import type { Preview } from "@storybook/react";
import "../src/app/globals.css";

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

처음 setting하면 지금 제가 위에 올린 코드랑 조금 다를텐데 tailwind를 적용하기 위해서 코드가 조금 바뀌어 있습니다.

tailwind를 적용하기 위해서 tailwind설정이 들어가 있는 css파일인 globals.css를 임포트하여 적용하였습니다.

스토리북 작성

stories에 들어가면 기본적으로 작성되어 있는 컴포넌트 들이 있습니다.

저는 그 컴포넌트 및 css를 지우고 제가 전에 사용했던 프로젝트의 컴포넌트를 가져와서 작성했습니다.

//Button.tsx
import React from "react";

interface ButtonProps {
  text: string;
  onClick?: () => void;
  onTypeClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  size?: "sm" | "md" | "big" | "extra";
  border?: boolean;
  color?: "black" | "gray3" | "primary" | "secondary" | "gray2";
  disabled?: boolean;
}

const Button = ({
  text,
  onClick,
  onTypeClick,
  size,
  border,
  color,
  disabled,
}: ButtonProps) => {
  const height =
    (size === "sm" && "h-[22px]") ||
    (size === "md" && "h-9") ||
    (size === "big" && "h-12") ||
    (size === "extra" && "h-14");

  const col =
    (color === "black" &&
      border &&
      "border border-solid border-black text-black") ||
    (color === "gray2" &&
      border &&
      "border border-solid border-gray-2 text-black") ||
    (color === "gray3" &&
      border &&
      "border border-solid border-gray-3 text-black") ||
    (color === "primary" &&
      border &&
      "border border-solid border-primary-1 text-primary-1") ||
    (color === "secondary" &&
      border &&
      "border border-solid border-secondary-1 text-secondary-1") ||
    (color === "black" && "bg-black text-white") ||
    (color === "gray2" && "bg-gray-2 text-white") ||
    (color === "gray3" && "bg-gray-3 text-white") ||
    (color === "primary" && "bg-primary-1 text-white") ||
    (color === "secondary" && "bg-secondary-1 text-white");

  const buttonClass =
    "flex items-center justify-center px-3 py-1 rounded-lg w-full";

  return (
    <button
      onClick={onClick ? onClick : onTypeClick}
      className={[buttonClass, height, col].join(" ")}
      disabled={disabled}
    >
      {text}
    </button>
  );
};

export default Button;

// Input.tsx

import React from "react";

const Input = ({
  size,
  helperText,
  color,
  maxLength,
  name,
  pattern,
  type,
  value,
  onChange,
  placeholder,
}: {
  size?: "big" | "small";
  helperText?: string;
  color?: "black" | "gray2" | "success" | "error";
  maxLength?: number;
  name?: string;
  pattern?: RegExp;
  type: "text" | "password";
  value?: string;
  onChange?: () => void;
  placeholder?: string;
}) => {
  const height = size === "big" ? "h-14" : "h-9";

  const borderColor =
    (color === "black" && "border-zinc-500") ||
    (color === "gray2" && "border-gray2") ||
    (color === "success" && "border-success") ||
    (color === "error" && "border-red-400") ||
    (!color && "border-zinc-300");

  const helperTextColor = color === "error" ? "text-red-400" : "text-zinc-300";

  const inputStyle =
    "p-2 px-3 w-full border border-solid rounded-lg text-sm font-normal";

  return (
    <div className="h-[50px]">
      <input
        type={type}
        value={value}
        name={name}
        pattern={`${pattern}`}
        onChange={onChange}
        maxLength={maxLength}
        placeholder={placeholder}
        className={[inputStyle, borderColor, height].join(" ")}
      />

      {helperText && (
        <p className={[helperTextColor, "text-sm"].join(" ")}>{helperText}</p>
      )}
    </div>
  );
};

export default Input;

위의 2개의 컴포넌트를 활용하여 작성하였고

//Button.stories.ts
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";

import Button from "@/components/Button";

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
  title: "Example/Button",
  component: Button,
  parameters: {
    // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
    layout: "centered",
  },
  // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
  tags: ["autodocs"],
  // More on argTypes: https://storybook.js.org/docs/api/argtypes
  argTypes: {
    text: { control: "text" },
    border: { control: "boolean" },
    disabled: { control: "boolean" },
  },
  // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
  args: { color: "primary", onClick: fn(), size: "big" },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
  args: {
    color: "primary",
    text: "Primary button",
  },
};

export const Secondary: Story = {
  args: {
    color: "secondary",
    text: "Secondary button",
  },
};

export const Gray2: Story = {
  args: {
    color: "gray2",
    text: "Gray-2 button",
  },
};

export const Gray3: Story = {
  args: {
    color: "gray3",
    text: "Gray-3 button",
  },
};

export const Black: Story = {
  args: {
    color: "black",
    text: "Black button",
  },
};

export const Sm: Story = {
  args: {
    size: "sm",
    text: "sm button",
  },
};

export const Md: Story = {
  args: {
    size: "md",
    text: "md button",
  },
};

export const Big: Story = {
  args: {
    size: "big",
    text: "bg button",
  },
};

export const Extra: Story = {
  args: {
    size: "extra",
    text: "Extra button",
  },
};

export const Border: Story = {
  args: {
    color: "primary",
    border: true,
    text: "Border button",
  },
};

export const NoneBorder: Story = {
  args: {
    color: "primary",
    border: false,
    text: "Border button",
  },
};

// Input.stories.ts

import type { Meta, StoryObj } from "@storybook/react";

import Input from "@/components/Input";

const meta = {
  title: "Example/Input",
  component: Input,
  parameters: {
    layout: "centered",
  },
  tags: ["autodocs"],
  argTypes: {
    placeholder: { control: "text" },
    helperText: { control: "text" },
    maxLength: { control: "number" },
  },
  args: {
    type: "text",
    color: "black",
    size: "big",
    placeholder: "input",
  },
} satisfies Meta<typeof Input>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Black: Story = {
  args: {
    color: "black",
    placeholder: "Black input",
  },
};

export const Gray2: Story = {
  args: {
    color: "gray2",
    placeholder: "Gray-2 input",
  },
};

export const Success: Story = {
  args: {
    color: "success",
    placeholder: "Success input",
  },
};

export const Error: Story = {
  args: {
    color: "error",
    placeholder: "Error input",
  },
};

export const HelperText: Story = {
  args: {
    placeholder: "input",
    helperText: "helper text",
  },
};

export const ErrorHelperText: Story = {
  args: {
    color: "error",
    placeholder: "Error input",
    helperText: "error helperText",
  },
};

export const Big: Story = {
  args: {
    size: "big",
    placeholder: "Big input",
  },
};

export const Small: Story = {
  args: {
    size: "small",
    placeholder: "Small input",
  },
};

위의 2개의 스토리를 작성하였습니다.

const meta = {
  title: "Example/Input",
  component: Input,
  parameters: {
    layout: "centered",
  },
  tags: ["autodocs"],
  argTypes: {
    placeholder: { control: "text" },
    helperText: { control: "text" },
    maxLength: { control: "number" },
  },
  args: {
    type: "text",
    color: "black",
    size: "big",
    placeholder: "input",
  },
} satisfies Meta<typeof Input>;

위의 이 부분은 메인 컴포넌트의 설정 및 컨트롤 할 거를 설정하는 것입니다.

근데 만약에 type이 union으로 확실히 정해진 것만 있을 시 스토리북에서 자동으로 선택지를 주게 됩니다.

export const Black: Story = {
  args: {
    color: "black",
    placeholder: "Black input",
  },
};

이부분은 이 컴포넌트가 어떤 설정을 가지고 있는지 보여주는 설정입니다.

마치며

위와 같이 저는 스토리북을 작성하였고 스토리북이 컴포넌트를 문서화하기
좋다는 것을 사용하면서 알게 되었습니다.

작성한 프로젝트 repo입니다
Github Repo

0개의 댓글