아래 Task 컴포넌트가 있다고 상상하고 story를 만들어보자.
src/components/Task.jsx
import React from 'react';
export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
return (
<div className='list-item'>
<input type='text' value={title} readOnly={true} />
</div>
);
}
src/components/Task.stories.js
import React from 'react';
import Task from './Task';
// #1
export default {
component: Task,
title: 'Task'
}
// #2
const Template = args => <Task {...args} />;
// #3
import React from 'react';
import Task from './Task';
export default {
component: Task,
title: 'Task',
};
const Template = args => <Task {...args} />;
// #3
export const Default = Template.bind({});
Default.args = {
task: {
id: 1,
title: 'Default 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',
},
};
#1 export default로 객체를 내보내는데 내보낼 component를 지정하고 title같은 속성을 지정할 수 있다. 여기서 title은 스토리북을 실행했을 때 사이드바에 나타나는 그룹 제목이라고 생각하면 된다.
#2 컴포넌트를 재활용하기 위해 컴포넌트를 리턴하는 Template 함수를 만든다. 이 Template함수를 복사해서 사용할 것이다.
#3 Template함수를 복사하기 위해 Template.bind({})
를 실행하고 복사된 함수를 변수에 담는다.
담은 함수.args에다가 객체 형태로 넣어줄건데 그 객체 안에는 props를 설정할 수 있다.
.storybook/main.js
스토리위치를 지정하고 addon을 추가할 수 있다.
storybook 경로만 수정해주었다.module.exports = { stories: ['../src/components/**/*.stories.js'], addons: [ '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/preset-create-react-app', ], framework: '@storybook/react', core: { builder: '@storybook/builder-webpack5', }, };
.storybook/preview.js
css 위치를 지정할 수 있다.
그리고 parameter를 통해 storybook의 기능과 애드온을 제어할 수 있다. 이를 이용하여 parameter내부에 actions를 이용해 버튼 클릭 같은 것을 테스트해 볼 것이다.import '../src/index.css'; export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, };
src/components/Task.jsx
storybook 스크립트를 실행하면 story파일에서 props를 주는 템플릿들을 확인할 수 있다.
import React from 'react'; export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) { return ( <div className={`list-item ${state}`}> <label className='checkbox'> <input type='checkbox' defaultChecked={state === 'TASK_ARCHIVED'} disabled={true} name='checked' /> <span className='checkbox-custom' onClick={() => onArchiveTask(id)} /> </label> <div className='title'> <input type='text' value={title} readOnly={true} /> </div> <div className='actions' onClick={event => event.stopPropagation()}> {state !== 'TASK_ARCHIVED' && ( <a onClick={() => { onPinTask(id); }} <span className='icon-star'></span> </a> )} </div> </div> ); }
공식문서에서는 propTypes를 안내하고 있지만 트렌드에 따라 Typescript를 사용해보았다. 잘된다. 👏
components/Task.tsx
import React from 'react';
type TaskBoxProps = {
task: {
id: number;
title: string;
state: string;
};
onArchiveTask: (id: number) => void;
onPinTask: (id: number) => void;
};
export default function Task({
task: { id, title, state },
onArchiveTask,
onPinTask,
}: TaskBoxProps) {
return (
<div className={`list-item ${state}`}>
<label className='checkbox'>
<input
type='checkbox'
defaultChecked={state === 'TASK_ARCHIVED'}
disabled={true}
name='checked'
/>
<span className='checkbox-custom' onClick={() => onArchiveTask(id)} />
</label>
<div className='title'>
<input type='text' value={title} readOnly={true} />
</div>
<div className='actions' onClick={event => event.stopPropagation()}>
{state !== 'TASK_ARCHIVED' && (
<a
onClick={() => {
onPinTask(id);
}}
>
<span className='icon-star'></span>
</a>
)}
</div>
</div>
);
}
해당 스토리를 완성하고 저장해놓고 다음에 업데이트 되었을 때 바뀐 부분을 빠르게 캐치하고 테스트해볼 수 있는 방법을 실험해보려고 했으나 react18버전에서 구동이 되지 않는 것 같다. 해당 이슈를 어렵지 않게 찾을 수 있었다.
https://github.com/storybookjs/storybook/issues/17985