1. Storybook
스토리북은 앱의 비즈니스 로직, 데이터 및 컨텍스트와 별도로 UI 구성 요소를 구축하고 테스트, QA를 지원한다. 스토리북이 UI 개발에 가져다주는 이점은 확실하다.
컴포넌트 단위의 단위 테스트 레벨을 책임지도록 할 수 있다.
컴포넌트 UI 개발 단계에서 작업물을 확인하기 위한 플레이 그라운드로 사용할 수 있다.
기획, QA 레벨과의 공유와 커뮤니케이션 리소스를 최소화할 수 있다.
설치와 실행
스토리북에서 재공하는 스크립트를 통해 필요한 설정과 예제로 확인할 수 있는 문서와 컴포넌트 스토리북이 추가되며, 실행 명령어가 자동으로 추가된다.
~$ npx storybook init
~$ yarn storybook
.storybook
스토리북의 코어 설정들이 담겨있는 파일들로 구성되어 있다.
.storybook/main.js
.storybook/main.js
생성된 초기 파일은 다음과 같은데, 초기에 추가나 수정이 필요한 부분은 stories
, addons
이다.
stories
는 스토리북을 빌드할 때 타겟이 되는 파일들의 형식을 가진다. 기본적으로 스토리북 문서를 위한 stories.mdx
와 컴포넌트 스토리북을 위한 stories.@(js|jsx|ts|tsx)
가 설정되어 있다.
addons
은 스토리북을 작성할 때 필요한 플러그인 들이라고 이해하면 쉽다. 스토리북을 더욱 그럴싸하게 꾸며보고 싶다면, 필요한 에드온을 추가해서 설정해주면 된다.
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/preset-create-react-app"
],
"framework": "@storybook/react",
"core": {
"builder": "@storybook/builder-webpack5"
}
}
특정 파일(scss, svg, woff2..)들을 처리해주기 위해 웹팩 설정을 해주어야 한다면, webpackFinal
에서 설정을 처리해주면 된다.
module.exports = {
"stories": [],
"addons": [],
webpackFinal: async config => {
// alias path 설정
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, '..'),
//...
};
// inline svg 파일 처리
const fileLoaderRule = config.module.rules.find(
(rule) => rule.test && rule.test.test(".svg")
);
fileLoaderRule.exclude = /\.svg$/;
config.module.rules.push({
test: /\.svg$/,
enforce: "pre",
loader: require.resolve("@svgr/webpack")
});
// scss 파일 처리
config.module.rules.push({
test: /\.scss$/,
use: [
"style-loader",
'css-loader',
'resolve-url-loader',
"sass-loader"
],
include: path.resolve(__dirname, "../"),
});
// woff, woff2 폰트 파일 처리
config.module.rules.push({
test: /\.(woff|woff2)$/,
use: ["file-loader"],
include: path.resolve(__dirname, "../"),
});
// 정적 파일 루트 경로 설정
config.resolve.roots = [
path.resolve(__dirname, '../public'),
'node_modules',
];
return config
}
}
.storybook/preview.js
.storybook/preview.js
생성된 초기 파일은 다음과 같다. main.js
는 스토리북의 전역적인 설정들을 위한 파일이라면, preview.js
는 컴포넌트가 스토리북에서 랜더링되기 위한 의존성(?)들을 주입하기 위한 설정같다.
export const decorators = [
(Story) => (
<Provider store={store}>
<ContextProvider>
<ThemeProvider theme={theme}>
<GlobalStyles />
<Story />
</ThemeProvider>
</ContextProvider>
</Provider>
)
]
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}
decorators
는 가령 스토어나 컨텍스트의 의존성을 주입하거나, styled-components의 전역 스타일을 주입하는 것과 같이 스토리북이 랜더링되기 위해 필요한 상위 컴포넌트들을 래핑해줄 때 사용된다.
parameters
는 스토리북에서 속성들을 컨트롤하거나, 클릭과 같은 액션에 대해 필요한 기본적인 처리방식을 추가할 수 있다.
스토리북 문서
제품 스토리북 소개, 디자인 토큰들을 정리하기 위한 문서를 작성할 수 있다. 그냥 작성되는 건 아니고 mdx 파일을 파싱하기 위한 에드온이 필요한데, 초기에 추가되어 있는 에드온에 포함되어 있다.
본 섹션은 설명을 하기 보다는, 초기에 예제로 추가되는 stories/introduction.stroies.mdx
파일을 보면서 mdx
문법을 대략 이해하면 쉽게 작성할 수 있다.
컴포넌트 스토리북
버튼 스토리북 예제이다. 파일 config를 설정하고, 컴포넌트를 템플릿으로 내보내면 각각의 스토리북 문서가 작성된다.
import React from 'react';
import { Button } from './Button';
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: 'Example/Button',
component: Button,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
};
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Primary.args = {
primary: true,
label: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
label: 'Button',
};
export const Large = Template.bind({});
Large.args = {
size: 'large',
label: 'Button',
};
export const Small = Template.bind({});
Small.args = {
size: 'small',
label: 'Button',
};
사실, 컴포넌트 스토리북을 작성하는 것도 크게 어렵지는 않다. 다만, 입맛에 맞게 수정하거나, 고도화를 위해 시도했던 작업들을 정리해보자.
컴포넌트 특정 prop의 컨트롤의 사용성을 개선한다.
argTypes에는 props 각각의 설정을 커스텀할 수 있다. 가령, backgroundColor 컨트롤을 #ffffff
와 같은 hex 문자열로 전달받아도 좋지만, 색상 파레트로 입력받을 수 있으면 좋을 것 같다면 아래와 같이 control: 'color'
속성을 주어 스토리북에서 제공하는 컨트롤을 커스텀할 수 있다.
export default {
title: 'Example/Button',
component: Button,
argTypes: {
backgroundColor: { control: 'color' },
},
};
반면, 복잡하게 컨트롤이 필요하지 않다면, 비활성화 할 수 있다.
export default {
title: 'Example/Button',
component: Button,
argTypes: {
backgroundColor: {
control: {
disable: true,
}
},
},
};
컴포넌트 특정 prop의 테이블을 제거한다.
컴포넌트 특정 prop에 대해 아예 노출을 시키고 싶지 않다면, table에서 비활성화 할 수 있다.
export default {
title: 'Example/Button',
component: Button,
argTypes: {
backgroundColor: {
table: {
disable: true,
}
}
},
};
컴포넌트에서 의존적으로 변경되는 상태값을 관리할 수 있다.
가령, useState
와 같이 스토리북 내에서 직접 컨트롤할 수 있는 상태값을 관리하고 싶다면, @storybook/client-api에서 제공하는 useArgs
를 사용할 수 있다.
스토리북은 기본적으로 타입 기반으로 prop
의 성질을 구해 컨트롤을 결정하는 듯한데, useState
훅의 반환값으로 제공되는 setState
의 경우 타입 기반으로 캐치되면 스토리북에서 적절한 컨트롤이 불가능하다.
아래와 같이, DatePicker
컴포넌트에서 스토리북에서 랜더링된 컴포넌트를 날짜를 클릭하면, selectedDate
가 업데이트되도록 하기 위해서 useArgs
를 아래와 같이 사용할 수 있다.
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { useArgs } from '@storybook/client-api';
import DatePicker from '.';
export default {
title: 'Designs/common/DatePicker',
component: DatePicker,
args: {
selectedDate: new Date(),
},
argTypes: {
setSelectedDate: {
control: {
disable: true,
},
},
},
} as ComponentMeta<typeof DatePicker>;
const Template: ComponentStory<typeof DatePicker> = (args) => {
const [ {
selectedDate,
}, updateArgs ] = useArgs();
const setSelectedDate = (date: Date) => {
updateArgs({
selectedDate: date,
});
};
return (
<DatePicker
selectedDate={selectedDate}
setSelectedDate={setSelectedDate}
/>
);
};
export const Default = Template.bind({});
Last updated