일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 에러
- React Query
- Next.js
- 취업
- 개발자
- Git
- 코딩테스트
- Vite
- SSR
- csr
- Browser
- 비동기
- React
- Sass
- 자바스크립트
- TypeScript
- 차이
- 코딩
- 공부
- dynamic import
- http
- css
- JavaScript
- 백준
- 알고리즘
- 취업준비
- 프론트엔드
- html
- error
- DOM
- Today
- Total
minTech
[Storybook] 스토리북에 대해 알아보자 본문
오늘은 스토리북에 대해 공부해보려한다.
스토리북에 대한 강의를 몇 개 듣고, 이번 프로젝트에서 한 번 사용해보았다.
하지만 스토리북을 왜 사용하는지, 구체적으로 어떤 식으로 사용하는 것이 효율적인지 등 간단한 강의만 듣기에는 모르는 것이 많았다.
모르는 상태로 바로 진행했더니 어떤 식으로 작성해야하는 것인지도 잘 몰라서 애를 많이 먹었다.
그래서 한 번 관련 공식문서를 훑어보았다.
🤷🏽 스토리북의 사용법에 대해 간단하게 정리해보자
공식문서에 따르면 스토리북은 UI 구성 요소와 페이지를 격리하여 빌드하기 위해 만들어졌다.
따라서 전체 앱을 렌더링할 필요 없이 하나의 컴포넌트의 상태와, 다양한 엣지 케이스들을 눈으로 확인하면서 개발하고, 다른 사람에게 공유도 가능하다.
스토리북의 몇 가지 개념에 대해 정리해보겠다.
1. story
컴포넌트를 렌더링하는 방법을 설명하는 객체로, UI 구성 요소의 렌더링된 상태를 캡처하여 화면을 통해 보여준다.
➤ 스토리 생성
스토리를 생성하기 위해서는 'MyComponent.stories.tsx/js/ts/jsx' 라는 이름의 파일을 생성하면 된다.
(MyComponent는 컴포넌트 이름)
해당 파일은 스토리를 만들려는 컴포넌트의 파일과 같은 공간에 생성한다.
components/
├─ MyComponent/
│ ├─ MyComponent.js | ts | jsx | tsx
│ ├─ MyComponent.stories.js | ts | jsx | tsx
Q. 스토리북 내에서의 파일 위치는 어떻게 변경할 수 있을까?
=> 스토리북 내의 title 매개변수를 이용하면 사이드바에서의 스토리의 위치를 정의할 수 있다.
대규모의 스토리북을 구성하는 경우에는 해당 매개변수를 통해 파일 구조를 나눠 그룹으로 관리하는 것이 좋다.
import type { Meta, StoryObj } from '@storybook/react';
import Input from './Input';
const meta: Meta<typeof Input> ={
title: 'Components/Input',
component: Input,
}
위와 같이 작성된 경로대로 스토리북이 저장되어있음을 알 수 있다.
➤ 스토리 작성
☻ 스토리를 작성하기 전에 스토리의 구조에 대해 살펴보겠다.
모든 ES6 모듈 표준인 CSF(Component Story Format) 스토리 파일은 default export와 하나 이상의 named exports로 구성된다.
- default export
- 컴포넌트 그 자체, 구성 요소와 구성 요소들에 대한 메타 데이터를 정의한다.
- 해당 메타 데이터를 활용하여 스토리북을 구성한다.
import type { Meta, StoryObj } from '@storybook/react';
import Input from './Input';
const meta: Meta<typeof Input> = {
component: Input,
parameters: {
backgrounds: {
default: 'dark',
},
},
}
export default meta;
❓Q. default export 내에서 정의되는 필드는 무엇이 있을까?
- title: 스토리북 사이드바 내의 위치를 정의한다.
- component : 필수 요소, 자동 prop 테이블 생성 및 다른 구성 요소 메타 데이터 표시에 사용된다.
- parameter : 스토리에 대한 정적 메타데이터를 정의한다.
- decorator : 스토리를 렌더링할 때 임의의 마크업으로 구성 요소를 래핑한다.
- named exports
- 구성 요소의 스토리를 정의한다.
- UpperCamelCase를 사용하여 정의하는 것이 좋다.
import type { StoryObj } from '@storybook/react';
import { Input } from './Input';
type Story = StoryObj<typeof Input>;
export const Primary: Story = {
name: 'It's Primary' // 스토리 이름 정의
args: {
primary: true,
label: 'Button',
},
};
Q. 스토리를 정의하는 필드에는 무엇이 있을까?
- name : 스토리의 이름을 정의한다.
- args : 스토리북으로 javascript 객체에 인수를 정의한다.
- parameter : 스토리에 대한 정적 메타데이터를 정의한다.
- decorator : 스토리를 렌더링할 때 임의의 마크업으로 구성 요소를 래핑한다.
스토리 단위로도 decorators와 parameters 지정이 가능하다.
☻ 각각의 필드에 대해 자세하게 살펴보자!
➤ args
- args를 이용해 Storybook에서 구성 요소의 JavaScript 객체에 인수를 정의할 수 있다.
- 인수 값을 변경하면 인수에 영향을 미치는 애드온을 통해 스토리북 UI의 구성 요소와 상호작용이 가능하다.
- 그렇기 때문에 컴포넌트 수정 없이 args를 이용해 각각의 스토리마다 props, slots, 스타일 등을 동적으로 변경한다.
- 인수는 스토리, 컴포넌트, 글로벌 수준에서 정의가 가능하다.
- 단일 스토리의 인수 정의
import type { StoryObj } from '@storybook/react';
import { Input } from './Input';
type Story = StoryObj<typeof Input>;
export const Primary: Story = {
name: 'Input'
args: {
primary: true,
label: 'Input',
},
};
해당 파일 내에서 Javascript 객체 재사용을 통해 재사용이 가능하다.
import type { StoryObj } from '@storybook/react';
import { Input } from './Input';
type Story = StoryObj<typeof Input>;
export const Primary: Story = {
name: 'Input'
args: {
primary: true,
label: 'Input',
},
};
export const PrimaryLargeInput: Story = {
args: {
...Primary.args, // 재사용된 부분
width: 'large'
label: 'Primary Large Input'
}
}
2. 컴포넌트의 인수 정의
아래와 같이 export default 내에서 args 를 통해 스타일을 적용하면 해당 컴포넌트 내의 모든 스토리에 대해서 공통 스타일을 적용할 수 있다.
import type { Meta } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
argTypes: {
backgroundColor: { control: 'color' },
},
args: {
primary: true,
},
};
export default meta;
type Story = StoryObj<typeof Button>;
3. 글로벌 인수 정의
- 아래와 같이 전역으로 인수를 정의하며 스토리북 내의 모든 구성 요소에의 스토리에 인수가 적용된다.
- 해당 코드는 storybook 폴더 내의 Preview 파일에 작성한다.
// storybook/preview.js
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
values: [
{
name: "blue",
value: "blue",
},
{
name: "red",
value: "red"
},
{ name: 'light',
value: '#fff'
},
{ name: 'dark',
value: '#333'
},
]
}
},
};
export default preview;
결과를 보면 기존의 background 컬러에서 light와 dark 가 추가되었음을 알 수 있다.
➤ parameter
- 스토리에 대한 정적 메타데이터를 정의한다.
- 스토리가 선택될때마다 애드온이 스스로 재구성하도록 지시한다.
- 단일 스토리의 매개변수 정의
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const OnDark: Story = {
parameters: {
backgrounds: {
default: 'dark',
},
},
};
2. 컴포넌트의 매개변수 정의
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
parameters: {
backgrounds: {
default: 'dark',
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Basic: Story = {};
3. 글로벌 매개변수 정의
//storybook/preview.js
import { Preview } from '@storybook/your-renderer';
const preview: Preview = {
parameters: {
backgrounds: {
values: [
{ name: 'light', value: '#fff' },
{ name: 'dark', value: '#333' },
],
},
},
};
export default preview;
➤ decorators
- 스토리를 추가 "렌더링" 기능으로 래핑하는 방법이다.
- 일반적으로 추가 마크업이나 컨텍스트 모킹으로 감싸는데 사용된다.
1. 추가 마크업
- 예를 들어 해당 스토리 ui 주변에 간격을 주고 싶다면 decorators를 이용하여 padding이나 margin을 넣을 수 있다.
const meta: Meta<typeof Input> ={
component: Input,
parameters: {
backgrounds: {
default: 'red',
}
},
decorators: [
(Story) => (
<div style={{ padding: '2rem'}}>
<Story /> // decorates는 함수로도 사용가넝! Story()로 대체해서 작성해도 ok
</div>
)
]
}
2. 컨텍스트 모킹
- decorators의 두 번째 인수는 스토리 컨텍스트이다.
- args- 스토리 인수. args데코레이터에서 일부를 사용하고 스토리 구현 자체에 드롭할 수 있다.
- argTypes- 스토리를 사용자 지정하고 세부적으로 조정할 수 있습니다
- globals- 툴바 기능을 사용하면 Storybook의 UI를 사용하여 값을 변경할 수 있다.
- hooks- Storybook의 API 후크(예: useArgs).
- parameters- 스토리의 정적 메타데이터로, 일반적으로 Storybook의 기능 및 애드온 동작을 제어하는 데 사용된다
- viewMode- Storybook의 현재 활성 창(예: 캔버스, 문서).
예를 들어 parameters의 page 컨덱스트를 사용하려한다.
참고로 해당 컨텍스트는 스토리의 인수 또는 다른 메타데이터에 따라 데코레이터의 동작을 조정하는데 사용된다,
이를 정의하여 레이아웃을 선택적으로 저장하려면 아래와 같이 사용이 가능하다.
import React from 'react';
import type { Preview } from '@storybook/react';
const preview: Preview = {
decorators: [
(Story, { parameters }) => {
const { pageLayout } = parameters;
switch (pageLayout) {
case 'page':
return (
<div className="page-layout"><Story /></div>
);
case 'page-mobile':
return (
<div className="page-mobile-layout"><Story /></div>
);
default:
return <Story />;
}
},
],
};
export default preview;
- decoratos도 단일 스토리, 컴포넌트 스토리, 글로벌 수준으로 데코레이터 설정이 가능하다.
➤ loader
- 로더는 스토리와 데코레이터에 대한 데이터를 로드하는 비동기 함수로, 해당 로더를 통해 API에서 데이터를 가져와 보여줄 수 있다.
- 로더는 스토리가 렌더링되기 전에 실행되고, 로드된 데이터를 렌더 컨텍스트를 통해 스토리에 주입되는 형식이다.
import type { Meta, StoryObj } from '@storybook/react';
import { TodoItem } from './TodoItem';
const meta: Meta<typeof TodoItem> = {
component: TodoItem,
render: (args, { loaded: { todo } }) => <TodoItem {...args} {...todo} />,
};
export default meta;
type Story = StoryObj<typeof TodoItem>;
export const Primary: Story = {
loaders: [
async () => ({
todo: await (await fetch('https://jsonplaceholder.typicode.com/todos/1')).json(),
}),
],
};
- 로더의 경우도 마찬가지로 preview.js를 통한 글로벌 로더 설정과 로더 상속이 가능하다.
➤ 다중 구성 요소 스토리
두 개 이상의 구성 요소를 렌더링하기 위한 스토리 작성할 때
1. 구성 요소들이 부모-자식 관계로 있는 경우에는 subcomponent 옵션에 자식 컴포넌트를 넣는다.
// 부모 구성 요소 스토리
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { List } from './List';
import { ListItem } from './ListItem';
const meta: Meta<typeof List> = {
component: List,
subcomponents: { ListItem }, //자식 구성 요소
};
export default meta;
type Story = StoryObj<typeof List>;
export const Empty: Story = {};
export const OneItem: Story = {
render: (args) => (
<List {...args}>
<ListItem />
</List>
),
};
이 방법의 한계점은 자식은 오직 문서화 목적으로만 사용되어야한다.
즉, 부모에서 자식 구성요소의 args 를 수동으로 정의 / 재정의할 수가 없으며, 문서화된 하위 구성 요소의 표에는 prop 값을 변경하는 컨트롤이 포함되어있지 않는 오직 viewer의 역할만 수행해야한다.
그렇다면 문제를 어떻게 해결해야할까?
⬇️
2. 스토리 정의를 재사용하여 스토리에서의 반복을 줄인다.
import type { Meta, StoryObj } from '@storybook/react';
import { List } from './List';
import { ListItem } from './ListItem';
import { Selected, Unselected } from './ListItem.stories';
const meta: Meta<typeof List> = {
component: List,
};
export default meta;
type Story = StoryObj<typeof List>;
export const ManyItems: Story = {
render: (args) => (
<List {...args}>
<ListItem {...Selected.args} />
<ListItem {...Unselected.args} />
<ListItem {...Unselected.args} />
</List>
),
};
하위 구성 요소의 args를 가져와서 하위 구성 요소에 직접 집어넣는 방식이다.
이렇게 되면 인수마다 달라지는 하위 구성 요소의 결과를 볼 수 있다.
하지만, 이도 한계점이 있다. 아직 부모 구성 요소의 스토리에서 자식 구성 요소의 스토리를 제어할 수는 없다.
따라서 자식의 인수를 변경할 수 없다.
그렇다면 이 문제는 어떻게 해결할까?
⬇️
3. arg로 children을 사용한다.
import type { Meta, StoryObj } from '@storybook/react';
import { List } from './List';
import { Unchecked } from './ListItem.stories';
const meta: Meta<typeof List> = {
title: 'List',
component: List,
};
export default meta;
type Story = StoryObj<typeof List>;
export const OneItem: Story = {
args: {
children: <Unchecked {...Unchecked.args} />,
},
}
이렇게 되면 부모 구성 요소에서 자식 스토리의 인수를 컨트롤할 수 있게 된다.
arg children은 JSON 직렬화가 가능해야하기 때문에 빈 값을 사용하거나 불완전한 값을 넣게 되면 오륙 생긴다.
따라서 이 부분을 잘 고려하고 넣어주도록 해야한다.
4. 템플릿 컴포넌트를 생성한다.
const ListTemplate: Story = {
render: ({ items, ...args }) => {
return (
<List>
{items.map((item) => (
<ListItem {...item} />
))}
</List>
);
},
};
export const OneItem = {
...ListTemplate,
args: {
items: [{ ...Unchecked.args }],
},
};
여기까지가 공식문서에서의 story의 가이드를 간단하게 간추려 작성해본 것이다 .
다음은 docs이다.
'Project' 카테고리의 다른 글
[Project, React] 한 파일에 있는 이미지 한 번에 import 하기 (1) | 2024.03.15 |
---|---|
[Project, React] 타이핑 효과 적용하기 (0) | 2024.03.13 |
[Project] [내 소개 웹사이트] 개인 프로젝트 시작 (1) | 2024.03.08 |