스토리북 조금 더 잘 사용하기. Auto Docs, decorators, play function. feat Storybook 8.2

글쓰고 있는 현재 스토리북은 8.2버전까지 출시되었습니다. 이번 업데이트를 통해서 다양한 테스트 툴에서 사용되고 있는 jasmine 스타일의 테스트 코드로써 스토리를 작성할 수 있게끔, 러닝 커브를 줄이려는 모습을 보여주고 있다는 생각도 들고, 다른 테스트툴과의 연결성을 신경쓰는 듯한 모습을 보여주고 있어서 추후 더 기대되는 툴이라고 생각되어 글을 작성하게 되었습니다!

스토리북을 처음 사용하시는 분들을 위한 글로 처음부터 차근차근 정리하는 식으로 글을 구성하였습니다.

이 글은 스토리북을 사용하여 리액트 컴포넌트를 테스트하는 방법을 다룹니다. 우선 스토리북이 무엇인지 부터 알아가보도록 하죠!

1️⃣ 스토리북(Storybook)이란 무엇인가?

스토리북은 UI 컴포넌트 개발을 위한 오픈소스 도구입니다. 이를 사용하면 UI 컴포넌트를 독립적으로 개발하고 테스트할 수 있으며, 시각적으로 확인하면서 개발을 진행할 수 있습니다. 스토리북의 가장 큰 강점은 코드와 디자인의 일관성을 유지하면서 개발 속도를 높일 수 있다는 점입니다. 이는 특히 대규모 프로젝트에서 UI 컴포넌트가 많아질 때 유용합니다.

아래 링크를 통해 다양한 스토리 예시를 확인할 수 있습니다!

 

Component Encyclopedia | Storybook

Explore 1000s of components and libraries

storybook.js.org

 

시각적 회귀(Visual Regression)

스토리북을 사용하면 시각적 회귀 테스트를 통해 UI가 변경되지 않았는지 확인할 수 있습니다. 시각적 회귀는 이전 버전과 현재 버전의 UI를 비교하여 시각적 차이를 감지하는 테스트입니다. 이는 코드 수정이 의도치 않은 UI 변경을 초래하는 것을 방지하는 데 매우 유용합니다.

독립적인 컴포넌트 개발

스토리북은 컴포넌트를 독립적으로 개발하고 테스트할 수 있는 환경을 제공합니다. 이로 인해 컴포넌트 간의 의존성을 최소화하고, 다른 부분에 영향을 미치지 않으면서 컴포넌트를 개선할 수 있습니다.

문서화 자동화

스토리북을 사용하면 컴포넌트의 문서를 자동으로 생성할 수 있습니다. 이는 개발자뿐만 아니라 디자이너, QA 팀 등 다른 팀원들에게도 유용한 정보를 제공합니다. 예를 들어, 컴포넌트의 사용 방법, 속성, 다양한 상태 등을 명확하게 설명할 수 있습니다.

디자인 시스템 구축

스토리북은 일관된 디자인 시스템을 구축하는 데 도움이 됩니다. 모든 UI 컴포넌트를 하나의 장소에서 관리하고, 이를 기준으로 다양한 페이지나 애플리케이션을 구축할 수 있습니다. 이렇게 하면 디자인의 일관성을 유지하고 재사용성을 극대화할 수 있습니다.

기존 테스트와의 차이점

스토리북과 기존 테스트의 가장 큰 차이점은 개발 방식입니다. 기존의 단위 테스트나 통합 테스트는 주로 로직과 기능의 정확성을 검증하는 데 초점을 맞춥니다. 반면 스토리북은 시각적 요소와 UI 상호작용을 중점적으로 테스트합니다.

스토리북을 통해 컴포넌트의 다양한 상태를 정의하고, 이를 시각적으로 확인하면서 개발할 수 있습니다. 또한, 다양한 사용 사례에 따라 컴포넌트를 분리하여 관리할 수 있어, 컴포넌트의 복잡성이 증가하더라도 이를 쉽게 관리할 수 있습니다.

즉 CDD (Component Driven Development)가 가능하게 되는 것입니다!

2️⃣ 리액트에서의 스토리북 시작하기

리액트에서 스토리북을 사용하는 방법을 살펴보겠습니다. 스토리북을 설치하려면 다음 명령어를 사용합니다.

 

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

 

npx storybook@latest init

이 명령어를 실행하면 스토리북이 프로젝트에 설정되고, 필요한 파일들이 생성됩니다. 설정이 완료되면 스토리북을 실행할 수 있습니다:

npm run storybook

스토리북의 기본적인 스토리 형식은 다음과 같습니다:

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

const meta: Meta<typeof Button> = {
  component: Button,
};

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

export const Primary: Story = {
  args: {
    primary: true,
    label: 'Button',
  },
};

이 예제는 간단한 버튼 컴포넌트를 정의하고, 이를 스토리북에서 테스트할 수 있도록 설정한 것입니다. Primary 스토리는 버튼이 primary 상태일 때의 모습을 보여줍니다.

3️⃣ 리액트 컴포넌트와 AutoDocs 기능을 통한 스토리북 예제

이제, 스토리북을 사용하여 리액트 컴포넌트를 구현하는 예제를 살펴보겠습니다. 다음은 ToggleSwitch 컴포넌트를 정의한 코드입니다.

import S, { StyleProps } from './style';

interface ToggleSwitchProps extends StyleProps {
  onChange: () => void;
}

export default function ToggleSwitch({ isChecked, isDisabled, onClick }: ToggleSwitchProps) {
  return (
    <S.Switch
      isChecked={isChecked}
      isDisabled={isDisabled}
      onClick={onClick}
    >
      <S.Knob
        isChecked={isChecked}
        isDisabled={isDisabled}
      />
    </S.Switch>
  );
}

위 컴포넌트는 단순한 토글 스위치입니다. isChecked, isDisabled와 같은 속성을 받아 상태를 제어하고, onChange 이벤트를 통해 스위치가 변경될 때 처리할 로직을 전달받습니다.

이제 이 컴포넌트를 스토리북에서 관리하기 위해 다음과 같은 스토리를 작성할 수 있습니다:

import { useArgs } from '@storybook/preview-api';
import type { Meta, StoryObj } from '@storybook/react';
import ToggleSwitch from './index';

const meta: Meta<typeof ToggleSwitch> = {
  title: 'Common/ToggleSwitch',
  component: ToggleSwitch,
  parameters: {
    layout: 'centered',
    docs: {
      description: {
        component: 'ToggleSwitch 컴포넌트는 스위치를 토글할 수 있는 기능을 제공합니다.',
      },
    },
  },
  tags: ['autodocs'],
  argTypes: {
    isChecked: {
      description: '스위치의 체크 상태를 나타냅니다.',
      control: { type: 'boolean' },
      table: {
        type: { summary: 'boolean' },
      },
    },
    isDisabled: {
      description: '스위치의 활성화 상태를 나타냅니다.',
      control: { type: 'boolean' },
      table: {
        type: { summary: 'boolean' },
      },
    },
    onClick: {
      description: '스위치 상태 변경 시 호출되는 콜백 함수입니다.',
      action: 'clicked',
      table: {
        type: { summary: '() => void' },
      },
    },
  },
};

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

export const Default: Story = {
  args: {
    isChecked: false,
  },
};

export const Disabled: Story = {
  args: {
    isChecked: false,
    isDisabled: true,
  },
};

이 스토리를 통해 ToggleSwitch 컴포넌트의 다양한 상태를 정의하고, 각 상태를 시각적으로 확인할 수 있습니다. 기본적으로 스토리북에서 제공하는 자동 문서화 기능을 사용하여 컴포넌트에 대한 설명을 추가했습니다.

스토리북은 tags: ['autodocs'] 라는 코드를 한줄 추가하여, 자동으로 문서화를 도와줍니다.

내부 description 값을 통해 해당 Props과 Component에 대한 내용을 수정할 수 있습니다.

관련 공식 문서는 아래를 참고하세요.

 

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

 

3️⃣ Decorator를 활용한 기능 확장

스토리북에서는 Decorator를 사용하여 컴포넌트의 스토리를 확장할 수 있습니다.

예를 들어, useArgs 훅을 사용하여 스토리의 상태를 관리하고 이를 UI에 반영하는 방법은 다음과 같습니다.

export const Disabled: Story = {
  args: {
    isChecked: false,
    isDisabled: true,
  },
  decorators: [
  (Story, context) => {
    const [args, updateArgs] = useArgs();

    const handleChange = () => {
      updateArgs({ isChecked: !context.args.isChecked });
    };

    return (
         <Story
           args={{
             ...args,
             onClick: handleChange,
           }}
         />
       );
     },
   ],
};

이 코드는 useArgs 훅을 사용하여 스토리의 상태를 동적으로 업데이트합니다. 이를 통해 스토리북 UI 내에서 컴포넌트의 상태 변화를 실시간으로 확인할 수 있습니다.

useArgs와 useState의 차이점?

useArgs를 사용하는 이유는 스토리북의 UI와 상호작용을 더욱 쉽게 할 수 있기 때문입니다. useState를 사용하면 컴포넌트 내부에서만 상태를 관리할 수 있지만, useArgs는 스토리북의 UI에서 상태를 제어하고 업데이트할 수 있습니다. 이를 통해 테스트와 개발을 더 효율적으로 할 수 있습니다.

하지만 useArgs의 경우 스토리북 UI가 재 렌더링 되는 것 때문에, 스토리북에서 제공하는 Addon들과 함께 사용할 수 없습니다. 이와 관련된 글은 다음 포스팅을 참고하세요!

4️⃣ 스토리북의 Play function을 이용한 Interaction 테스트

 

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

 

스토리북에서는 play function을 사용하여 컴포넌트의 상호작용 테스트를 수행할 수 있습니다.

npx storybook@latest init 이 명령어로 storybook을 시작하였다면 아래 의존성은 설치할 필요가 없습니다.

npm install @storybook/test @storybook/addon-interactions --save-dev

ToggleSwitch 컴포넌트의 클릭 이벤트를 테스트하려면 다음과 같이 작성할 수 있습니다.

import { userEvent, within } from '@storybook/test';
// ...

export const Default: Story = {
  args: {
    isChecked: false,
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const toggleSwitch = canvas.getByTestId('toggle-switch');

    await userEvent.click(toggleSwitch);
    expect(toggleSwitch).toBeTruthy();
  },
};

이 코드는 스토리북 내에서 컴포넌트를 렌더링하고, 특정 상호작용을 수행한 후 결과를 검증하는 테스트입니다.

Test Runner를 통한 테스트 자동화 하기

 

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

 

스토리북은 또한 Test Runner를 통해 자동화된 테스트를 수행할 수 있습니다.

의존성을 설치하고 package.json에 script를 추가합니다.

npm install @storybook/test-runner --save-dev
{
  "scripts": {
    "test-storybook": "test-storybook"
  }
}

다음 명령어를 사용하여 Test Runner를 실행할 수 있습니다.

npm run test-storybook

이 명령어를 통해 스토리북의 모든 스토리에 대해 정의된 테스트를 자동으로 실행할 수 있습니다. 테스트 속도를 높이기 위해 특정 스토리만 테스트하도록 설정할 수 있습니다.

//.storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  tags: {
    include: ['test-only', 'pages'],
    exclude: ['no-tests', 'tokens'],
    skip: ['skip-test', 'layout'],
  },
};

export default config;

위와 같이 설정하면, 특정 태그를 가진 스토리만 테스트할 수 있습니다. 이는 테스트 시간을 줄이고, 필요 없는 테스트를 방지하는 데 도움이 됩니다.

import { userEvent, within } from '@storybook/test';
//...
export const Default: Story = {
  args: {
    isChecked: false,
  },
  tags: ['test-only'], // tag를 추가합니다.
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const toggleSwitch = canvas.getByTestId('toggle-switch');

    await userEvent.click(toggleSwitch);
    expect(toggleSwitch).toBeTruthy();
  },
};

→ 실제로 사용해보니, exclude와 skip의 차이가 없네요..? 모두 skip로 표시됩니다.

exclude같은 경우 테스트 목록에 포함되지 않는 것, skip은 테스트 목록에는 포함되지만 테스트는 실행되지 않는 것으로 알고있는데, exclude 역시 skip으로 표시가 됩니다.

아무튼 skip은 할 수 있습니다.

Storybook 8.2에서 추가된 mount

Storybook 8.2에서는 before 훅(클린업 함수를 반환하여 after 훅으로 사용할 수 있음)과 React, Vue 3, Svelte용 play의 선택적 mount 인수를 도입했습니다. 이를 통해 이제는 ‘Arrange’, ‘Act’, ‘Assert’를 하나의 play 함수 내에서 할 수 있어, Jasmine 스타일 도구와 동일한 흐름으로 컴포넌트 테스트를 작성할 수 있습니다.

import { fn, expect } from "@storybook/test";
import { ToolbarMenu } from "./ToolbarMenu";

export default {
  component: ToolbarMenu,
};

export const Disabled = {
  args: { disabled: true, onSelected: fn() },
  play: async ({ mount, args }) => {
    // Arrange
    const items = await loadItems(10);
    const canvas = await mount(<ToolbarMenu items={items} />);

    // Act
    await userEvent.click(canvas.getByRole("button"));

    // Assert
    expect(canvas.getAllByRole("button").length).toBe(items.length);
    expect(args.onSelected).not.toHaveBeenCalled();
  },
};

BeforeAll 은 다음과 같이 상용할 수 있습니다.

import { Preview } from '@storybook/your-renderer';
 
import { init } from '../project-bootstrap';
 
const preview: Preview = {
  async beforeAll() {
    await init();
  },
};
 
export default preview;

끝으로

스토리북은 컴포넌트를 위한 모든 테스트를 지원하는 것으로 보입니다. CDD에서 아주 중요한 툴이라고 생각됩니다. 복잡해지는 현대 웹 UI/ 생태계에서 필수적인 툴이라는 생각도 들구요.

오탈자/잘못된 정보가 있으면 댓글로 남겨주세요!