npx create-react-app storybook-practice --template typescript
npm run start
위 cli 명령어를 입력해 우선 스토리북을 실습할 디렉토리를 생성해주었다.
npm run start
를 통해 로컬 호스트에서 앱이 잘 실행되는 것을 확인!
스토리북 공식문서에서 안내해주는 바와 같이 스토리북을 설치하고 초기화한다.
스토리북 공식문서는 무려 React를 위한 튜토리얼을 따로 제공한다.
# Add Storybook:
npx -p @storybook/cli sb init
설치를 완료하고 나면 package.json
devDependencies에서 위와 같은 모습을 확인할 수 있다.
설치가 잘 되었는지 확인하기 위해 아래 명령어들을 cli에 입력해보자!
# Run the test runner (Jest) in a terminal:
yarn test --watchAll
# Start the component explorer on port 6006:
yarn storybook
src/index.css
에 공식문서가 미리 마련해둔 CSS setting을 그대로 옮기고, 아래 명령어를 통해서 폰트와 아이콘도 설정해준다.
npx degit chromaui/learnstorybook-code/src/assets/font src/assets/font
npx degit chromaui/learnstorybook-code/src/assets/icon src/assets/icon
CDD 방법론에 따라 UI를 개발할 것이므로 간단한 컴포넌트를 미리 제작한다.
사실 리액트는 컴포넌트 기반 개발이 default라고 처음부터 배웠으나, 지금까지는 늘 페이지부터 개발하고, 큰 컴포넌트부터 작은 컴포넌트로 개발해 나갔던 것이 사실이다. 이 기회에 제대로 작은 컴포넌트 부터 빌드업해 나가는 개발 방식에 익숙해지자!
스토리북은 기본적으로 컴포넌트와 그 하위 스토리! 이렇게 두 가지 기본 단계로 구성되어 있다.
이와 같은 형태인데, 각 스토리가 해당 컴포넌트에 대응되는 것이다.
물론 스토리는 꼭 하나일 필요는 없다. 컴포넌트 별로 원하는 만큼의 스토리 파일을 생성해 사용할 수 있다.
export default {
component: Task, // 이 스토리 파일이 종속된 컴포넌트
title: "Task", // 스토리북 앱의 사이드바에서 컴포넌트를 참조할 이름
};
스토리북에게 우리가 문서화하는 컴포넌트를 알려주기 위해 위와 같은 default export
를 생성해주어야 한다. 우리는 하나의 컴포넌트에 여러 가지 스토리를 넘겨주면서 테스트를 할 것이기 떄문에 아래와 같이 하나의 Template 변수를 만들어주고, 해당 패턴을 활용해 스토리에 도입할 것이다.
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",
},
};
.storybook/main.js
파일을 다음과 같이 수정한다.
module.exports = {
stories: ['../src/components/**/*.stories.js'],
// 우리가 작성할 스토리 파일들의 위치!
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/preset-create-react-app',
],
};
preview.js
도 다음과 같이 수정하여준다.
import '../src/index.css'; //👈 The app's CSS file goes here
//👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI.
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
};
매개변수는 스토리북의 기능과 애드온 동작을 제어하기 위해 사용된다.
우리의 경우 이를 활용하여 action이 처리되는 방식을 구성할 것이다.
actions는 클릭되었을 때 스토리북 UI의 actions 패널에 나타날 콜백을 생성할 수 있도록 해준다.
이제 스토리북 서버를 재시작해보자.
이제 스토리북에 세 가지 테스트 케이스가 생성된 것을 확인해볼 수 있다.
React에서 propTypes를 활용해 데이터의 요구사항을 명하시여준다.
import PropTypes from 'prop-types';
Task.propTypes = {
/** Composition of the task */
task: PropTypes.shape({
/** Id of the task */
id: PropTypes.string.isRequired,
/** Title of the task */
title: PropTypes.string.isRequired,
/** Current state of the task */
state: PropTypes.string.isRequired,
}),
/** Event to change the task to archived */
onArchiveTask: PropTypes.func,
/** Event to change the task to pinned */
onPinTask: PropTypes.func,
};
이렇게 데이터 요구사항까지 정해부고나면,
이제 서버나 프론트엔드 앱 전체를 실행하지 않고도 컴포넌트 단위의 개발이 가능해진다는 걸 알 수 있다.
그동안은 늘 props를 내려줘야만 하위컴포넌트를 비로소 제대로 개발할 수 있다고 생각했었다.
mockData를 만드는 것이 좀 귀찮기도 했었고, 그러나 이러한 방식이라면 충분히 컴포넌트 드리븐 개발이 가능할 것이다.
현재로서는 직접 입력해준 story들을 확인하는 것까지만 가능하다.
이를 직접 test를 돌릴 수 있도록 StoryShots Addon을 설치해준다.
yarn add -D @storybook/addon-storyshots react-test-renderer
그리고 테스트파일을 생성해주면 yarn test
로 테스트를 실행할 수 있게 된다!
위에서 만들었던 Task 컴포넌트를 조합해 TaskList를 만들 수 있다.
우리는 다음의 네 가지 상황에 대응할 수 있는 TaskList 스토리를 만들어줄 것이다.
위에서 했던 것과 같이 TaskList.js
와 TaskList.stories.js
를 각각 생성해준다.
// src/components/TaskList.stories.js
export default {
component: TaskList,
title: 'TaskList',
decorators: [story => <div style={{ padding: '3rem' }}>{story()}</div>],
};
데코레이터는 스토리에 임의의 래퍼를 제공해주는 방법.
또한 데코레이터는 providers에서 스토리를 감싸줄 때 사용될 수 있다.
설정을 마치면 스토리북에서 위와 같은 화면을 확인할 수가 있다!
뼈대만 잡아놓은 컴포넌트 구조를 디테일하게 잡아준다. (상세 내용은 공식문서 참고)
Task에서는 렌더링이 잘 되는지 확인하는 것 이상의 많은 복잡성이 필요하지는 않았다.
TaskList에서는 복잡성이 더해지기 떄문에 Jest를 활용해 단위테스트를 도입하도록 한다.
import React from "react";
import ReactDOM from "react-dom";
import "@testing-library/jest-dom/extend-expect";
import { WithPinnedTasks } from "./TaskList.stories"; //👈 Our story imported here
it("renders pinned tasks at the start of the list", () => {
const div = document.createElement("div");
//👇 Story's args used with our test
ReactDOM.render(<WithPinnedTasks {...WithPinnedTasks.args} />, div);
// We expect the task titled "Task 6 (pinned)" to be rendered first, not at the end
const lastTaskInput = div.querySelector(
'.list-item:nth-child(1) input[value="Task 6 (pinned)"]'
);
expect(lastTaskInput).not.toBe(null);
ReactDOM.unmountComponentAtNode(div);
});
이제 각 유닛별 테스트가 가능해졌다!