ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Storybook 컴포넌트 만들기 2
    프론트엔드 2023. 4. 18. 10:46
    728x90
    반응형
    SMALL

    저번 포스팅에서 Storybook을 이용해 Task 컴포넌트를 생성했습니다. 이번 포스팅에서는 이 Task 컴포넌트를 여러개 생성하여 TaskList 컴포넌트를 만들어보겠습니다.

     

     

    1. TaskList의 스토리

    DEFAULT - 일반 Task들의 리스트입니다.

    PINNED - 핀으로 고정된 Task들이 있을 경우 해당 Task를 일반 Task보다 위에 위치하도록 해줘야 합니다.

    Empty - TaskList가 비어있는 경우입니다.

    LOADING - Task가 비동기식으로 전달 될 경우 연결이 없는 상태를 렌더링해줍니다.

     

    2. TaskList story 구성

    우선 story default export에서 내보낼 TaskList 컴포넌트를 구현만 해줍니다.

    function TaskList() {
      return <>TaskList</>;
    }
    
    export default TaskList;

    그 다음, 각 상태에 대한 story를 작성해줍니다.

    import { StoryFn } from '@storybook/react';
    import TaskList from '.';
    import * as TaskStories from 'src/components/Task/index.stories';
    
    export default {
      component: TaskList,
      title: 'TaskList',
      decorators: [
        (Story: StoryFn) => (
          <div style={{ padding: '3rem' }}>
            <Story />
          </div>
        ),
      ],
    };
    
    const Template: StoryFn = (args) => <TaskList {...args} />;
    
    export const Default = Template.bind({});
    Default.args = {
      tasks: [
        { ...TaskStories.Default.args!.task, id: '1', title: 'Task 1' },
        { ...TaskStories.Default.args!.task, id: '2', title: 'Task 2' },
        { ...TaskStories.Default.args!.task, id: '3', title: 'Task 3' },
        { ...TaskStories.Default.args!.task, id: '4', title: 'Task 4' },
        { ...TaskStories.Default.args!.task, id: '5', title: 'Task 5' },
        { ...TaskStories.Default.args!.task, id: '6', title: 'Task 6' },
      ],
    };
    
    export const WithPinnedTasks = Template.bind({});
    WithPinnedTasks.args = {
      tasks: [
        ...Default.args.tasks.slice(0, 5),
        { id: '6', title: 'Task 6 Pinned', state: 'TASK_PINNED' },
      ],
    };
    
    export const Loading = Template.bind({});
    Loading.args = {
      tasks: [],
      loading: true,
    };
    
    export const Empty = Template.bind({});
    Empty.args = {
      ...Loading.args,
      loading: false,
    };

    default export구간에 decorators가 추가되었는데, decorators는 스토리에 임의의 래퍼(Wrapper)를 제공하는 한 방법입니다. 이번엔 렌더링된 컴포넌트를 주위와 padding값을 주기 위해 사용했습니다.

    데코레이터는 context와 관련된 "providers"를 스토리에 감싸줄 때 사용될 수 있습니다.

     

    3. TaskList 구성

    Task 컴포넌트 style을 조금씩 수정하면서 TaskList를 구성해줬습니다.

    import { BsCheckCircle } from 'react-icons/bs';
    import { TaskType } from 'src/types';
    import style from './style.module.css';
    import Task from 'src/components/Task';
    
    interface Props {
      tasks: TaskType[];
      loading: boolean;
      onPinTask: (id: string) => void;
      onArchiveTask: (id: string) => void;
    }
    
    function TaskList({ tasks, loading, onPinTask, onArchiveTask }: Props) {
      const LoadingRow = (
        <div className={style['loader-wrapper']}>
          <span className={style.loader} />
        </div>
      );
    
      if (loading) {
        return (
          <div className={style['list-items']}>
            {LoadingRow}
            {LoadingRow}
            {LoadingRow}
            {LoadingRow}
            {LoadingRow}
            {LoadingRow}
            {LoadingRow}
          </div>
        );
      }
      if (tasks.length === 0) {
        return (
          <div className={`${style['list-items']} ${style['center']}`}>
            <BsCheckCircle size={30} />
            <p>Task List is Empty</p>
          </div>
        );
      }
    
      const tasksInOrder = [
        ...tasks.filter((task) => task.state === 'TASK_PINNED'),
        ...tasks.filter((task) => task.state !== 'TASK_PINNED'),
      ];
    
      return (
        <div className={style['list-items']}>
          {tasksInOrder.map((task) => (
            <Task
              key={task.id}
              task={task}
              onPinTask={onPinTask}
              onArchiveTask={onArchiveTask}
            />
          ))}
        </div>
      );
    }
    
    export default TaskList;
    export type { Props };

    각 state에 따라 반환하는 컴포넌트를 달리하도록 구성해봤습니다. storybook document처럼 조건에 따라 여러 return문을 두는 컴포넌트를 이번에 처음 작성해봤는데 해당 조건에 상관 없는 연산을 안 할 수 있다는 것이 장점인 것 같습니다.

     

    4. 단위 테스트

    지금까지 스토리북 스토리, 스냅샷 테스트로 UI 변동사항들을 찾아낼 수 있었습니다. 이번에는 단위테스트를 적용하여 특정 시나리오에 대하여 버그가 발생하지 않았는지 찾아보도록 하겠습니다.

     

    우선 @testing-library/react 와 @storybook/testing-react 패키지를 설치해줍니다.

    npm install --save-dev @testing-library/react @storybook/testing-react

    @storybook/testing-react는 단위 테스트에서 스토리북 스토리를 재사용할 수 있게 해주는 애드온입니다. 스토리를 테스트에서 재사용함으로써 테스트 준비가 된 컴포넌트 시나리오 카탈로그를 갖게 됩니다. 또한, 모든 인수, 데코레이터, 그리고 스토리의 모든 정보들이 이 라이브러리에 의해 조합됩니다.

    한 마디로 작성된 스토리를 단위 테스트에 사용할 수 있게 해주는 모듈입니다!

     

    WithPinnedTasks 스토리의 경우 pin이 된 Task를 TaskList 가장 위쪽에 배치시켜줘야하는데, 실제로 그런지 확인하는 테스트코드를 작성해보도록 하겠습니다.

    import { render, screen } from '@testing-library/react';
    
    import { composeStories } from '@storybook/react';
    
    import * as TaskListStories from './index.stories';
    
    const { WithPinnedTasks } = composeStories(TaskListStories);
    
    it('render pinned tasks at the start of the line', () => {
      render(<WithPinnedTasks />);
    
      const firstInput = screen.getAllByRole('textbox')[0] as HTMLInputElement;
    
      expect(firstInput.value).toEqual('Task 6 Pinned');
    });

    componseStories를 통해 이미 작성된 Story로 컴포넌트를 구성했습니다. 이 때, 스토리를 구성하면서 넣은 인자값들에 대한 것들도 모두 가져와지게 됩니다. 이 때 story에서 named export된 것들 중 WithPinnedTasks 스토리를 가져옵니다.

     

    render와 screen 함수로 렌더링된 WithPinnedTasks 컴포넌트를 불러오고, getAllByRole을 통해 첫번째로 렌더링된 input태그를 불러옵니다. 해당 input태그의 value가 Task 6 Pinned인지 확인하여 pin된 Task가 가장 위쪽에 위치하고 있다는 것을 확인할 수 있습니다.

    테스트 결과

    여기서, pin된 Task를 가장 아래쪽으로 보내서 테스트가 제대로 동작하고 있는지 확인해보겠습니다.

    바뀐 taskInOrder

    TaskList에서 Task를 렌더링해줄 때 사용한 taskInOrder에서 pin된 Task와 보통 Task의 순서를 바꿔줬습니다.

    테스트 결과2

    테스트 결과는 실패로, pin된 Task가 가장 위쪽에 있지 않는다고 알려줍니다.

     

     

    오늘은 좀 더 복잡한 컴포넌트를 구성해보았습니다. 다음 포스팅에서는 컴포넌트에서 상태관리도구를 사용했을 때의 storybook 사용방법을 알아보도록 하겠습니다.

    반응형
    LIST

    '프론트엔드' 카테고리의 다른 글

    Storybook 화면 구성하기  (0) 2023.04.20
    Storybook 데이터 연결하기  (0) 2023.04.19
    Storybook 컴포넌트 만들기 1  (0) 2023.04.17
    Storybook CRA에 적용하기  (0) 2023.04.17
    TDD를 해보자  (0) 2023.02.07

    댓글

Designed by Tistory.