지난 글에서 간단하게 Storybook이 무엇이고 어떤 기능을 제공하는지 알아보았습니다.

이번글에서는 React.js에서 Storybook을 어떻게 적용하고 사용하는지 간단한 투두리스트를 만들면서 알아보려고 합니다.

📙 Storybook-cli 설치 및 시작

먼저 간단하게 create-react-app을 이용하여 프로젝트를 생성하겠습니다.

$ create-react-app storybook-todo

글로벌로 storybook-cli를 설치합니다. 이것을 이용하면 복잡한 설정이나 설치해야할 것을 자동으로 해줍니다.

// npm 설치
$ npm install --save -g @storybook/cli

// yarn 설치
$ yarn add global @storybook/cli

프로젝트의 루트폴더에서 다음 명령어를 이용하면 필요한 의존성을 자동으로 설치해주고 package.jsonscripts 실행 및 빌드 명령어도 추가됩니다.

$ getstorybook init

프로젝트를 시작합니다.9009포트(default)에 프로젝트가 열립니다.

$ npm storybook
$ yarn storybook

캡처.PNG

⚙️ Storybook 설정하기

Storybook을 이용하여 컴포넌트를 테스트할 때, Storybook에 나오는 테스트 케이스를 작성할 때는 test.stories.js라고 파일명을 짓습니다.

.stories.js 라고 된 모든 파일을 불러오기 위해서 필요한 모듈을 설치해줍니다.

$ yarn add -d require-context.macro

아까 명령어를 통해 Storybook을 프로젝트에 추가했다면 루트폴더에 .storybook이라는 폴더가 생겼을 겁니다.

config.js에 들가서 아래와 같이 설정해줍니다.

.storybook/config.js

import { configure } from '@storybook/react';
import requestContext from 'require-context.macro';
import '../src/util.scss';

// *.stories.js으로 끝나는 모든 파일을 import 합니다.
const req = requestContext('../src/components', true, /\.stories\.js$/);

function loadStories() {
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);

✍️ Storybook 작성하기

실제 예제에서는 TastItem, TaskListTaskInput가 있지만, 다 작성하기에는 많기 때문에 이 글에서는 TastItem을 중점적으로 설명하겠습니다.

  • 실제 예제와 스타일은 관련 코드는 글 맨아래 링크의 레포지토리를 참조해주세요 :)

TaskItem 컴포넌트

먼저 예제로 투두리스트에 들어갈 TaskItem을 만들어 보겠습니다.

/src/components/Task 폴더에 TaskItem폴더를 만들고, TaskItem.js파일을 만들어 아래 내용을 작성합니다.

./src/components/Task/TaskItem/TaskItem.js

import React from 'react';
import PropTypes from 'prop-types';
import { FaRegStar, FaStar, FaRegTrashAlt } from 'react-icons/fa'; // 아이콘 관련 라이브러리
import './TaskItem.scss';

const Task = ({
  task: { idx, content, archive, pinned }, // 과제 정보
  onPinTask, // 과제 핀 설정 함수
  onArchiveTask, // 과제 완료 설정 함수
  onRemoveTask, // 과제 삭제 함수
}) => {

  const onArchive = (e) => {
    onArchiveTask();
    e.stopPropagation();
  }

  const onPin = (e) => {
    onPinTask();
    e.stopPropagation();
  }

  const onRemove = (e) => {
    onRemoveTask();
    e.stopPropagation();
  }


  return (
    <div className={'Task-container'} onClick={onArchive}>
      <div className={'Task'}>
        <div className={`Task-content`}>
          {archive ? (
            <>
              <span className={'Task-content-fin'}>{ content }</span>
              <span className={'Task-content-fin-icon'}>finish</span>
            </>
          ) : (
            <span>{content}</span>
          )}
        </div>

        <div className={'Task-pin'} onClick={onPin}>
          {pinned ? 
            <FaStar className={'Task-pin-iconDone'} /> : 
            <FaRegStar className={'Task-pin-icon'} />}
        </div>

        <div className={'Task-remove'} onClick={onRemove}>
          <FaRegTrashAlt className={'Task-remove-icon'} />
        </div>
      </div>
    </div>
  );
};

// (PropTypes 생략)

export default Task;

TaskItem에 대한 Story 작성

TaskItem폴더에 TaskItem.stories.js이라는 파일을 만들어 줍니다.

아까 위에서 설정하였기 때문에 자동으로 import가 됩니다.

import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; // 액션 에드온
import TaskItem from './TaskItem';

// Test Data
export const task = {
  idx: 1,
  content: '오늘할일',
  archive: false,
  pinned: false,
};

// 이렇게 액션을 사용하면 함수를 직접 선언 하지 않아도 이벤트가 발생하였슬 때,
// 액션에 정의한 함수가 발생합니다.
export const actions = {
  onPinTask: action('onPinTask'),
  onArchiveTask: action('onArchiveTask'),
  onRemoveTask: action('onRemoveTask'),
};

// 스토리 추가
storiesOf('TaskItem', module) // Storybook에 표시될 폴더명
  // 데코레이터를 이용하면 아래와 같이 테스트할 스토리의 래핑 컴포넌트를 작성할 수 있습니다.
  .addDecorator(story => <div style={{ padding: '0 20rem' }}>{story()}</div>)
  // add('스토리명', 스토리 랜더링)
  .add('default', () => <TaskItem task={task} {...actions} />)
  .add('archived', () => <TaskItem task={{...task, archive: true}} {...actions} />)
  .add('pinned', () => <TaskItem task={{...task, pinned: true}} {...actions} />)
  .add('archived and pinned', () => 
       <TaskItem task={{...task, archive: true, pinned: true}} {...actions} />);

이제 storybook을 켜서 어떻게 되었는 지 확인해보겠습니다.

결과.PNG

Story에 추가한 4가지 케이스가 모두 있고 어떻게 작동하지는 보입니다.

이렇게 만든 여러가지 컴포넌트들을 조합하고 props로 값을 넘겨주면서 하나의 페이지로 만들수 있습니다.

👍 마치며 ...

  • 컴포넌트를 조합하여 완성한 투두리스트
    캡처.PNG

Storybook을 이용하여 Story를 작성하는 법을 알아보았습니다.
잘 활용한다면 컴포넌트를 설계할 때, 재사용가능한 컴포넌트로 만들고 조합하여 사용할 수 있을 것 같습니다.

다음글에서는 Storybook에서 지원하는 snapshot testing 에 대해 알아보겠습니다. snapshot testing를 사용하면 손쉽게 테스트를 진행할 수 있습니다.

예제 레퍼지토리) https://github.com/wlsdud2194/storybook-tutorials

Ps. 글에 오류나 수정사항이 있다면 댓글 부탁드립니다 :)