🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Storybook 핵심개념 완벽정리 컴포넌트개발 - 슬라이드 1/9
A

AI Generated

2025. 11. 4. · 16 Views

Storybook 핵심 개념 완벽 정리 컴포넌트 개발

Storybook을 활용한 컴포넌트 주도 개발의 핵심 개념을 처음부터 끝까지 정리했습니다. Story 작성부터 Addon 활용, 문서화까지 실무에서 바로 사용할 수 있는 모든 내용을 담았습니다. 초급 개발자도 쉽게 따라할 수 있도록 친절하게 설명합니다.


목차

  1. Story 기본 구조
  2. Args와 ArgTypes
  3. Decorators
  4. Play Function
  5. Controls Addon
  6. Actions Addon
  7. Docs Addon
  8. CSF3 문법

1. Story 기본 구조

시작하며

여러분이 큰 프로젝트에서 버튼 컴포넌트를 개발할 때 이런 상황을 겪어본 적 있나요? 전체 앱을 실행하고, 로그인하고, 특정 페이지로 이동해야만 작업 중인 버튼을 확인할 수 있는 상황 말이죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 컴포넌트 하나를 수정할 때마다 전체 앱을 거쳐야 하니 개발 속도가 느려지고, 다양한 상태를 테스트하기도 어렵습니다.

게다가 팀원들과 컴포넌트를 공유하려면 복잡한 설명이 필요하죠. 바로 이럴 때 필요한 것이 Storybook의 Story입니다.

Story를 사용하면 컴포넌트를 독립적으로 개발하고 테스트할 수 있어서, 전체 앱 없이도 컴포넌트의 모든 상태를 확인할 수 있습니다.

개요

간단히 말해서, Story는 특정 상태의 컴포넌트를 렌더링하는 함수입니다. 실무에서 버튼 컴포넌트를 개발한다고 생각해보세요.

기본 버튼, 비활성화된 버튼, 로딩 중인 버튼, 큰 사이즈 버튼 등 다양한 상태를 확인해야 합니다. Story를 사용하면 이 모든 상태를 독립적으로 만들어서 한 화면에서 비교할 수 있습니다.

기존에는 각 상태를 확인하기 위해 코드를 수정하고 다시 빌드해야 했다면, 이제는 Story를 여러 개 만들어서 즉시 전환하며 확인할 수 있습니다. Story의 핵심 특징은 재사용성, 독립성, 문서화입니다.

한 번 작성한 Story는 개발, 테스트, 문서화에 모두 활용되며, 다른 컴포넌트에 의존하지 않고 독립적으로 실행됩니다. 이러한 특징들이 컴포넌트 주도 개발(CDD)의 핵심이 됩니다.

코드 예제

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

// Meta 객체: 컴포넌트 전체 설정
const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
};

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

// 기본 Story: Primary 버튼
export const Primary: Story = {
  args: {
    label: 'Click me',
    variant: 'primary',
  },
};

// 두 번째 Story: Disabled 상태
export const Disabled: Story = {
  args: {
    label: 'Disabled',
    variant: 'primary',
    disabled: true,
  },
};

설명

이것이 하는 일: Storybook에서 Button 컴포넌트의 다양한 상태를 독립적으로 보여주는 Story들을 정의합니다. 첫 번째로, Meta 객체는 컴포넌트 전체의 설정을 담당합니다.

title은 Storybook 사이드바에서 보이는 경로를 결정하고(Components 폴더 안의 Button), component는 어떤 컴포넌트를 표시할지 지정합니다. tags에 'autodocs'를 추가하면 자동으로 문서 페이지가 생성되죠.

이 설정은 모든 Story에 공통으로 적용됩니다. 그 다음으로, 각 Story 객체가 실행되면서 특정 상태의 컴포넌트를 렌더링합니다.

Primary Story는 args로 label과 variant를 전달해서 기본 상태의 버튼을 보여주고, Disabled Story는 disabled: true를 추가해서 비활성화된 상태를 보여줍니다. 내부에서는 이 args가 컴포넌트의 props로 전달되어 실제로 렌더링됩니다.

마지막으로, default export로 Meta를 내보내고 named export로 각 Story를 내보내면 Storybook이 자동으로 인식해서 UI에 표시합니다. type Story = StoryObj<typeof Button>은 TypeScript 타입 안정성을 제공해서 잘못된 props를 전달하는 실수를 방지해줍니다.

여러분이 이 코드를 사용하면 전체 앱을 실행하지 않고도 버튼의 모든 상태를 한눈에 확인할 수 있습니다. 새로운 상태가 필요하면 Story를 추가하기만 하면 되고, 팀원들과 공유할 때도 Storybook 링크만 보내면 됩니다.

또한 각 Story는 시각적 회귀 테스트의 기준점이 되어 디자인 시스템 유지보수에도 큰 도움이 됩니다.

실전 팁

💡 Story 이름은 컴포넌트의 상태를 명확히 표현하세요. 'Story1', 'Story2'보다는 'Primary', 'Disabled', 'Loading'처럼 직관적인 이름이 좋습니다.

💡 흔한 실수: Meta 객체를 default export하지 않으면 Storybook이 인식하지 못합니다. 반드시 export default meta를 포함하세요.

💡 성능 팁: Story가 많아지면 로딩이 느려질 수 있습니다. 복잡한 컴포넌트는 lazy loading을 고려하고, 필요한 Story만 활성화하세요.

💡 디버깅 팁: Story가 제대로 표시되지 않으면 브라우저 콘솔을 확인하세요. args의 타입이 맞지 않거나 필수 props가 누락된 경우가 많습니다.

💡 발전된 사용법: 여러 Story에서 공통으로 사용하는 args는 Meta 객체의 args 필드에 정의하고, 각 Story에서 필요한 부분만 오버라이드하세요.


2. Args와 ArgTypes

시작하며

여러분이 Select 컴포넌트를 만들고 있는데, 팀원이 "옵션 개수가 100개일 때는 어떻게 보이나요?"라고 물어본 적 있나요? 코드를 수정해서 데이터를 바꾸고, 다시 빌드하고, 확인하는 과정을 반복해야 했을 겁니다.

이런 문제는 컴포넌트의 다양한 상황을 테스트할 때 특히 심각합니다. Props를 하나 바꿀 때마다 코드를 수정해야 하고, 팀원들은 직접 코드를 보지 않으면 어떤 props가 있는지조차 알 수 없죠.

게다가 디자이너나 PM은 코드를 수정할 수 없어서 개발자에게 계속 요청해야 합니다. 바로 이럴 때 필요한 것이 Args와 ArgTypes입니다.

Args로 props를 동적으로 제어하고, ArgTypes로 UI 컨트롤을 정의하면, 코드 수정 없이 브라우저에서 직접 props를 바꿔가며 테스트할 수 있습니다.

개요

간단히 말해서, Args는 컴포넌트에 전달되는 props이고, ArgTypes는 이 props를 어떻게 제어할지 정의하는 메타데이터입니다. 실무에서 Select 컴포넌트를 만든다고 생각해보세요.

size, disabled, options, placeholder 같은 다양한 props가 있을 텐데, 이들의 조합을 모두 테스트하려면 수십 개의 Story를 만들어야 합니다. 하지만 Args와 ArgTypes를 사용하면 하나의 Story만 만들고, Storybook UI에서 드롭다운, 체크박스, 텍스트 입력으로 props를 바꿔가며 모든 경우를 확인할 수 있습니다.

기존에는 각 조합마다 하드코딩된 Story를 만들어야 했다면, 이제는 동적으로 제어 가능한 하나의 Story로 충분합니다. Args의 핵심 특징은 동적 제어, 타입 안정성, 자동 문서화입니다.

UI에서 실시간으로 props를 변경할 수 있고, TypeScript와 통합되어 타입 체크를 제공하며, 자동으로 문서에 props 테이블을 생성합니다. 이러한 특징들이 컴포넌트 API를 투명하게 만들고, 비개발자도 쉽게 테스트할 수 있게 합니다.

코드 예제

// Select.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Select } from './Select';

const meta: Meta<typeof Select> = {
  title: 'Components/Select',
  component: Select,
  // ArgTypes: 각 prop의 컨트롤 정의
  argTypes: {
    size: {
      control: 'select',
      options: ['small', 'medium', 'large'],
      description: '선택 상자의 크기',
    },
    disabled: {
      control: 'boolean',
      description: '비활성화 상태',
    },
    placeholder: {
      control: 'text',
      description: '플레이스홀더 텍스트',
    },
    options: {
      control: 'object',
      description: '선택 가능한 옵션 배열',
    },
  },
};

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

// Args로 기본값 설정
export const Default: Story = {
  args: {
    size: 'medium',
    disabled: false,
    placeholder: '옵션을 선택하세요',
    options: [
      { value: '1', label: 'Option 1' },
      { value: '2', label: 'Option 2' },
      { value: '3', label: 'Option 3' },
    ],
  },
};

설명

이것이 하는 일: Select 컴포넌트의 모든 props를 Storybook UI에서 동적으로 제어할 수 있게 만듭니다. 첫 번째로, argTypes 객체는 각 prop의 컨트롤 타입을 정의합니다.

size는 'select' 컨트롤로 드롭다운에서 선택하게 하고, disabled는 'boolean' 컨트롤로 체크박스로 토글하게 합니다. placeholder는 'text' 컨트롤로 직접 입력할 수 있게 하고, options는 'object' 컨트롤로 JSON 편집기를 제공합니다.

description을 추가하면 각 prop의 설명이 문서에 자동으로 표시되죠. 그 다음으로, Story의 args 객체가 실제 props 값을 정의합니다.

이 값들은 Storybook UI의 Controls 패널에 초기값으로 표시되고, 사용자가 UI에서 값을 변경하면 실시간으로 컴포넌트가 리렌더링됩니다. 예를 들어 size를 'large'로 바꾸면 즉시 큰 Select가 보이고, disabled를 true로 바꾸면 비활성화된 상태가 보입니다.

마지막으로, TypeScript의 타입 추론이 작동해서 잘못된 타입의 값을 입력하면 경고가 표시됩니다. StoryObj<typeof Select>가 Select 컴포넌트의 props 타입을 자동으로 추론해서, args에 존재하지 않는 prop을 추가하거나 잘못된 타입의 값을 넣으면 컴파일 에러가 발생합니다.

여러분이 이 코드를 사용하면 코드 수정 없이 모든 props 조합을 테스트할 수 있습니다. 디자이너는 size와 placeholder를 바꿔가며 디자인을 확인하고, QA는 disabled 상태와 다양한 options를 테스트하며, PM은 실제 데이터를 입력해서 실사용 환경을 시뮬레이션할 수 있습니다.

또한 자동 생성되는 문서에는 모든 props가 테이블로 정리되어 있어서, API 문서를 따로 작성할 필요가 없습니다.

실전 팁

💡 control 타입을 적절히 선택하세요. boolean은 'boolean', enum은 'select' 또는 'radio', 숫자는 'number' 또는 'range'가 적합합니다.

💡 흔한 실수: options 같은 복잡한 객체는 'object' 컨트롤을 사용하되, 너무 복잡하면 미리 정의된 몇 가지 시나리오를 별도 Story로 만드는 것이 더 나을 수 있습니다.

💡 성능 팁: argTypes의 table 필드로 특정 prop을 문서에서 숨기거나, category로 그룹화해서 문서 가독성을 높이세요. { table: { disable: true } }로 숨길 수 있습니다.

💡 디버깅 팁: Controls 패널에서 값을 바꿨는데 변화가 없다면, 컴포넌트가 해당 prop을 실제로 사용하는지 확인하세요. console.log로 props를 출력해보면 도움이 됩니다.

💡 발전된 사용법: argTypes의 mapping 기능으로 복잡한 객체를 간단한 문자열로 매핑하세요. 예: icon prop에 실제 Icon 컴포넌트 대신 'home', 'settings' 같은 문자열을 사용하고, mapping으로 변환할 수 있습니다.


3. Decorators

시작하며

여러분이 Modal 컴포넌트를 개발하는데, 모든 Story에서 배경색을 어둡게 하고 중앙 정렬을 해야 하는 상황을 겪어본 적 있나요? 각 Story마다 똑같은 래퍼 코드를 반복해서 작성하게 됩니다.

이런 문제는 실제 개발 현장에서 매우 흔합니다. 특히 Modal, Tooltip, Dropdown처럼 특정 레이아웃이나 컨텍스트가 필요한 컴포넌트를 개발할 때 더욱 그렇죠.

각 Story에 중복 코드가 쌓이면 유지보수가 어려워지고, 나중에 레이아웃을 변경하려면 모든 Story를 수정해야 합니다. 바로 이럴 때 필요한 것이 Decorators입니다.

Decorator를 사용하면 모든 Story에 공통으로 적용될 래퍼를 한 번만 정의해서, 코드 중복을 없애고 일관된 환경을 제공할 수 있습니다.

개요

간단히 말해서, Decorator는 Story를 감싸는 래퍼 함수로, 공통 레이아웃이나 컨텍스트를 제공합니다. 실무에서 Theme Provider가 필요한 컴포넌트들을 개발한다고 생각해보세요.

모든 Story에서 ThemeProvider로 감싸야 하는데, 각 Story마다 작성하면 수십 줄의 중복 코드가 생깁니다. Decorator를 사용하면 Meta 레벨에서 한 번만 정의하고, 모든 Story에 자동으로 적용됩니다.

기존에는 각 Story에서 render 함수를 작성해서 래퍼를 추가했다면, 이제는 decorators 배열에 추가하기만 하면 됩니다. Decorator의 핵심 특징은 재사용성, 계층 구조, 합성 가능성입니다.

한 번 정의하면 여러 Story에 적용할 수 있고, 글로벌/컴포넌트/Story 레벨에서 계층적으로 적용되며, 여러 Decorator를 조합해서 사용할 수 있습니다. 이러한 특징들이 Story 코드를 깔끔하게 유지하고, 일관된 개발 환경을 보장합니다.

코드 예제

// Modal.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Modal } from './Modal';
import { ThemeProvider } from '../providers/ThemeProvider';

const meta: Meta<typeof Modal> = {
  title: 'Components/Modal',
  component: Modal,
  // Decorators: 모든 Story에 적용될 래퍼
  decorators: [
    // Theme Provider로 감싸기
    (Story) => (
      <ThemeProvider theme="dark">
        <Story />
      </ThemeProvider>
    ),
    // 중앙 정렬 레이아웃 적용
    (Story) => (
      <div style={{
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        height: '100vh',
        backgroundColor: '#1a1a1a',
      }}>
        <Story />
      </div>
    ),
  ],
};

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

// Story는 이제 간결하게 작성 가능
export const Default: Story = {
  args: {
    title: 'Welcome',
    content: 'This is a modal dialog',
    isOpen: true,
  },
};

// 특정 Story에만 추가 Decorator 적용
export const WithCustomBackground: Story = {
  args: {
    title: 'Custom',
    isOpen: true,
  },
  decorators: [
    (Story) => (
      <div style={{ backgroundColor: '#ff0000' }}>
        <Story />
      </div>
    ),
  ],
};

설명

이것이 하는 일: Modal 컴포넌트의 모든 Story에 Theme Provider와 중앙 정렬 레이아웃을 자동으로 적용합니다. 첫 번째로, decorators 배열에 정의된 함수들이 각 Story를 감쌉니다.

첫 번째 Decorator는 ThemeProvider로 감싸서 테마를 제공하고, 두 번째 Decorator는 div로 감싸서 중앙 정렬과 어두운 배경을 적용합니다. 각 Decorator는 (Story) => JSX 형태의 함수로, Story 컴포넌트를 받아서 래퍼로 감싼 JSX를 반환합니다.

그 다음으로, Storybook이 Story를 렌더링할 때 Decorator들을 역순으로 적용합니다. 배열의 마지막 Decorator가 가장 바깥쪽에 위치하고, 첫 번째 Decorator가 Story에 가장 가깝게 위치합니다.

Default Story의 경우, 실제 렌더링되는 구조는 <div(중앙정렬)><ThemeProvider><Modal /></ThemeProvider></div> 형태가 됩니다. 마지막으로, 특정 Story에만 추가 Decorator를 적용할 수도 있습니다.

WithCustomBackground Story처럼 Story 레벨에서 decorators를 정의하면, Meta 레벨의 Decorator에 추가로 적용됩니다. 이 경우 빨간 배경 div가 가장 안쪽에 추가되어, Meta의 Decorator들과 합성됩니다.

여러분이 이 코드를 사용하면 모든 Modal Story에서 중복 코드 없이 일관된 환경을 제공할 수 있습니다. Theme이 변경되면 Decorator만 수정하면 되고, 새로운 Story를 추가할 때도 자동으로 같은 환경이 적용됩니다.

또한 글로벌 Decorator를 .storybook/preview.js에 정의하면 전체 프로젝트의 모든 Story에 적용할 수 있어서, Router나 Redux Store 같은 전역 컨텍스트를 한 번에 제공할 수 있습니다.

실전 팁

💡 Decorator 순서가 중요합니다. 배열의 첫 번째가 Story에 가장 가깝고, 마지막이 가장 바깥쪽입니다. Provider는 보통 첫 번째에, 레이아웃은 나중에 배치하세요.

💡 흔한 실수: Decorator 함수가 컴포넌트를 반환하지 않고 undefined를 반환하면 에러가 발생합니다. 반드시 JSX나 <Story />를 반환하세요.

💡 성능 팁: Decorator 내부에서 복잡한 계산이나 API 호출을 피하세요. Story가 리렌더링될 때마다 Decorator도 실행되므로 성능에 영향을 줄 수 있습니다.

💡 디버깅 팁: Decorator가 예상대로 작동하지 않으면 React DevTools로 컴포넌트 트리를 확인하세요. Decorator가 올바른 순서로 적용되었는지 볼 수 있습니다.

💡 발전된 사용법: .storybook/preview.js의 globalTypes와 decorators를 결합해서 테마 전환 기능을 만드세요. toolbar에서 라이트/다크 테마를 선택하면 자동으로 Provider가 바뀌도록 구현할 수 있습니다.


4. Play Function

시작하며

여러분이 로그인 폼 컴포넌트를 개발하면서, 이메일 입력 → 비밀번호 입력 → 제출 버튼 클릭 시나리오를 매번 수동으로 테스트하고 있나요? 각 Story를 열 때마다 같은 동작을 반복하는 것은 시간 낭비입니다.

이런 문제는 인터랙션이 많은 컴포넌트를 개발할 때 특히 심각합니다. 폼, 멀티스텝 위저드, 드래그앤드롭 같은 컴포넌트는 특정 상태에 도달하기까지 여러 단계의 사용자 입력이 필요하죠.

매번 수동으로 테스트하면 휴먼 에러가 발생하기 쉽고, 회귀 테스트도 어렵습니다. 바로 이럴 때 필요한 것이 Play Function입니다.

Play Function을 사용하면 사용자 인터랙션을 코드로 작성해서 자동으로 실행하고, 결과를 검증할 수 있습니다. Story를 열면 자동으로 시나리오가 실행되어 원하는 상태를 즉시 확인할 수 있습니다.

개요

간단히 말해서, Play Function은 Story가 렌더링된 후 자동으로 실행되는 함수로, 사용자 인터랙션을 시뮬레이션합니다. 실무에서 검색 컴포넌트를 개발한다고 생각해보세요.

검색어 입력 → 자동완성 표시 → 항목 선택 같은 복잡한 시나리오를 테스트하려면 매번 타이핑하고 클릭해야 합니다. Play Function을 사용하면 이 모든 동작을 코드로 작성해서, Story를 열 때마다 자동으로 실행되게 만들 수 있습니다.

기존에는 수동으로 인터랙션을 반복하거나 별도의 E2E 테스트를 작성해야 했다면, 이제는 Story 안에서 직접 인터랙션을 정의하고 테스트할 수 있습니다. Play Function의 핵심 특징은 자동화, Testing Library 통합, 시각적 피드백입니다.

사용자 동작을 자동으로 재현하고, @storybook/testing-library의 API로 실제 사용자처럼 상호작용하며, Storybook UI에서 각 단계를 시각적으로 볼 수 있습니다. 이러한 특징들이 개발과 테스트를 동시에 가능하게 하고, 인터랙션 테스트를 문서화합니다.

코드 예제

// LoginForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect } from '@storybook/test';
import { LoginForm } from './LoginForm';

const meta: Meta<typeof LoginForm> = {
  title: 'Components/LoginForm',
  component: LoginForm,
};

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

// Play Function으로 자동 인터랙션
export const FilledForm: Story = {
  play: async ({ canvasElement }) => {
    // canvasElement에서 DOM 요소 찾기
    const canvas = within(canvasElement);

    // 이메일 입력 필드 찾아서 타이핑
    const emailInput = canvas.getByLabelText('Email');
    await userEvent.type(emailInput, 'user@example.com', {
      delay: 100, // 실제 타이핑처럼 지연
    });

    // 비밀번호 입력
    const passwordInput = canvas.getByLabelText('Password');
    await userEvent.type(passwordInput, 'securePassword123');

    // 제출 버튼 클릭
    const submitButton = canvas.getByRole('button', { name: /submit/i });
    await userEvent.click(submitButton);

    // 결과 검증
    const successMessage = await canvas.findByText(/login successful/i);
    expect(successMessage).toBeInTheDocument();
  },
};

// 에러 상태 테스트
export const InvalidEmail: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByLabelText('Email'), 'invalid-email');
    await userEvent.click(canvas.getByRole('button', { name: /submit/i }));

    // 에러 메시지 확인
    expect(await canvas.findByText(/invalid email/i)).toBeInTheDocument();
  },
};

설명

이것이 하는 일: LoginForm Story를 열면 자동으로 이메일과 비밀번호를 입력하고 제출 버튼을 클릭해서 결과를 검증합니다. 첫 번째로, play 함수는 canvasElement를 받아서 within으로 감쌉니다.

canvasElement는 Story가 렌더링된 DOM 영역이고, within(canvasElement)는 이 영역 내에서만 요소를 찾는 스코프를 만듭니다. 이렇게 하면 다른 Story나 Storybook UI의 요소와 충돌하지 않고, 현재 Story의 요소만 안전하게 조작할 수 있습니다.

그 다음으로, userEvent API가 실제 사용자처럼 상호작용합니다. getByLabelText로 접근성 높은 방식으로 입력 필드를 찾고, userEvent.type으로 키보드 이벤트를 발생시키며 텍스트를 입력합니다.

delay 옵션을 주면 실제 타이핑 속도처럼 천천히 입력되어 시각적으로 확인하기 좋습니다. userEvent.click은 마우스 클릭 이벤트를 완전히 시뮬레이션해서, 실제 사용자가 클릭한 것과 동일하게 동작합니다.

마지막으로, expect와 Testing Library의 쿼리로 결과를 검증합니다. findByText는 비동기적으로 요소가 나타날 때까지 기다리므로, API 호출 후 나타나는 메시지 같은 비동기 업데이트를 테스트할 수 있습니다.

expect(successMessage).toBeInTheDocument()로 검증하면 Storybook의 Interactions 패널에 결과가 표시되어, 테스트 통과 여부를 시각적으로 확인할 수 있습니다. 여러분이 이 코드를 사용하면 Story를 열 때마다 자동으로 시나리오가 실행되어, 수동 테스트 시간을 크게 절약할 수 있습니다.

Interactions 패널에서 각 단계를 디버그할 수 있고, 스크린샷이나 비디오를 캡처해서 버그 리포트에 첨부할 수도 있습니다. 또한 test-runner를 사용하면 모든 Play Function을 CI에서 자동으로 실행해서, 회귀 테스트를 자동화할 수 있습니다.

실전 팁

💡 쿼리 우선순위를 지키세요. getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId 순서로 사용하면 접근성 높은 테스트를 작성할 수 있습니다.

💡 흔한 실수: await 없이 비동기 함수를 호출하면 인터랙션이 완료되기 전에 다음 단계가 실행됩니다. 모든 userEvent 호출에 await를 붙이세요.

💡 성능 팁: delay 옵션은 디버깅할 때만 사용하세요. 프로덕션에서는 delay 없이 실행해서 테스트 속도를 높이세요.

💡 디버깅 팁: Play Function이 실패하면 Interactions 패널에서 각 단계의 스크린샷과 로그를 확인할 수 있습니다. step 함수로 각 단계에 이름을 붙이면 더 읽기 쉽습니다.

💡 발전된 사용법: waitFor로 복잡한 비동기 조건을 기다리세요. 예: await waitFor(() => expect(canvas.getByText('Loading')).not.toBeInTheDocument())로 로딩이 끝날 때까지 기다릴 수 있습니다.


5. Controls Addon

시작하며

여러분이 Button 컴포넌트를 PM에게 시연하는데, "좀 더 큰 사이즈는 어떻게 보이나요?"라는 질문을 받았나요? 코드를 수정하고 다시 빌드해서 보여주느라 회의 시간이 길어진 경험이 있을 겁니다.

이런 문제는 비개발자와 협업할 때 자주 발생합니다. 디자이너는 패딩을 조금 줄여보고 싶고, PM은 다른 텍스트로 바꿔보고 싶은데, 매번 개발자가 코드를 수정해야 합니다.

실시간으로 피드백을 받기 어렵고, 커뮤니케이션 비용이 증가하죠. 바로 이럴 때 필요한 것이 Controls Addon입니다.

Controls를 활성화하면 Storybook UI에 자동으로 생성된 컨트롤 패널이 나타나서, 누구나 코드 수정 없이 props를 바꿔가며 실시간으로 확인할 수 있습니다.

개요

간단히 말해서, Controls Addon은 Story의 args를 UI에서 동적으로 조작할 수 있는 패널을 자동으로 생성합니다. 실무에서 Card 컴포넌트를 만들었다고 생각해보세요.

title, description, imageUrl, variant 같은 props가 있는데, 모든 조합을 미리 Story로 만들 수는 없습니다. Controls를 사용하면 하나의 Story만 만들고, 패널에서 드롭다운으로 variant를 선택하고, 텍스트 박스로 title을 수정하며, 모든 경우를 실시간으로 확인할 수 있습니다.

기존에는 각 상태마다 하드코딩된 Story를 만들거나, 코드를 직접 수정해야 했다면, 이제는 브라우저에서 클릭만으로 모든 것을 제어할 수 있습니다. Controls의 핵심 특징은 자동 생성, 타입 기반 UI, 실시간 업데이트입니다.

TypeScript 타입이나 PropTypes에서 자동으로 컨트롤을 생성하고, 각 타입에 맞는 UI(텍스트 박스, 드롭다운, 컬러 피커 등)를 제공하며, 값이 변경되면 즉시 컴포넌트가 리렌더링됩니다. 이러한 특징들이 개발자와 비개발자 간의 협업을 원활하게 하고, 탐색적 테스트를 가능하게 합니다.

코드 예제

// Card.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Card } from './Card';

const meta: Meta<typeof Card> = {
  title: 'Components/Card',
  component: Card,
  // ArgTypes로 Controls 커스터마이징
  argTypes: {
    variant: {
      control: 'select',
      options: ['default', 'elevated', 'outlined'],
      description: '카드 스타일 변형',
      table: {
        type: { summary: 'string' },
        defaultValue: { summary: 'default' },
      },
    },
    title: {
      control: 'text',
      description: '카드 제목',
    },
    imageUrl: {
      control: 'text',
      description: '이미지 URL',
    },
    padding: {
      control: { type: 'range', min: 0, max: 48, step: 4 },
      description: '내부 패딩 (px)',
    },
    backgroundColor: {
      control: 'color',
      description: '배경색',
    },
    showFooter: {
      control: 'boolean',
      description: '푸터 표시 여부',
    },
  },
};

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

// 기본 Story - Controls로 모든 props 제어 가능
export const Interactive: Story = {
  args: {
    variant: 'default',
    title: 'Interactive Card',
    imageUrl: 'https://via.placeholder.com/300',
    padding: 16,
    backgroundColor: '#ffffff',
    showFooter: true,
  },
};

설명

이것이 하는 일: Card 컴포넌트의 모든 props를 Storybook UI의 Controls 패널에서 실시간으로 조작할 수 있게 만듭니다. 첫 번째로, argTypes 객체가 각 prop의 컨트롤 타입을 명시적으로 정의합니다.

variant는 'select' 컨트롤로 드롭다운을 제공하고, padding은 'range' 컨트롤로 슬라이더를 제공하며, backgroundColor는 'color' 컨트롤로 컬러 피커를 제공합니다. control이 명시되지 않으면 TypeScript 타입에서 자동으로 추론되지만, 명시적으로 지정하면 더 나은 UX를 제공할 수 있습니다.

그 다음으로, Story의 args가 Controls 패널의 초기값으로 표시됩니다. 사용자가 패널에서 variant를 'elevated'로 바꾸면, Storybook은 내부적으로 args를 업데이트하고 컴포넌트를 리렌더링합니다.

이 모든 과정이 React의 state 관리로 구현되어 있어서, 변경 사항이 즉시 반영됩니다. 마지막으로, table 필드는 Docs 페이지의 props 테이블을 커스터마이징합니다.

type.summary는 타입 정보를 보여주고, defaultValue.summary는 기본값을 표시합니다. description은 Controls 패널과 Docs 페이지 양쪽에 모두 표시되어, 각 prop의 역할을 설명합니다.

여러분이 이 코드를 사용하면 개발자가 아닌 팀원도 Storybook을 열어서 직접 컴포넌트를 조작하며 피드백을 줄 수 있습니다. 디자이너는 padding과 backgroundColor를 조정해서 최적의 조합을 찾고, PM은 다양한 title과 variant를 시도해서 요구사항을 구체화할 수 있습니다.

또한 QA는 극단적인 값(매우 긴 텍스트, 0 패딩 등)을 입력해서 엣지 케이스를 테스트할 수 있어서, 버그를 조기에 발견할 수 있습니다.

실전 팁

💡 range 컨트롤에는 반드시 min, max, step을 명시하세요. 적절한 범위를 설정하면 사용자가 유효한 값만 입력하도록 유도할 수 있습니다.

💡 흔한 실수: 복잡한 객체나 함수 props는 Controls로 편집하기 어렵습니다. table: { disable: true }로 숨기고, 별도 Story로 시나리오를 만드세요.

💡 성능 팁: Docs 페이지에서는 Controls가 disabled 상태로 표시됩니다. args를 변경하려면 Canvas 탭으로 이동하세요.

💡 디버깅 팁: Controls가 나타나지 않으면 .storybook/main.js에 '@storybook/addon-controls'가 추가되어 있는지 확인하세요. 대부분의 preset에는 기본 포함되어 있습니다.

💡 발전된 사용법: if 조건으로 특정 prop 값에 따라 다른 Controls를 보여줄 수 있습니다. 예: showFooter가 true일 때만 footerText 컨트롤을 활성화하려면 argTypes에서 if 필드를 사용하세요.


6. Actions Addon

시작하며

여러분이 Button 컴포넌트를 개발하면서, onClick 핸들러가 올바른 인자와 함께 호출되는지 확인하고 싶은데, 어떻게 해야 할지 막막했던 경험이 있나요? console.log를 추가하고 브라우저 콘솔을 확인하는 것은 번거롭습니다.

이런 문제는 이벤트 핸들러가 많은 컴포넌트를 개발할 때 특히 불편합니다. onChange, onSubmit, onFocus, onBlur 같은 여러 핸들러가 올바르게 호출되는지, 어떤 데이터를 전달하는지 일일이 확인하기 어렵죠.

또한 팀원들과 공유할 때 "이 버튼을 클릭하면 이런 이벤트가 발생합니다"라고 설명하기도 어렵습니다. 바로 이럴 때 필요한 것이 Actions Addon입니다.

Actions를 사용하면 모든 이벤트 핸들러 호출이 자동으로 로깅되어, Storybook UI의 Actions 패널에서 호출 시간, 인자, 횟수를 시각적으로 확인할 수 있습니다.

개요

간단히 말해서, Actions Addon은 이벤트 핸들러 호출을 자동으로 캡처해서 UI 패널에 표시하는 도구입니다. 실무에서 Form 컴포넌트를 개발한다고 생각해보세요.

onSubmit, onChange, onError 같은 다양한 콜백이 있는데, 각각이 언제 어떤 데이터와 함께 호출되는지 확인하려면 코드를 수정하고 콘솔을 모니터링해야 합니다. Actions를 사용하면 자동으로 모든 호출이 기록되고, 패널에서 JSON 형태로 인자를 확인할 수 있습니다.

기존에는 콘솔에 로그를 출력하거나 디버거를 사용해야 했다면, 이제는 Storybook UI에서 깔끔하게 정리된 로그를 볼 수 있습니다. Actions의 핵심 특징은 자동 감지, 구조화된 로그, 재현 가능성입니다.

'on'으로 시작하는 props를 자동으로 감지해서 로깅하고, 호출 시간과 인자를 구조화된 형태로 보여주며, 로그를 보고 동일한 인터랙션을 재현할 수 있습니다. 이러한 특징들이 이벤트 디버깅을 쉽게 만들고, 컴포넌트의 동작을 문서화합니다.

코드 예제

// Form.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { Form } from './Form';

const meta: Meta<typeof Form> = {
  title: 'Components/Form',
  component: Form,
  // 'on'으로 시작하는 모든 props를 자동 로깅
  argTypes: {
    onSubmit: { action: 'submitted' },
    onChange: { action: 'changed' },
    onError: { action: 'error occurred' },
  },
};

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

// 기본 Story - Actions 자동 로깅
export const Default: Story = {
  args: {
    // action() 함수로 명시적으로 정의
    onSubmit: action('form-submitted'),
    onChange: action('field-changed'),
    onFocus: action('field-focused'),
    onBlur: action('field-blurred'),
  },
};

// 커스텀 데이터 로깅
export const WithValidation: Story = {
  args: {
    onSubmit: (data) => {
      // 유효성 검사 결과도 로깅
      const isValid = data.email && data.password.length >= 8;
      action('form-submitted')({
        data,
        isValid,
        timestamp: new Date().toISOString(),
      });
    },
    onError: (errors) => {
      action('validation-failed')({
        errorCount: errors.length,
        errors,
      });
    },
  },
};

설명

이것이 하는 일: Form 컴포넌트의 모든 이벤트 핸들러 호출을 Actions 패널에 자동으로 로깅합니다. 첫 번째로, argTypes의 action 필드가 자동 로깅을 활성화합니다.

onSubmit: { action: 'submitted' }로 정의하면, Storybook이 자동으로 onSubmit prop에 로깅 함수를 주입합니다. 컴포넌트에서 onSubmit이 호출될 때마다 'submitted'라는 이름으로 Actions 패널에 기록되죠.

이 방법은 간단한 로깅에 적합합니다. 그 다음으로, action() 함수를 사용하면 더 명시적으로 제어할 수 있습니다.

action('form-submitted')는 로깅 함수를 생성하고, 이 함수가 호출되면 전달된 모든 인자가 자동으로 직렬화되어 패널에 표시됩니다. 객체, 배열, 심지어 이벤트 객체도 JSON 형태로 확인할 수 있어서, 어떤 데이터가 전달되었는지 명확히 알 수 있습니다.

마지막으로, 커스텀 로직을 추가해서 더 풍부한 정보를 로깅할 수 있습니다. WithValidation Story처럼 핸들러 함수를 직접 작성하고, 내부에서 action()을 호출하면 유효성 검사 결과나 타임스탬프 같은 추가 정보를 함께 로깅할 수 있습니다.

이렇게 하면 단순한 호출 기록을 넘어서, 컴포넌트의 비즈니스 로직까지 추적할 수 있습니다. 여러분이 이 코드를 사용하면 모든 이벤트 흐름을 시각적으로 추적할 수 있습니다.

폼을 제출했을 때 onChange가 몇 번 호출되었는지, 각 호출마다 어떤 값이 전달되었는지, 에러가 발생했는지 한눈에 볼 수 있습니다. 팀원들과 버그를 논의할 때도 Actions 패널의 로그를 스크린샷으로 공유하면, "3번째 onChange 호출에서 값이 잘못 전달되었습니다"처럼 명확히 소통할 수 있습니다.

또한 Play Function과 결합하면 자동화된 인터랙션의 모든 이벤트를 기록해서, 회귀 테스트의 증거로 활용할 수 있습니다.

실전 팁

💡 .storybook/preview.js에서 actions: { argTypesRegex: '^on[A-Z].*' }로 설정하면 'on'으로 시작하는 모든 props를 자동으로 로깅합니다. 개별 설정이 필요 없어집니다.

💡 흔한 실수: action() 호출을 렌더링 중에 하면 매번 새로운 함수가 생성되어 불필요한 리렌더링이 발생합니다. args 레벨에서 한 번만 정의하세요.

💡 성능 팁: 대량의 이벤트가 발생하는 컴포넌트(예: 드래그 중 mousemove)는 Actions 패널이 느려질 수 있습니다. 패널을 닫거나 debounce를 적용하세요.

💡 디버깅 팁: Actions 패널에서 각 로그를 클릭하면 전달된 인자의 상세 내용을 JSON 뷰어로 볼 수 있습니다. 중첩된 객체도 펼쳐서 확인 가능합니다.


7. Docs Addon

시작하며

여러분이 컴포넌트 라이브러리를 만들었는데, 팀원들이 "이 컴포넌트 어떻게 사용하나요?", "어떤 props가 있나요?"라고 계속 물어오는 상황을 겪어본 적 있나요? 별도로 문서를 작성하고 유지보수하는 것은 시간이 많이 걸립니다.

이런 문제는 컴포넌트가 늘어날수록 더 심각해집니다. 문서가 코드와 따로 관리되면 싱크가 안 맞고, props가 변경되어도 문서는 업데이트되지 않아서 팀원들이 혼란을 겪죠.

또한 Notion이나 Confluence에 문서를 작성하면 코드와 멀리 떨어져 있어서 찾기도 어렵습니다. 바로 이럴 때 필요한 것이 Docs Addon입니다.

Docs를 사용하면 코드에서 자동으로 문서를 생성해서, 별도의 작성 없이 항상 최신 상태의 완벽한 문서를 제공할 수 있습니다.

개요

간단히 말해서, Docs Addon은 컴포넌트의 소스 코드와 Story에서 자동으로 문서 페이지를 생성합니다. 실무에서 Tooltip 컴포넌트를 만들었다고 생각해보세요.

TypeScript 타입, JSDoc 주석, Story들이 이미 있는데, 이들을 기반으로 Docs가 자동으로 props 테이블, 사용 예제, 인터랙티브 플레이그라운드를 만들어줍니다. 코드가 변경되면 문서도 자동으로 업데이트되죠.

기존에는 마크다운 문서를 별도로 작성하고 수동으로 업데이트해야 했다면, 이제는 코드만 작성하면 문서가 자동으로 생성됩니다. Docs의 핵심 특징은 자동 생성, 인터랙티브 예제, MDX 확장성입니다.

TypeScript 타입과 JSDoc에서 자동으로 문서를 만들고, Story를 문서 안에 임베드해서 직접 조작할 수 있으며, MDX로 커스텀 콘텐츠를 추가할 수 있습니다. 이러한 특징들이 문서 작성 비용을 제로에 가깝게 만들고, 코드와 문서의 일관성을 보장합니다.

코드 예제

// Tooltip.tsx
/**
 * Tooltip은 요소에 마우스를 올렸을 때 추가 정보를 보여줍니다.
 *
 * @example
 * ```tsx
 * <Tooltip content="도움말">
 *   <Button>Hover me</Button>
 * </Tooltip>
 * ```
 */
export interface TooltipProps {
  /** 툴팁에 표시될 내용 */
  content: string;
  /** 툴팁이 나타날 위치 */
  placement?: 'top' | 'bottom' | 'left' | 'right';
  /** 지연 시간 (밀리초) */
  delay?: number;
  /** 자식 요소 */
  children: React.ReactNode;
}

// Tooltip.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Tooltip } from './Tooltip';

const meta: Meta<typeof Tooltip> = {
  title: 'Components/Tooltip',
  component: Tooltip,
  // autodocs 태그로 자동 문서화 활성화
  tags: ['autodocs'],
  // 컴포넌트 설명
  parameters: {
    docs: {
      description: {
        component: '툴팁은 접근성을 고려해 설계되었으며, 키보드 포커스에도 반응합니다.',
      },
    },
  },
  argTypes: {
    placement: {
      control: 'select',
      description: '툴팁 위치를 변경합니다',
      table: {
        type: { summary: 'string' },
        defaultValue: { summary: 'top' },
      },
    },
  },
};

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

/**
 * 기본 툴팁 사용 예제입니다.
 * 버튼에 마우스를 올리면 툴팁이 나타납니다.
 */
export const Default: Story = {
  args: {
    content: '이것은 툴팁입니다',
    placement: 'top',
    children: <button>Hover me</button>,
  },
};

설명

이것이 하는 일: Tooltip 컴포넌트의 모든 정보를 자동으로 수집해서 완전한 문서 페이지를 생성합니다. 첫 번째로, JSDoc 주석이 문서의 기본 내용을 제공합니다.

TooltipProps 인터페이스의 /** */ 주석이 각 prop의 설명으로 변환되고, @example 태그의 코드는 문서에 예제 코드 블록으로 표시됩니다. TypeScript 타입(string, 'top' | 'bottom' 등)은 자동으로 props 테이블의 Type 열에 표시되죠.

그 다음으로, Story의 tags: ['autodocs']가 자동 문서 생성을 활성화합니다. Storybook은 컴포넌트를 분석해서 Docs 탭을 생성하고, 상단에 컴포넌트 설명, 중간에 props 테이블, 하단에 모든 Story를 인터랙티브하게 배치합니다.

각 Story 위에는 Story의 JSDoc 주석(/** 기본 툴팁 사용 예제입니다 */)이 설명으로 표시됩니다. 마지막으로, parameters.docs로 문서를 커스터마이징할 수 있습니다.

description.component는 props 테이블 위에 추가 설명을 표시하고, argTypes의 table 필드는 props 테이블의 각 행을 커스터마이징합니다. Story 레벨에서도 parameters를 정의해서 특정 Story의 문서만 수정할 수 있습니다.

여러분이 이 코드를 사용하면 코드 작성만으로 완전한 문서가 자동 생성됩니다. 새로운 팀원이 합류하면 Storybook의 Docs 탭만 보여주면 되고, props가 추가되거나 변경되면 문서도 자동으로 업데이트됩니다.

또한 Storybook을 정적 사이트로 빌드해서 배포하면, 별도의 문서 사이트 없이 팀 전체가 최신 컴포넌트 문서를 참조할 수 있습니다. MDX 파일을 추가하면 설치 가이드, 디자인 철학 같은 커스텀 페이지도 만들 수 있어서, 완전한 디자인 시스템 문서를 구축할 수 있습니다.

실전 팁

💡 JSDoc을 꼼꼼히 작성하세요. 각 prop에 /** 설명 */을 추가하면 props 테이블의 Description 열에 자동으로 표시됩니다.

💡 흔한 실수: tags: ['autodocs']를 빼먹으면 Docs 탭이 생성되지 않습니다. Meta 객체에 반드시 포함하세요.

💡 성능 팁: 문서에 표시할 필요 없는 내부 props는 argTypes에서 table: { disable: true }로 숨기세요. 문서가 깔끔해집니다.

💡 디버깅 팁: props 테이블이 비어있다면 TypeScript 타입이 제대로 export되지 않았거나, react-docgen이 인식하지 못하는 복잡한 타입일 수 있습니다. 인터페이스를 단순화해보세요.

💡 발전된 사용법: MDX로 .stories.mdx 파일을 만들면 문서를 완전히 커스터마이징할 수 있습니다. Canvas, ArgsTable, Story 같은 Doc Blocks를 사용해서 원하는 대로 배치하세요.


8. CSF3 문법

시작하며

여러분이 Storybook을 오래 사용했다면, 예전에는 export const Primary = () => <Button label="Click" />처럼 함수로 Story를 작성했던 기억이 있을 겁니다. 보일러플레이트 코드가 많고, args를 사용하려면 Template을 만들어야 했죠.

이런 문제는 Story가 많아질수록 코드 중복을 증가시킵니다. 각 Story마다 render 함수를 작성하고, Template.bind({})를 호출하고, args를 별도로 정의해야 해서 코드가 길어지고 읽기 어려워졌습니다.

또한 TypeScript와의 통합도 완벽하지 않아서 타입 안정성이 부족했죠. 바로 이럴 때 필요한 것이 CSF3(Component Story Format 3.0) 문법입니다.

CSF3를 사용하면 객체 리터럴만으로 간결하게 Story를 정의하고, 완전한 TypeScript 지원과 더 나은 개발자 경험을 얻을 수 있습니다.

개요

간단히 말해서, CSF3는 Story를 객체로 정의하는 새로운 문법으로, 기존 CSF2보다 간결하고 타입 안전합니다. 실무에서 여러 개의 Button Story를 만든다고 생각해보세요.

CSF2에서는 Template을 만들고 각 Story마다 bind()를 호출해야 했지만, CSF3에서는 그냥 객체에 args를 정의하면 끝입니다. 코드 줄 수가 절반으로 줄어들고, 읽기도 훨씬 쉬워집니다.

기존에는 함수 기반 Story와 Template 패턴을 사용했다면, 이제는 선언적인 객체 리터럴로 모든 것을 표현할 수 있습니다. CSF3의 핵심 특징은 간결성, 타입 안정성, 유연성입니다.

보일러플레이트 코드가 최소화되고, StoryObj 타입으로 완전한 타입 체크를 제공하며, render 함수로 필요할 때만 커스터마이징할 수 있습니다. 이러한 특징들이 Story 작성을 더 빠르고 안전하게 만들고, 코드 가독성을 크게 향상시킵니다.

코드 예제

// Button.stories.tsx - CSF3 방식
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

// Meta 정의는 동일
const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
};

export default meta;
// StoryObj 타입으로 완전한 타입 안정성
type Story = StoryObj<typeof Button>;

// Story는 간단한 객체 리터럴
export const Primary: Story = {
  args: {
    label: 'Primary Button',
    variant: 'primary',
  },
};

export const Secondary: Story = {
  args: {
    label: 'Secondary Button',
    variant: 'secondary',
  },
};

// render 함수로 커스텀 렌더링
export const WithIcon: Story = {
  args: {
    label: 'With Icon',
  },
  render: (args) => (
    <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
      <svg width="16" height="16">...</svg>
      <Button {...args} />
    </div>
  ),
};

// 다른 Story 확장하기
export const LargePrimary: Story = {
  ...Primary,
  args: {
    ...Primary.args,
    size: 'large',
  },
};

설명

이것이 하는 일: Button 컴포넌트의 다양한 Story를 최소한의 코드로 정의하고, TypeScript의 완전한 타입 체크를 받습니다. 첫 번째로, type Story = StoryObj<typeof Button>이 타입 안정성의 핵심입니다.

StoryObj는 Button 컴포넌트의 props 타입을 추론해서, Story 객체의 args에 잘못된 prop을 추가하면 즉시 컴파일 에러를 발생시킵니다. 예를 들어 variant에 'invalid'를 넣으면 TypeScript가 에러를 표시하죠.

이렇게 하면 런타임 에러를 방지하고, IDE의 자동완성 기능도 완벽하게 작동합니다. 그 다음으로, 각 Story는 단순한 객체 리터럴로 정의됩니다.

Primary와 Secondary Story처럼 args만 다르면, 각각 별도의 객체로 정의하기만 하면 됩니다. Storybook은 내부적으로 이 args를 컴포넌트의 props로 전달해서 렌더링하므로, render 함수를 직접 작성할 필요가 없습니다.

코드가 극도로 간결해지고, Story의 의도가 명확히 드러납니다. 마지막으로, 필요할 때만 render 함수로 커스터마이징할 수 있습니다.

WithIcon Story처럼 컴포넌트 주변에 추가 요소를 배치하거나 복잡한 레이아웃이 필요할 때, render: (args) => JSX 형태로 정의합니다. args를 받아서 원하는 대로 렌더링하면 되죠.

또한 LargePrimary처럼 스프레드 연산자로 다른 Story를 확장할 수 있어서, 공통 args를 재사용하고 일부만 오버라이드할 수 있습니다. 여러분이 이 코드를 사용하면 Story 작성 시간이 크게 단축됩니다.

Template.bind({})나 export const Story = Template.bind({}) 같은 보일러플레이트가 사라지고, 핵심 내용만 남아서 코드 리뷰도 쉬워집니다. TypeScript 에러를 통해 잘못된 props를 조기에 발견할 수 있고, IDE의 자동완성으로 빠르게 작성할 수 있습니다.

또한 기존 CSF2 Story를 CSF3로 마이그레이션하기도 쉬워서, 점진적으로 프로젝트를 업그레이드할 수 있습니다.

실전 팁

💡 항상 type Story = StoryObj<typeof Component>를 정의하세요. 이 한 줄이 모든 Story에 타입 안정성을 제공합니다.

💡 흔한 실수: render 함수를 사용할 때 args를 컴포넌트에 전달하는 것을 잊으면 Story가 제대로 작동하지 않습니다. <Button {...args} />처럼 스프레드로 전달하세요.

💡 성능 팁: 대부분의 Story는 render 없이 args만으로 정의하세요. render는 정말 필요할 때만 사용해야 코드가 간결합니다.

💡 디버깅 팁: Story가 예상과 다르게 렌더링되면, Meta의 render가 Story의 render보다 우선순위가 낮다는 점을 기억하세요. Story 레벨의 render가 항상 오버라이드합니다.

💡 발전된 사용법: satisfies 연산자로 타입 안정성과 타입 추론을 동시에 얻으세요. export const Primary = { args: {...} } satisfies Story는 타입을 체크하면서도 구체적인 타입을 유지합니다.


#Storybook#Stories#CSF#Args#Decorators#React

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.
이전3/3
다음