ConnecTo
2022/11/18 - Story book
codevil
2022. 11. 18. 19:48
What is Story
Story는 컴포넌트가 UI에 렌더링 된 상태를 캡처하고, 인자(args)가 주어지면 컴포넌트 상태를 반환하는 함수입니다.
Story Process
Story는 컴포넌트 파일이 위치한 디렉토리 안에 작성합니다. 이 파일은 개발용이며 프로덕션 번들에는 포함되지 않습니다. Story를 구성할 컴포넌트를 작성한 후, 컴포넌트 파일과 같은 위치에 Story 파일을 추가합니다.
Editable Config
- title
- Storybook 사이드 바에 표시되는 컴포넌트 이름
- component
- Story를 작성할 컴포넌트 작성
- args
- 모든 Story에 공통 적용할 전달 인자 설정
- argTypes
- 각 Story 인자의 행동 방식 설정
- decorators
- Story를 감싸는 렌더링 함수
- parameters
- Story에 대한 정적 메타 데이터 정의
- excludeStories
- Story를 내부낼 때 렌더링에서 제외 설정
How to use
Story 작성
export const SecondaryButton = () => <Button />
//Story이름은 TitleCase로 작성되는 것이 권장됩니다.
//만일 Storybook에 표시되는 Story의 이름을 변경해야한다면
SecondaryButton.storyName = 'ChangedName'
//이름을 바꾸려면 storyName을 이용해서 바꿔야 한다.
export const Tetiary = () => <Button/>
Story Template
const Template = (args) => <Button {...args/>
인자(arguments)는 줄여서 args를 사용하며, Storybook을 다시 시작하지 않아도, 컨트롤(control) 애드온을 사용해 컴포넌트를 실시간 업데이트 하여 화면에 표시할 수 있습니다.
Copied Template
export const Primary = Template.bind({});
export const Secondary = Template.bind({});
export const Tertiary = Template.bind({});
Template.bind({})는 함수의 복사본을 만드는 JavaScript의 표준 기법입니다. 이 방법을 사용하면 각 story가 고유한 속성(properties)을 갖지만, 동시에 동일한 구현을 사용하도록 할 수 있습니다.
Story Example
Story.stories.js
const Meta = {
// 컴포넌트 설명을 입력하면 Storybook에 카테고리 되어 표시됩니다.
title: 'FormControl/StoryInput',
// 컴포넌트 설정
component: StoryInput,
// 전달인자 공통 설정
args: {
label: '이메일',
type: 'email',
placeholder: 'yamoo9@euid.dev',
},
// 전달 인자 유형 설정
argTypes: {
backgroundColor: { control: 'color' },
disabled: { control: 'boolean' },
},
};
export default Meta;
// 컴포넌트 템플릿
// 함수의 복사본을 만드는 표준 JavaScript 기법
const Template = (args) => <StoryInput {...args} />;
// sm 사이즈 컴포넌트
export const SmSize = Template.bind({});
SmSize.storyName = 'Small';
SmSize.args = {
id: 'sm-size-kwdj1',
size: 'sm',
};
SmSize.parameters = {
viewport: {
defaultViewport: 'iphonex',
},
};
// md 사이즈 컴포넌트
export const MdSize = Template.bind({});
MdSize.storyName = 'Medium';
MdSize.args = {
id: 'md-size-kwdj5',
size: 'md',
};
MdSize.parameters = {
viewport: {
defaultViewport: 'iphonexr',
},
};
// lg 사이즈 컴포넌트
export const LgSize = Template.bind({});
LgSize.storyName = 'Large';
LgSize.args = {
id: 'lg-size-kwdj8',
size: 'lg',
};
Story.scss
@use 'sass:map';
$colors: (
dark: (
label: #767f96,
input: (
border: #1f57e7,
bg: #292f3a,
fg: #f5f5f5,
),
),
light: (
label: #595d65,
input: (
border: #9f9da9,
bg: #fdfdfd,
fg: #08163a,
),
),
);
@function getLabelColor($theme-name: light) {
$theme: map.get($colors, $theme-name);
@return map.get($theme, label);
}
@function getInputColor($name, $theme-name: light) {
$theme: map.get($colors, $theme-name);
$input: map.get($theme, input);
@return map.get($input, $name);
}
.storyInput {
$size: 14px;
display: inline-flex;
flex-direction: column;
label {
margin-bottom: 0.4em;
color: getLabelColor();
}
input {
border: 2px solid rgba(getInputColor(border), 0.4);
border-radius: 8px;
padding: 1em;
background: getInputColor(bg);
color: getInputColor(fg);
&:focus {
outline: 0;
border: 2px solid getInputColor(border);
box-shadow: 2px solid rgba(getInputColor(border), 0.4);
}
}
// Dark Mode
.dark & {
label {
color: getLabelColor(dark);
}
input {
border-color: rgba(getInputColor(border, dark), 0.4);
background: getInputColor(bg, dark);
color: getInputColor(fg, dark);
&:focus {
border-color: getInputColor(border, dark);
box-shadow: 2px solid rgba(getInputColor(border, dark), 0.4);
}
}
::placeholder {
color: #767f96;
}
}
// Size
&.sm {
label,
input {
font-size: $size * 0.8;
}
}
&.md,
input {
label {
font-size: $size;
}
}
&.lg {
label,
input {
font-size: $size * 1.2;
}
}
}
Story.js
import './StoryInput.scss';
import { string, bool, oneOf } from 'prop-types';
// Story를 구성할 컴포넌트를 작성합니다.
const StoryInput = ({ id, label, className, size, ...restProps }) => (
<div className={`storyInput ${className} ${size}`.trim()}>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" {...restProps} />
</div>
);
export default StoryInput;
// 컴포넌트 속성 검사를 설정하면 Story 문서에 반영됩니다.
// 컴포넌트에 필요한 데이터 형태를 명시하려면 React에서 PropTypes를 사용하는 것이 가장 좋습니다.
// 이는 자체적 문서화일 뿐만 아니라, 문제를 조기에 발견하는 데 도움이 됩니다.
StoryInput.propTypes = {
/** label 요소와 input 요소를 연결하는 key */
id: string.isRequired,
/** UI에 표시되는 레이블 */
label: string.isRequired,
/** 레이블을 UI에서 감춤 (스크린 리더 사용자에게는 읽힘) */
labelHidden: bool,
/** 플레이스홀더 */
placeholder: string,
/** 커스텀 클래스 이름 */
className: string,
/** 설정 가능한 인풋 타입 */
type: oneOf(['text', 'email', 'password', 'search']),
/** 인풋 크기 */
size: oneOf(['sm', 'md', 'lg']),
};
// 컴포넌트 기본 속성을 설정하면 Story 문서에 반영됩니다.
StoryInput.defaultProps = {
size: 'md',
type: 'text',
className: '',
labelHidden: false,
};
Setting to start
{
"scripts": {
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public"
}
}
Usage of parameters
const Meta = {
title: 'Button',
component: Button,
parameters: {
backgrounds: {
values: [
{ name: 'darkred', value: '#340000' },
{ name: 'storypink', value: '#fb6597' }
],
},
},
};
export default Meta;
Usage of decorators
const Meta = {
title: 'Button',
component: Button,
decorators: [
(Story) => (
<div style={{ margin: '40px' }}>
<Story />
</div>
),
],
};
export default Meta;