React 프로젝트에 Storybook 도입하기

YUKI KIM·2022년 3월 4일
0

평소에 리액트 컴포넌트 디자인 패턴을 프로젝트에 도입해보고 싶다는 생각은 막연하게 해봤는데, 막상 직접 해본 적은 없었다. 이번에 기회가 되어서 Storybook을 프로젝트에 도입하는 걸 기록해보려고 한다!


Storybook 초기 세팅하기

우선 React 프로젝트를 생성하고 Storybook을 사용하기 위한 필요 패키지를 설치하도록 하자. React는 create-react-app으로 간단하게 만들었다.

npx create-react-app taskbox --template typescript
cd taskbox
npx -p @storybook/cli sb init

Storybook 실행

Storybook 설치에 꽤 오랜 시간이 걸린다. 설치가 완료되면 storybook을 실행시켜준다. 실행에도 꽤 많은 시간이 걸린다.

npm run storybook  # Start the component explorer on port 6006

위와 같은 화면이 뜨면 성공이다. 이제 우리는 여기서 UI 컴포넌트들을 독립적으로 관리할 수 있게 되었다.

Storybook 환경을 설정하는 명령어 만으로도 src/stories/ 폴더 아래에 기본적인 컴포넌트들이 생성됐다. 그럼 이제 공식문서를 따라가면서 연습해보자.

CSS 및 assets 폴더 세팅

우선, 공식문서에 있는 링크를 따라 index.css를 수정해준다. 그리고 아래의 명령어를 입력하여 src/assets/폴더에 font와 icon을 다운 받는다.

npx degit chromaui/learnstorybook-code/src/assets/font src/assets/font
npx degit chromaui/learnstorybook-code/src/assets/icon src/assets/icon

초기 세팅은 끝났다. 이제 컴포넌트 기반 개발, CDD (Component-Driven Development) 방법론에 따르는 UI를 만들어보도록 하자.

CDD는 컴포넌트로부터 시작하여 마지막 화면에 이르기까지 상향적으로 UI를 개발하는 과정입니다. CDD는 UI를 구축할 때 직면하게 되는 규모의 복잡성을 해결하는 데 도움이 됩니다.


간단한 컴포넌트 만들기

Task 컴포넌트를 만들거다. 각각은 체크 박스, task에 대한 정보, task를 위아래로 움직일 수 있도록 도와주는 pin 버튼이 필요하다. 그리고 다음과 같은 props가 필요하다.

  • title: task를 설명해주는 문자열
  • state: 현재 어떤 task가 목록에 있으며, 선택되어 있는지의 여부

그럼 이제 각각의 state에 따라 컴포넌트의 모습을 수동으로 테스트하면서 진행해보자. 여기서부터는 공식문서를 따라가며 기록하고 싶은 것만 기록하겠음.

Task 컴포넌트와 스토리

src/components/ 폴더에 Task.js, Task.stories.js를 각각 생성해준다. 아래는 Task.js이다.

export default function Task({task: {id, title, state}, onArchiveTask, onPinTask}) {
  return (
    <div className="list-item">
      <input type="text" value={title} readOnly={true} />
    </div>
  );
}

아래는 Task.stories.js로, Task의 세 가지 테스트 state를 스토리 파일에 작성한 것이다. Storybook에게 우리가 문서화하고 있는 컴포넌트에 대해 알려주기 위해, 아래 사항들을 포함하는 default export를 생성한다.

import Task from './Task';

export default {
  component: Task, // 이 스토리 파일이 종속된 컴포넌트
  title: "Task",   // 스토리북 앱의 사이드바에서 컴포넌트를 참조할 이름
};

const Template = args => <Task {...args} />; // 먼저 템플릿 생성

export const Default = Template.bind({});
Default.args = {
  task: {
    id: '1',
    title: 'Test Task',
    state: 'TASK_INBOX',
    updatedAt: new Date(2018, 0, 1, 9, 0),
  },
};

export const Pinned = Template.bind({});
Pinned.args = {
  task: {
    ...Default.args.task,
    state: 'TASK_PINNED',
  },
};

export const Archived = Template.bind({});
Archived.args = {
  task: {
    ...Default.args.task,
    state: 'TASK_ARCHIVED',
  },
};

스토리북을 재시작하면 세가지 테스트 케이스가 생성된 것을 확인할 수 있다. 또한 각각의 테스트 케이스를 클릭해보면 경우에 따라 state가 변경되는 것도 확인할 수 있다.

Storybook 구성 파일 수정

Storybook 세팅을 마치면 .storybook/ 아래에 main.js와 preview.js 파일이 생성된다. 각각의 파일은 아래와 같은 역할을 한다.

  • main.js: 메인 config 파일이다. 스토리북의 생성(generation)을 담당한다.
  • preview.js: stories의 렌더링을 설정한다.

공식 문서에서는 이 두 파일을 모두 수정하지만 난 preview.js에만 상단에 아까 수정했던 index.css import 구문만 추가하도록 할 거다.

import '../src/index.css';

요구사항 명시하기

컴포넌트에 필요한 데이터 형태를 명시하려면 React에서 propTypes를 사용하는 것이 가장 좋다. 이는 자체적 문서화일 뿐만 아니라, 문제를 조기에 발견하는 데 도움이 된다.

이렇게 데이터 요구사항을 확실히 정해놓으면 앱을 실행하지 않고 컴포넌트 단위로 개발 및 테스트를 하는 것이 수월해진다.

import PropTypes from 'prop-types';

export default function Task({task: {id, title, state}, onArchiveTask, onPinTask}) {
  // ...
}

Task.propTypes = {
  task: PropTypes.shape({
    id: PropTypes.string.isRequired,
    title: PropTypes.string.isRequired,
    state: PropTypes.string.isRequired,
  }),
  onArchiveTask: PropTypes.func,
  onPinTask: PropTypes.func,
};

복합적 컴포넌트 만들기

방금 만든 Task 컴포넌트의 모음인 TaskList 컴포넌트를 만들어보자. TaskList 컴포넌트에는 default, pinned, empty, loading state가 필요하다.

TaskList 컴포넌트와 스토리

앞서 만든 Task 컴포넌트 만드는 방식과 크게 다른건 없다. 그런데 여기서는 TaskList.stories.js에서 문서화하고 있는 컴포넌트에 대해 알려주기 위해 하는 default export에 decorators가 포함된다.

import TaskList from './TaskList';
import * as TaskStories from './Task.stories';

export default {
  component: TaskList,
  title: 'TaskList',
  decorators: [story => <div style={{ padding: '3rem' }}>{story()}</div>],
};

decorators는 스토리에 임의의 wrapper를 제공하는 방법입니다. 위의 예시에서는 렌더링 된 컴포넌트에 padding을 추가한다.

States 구현하기

TaskList 컴포넌트를 state에 따라 다르게 렌더링 되도록 하는 단계이다. 필요한 state는 default, pinned, empty, loading이라고 했다. 그리고 TaskList가 props로 받는 value는 loading, tasks, onPinTask, onArchiveTask이다. 각각의 state는 아래와 같이 나눈다.

  • loading: loading === true
  • empty: tasks.length === 0
  • default, pinned: tasks를 task의 state가 'TASK_PINNED'인 것부터 앞으로 오게 정렬한다. 그 정렬한 것을 렌더링 한다.

데이터 요구사항 및 props

Task는 TaskList의 하위 컴포넌트이기 때문에 렌더링에 필요한 적합한 형태의 데이터를 제공해야 한다. Task에서 사용한 propTypes을 재사용하면 간단하다.

TaskList.propTypes = {
  loading: PropTypes.bool,
  tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
  onPinTask: PropTypes.func,
  onArchiveTask: PropTypes.func,
};

TaskList.defaultProps = {
  loading: false,
};

레퍼런스

profile
유키링と 욘데 쿠다사이

0개의 댓글