본문 바로가기
개발개발/WorkRoot_워크루트

스토리북 (storybook) 기본 사용 방법

by yelimu 2025. 1. 3.

공통 컴포넌트인 input 컴포넌트의 스토리 코드를 보면서 기본 사용 방법을 정리해보려고 한다.


스토리 파일 경로

src/app/stories/design-system/components/input/text/input.stories.tsx

(next.js app router 기준)

 

스토리 파일은 src/app/stories 폴더 밑에 들어가게된다. 

 

파일명은 000.stories.tsx 

 

폴더 경로는 스토리북에서 보이는 경로와는 무관하며 

스토리북에 생성되는 폴더 경로는 meta 객체 안의 title 에 보이는 

Design System/Components/TextInput/TextInput  

이 경로로 생성 된다. 

 

우리 프로젝트의 스토리북 폴더 구조는 대략 이렇다. 

Design System/Components/TextInput


meta 객체 

 

 

 

스토리북 공홈에서 제공하는 튜토리얼에서는 

"문서화는 중요하지만 힘든 작업이다. 그러나 대부분의 문서는 생성되는 즉시 구식이 되어버린다." 

"그렇기에 다른 개발자에게 더 많은 문맥(왜, 언제, 어떻게) 를 제공할 필요가 있다." 고 한다. 

너무 공감하는 바이다. ..

 

이러한 문서의 정보(메타 데이터) 를 제공하는 역할로서 meta 객체가 사용된다. 

const meta = {
  title: "Design System/Components/TextInput/TextInput",
  component: BaseInput,
  parameters: {
    layout: "centered",
  },
  tags: ["autodocs"],
  argTypes: {
    variant: { ... },
    type: { ... },
    _storySize: { ... },
    disabled: { ... },
  },
} satisfies Meta<StoryProps>;

 

title: Storybook의 UI에서 스토리가 표시되는 경로를 설정 => 자동으로 폴더가 생성된다.

component: 스토리에서 다루는 실제 컴포넌트를 지정 => 해당 컴포넌트를 import 해야한다. 

parameters: Storybook에서 스토리를 렌더링할 때의 전역 설정

- layout: "centered"는 스토리의 컴포넌트를 화면 가운데 정렬합니다.

tags: 추가 정보를 제공하기 위한 태그

- "autodocs"는 자동 문서화를 활성화하는 태그입니다.

argTypes: 컴포넌트의 Props(스토리에서 사용할 속성)를 정의하고, 이를 Storybook에서 제어할 수 있도록 설정

 

argTypes 에 포함되는 control 방법과 선택 가능한 여러 옵션을 추가할 수 있다

argTypes: {
  variant: {
    control: "radio",
    options: ["white", "transparent"],
  },
  _storySize: {
    control: "radio",
    options: ["mobile", "desktop"],
    description: "입력창 크기 설정",
  },
  disabled: {
    control: "boolean",
  },
}

 

스토리북 하단에서 controls 패널을 확인할 수 있고, 

여기서 argsTypes 에 각 prop을 어떻게 제어할 것인지 설정해준 대로 제어할 수 있다. 

 

 


여러가지 속성과 옵션으로 prop을 컨트롤러에서 제어할 수 있다

controls 객체의 key 속성 value 예시
type  text, number, radio, inline-radio,
boolean, select, 
multi-select ,
color, range, date 등
control: { type: "color" }
options 사용가능한 값 control: { type: "multi-select" },
options: ["apple", "banana", "cherry"]
min, max, step,  숫자 값의 범위와 증감 단위 control: { type: "range", min: 0, max: 100, step: 10 }
description Props에 대한 설명  
defaultValue 컨트롤러의 초기 값  
table 표로 나타냄 table: {
disable: true, // 표에서 숨김
category: "Advanced Options",
type: { summary: "string" }
}

https://storybook.js.org/docs/6/essentials/controls

 

Storybook: Frontend workshop for UI development

Storybook is a frontend workshop for building UI components and pages in isolation. Thousands of teams use it for UI development, testing, and documentation. It's open source and free.

storybook.js.org

나도 다음에 더 다양하게 적용해봐야겠다 


 

StoryObj 로 스토리 정의하기

StoryObj 는 Storybook에서 스토리를 정의할 때 사용하는 타입이다. 

Storybook 7.0 이후로 스토리를 정의하는 방법이라고 한다. 

 

간략하게는 아래와 같이 사용할 수 있다. 

Error, Feedback이라는 두 개의 스토리가 생성되고 args 객체에서 설정된 값이 컴포넌트에 prop으로 전달된다.

이때 컴포넌트가 필수로 받아야 하는 prop을 빼먹으면 에러가 난다. 

따라서 컴포넌트의 prop을 수정했을때 스토리도 같이 수정이 되어야한다. 

export const Error: Story = {
  args: {
    type: "text",
    variant: "white",
    placeholder: "텍스트 입력",
    errormessage: "에러 메시지",
  },
};

export const Feedback: Story = {
  args: {
    type: "text",
    variant: "white",
    placeholder: "텍스트 입력",
    feedbackMessage: "피드백 메시지",
  },
};

 

StoryObj 의 주요 속성으로는 

args 외에도 parameters, play, decorators, argTypes, render 가 있다. 

- args: "이 값으로 시작하세요"
- argTypes: "이렇게 제어하고 문서화하세요"

 

나는 default 상태와 hover, focus 상태의 컴포넌트를 동시에 보여주고 싶어서 render를 사용했다. 

export const Default_Hover_Focus: Story = {
  args: {
//...
  },
  render: (args) => {
    const { _storySize, ...rest } = args as StoryProps;
    const sizeClass = _storySize ? storySizeMap[_storySize] : "";

    const StoryComponent = () => <BaseInput {...rest} size={sizeClass} />;

    return (
      <div className="space-y-4">
        <div>
          <p className="mb-2 text-sm text-grayscale-500">기본 상태:</p>
          <StoryComponent />
        </div>
//... 
        <div>
          <p className="mb-2 text-sm text-grayscale-500">Focus 상태:</p>
          <div className="[&>div>div]:border-primary-orange-300">
            <StoryComponent />
          </div>
        </div>
      </div>
    );
  },
};

 

decorators 와 render 속성의 주요 차이점은 아래와 같다

특징 render decorators
목적 스토리의 렌더링 방식을 직접 정의 스토리를 꾸미거나 외부 환경을 추가
적용 대상 특정 스토리에서만 사용 특정 스토리 또는 전역적으로 적용 가능
구현 방식 스토리를 렌더링하는 자체 로직 제공 스토리를 래핑(Wrapping)하는 컴포넌트 제공
전역 적용 불가능 가능 (글로벌 데코레이터 설정 가능)
사용 사례 렌더링을 완전히 커스터마이징해야 하는 경우 공통적인 스타일, 레이아웃, 컨텍스트, 테마 등을 적용해야 하는 경우

 

두 가지를 함께 사용하여 decorators를 통해 스토리의 외부 환경을 설정하고, render를 사용해 내부 로직을 커스터마이징할 수 있다고 한다 ~ 


코드 전문은 아래와 같다. 

 

// TextInput.stories.tsx
import { Meta, StoryObj } from "@storybook/react";
import { BaseInputProps } from "@/types/textInput";
import BaseInput from "@/app/components/input/text/BaseInput";

type StoryProps = BaseInputProps & {
  _storySize?: "mobile" | "desktop";
};

const meta = {
  title: "Design System/Components/TextInput/TextInput",
  component: BaseInput,
  parameters: {
    layout: "centered",
  },
  tags: ["autodocs"],
  argTypes: {
    variant: {
      control: "radio",
      options: ["white", "transparent"],
    },
    type: {
      control: "radio",
      options: ["text", "password"],
    },
    _storySize: {
      control: "radio",
      options: ["mobile", "desktop"],
      description: "입력창 크기 설정",
    },
    disabled: {
      control: "boolean",
    },
  },
} satisfies Meta<StoryProps>;

export default meta;

const storySizeMap = {
  mobile: "w-[327px] h-[54px]",
  desktop: "lg:w-[640px] lg:h-[64px]",
};

type Story = StoryObj<typeof BaseInput>;
export const Default_Hover_Focus: Story = {
  args: {
    type: "text",
    variant: "white",
    placeholder: "텍스트 입력",
  },
  render: (args) => {
    const { _storySize, ...rest } = args as StoryProps;
    const sizeClass = _storySize ? storySizeMap[_storySize] : "";

    const StoryComponent = () => <BaseInput {...rest} size={sizeClass} />;

    return (
      <div className="space-y-4">
        <div>
          <p className="mb-2 text-sm text-grayscale-500">기본 상태:</p>
          <StoryComponent />
        </div>
        <div>
          <p className="mb-2 text-sm text-grayscale-500">Hover 상태:</p>
          <div className="[&>div>div]:border-grayscale-200 [&>div>div]:bg-background-300">
            <StoryComponent />
          </div>
        </div>
        <div>
          <p className="mb-2 text-sm text-grayscale-500">Focus 상태:</p>
          <div className="[&>div>div]:border-primary-orange-300">
            <StoryComponent />
          </div>
        </div>
      </div>
    );
  },
};

export const Error: Story = {
  args: {
    type: "text",
    variant: "white",
    placeholder: "텍스트 입력",
    errormessage: "에러 메시지",
  },
};

export const Feedback: Story = {
  args: {
    type: "text",
    variant: "white",
    placeholder: "텍스트 입력",
    feedbackMessage: "피드백 메시지",
  },
};

 

아마 비효율적이고 엉성한 코드일지도 모르겠지만, 우선은 이렇게 작성했습니다 헤헷,, 

컴포넌트 별로 스토리도 제각기이지만 전체적인 큰 틀은 비슷할 것이기에 요것만 대표로 포스팅 해보았다.

나의 첫번째 스토리...* 


사실 프로젝트 도중에는 개념을 하나하나 알아가며 쓰기 보다는, ai 선생님께 무분별한 도움을 받아서 작성한 부분이 많다. 

그래서 뒤늦게나마 이렇게 정리를 해보는것이 큰 도움이 되는 것 같다.