StoryBook 개요

디자인 시스템을 안정적으로 구현시켜주는 테스팅 라이브러리 StoryBook을 공식문서를 통해 학습해보자
강석우's avatar
Mar 06, 2024
StoryBook 개요

스토리북이란?

컴포넌트를 앱의 비즈니스 로직과 컨텍스트의 간섭 없이 렌더링하는 격리된 iframe을 제공하는 라이브러리이다.

누구에게 필요한가?

디자인 시스템을 만들고 있으며 UI 테스트를 자동화 하기 원하는 프론트엔드 개발자들에게 유용하게 사용될 것 같다.

스토리북의 장점

  • UI 테스트에 노동이 덜 들어가게 되며 오류가 사라진다.

  • UI의 문서화가 가능해진다.

  • 스토리들은 UI가 실제로 어떻게 동작하는지 보여준다.

  • UI 흐름 자동화를 통해 인터페이스 테스트가 자동화 된다.

설치법 및 실행법

설치법

설치링크

yarn dlx storybook@latest init

or

npx storybook@latest init

설치를 하게 되면 콘솔창에 실행법 및 설명이 나오게 된다.

실행법

yarn storybook

위의 명령어를 입력하면 스토리북이 실행되고 아래와 같은 창이 브라우저에 뜨게 된다.

컴포넌트 스토리의 구성

Default export

default export metadata는 storybook이 스토리를 나열하는 방식을 제어하고 애드온이 사용하는 정보를 제공한다.

import type { Meta } from '@storybook/react';

import { Button } from './Button';

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

export default meta;

Defining Stories

컴포넌트의 스토리를 정해주자.
스토리북에서 컴포넌트에 들어갈 스토리의 네이밍은 카멜케이스로 작성하기를 권장하고있다.
아래는 Primary 스토리를 추가한 케이스이다.

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>;

/*
 *👇 Render functions are a framework specific feature to allow you control on how the component renders.
 * See https://storybook.js.org/docs/api/csf
 * to learn how to use render functions.
 */
export const Primary: Story = {
  render: () => <Button primary label="Button" />,
};

그래서 스토리 어떻게 작성하는건데?

스토리는 어떻게 컴포넌트를 렌더링 할건지 설명하는 함수이다.
컴포넌트당 여러개의 스토리를 지정해줄 수 있으며 가장 쉽게 구현하는 방법은 여러개의 각각 다른 스토리를 선언해주는 것이다.

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>;

/*
 *👇 Render functions are a framework specific feature to allow you control on how the component renders.
 * See https://storybook.js.org/docs/api/csf
 * to learn how to use render functions.
 */
export const Primary: Story = {
  render: () => <Button backgroundColor="#ff0" label="Button" />,
};

export const Secondary: Story = {
  render: () => <Button backgroundColor="#ff0" label="😄👍😍💯" />,
};

export const Tertiary: Story = {
  render: () => <Button backgroundColor="#ff0" label="📚📕📈🤓" />,
};

args 사용하기

위의 코드는 bolierplate가 많기 때문에 args를 사용해서 코드를 줄일 수가 있다.

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: {
    backgroundColor: '#ff0',
    label: 'Button',
  },
};

export const Secondary: Story = {
  args: {
    ...Primary.args,
    label: '😄👍😍💯',
  },
};

export const Tertiary: Story = {
  args: {
    ...Primary.args,
    label: '📚📕📈🤓',
  },
};

또한 args를 사용해 여러 스토리들을 섞어 하나의 스토리로 표현하는 것 또한 가능하다.

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

import { ButtonGroup } from '../ButtonGroup';

//👇 Imports the Button stories
import * as ButtonStories from './Button.stories';

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

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

export const Pair: Story = {
  args: {
    buttons: [{ ...ButtonStories.Primary.args }, { ...ButtonStories.Secondary.args }],
    orientation: 'horizontal',
  },
};

play function 사용하기

play function 과 @storybook/addon-interactions 을 사용해서 유저와의 인터렉션이 필요한 시나리오를 테스트 할 수 있다.

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

import { within, userEvent } from '@storybook/testing-library';

import { expect } from '@storybook/jest';

import { LoginForm } from './LoginForm';

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

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

export const EmptyForm: Story = {};

/*
 * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const FilledForm: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // 👇 Simulate interactions with the component
    await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');

    await userEvent.type(canvas.getByTestId('password'), 'a-random-password');

    // See https://storybook.js.org/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    await userEvent.click(canvas.getByRole('button'));

    // 👇 Assert DOM structure
    await expect(
      canvas.getByText(
        'Everything is perfect. Your account is ready and we should probably get you started!'
      )
    ).toBeInTheDocument();
  },
};

위의 코드에 대해서는 실습을 통해서 학습이 더 필요할 것 같다.

parameters 사용하기

파라미터는 Storybook에서 이야기의 정적 메타데이터를 정의하는 방법이다.
컴포넌트 수준의 backgrounds 파라미터를 추가할 수 있다.

import React from 'react';
import Button from './Button';

export default {
  title: 'Components/Button',
  component: Button,
  parameters: {
    backgrounds: {
      default: 'light',
      values: [
        { name: 'light', value: '#f0f0f0' },
        { name: 'dark', value: '#333333' },
      ],
    },
  },
};

export const Primary = () => <Button variant="primary">Primary Button</Button>;
export const Secondary = () => <Button variant="secondary">Secondary Button</Button>;

decorators 사용하기

데코레이터(Decorators)는 스토리를 렌더링할 때 컴포넌트를 임의의 마크업으로 감싸는 메커니즘이다.
아래의 코드는 decorators 를 사용해 스토리에 margin값을 추가한 작업이다.

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

import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
  decorators: [
    (Story) => (
      <div style={{ margin: '3em' }}>
        <Story />
      </div>
    ),
  ],
};

export default meta;

Args

"story"는 컴포넌트를 렌더링하는 방식을 정의하는 인수들로 구성된 컴포넌트이며
"args"는 이러한 인수들을 단일 js 객체로 정의하는 매커니즘이다.
인수(arg)의 값이 변경되면 컴포넌트가 다시 렌더링되어 Storybook의 UI에서 args를 변경하는 애드온을 통해 컴포넌트와 상호작용할 수 있다.

args는 story, component, global 레벨에서 선언될 수 있다.

story args

args를 각각의 story 마다 선언해줬다.

import type { Meta, StoryObj } from '@storybook/your-framework';

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',
  },
};

export const PrimaryLongName: Story = {
  args: {
    ...Primary.args,
    label: 'Primary with a really long name',
  },
};

component args

args를 컴포넌트 레벨에서 선언해주었다.
이제 해당 컴포넌트에서는 해당 args 가 적용된다. 물론 story에서 중첩해서 써준다면 속성을 덮을 수 있다.

import type { Meta } from '@storybook/react';

import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
  //👇 Creates specific argTypes
  argTypes: {
    backgroundColor: { control: 'color' },
  },
  args: {
    //👇 Now all Button stories will be primary.
    primary: true,
  },
};

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

global args

전역레벨에서 args를 정의했고 이제 모든 컴포넌트들은 덮어쓰지않는 한 해당 arg 속성을 갖게 된다.

import { Preview } from '@storybook/your-renderer';

const preview: Preview = {
  // The default value of the theme arg for all stories
  args: { theme: 'light' },
};

export default preview;

Share article

석우의 개발블로그