Storybook이 필요한 이유?
프론트엔드 개발을 하다 보면, 조건에 따라 렌더링 해 줘야 하는 컴포넌트들이 있습니다. 그 컴포넌트들은 조건에 맞지 않는다면 보이지 않거나, 조건에 따라 다르게 렌더링 됩니다.
그 컴포넌트들을 코드를 변경해 조건을 바꿔보며 일일이 테스트 해볼 수는 있지만 항상 그렇게 하기에는 매우 귀찮습니다. 이 때 storybook을 사용하면, 각각의 조건의 story를 작성하여 컴포넌트 들을 확인해 볼 수 있고, 페이지 전체가 아닌 그 컴포넌트만 따로 확인하기에도 아주 좋습니다. 별도의 코드 수정 없이도 손쉽게 표시해 줄 내용이나, 조건을 변경해 볼 수도 있습니다.
또한, Storybook에서 제공되는 docs를 통해 우리가 작성한 컴포넌트의 documentation을 손쉽게 작성할 수 도 있습니다. 이 외에도 다양한 이유들로 Storybook은 사랑을 받고 있습니다.
Storybook이란?
Storybook is an open source tool for building UI components and pages in isolation. It streamlines UI development, testing, and documentation.
Storybook은 UI 컴포넌트를 독립된 환경에서 개발할 수 있게 만든 도구입니다.
조건에 따라서 변하는 UI를 독립적으로 테스트 해 볼 수 있고, Addon을 추가해 다양한 기능을 사용할 수 있습니다.
Storybook 시작하기
storybook을 추가하고자 하는 프로젝트 storybook을 설치한뒤, storybook을 시작합니다.
# Add Storybook:
npx -p @storybook/cli sb init
Bash
복사
그러면 프로젝트의 root 디렉토리에 .storybook 폴더가 생성되고, 그 안에 main.js, preview.js파일이 생성됩니다.
project
├─── .storybook
│ ├──── main.js
│ └──── preview.js
└─── ...
Markdown
복사
main.js에서는 addon을 추가할 수 있고, TypeScript 설정, path alias 설정 등, 다양한 webpack 설정들을 해 줄수 있습니다.
preview.js에서는 global css 파일들을 import해서 프로젝트에서 적용되는 global css들을 storybook 에서도 적용되도록 할 수 있고, Storybook의 canvas 배경 색 설정등 storybook에 global하게 적용될 부분들을 설정해 줄 수 있습니다.
더 자세한 사항은 공식문서를 참고하면 좋습니다.
또 예시 story코드들이 자동으로 생성되고, package.json 파일에 script가 추가되어
yarn storybook
# 혹은
npm run storybook
Bash
복사
으로 실행시켜 볼 수 있습니다.
Storybook Addons
Storybook Addon은 Storybook에 다양한 기능을 추가해 줄 수 있는 패키지들입니다. npm, yarn 등의 패키지 매니저를 통해 설치하고, “.storybook/main.js" 파일에 추가해 사용할 수 있습니다.
addon-links 나 addon-essentials와 같이 기본적으로 추가되어 있는 addon 도 있고, SCSS를 사용할 경우 추가해줘야 하는 “@storybook/preset-scss”나 storybook을 이용해 테스트를 하기 위한 “@storybook/testing-react”등 다양한 addon 들이 있습니다.
추가하는 법은 필요한 addon 을 찾은 뒤, npm 혹은 yarn 등을 이용해 설치하고,
// .storybook/main.js
module.exports = {
...
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/preset-scss",
"@storybook/testing-react",
],
...
};
JavaScript
복사
.storybook/main.js
이런식으로 추가해 주면 됩니다. 자세한 사항은 공식문서를 참고하면 도움이 됩니다.
Story 작성하는 방법
아주 간단한 컴포넌트를 예를 들면 저희 Youha에서 사용할수 있는 채팅 기능에서 메세지 컴포넌트를 예로 들도록 하겠습니다.
현재 티켓플레이스에서는 TypeScript를 이용하기에 TypeScript로 예제를 만들었습니다
시간과, 메세지 내용, 상대방이 읽었는지 여부를 표시해주는 간단한 컴포넌트 입니다.
// ChatMessage.tsx
interface Props {
/** 메세지 내용 */
messageText: string;
/** 보낸 시간 */
sentTime: string;
/** 메세지를 보낸 사람인지 여부 */
isSender: boolean;
/** 메세지를 읽지 않은 사람의 수 */
unreadCount?: number;
/** 첨부파일 url */
fileUrl?: string;
/** 첨부파일 이름 */
fileName?: string;
/** 첨부파일의 종류, image일 경우 이미지를 보여준다 */
fileType?: string;
}
/**
* 채팅 메세지 컴포넌트
*/
const ChatMessage: React.FunctionComponent<Props> = ({ messageText, sentTime, isSender, unreadCount, fileUrl, fileName, fileType }) => {
return (
<>
{fileUrl && fileName && <ChatMessageFile fileUrl={fileUrl} fileName={fileName} fileType={fileType} />}
<p>{messageText}</p>
<p>
{sentTime}
{unreadCount === 0 ? " · ✓✓" : " · ✓"}
</p>
</>
);
};
TypeScript
복사
ChatMessage.tsx
이 컴포넌트의 story를 작성하도록 하겠습니다.
// ChatMessage.stories.tsx
import React from "react";
import { ComponentStory } from "@storybook/react";
import ChatMessage from "./ChatMessage";
import { Message } from "@chatscope/chat-ui-kit-react";
export default {
component: ChatMessage,
title: "ChatMessage",
parameters: {
componentSubtitle: "채팅 메세지 컴포넌트",
},
};
// 이 컴포넌트는 외부 라이브러리로 감싸져 있을 때 정확히 UI 테스트가 가능하기 때문에,
// 다음과 같은 방법을 사용했습니다.
const Template: ComponentStory<typeof ChatMessage> = (args) => (
<Message
model={{
direction: args.isSender ? "outgoing" : "incoming",
position: "single",
type: "custom",
}}
>
<Message.CustomContent>
<ChatMessage {...args} />
</Message.CustomContent>
</Message>
);
export const Basic = Template.bind({});
Basic.args = {
messageText: "안녕하세요",
sentTime: "3:13 pm",
isSender: true,
unreadCount: 0,
};
export const SentFromOther = Template.bind({});
SentFromOther.args = {
messageText:
"광고단가가 어떻게 될까요? 자세한 내용은 아래 문서를 참고해주시기 바랍니다.",
sentTime: "3:13 pm",
isSender: false,
unreadCount: 10,
};
export const CheckedMessage = Template.bind({});
CheckedMessage.args = {
messageText:
"광고단가가 어떻게 될까요? 자세한 내용은 아래 문서를 참고해주시기 바랍니다.",
sentTime: "3:13 pm",
isSender: true,
unreadCount: 0,
};
export const MessageWithImage = Template.bind({});
MessageWithImage.args = {
messageText: "",
sentTime: "3:13 pm",
isSender: true,
unreadCount: 0,
fileUrl: "https://picsum.photos/200",
fileName: "123.jpeg",
fileType: "image/jpeg",
};
TypeScript
복사
ChatMessage.stories.tsx
결과입니다.
Storybook을 실행시 왼쪽 내비게이션바에서 작성한 스토리들을 확인할 수 있습니다.
각각 스토리를 눌러보았을때, canvas에서 표시되는 UI를 확인해 볼 수 있고, 하단의 controls에서는 직접 argument에 들어가는 값을 바꿔보며, 다른 데이터가 들어갔을때 어떻게 변하는지 확인해 볼 수 있습니다.
docs를 눌러보면 다음과 같은 화면을 볼 수 있습니다. 작성한 story에서 export default로 설정한 title이 맨 위 가장 큰 글씨로, parameter의 componentSubtitle이 그 아래 작은 글씨로 표시되는것을 볼 수 있고, 컴포넌트 파일에 주석으로 적어놓은 설명이 그 아래 표시됩니다.
또한 TypeScript를 사용해 type을 지정해 놓은 prop들의 경우 필수인 부분과, 넣어줄 수 있는 type이 표시되고, 각각 prop의 설명을 주석으로 적어놓은 부분들이 표시되어, 작성한 컴포넌트의 documentation역할을 해 줄수 있습니다.
그 아래로는 작성한 해당 컴포넌트의 스토리들을 한 눈에 볼 수 있습니다.
Storybook을 이용한 테스팅
앞서 소개한 addon 중 @storybook/testing-react addon을 이용하면 storybook을 이용해 테스팅을 해 볼 수 있습니다. 간단히 예시로 테스트를 작성해 보겠습니다.
import React from "react";
import { render, screen } from "@testing-library/react";
import { composeStories } from "@storybook/testing-react";
// 작성한 스토리로부터 모든 스토리들을 가져온다.
import * as stories from "./ChatMessage.stories";
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName!, Story]);
// 가져온 모든 스토리들에 대한 Snapshot Testing을 진행한다.
test.each(testCases)("Renders %s story", async (_storyName, Story) => {
const tree = await render(<Story />);
expect(tree.baseElement).toMatchSnapshot();
});
// 작성한 스토리중 "Basic"에 해당하는 스토리에 대해 렌더링 테스트
const { Basic } = composeStories(stories);
// messageText에 "안녕하세요"를 넣었을 때, "안녕하세요"가 렌더링이 정상적으로 되는지 확인하는 코드
test("renders", () => {
render(<Basic messageText="안녕하세요" />);
const message = screen.getByText("안녕하세요");
expect(message).toBeInTheDocument();
});
TypeScript
복사
테스트 결과
처음 실행 시, 위와 같이 snapshot을 생성합니다.
그 이후에는 생성되어 있는 snapshot과 일치하는지 테스트를 하게 됩니다.
다음과 같이 작성한 모든 스토리에 대한 렌더링 테스트를 간단하게 해 볼 수 있고, react-testing-library를 이용해 테스트를 작성하는 방식과 동일하게 테스트를 진행할 수도 있습니다.
less 와 같이 쓰기
이 부분에서 정말 많이 헤맸던 경험이 있는데, 성공하고 나니 굉장히 별 것 아니였던 기억이 납니다. 제가 less를 추가하고 오류가 나지 않았던 방식은 다음과 같습니다. scss는 간단하게 @storybook/preset-scss addon을 설치하고 추가하는것으로 작동이 되었습니다.
module.exports = {
...
webpackFinal: async (config) => {
config.module.rules.push({
test: /\.less$/,
use: [
require.resolve("style-loader"),
require.resolve("css-loader"),
{
loader: require.resolve("less-loader"),
options: {
javascriptEnabled: true,
},
},
],
});
...
}
JSON
복사
path alias
path alias를 적용하면, 컴포넌트를 import 할때 상대경로로 사용해야 하는 것을 절대경로로 적용할 수 있고, 더 짧고 편하게 사용할 수 있습니다.
// 적용 전
import { ReduxState } from "../../common/controllers/store";
// 적용 후
import { ReduxState } from "@common/controllers/store";
Bash
복사
또한 프로젝트와 스토리북의 path alias를 동일하게 설정함으로써 스토리북에서 컴포넌트에서 import하는것에 오류를 방지할 수 있습니다.
저희는 Next.js 와 TypeScript를 사용하고 있어서 그에 기준으로 예시를 들어보겠습니다.
project
├─── .storybook
│ ├──── main.js
│ └──── preview.js
├─── src
│ ├──── backend
│ ├──── common
│ └──── pages
├─── tsconfig.json
├─── package.json
└─── next.config.js ...
Markdown
복사
이런 형식의 프로젝트가 있다고 했을 때, src, backend, common, pages 에 대한 path alias를 적용하고 싶다면, 프로젝트의 tsconfig.json 파일에 다음과 같이 설정을 추가해 줍니다.
// tsconfig.json
{
"compilerOptions":{
...
"baseUrl":".",
"paths":{
"@src/*":["src/*"],
"@backend/*":["src/backend/*"],
"@common/*":["src/common/*"],
"@pages/*":["src/pages/*"],
}
...
}
}
JSON
복사
또 storybook에도 추가해 줍니다.
// .storybook/main.js
module.exports = {
webpackFinal: async (config) => {
config.resolve.alias = {
...config.resolve.alias,
"@src": path.resolve(__dirname, "../src"),
"@backend": path.resolve(__dirname, "../src/backend"),
"@common": path.resolve(__dirname, "../src/common"),
"@chattings": path.resolve(__dirname, "../src/chattings"),
}
return config;
},
}
JavaScript
복사
이제 Storybook과 next.js의 프로젝트 모두에서 path alias가 되었습니다. 더욱 짧고 깔끔하게 에러 없이 import를 할 수 있습니다.
이렇게 Storybook은, UI 컴포넌트를 독립된 환경에서 작성하고 테스트를 해 볼 수 있고, 다양한 addon을 통해 test까지도 해 볼수 있는 아주 유용한 도구입니다. React뿐만 아니라 Vue, Angular Svelte, Ember 등 다양한 프레임워크를 지원하며 github star수도 2022년 3월 초 기준 69.1k이고, npm weekly download 수도 계속해서 증가하고 있는 인기 있는 도구 입니다.
@storybook/react의 weekly downloads
아직 storybook을 사용하지 않으셨다면 한번 사용해 보시는것을 추천드립니다.
저자소개
나건일은 유하에서 프론트엔드 개발자로 일하고 있습니다. Youha의 서비스 웹페이지뿐 아니라 내부의 Back office들을 개발하는데 열과 성을 다하고 있습니다. Youha의 채팅기능을 비롯한 많은 새로운 기능들을 개발하는데 더 효과적으로 개발하는 것을 고민하고 좋은 방법론들을 실천하고 있습니다. 특별히 여러 개발자들이 그와 함께 일하고 싶어할만큼 친화력이 좋습니다.