-
Storybook 컴포넌트 만들기 1프론트엔드 2023. 4. 17. 18:59728x90반응형SMALL
이전 포스트에서 Storybook 설정 및 배포 방법에 대해 알아보았습니다. 이번 포스트에서는 컴포넌트의 상태를 설정하여 스토리를 구성하고 테스트하는 과정을 살펴보도록 하겠습니다.
이번 포스트는 React + Storybook를 레퍼런스로 하고 있습니다.
1. Task 컴포넌트 만들기
Task는 체크박스, 정보, 위아래로 움직일 수 있도록 해주는 pinned이 있습니다.
Task의 상태는 TASK_INBOX, TASK_PINNED, TASK_ARCHIVED가 있습니다.
이를 위해 정의된 props는 아래와 같습니다.
- title - task를 설명해주는 문자열
- state - 현재 어떤 task가 목록에 있으며, 선택되어 있는지의 여부
Task 컴포넌트는 아래와 같이 정의해줍니다.
import { TaskType } from 'src/types'; import style from './style.module.css'; interface Props { task: TaskType; onArchiveTask: VoidFunction; onPinTask: VoidFunction; } function Task({ task, onArchiveTask, onPinTask }: Props) { return ( <div className={style['list-item']}> <input type='text' value={task.title} readOnly className={style.input} /> </div> ); } export default Task; export type { Props };
해당 컴포넌트를 테스트하기 위한 story파일은 세 가지 상태에 대한 테스트를 작성해줍니다.
import { StoryFn } from '@storybook/react'; import Task, { Props as TaskProps } from '.'; export default { component: Task, title: 'Task', }; const Template: StoryFn<TaskProps> = (args: any) => <Task {...args} />; export const Default = Template.bind({}); Default.args = { task: { id: '1', title: 'Test Task', state: 'Task_INBOX', updateAt: new Date(2021, 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', }, };
스토리북은 컴포넌트와 하위 스토리 두 가지로 구성되어 있습니다.
각 스토리는 컴포넌트의 상태에 따라 어떤 모습이어야 하는지를 보여줍니다.
export 구성
storybook파일의 default export는 component와 title를 속성으로하는 객체입니다.
- component : 해당 컴포넌트
- title : 스토리북 앱의 사이드바에서 컴포넌트를 참조하는 이름
named export 항목은 각 상태에 따른 스토리를 생성하는 함수를 내보냅니다. 스토리는 상태에 따라 렌더링된 컴포넌트를 반환하는 함수입니다.
여기서 동일한 컴포넌트를 반환하는 Template 함수를 생성한 뒤 .bind({})를 통해 이 함수를 복사하여 각 스토리가 고유의 속성(상태)을 가지면서 동일한 컴포넌트를 사용할 수 있도록 해줍니다.
args 동적 수정
Controls addon을 통해 동적으로 인자 수정 각 스토리의 args를 사용하여 스토리북을 다시 실행하지 않고도, Controls addon에서 컴포넌트를 실시간으로 수정해볼 수 있습니다. storybook 앱에서 args의 값을 변동시키면 컴포넌트도 함께 변동되게 됩니다.
2. 환경 설정
storybook 튜토리얼 내에서는 main.js와 preview.js를 수정해 주었습니다. 하지만 저와 같은 경우 storybook을 설치하면서 생성된 환경 설정 코드들이 튜토리얼에서 수정한 코드와 동일하여 따로 수정해주지는 않았습니다.
// main.ts import type { StorybookConfig } from "@storybook/react-webpack5"; const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], addons: [ "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/preset-create-react-app", "@storybook/addon-interactions", ], framework: { name: "@storybook/react-webpack5", options: {}, }, docs: { autodocs: "tag", }, staticDirs: ["../public"], }; export default config;
.storybook/main.js는 storybook의 전체적인 설정을 해줄 수 있습니다. 여기서 stories 속성은 story를 작성한 파일 명에 대한 정규식입니다. 이 설정은 src/**/*.stories.<js | jsx | ts | tsx>로 해주어 src 안의 모든 story 파일을 읽어줄 수 있도록 해줍니다.
// preview.ts import type { Preview } from "@storybook/react"; const preview: Preview = { parameters: { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, }, }; export default preview;
.storybook/preview.js는 스토리북의 기능과 애드온의 동작을 제어하기 위해 사용됩니다. 여기서 actions 애드온에 대한 설정을 해주었는데, actions는 컴포넌트에 상호작용이 일어났을 경우 actions 패널에 나타날 콜백함수를 생성할 수 있도록 해줍니다. 현재 설정은 인자로 'on'으로 시작하는 함수명이 등장할 경우 자동적으로 해당 함수에 대한 콜백 액션이 일어날 수 있도록 해줍니다.
3. 상태에 따른 컴포넌트 구현하기
현재 스토리로 작성한 세가지 상태에 따라 달라지는 컴포넌트의 형태를 구현해줍니다.
import { AiFillStar, AiOutlineStar } from 'react-icons/ai'; import { MdCheckBoxOutlineBlank, MdCheckBox } from 'react-icons/md'; import { TaskType } from 'src/types'; import style from './style.module.css'; interface Props { task: TaskType; onArchiveTask: (id: string) => void; onPinTask: (id: string) => void; } function Task({ task, onArchiveTask, onPinTask }: Props) { return ( <div className={style['list-item']}> <div> {task.state === 'TASK_ARCHIVED' ? ( <MdCheckBox onClick={() => onArchiveTask(task.id)} size={20} color='#243e57' /> ) : ( <MdCheckBoxOutlineBlank onClick={() => onArchiveTask(task.id)} size={20} color='#243e57' /> )} <input type='text' className={style.input} value={task.title} readOnly={true} placeholder='Input title' /> </div> <div onClick={(event) => event.stopPropagation()}> {task.state !== 'TASK_ARCHIVED' && ( <button onClick={() => onPinTask(task.id)} className={style.button}> {task.state === 'TASK_PINNED' ? ( <AiFillStar size={20} color='#243e57' /> ) : ( <AiOutlineStar size={20} color='#243e57' /> )} </button> )} </div> </div> ); } export default Task; export type { Props };
위의 코드로 생성된 컴포넌트에 대한 스토리를 storybook 앱에서 확인할 수 있습니다.
storybook app 실행 화면 4. 스냅샷 테스트
story 앱을 개발하는 동안 컴포넌트의 외관을 바꾸지는 않았는지 확인할 수 있습니다. 수동적으로 하기 보다는 자동화할 수 있도록 스냅샷 테스트를 진행해보도록 하겠습니다.
스냅샷 테스트는 주어진 입력에 대해 컴포넌트의 '구분하기 좋은' 출력값을 기록한 다음, 향후 출력값이 변할 때마다 컴포넌트에 플래그를 지정하는 방식을 말합니다. 이는 컴포넌트에 수정사항이 생길 경우 바뀐 부분을 빠르게 확인할 수 있게 해줍니다.
addon 의존성 설치
npm install --save-dev @stroybook/addon-storyshots react-test-renderer
스냅샷 설정
// src/__test__/storybook.test.ts import initStoryshots from '@storybook/addon-storyshots'; initStoryshots();
이후 yarn test를 입력하여 테스트를 실행할 수 있습니다.
test 실행화면 첫 번째 실행 후, 컴포넌트의 story에 대한 스냅샷이 생성된 것을 확인할 수 있습니다. 저와 같은 경우 src/__test__ 디렉토리 안에 __snapshots__디렉토리가 생성되고 storybook.test.ts.snap라는 파일이 생성됐습니다.
이 후 컴포넌트에 약간의 변화를 준 뒤 테스트를 실행하면 스냅샷과 다른 스토리에 대한 테스트는 실패가 됩니다. 이 점을 통해 의도하지 않은 컴포넌트에 변화가 생겼는지 확인할 수 있습니다.
스냅샷과 다르게 컴포넌트를 업데이트 할 경우 + 후기
오늘은 간단한 컴포넌트에 대한 story를 적용해봤습니다. 컴포넌트의 스토리에 대한 스냅샷 테스트도 실행해보고 점차 CDD에 다가갈 수 있는 것 같습니다. 다음으로는 좀 더 복잡한 컴포넌트에 대한 스토리 구성을 주제로 포스팅해볼 계획입니다. 감사합니다.
반응형LIST'프론트엔드' 카테고리의 다른 글
Storybook 데이터 연결하기 (0) 2023.04.19 Storybook 컴포넌트 만들기 2 (0) 2023.04.18 Storybook CRA에 적용하기 (0) 2023.04.17 TDD를 해보자 (0) 2023.02.07 jest + React testing library + msw 테스트 초기 설정 (0) 2023.01.11