티스토리 뷰

 

스토리북은 스토리의 집합이다. 각각의 스토리는 컴포넌트의 하나의 시각적 상태를 나타낸다.

 

기술적으로 얘기하면 스토리는 스크린에 렌더링될 수 있는 어떤것을 리턴하는 함수이다.

기본 스토리

버튼 컴포넌트의 스토리에 대한 기본 예제이다.

import React from 'react';
import { action } from '@storybook/addon-actions';
import Button from './Button';

export default {
  component: Button,
  title: 'Button',
};

export const text = () => <Button onClick={action('clicked')}>Hello Button</Button>;

export const emoji = () => (
  <Button onClick={action('clicked')}>
    <span role="img" aria-label="so cool">
      😀 😎 👍 💯
    </span>
  </Button>
);

named export(text, emoji)를 하게 되면 스토리북에서 스토리로 정의된다.

default export는 스토리 그룹(Button)에 대한 메타데이터를 정의한다. 

export default {
  component: Button,
  title: 'Button',
};

위에서 이런 메타데이터를 정의했는데 이러한 포맷을 CSF(Component Story Format)이라고 한다. 링크

타이틀은 유니크해야한다.

 

스토리북 5.2 버전 이전에는 스토리를 정의하기 위해서 storiesOf API를 사용했어야 했다. 하지만 이제는 리액트 네이티브를 제외하고 모든 프레임워크에서 CSF 포맷을 사용할 수 있게 되었다.

스토리의 위치

•
└── src
└── components
    └── button
        ├── button.js
        └── button.stories.js

유지보수를 쉽게 하려면 스토리북 파일 (.stories.js)을 컴포넌트의 위치와 동일하게 하는게 좋다. 다른 방법도 두개 더 소개하고 있는데 그냥 이렇게 하는게 젤 깔끔한것같다.

스토리북 로드하기

스토리들은 .storybook/main.js 파일 또는 .storybook/preview.js 파일에 로드된다.

// .storybook/main.js
module.exports = {
stories: ['../src/components/**/*.stories.js'],
};

glob 패턴으로 components폴더 아래에 있는 모든 stories.js 파일을 불러올 수 있도록 명시하는게 좋다.

 

아니면 다음과 같이 .storybook/preview.js 파일에서 다음과 같이 불러올수도 있다.

import { configure } from '@storybook/react';

configure(require.context('../src/components', true, /\.stories\.js$/), module);

require.context 살펴보기

 

src/components 폴더를 기준으로 하위 디렉토리에 있는 모든 *.stories.js 파일을 불러온다.

만약 src/components가 아닌 다른 폴더에서도 스토리를 불러오고 싶으면 다음과 같이 해주면 된다.

import { configure } from '@storybook/react';

configure(
  [
    require.context('../src/components', true, /\.stories\.js$/),
    require.context('../lib', true, /\.stories\.js$/),
  ],
  module
);

만약에 스토리 파일을 불러오는 커스텀 로직을 작성하고 싶으면 loader function을 작성하면 된다. 중요한것은 CSF를 사용하고 싶으면 module exports의 배열을 리턴해야 한다는것이다.

import { configure } from '@storybook/react';

const loaderFn = () => [
  require('./welcome.stories.js'),
  require('./prelude.stories.js'),
  require('./button.stories.js'),
  require('./input.stories.js'),
];

configure(loaderFn, module);

이런식으로 loader function을 정의하면 정해진 순서대로 스토리 파일을 불러올 수 있다. 각 스토리 파일에는 위에서 살펴봤듯이 named export와 default export가 섞여 있으며 default export에는 메타데이터가 들어있다. 이렇게 export된 객체들이 module.exports 객체에 모여있는데 loader function은 module.exports의 배열을 리턴해야 한다.

 

이런식으로 수동 스토리 import 방식과 자동 스토리 import방식을 섞어 사용할 수 도 있다.

import { configure } from '@storybook/react';

const loaderFn = () => {
  const allExports = [require('./welcome.stories.js')];
  const req = require.context('../src/components', true, /\.stories\.js$/);
  req.keys().forEach(fname => allExports.push(req(fname)));
  return allExports;
};

configure(loaderFn, module);

이부분을 아직 정확히 해석하기는 힘들지만, 하나씩 살펴보자.

먼저 allExports라는 배열을 선언하고 welcome.stories.js를 불러왔다.

 

그다음이 중요한데, src/components아래에 있는 모든 스토리 파일을 불러와서 변수 req에 담는다. 이 req에 담긴 것은 json 형태로 되어 있는데 key는 파일의 경로가 들어있다. 각 키들을 순회하면서 require(각 파일의 경로)하고 allExports 배열에 넣는다.

 

이렇게 하면, 수동으로 넣어준 welcome.stories.js 를 가장 먼저 불러온 뒤에, src/components폴더 아래에 있는 모든 스토리들을 동적으로 불러올 수 있다.

 

CSF 포맷을 사용할 수 없는 스토리북 5.2 버전 이전이나 리액트 네이티브에서는 다음과 같이 loader function의 리턴값이 없어야 한다.

import { configure } from '@storybook/react';

const loaderFn = () => {
  // manual loading
  require('./welcome.stories.js');
  require('./button.stories.js');

  // dynamic loading, unavailable in react-native
  const req = require.context('../src/components', true, /\.stories\.js$/);
  req.keys().forEach(fname => req(fname));
};

configure(loaderFn, module);

보면 require만 하고 있고 리턴은 안해주고 있다.

 

Decorators

데코레이터는 스토리를 특정 컴포넌트로 감싸준다. 예를들어, 어떤 스토리의 텍스트를 정렬 시키고 싶을때 스토리를 데코레이터로 감싸주면 된다.

 

데코레이터는 global, component, individually의 3가지 레벨로 정의할 수 있다.

global decorators는 스토리북 설정 파일에 적용 되어 있다.

component 레벨의 데코레이터는 스토리북 파일에 정의 되어 있다.

individually 레벨의 데코레이터는 각 스토리에 정의 되어 있다.

 

.storybook/preview.js

import React from 'react';
import { addDecorator } from '@storybook/react';

addDecorator(storyFn => <div style={{ textAlign: 'center' }}>{storyFn()}</div>);

 

global decorators를 preview 파일에 적용 시켜 뒀다. 이제 모든 스토리에 텍스트를 가운데 정렬 해주는 컴포넌트가 감싸지게 된다. 마치 High Order Components와 비슷한 것 같다.

 

컴포넌트 단위의 데코레이터는 스토리북 파일에 정의된다.

 

src/components/MyComponent.stories.js

import React from 'react';
import MyComponent from './MyComponent';

export default {
  title: 'MyComponent',
  decorators: [storyFn => <div style={{ backgroundColor: 'yellow' }}>{storyFn()}</div>],
};

스토리북 파일 상단에 default로 export된 객체를 정의해주면 메타데이터로 사용된다고 말했었다. 이곳에 데코레이터를 정의해주면 해당 컴포넌트에서 만들어지는 모든 스토리에 이 데코레이터가 적용된다.

 

스토리 단위의 데코레이터는 이렇게 적용된다.

export const normal = () => <MyComponent />;
export const special = () => <MyComponent text="The Boss" />;
special.story = {
  decorators: [storyFn => <div style={{ border: '5px solid red' }}>{storyFn()}</div>],
};

special이라고 하는 특정 스토리에만 5px짜리 빨간색 데코레이터가 적용된다.

 

데코레이터는 스토리를 포맷하기 위해서 사용될 뿐만 아니라 다음과 같은 상황에서도 유용하다.

 

1. 테마를 적용할때 모든 스토리에 일일이 정의하는게 아니라 데코레이터로 적용함.

 

2. 리덕스와 같은 상태 관리 라이브러리를 사용할때 스토어에 있는 데이터를 가져올 수 있음.

 

3. 스토리북의 애드온들이 데코레이터에 아주 많이 의존하고 있다. 예를들어 Knobs 애드온을 적용할때 UI 컴포넌트에 전달되는 props를 조작할 수 있게 해주는데 변경된 props를 UI 컴포넌트에 반영하는 과정에서 데코레이터가 사용된다.

 

Parameters

파라미터는 각 스토리에 대한 커스텀 메타데이터이다. 데코레이터 처럼, 글로벌, 컴포넌트, 로컬 3개의 레벨에서 정의 될 수 있다. 각 스토리에 마크다운 노트를 붙여주는 예제를 살펴보자.

 

.storybook/preview.js

import { load, addParameters } from '@storybook/react';
import defaultNotes from './instructions.md';

addParameters({ notes: defaultNotes });

글로벌하게 Parameters를 적용하기 위해서 데코레이터와 마찬가지로 preview.js에 코드를 작성하면 된다. 기본적으로 제공되어야 하는 defaultNotes(instructions.md)를 모든 스토리에 글로벌하게 제공하고 있다. 컴포넌트에 아무런 documentaion이 제공되지 않았을때 이 global하게 적용된 instructions.md 파일이 사용될것이다.

 

만약에 컴포넌트나 스토리 레벨에서 documentation을 정의한다면 global하게 적용된 documentation은 무시될것이다.

export default {
  title: 'MyComponent',
  parameters: { notes: componentNotes },
};

export const small = () => <MyComponent text="small" />;
export const medium = () => <MyComponent text="medium" />;
export const special = () => <MyComponent text="The Boss" />;
special.story = {
  parameters: { notes: specialNotes },
};

컴포넌트 레벨parameters에서 componentNotes를 제공하고 있다. 또한 special 스토리의 경우 스토리 레벨에서의 local parameters를 정의하고 있다.

 

smallmedium 스토리의 경우 componentNotes가 적용될것이고, special 스토리의 경우 specialNotes가 적용될것이다. 여기에 적힌 모든 스토리는 defaultNotes를 사용하지 않게 된다.(무시된다.)

 

스토리북에서 스토리 검색하기

스토리북 상단에는 이런 검색창이 존재한다. 스토리북은 검색 결과에 어떤 컴포넌트를 노출 시킬지도 커스텀 할 수 있다.

export const callout = () => <Callout>Some children</Callout>;
callout.story = {
  parameters: { notes: 'popover tooltip' },
};

callout 스토리에 parameters로 popovertooltip notes를 전달했다. 이렇게 하면 스토리북 검색창에서 tooltip을 검색했을때 이 스토리가 노출된다. (스토리북 5버전 이상 부터 가능)

 

스토리 계층 구조 정의하기

스토리들은 "/" 구분자로 계층을 구분 할 수 있다.

 

예를들어 아래 Button과 Checkbox 컴포넌트는 Design System/Atoms에 정의 되어 있다고 표시 할 수 있다.

// Button.stories.js
import React from 'react';
import Button from './Button';

export default {
  title: 'Design System/Atoms/Button',
};
export const normal = () => <Button onClick={action('clicked')}>Hello Button</Button>;

컴포넌트 레벨에서 타이틀을 "/"로 구분하면 스토리북에서도 그대로 계층구조로 구분되어 정리된다.

 

// Checkbox.stories.js
import React from 'react';
import Checkbox from './Checkbox';

export default {
  title: 'Design System/Atoms/Checkbox',
};
export const empty = () => <Checkbox label="empty" />;
export const checked = () => <Checkbox label="checked" checked />;

이렇게 정의하면 empty와 checked 스토리는 Design System/Atoms/Checkbox 컴포넌트에 하위 스토리로 정의된다.

 

여기에서 Design Systemtop-level heading에 해당한다. 이 top-level heading을 스토리북에서 표시하고 싶으면 설정파일에서 따로 설정을 해줘야 한다.

ATOMS, MOLECULES, TEMPLATES는 top-level heading이다.

이 top-level heading을 표시하고 싶으면(링크),

 

.storybook/preview.js에서

import { addParameters } from '@storybook/react';

addParameters({
  options: {
    /**
     * display the top-level grouping as a "root" in the sidebar
     * @type {Boolean}
     */
    showRoots: true,
  },
});

이런식으로 showRoots 속성을 true로 만들어주는 파라미터를 글로벌하게 정의해야한다.

 

스토리에 url 경로를 고정 시키기

스토리의 이름과 계층구조를 바꾸고 싶은데 url은 고정시키고 싶을 수 있다.

export default {
  title: 'Foo/Bar',
};

export const Baz = () => <MyComponent />;

스토리북은 Baz 스토리에 대해서 ID를 부여할때 foo-bar--baz와 같은 형식으로 부여한다. 스토리 앞에는 두번의 대시(-)가 들어간다. 따라서 이 Baz 스토리의 스토리북 상의 URL은 ?path=/story/foo-bar--baz. 가 된다.

 

여기서 내가 이 스토리의 위치를 OtherFoo/Bar로 변경하고 싶다고 해보자. 또한 스토리의 이름은 Baz -> Moo로 변경하고 싶다. 그럴땐 이렇게 해주면 된다.

export default {
  title: 'OtherFoo/Bar',
  id: 'Foo/Bar', // or 'foo-bar' if you prefer
};

export const Baz = () => <MyComponent />;
Baz.story = {
  name: 'Moo',
};

스토리북은 story ID >>>> story title 로 여긴다.

 

그래서 위 Baz 스토리의 경로는 여전히 foo-bar--baz가 된다. 하지만 스토리북 상에서 baz는 OtherFoo 그룹에 Bar 컴포넌트 폴더 안에 Moo로 정의되어 있을것이다.

 

즉, url만 고정되었고 컴포넌트의 표시 위치는 변경되었다.

 

 

 

 

 

 

 

 

 

출처

https://storybook.js.org/docs/basics/writing-stories/

 

 

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
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
글 보관함