-
Storybook 컴포넌트 만들기 2프론트엔드 2023. 4. 18. 10:46728x90반응형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