[Storybook] 스토리북 간단하게 시동걸기

Gyuwon Lee·2022년 12월 25일
1

디자인 시스템

목록 보기
1/6
post-thumbnail

Storybook v7.0 에서는 CSF(Component Story Format)v3 형식을 따릅니다. 본 글은 v6.5의 CSFv2 형식을 따르고 있습니다.

Storybook을 사용해보며 느낀 것은, 다양한 기능만큼 configuration 항목 역시 다양하여 '미리 공부하고 사용' 해보려다가는 시작도 전에 지칠 수 있다는 것이다.

모든 기능을 다 사용할 일은 잘 없기도 하고, 우선 간단한 story 파일을 만들어 놓고 입맛에 맞게 docs configuration을 조금씩 공부하며 바꾸어나가는 편이 훨씬 경제적이다. 공식문서를 따라가보는 것도 좋지만, 내용이 많고 (개인적으로) 체계적이지 않다고 느꼈다.

따라서 이 글에서는 스토리북을 설치하고 문서를 작성하는 데 필요한 기본적인 사항 위주로 짚어보려 한다. 그리고 ready-to-use UI 라이브러리인 MUI를 스토리북과 함께 설치해, 다른 글에서 스토리북의 여러 기능을 활용하는 실전 case들을 다루어볼 것이다.

1. 스토리북을 설치하자 (with MUI)

스토리북을 설치하고, 초기 세팅과 각 파일을 간단하게 살펴보자.

# 현재 경로를 CRA 프로젝트로 init
yarn create react-app . --template typescript
# add mui
yarn add @mui/material @emotion/react @emotion/styled
# add storybook
npx storybook@latest init

스토리북을 최초 설치하면, root 경로의 .storybook 폴더와 /src 경로에 stories 폴더가 생성된다.

main.ts

.storybook의 main.ts에서 스토리북의 각종 config를 설정할 수 있다.
stories 속성을 통해 story 파일들을 불러올 경로를 지정해줄 수 있다.

stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],

스토리북의 핵심인 addons 목록도 이 파일에서 확인할 수 있다.

addons: [
  "@storybook/addon-links",
  "@storybook/addon-essentials",
  "@storybook/preset-create-react-app",
  "@storybook/addon-interactions",
],

preview.ts

동일 경로의 preview.ts는 해당 프로젝트의 모든 Story에 global하게 적용될 포맷을 세팅하는 곳이다.

To control the way stories are rendered and add global decorators and parameters, create a .storybook/preview.js file. This is loaded in the Canvas UI, the “preview” iframe that renders your components in isolation. Use preview.js for global code (such as CSS imports or JavaScript mocks) that applies to all stories. (Docs)

스토리북을 실행시켜 보면 컴포넌트가 렌더링되는 Canvas 영역(한 개의 커다란 iframe 태그 내부)을 찾을 수 있는데, 이 영역을 preview 영역이라고 한다.

preview.js 파일은 모듈화시킬 수 있다. 아래 세 가지 key들을 export하는 형태면 된다.

  • decorators - 전역 decorators의 배열

    • decorators란 컴포넌트 렌더링 시점에서 추가적인 스타일이나 provider 등을 wrapping하기 위해 사용되는 function이다.
    • story / component / global 수준에서 각각 지정해줄 수 있다. 이 중 preview.ts의 decorators는 global decorator다.
  • parameters - 전역 parameters 객체

    • 우리가 함수의 인자를 파라미터 라고 부르듯, parameters 역시 스토리(컴포넌트)가 받는 값이라고 생각하면 간단하다. story / component / global 수준에서 각각 지정해줄 수 있다.
    • 즉 스토리북 문서 상에서 사용자가 변경시킬 수 있는 값, features와 addons에 적용된다.
    • 다만 사용자가 완전히 커스텀할 수 있는 decorators, globalTypes와 달리 parameters 객체는 backgrounds, controls, docs 같이 예약된 속성들을 필요로 한다.
      • 다만 현재 사용가능한 속성들이 정리된 API 문서는 따로 없어서 공식문서와 구글링을 통해 알맞은 속성을 찾아내야 한다(...). 해당 문서는 아직 작성 중이라고 한다.
  • globalTypes - definition of globalTypes

    • 앞선 decorators, parameters와 다르게 오직 global 수준에서만 (not story-specific) 사용되는 render inputs를 의미한다.
    • 따라서 주로 모든 스토리의 렌더링에 공통적으로 적용되는 decorators 안에서 사용된다.
    • 하지만 context.globals 로 어디서든 접근할 수 있기는 하다.
    • 전체적인 theme, mode(light / dark), locale 등의 값이 주로 globalTypes에 해당하는데, 이러한 값들을 간편하게 변경할 수 있도록 toolbar에 등록해 사용할 수 있다.
globalTypes: {
  // type key
  theme: {
    description: 'Global theme for components',
    defaultValue: 'light',
    toolbar: {
      // 이곳에서 toolbar 관련 config를 작성할 수 있다.
    },
  },
},

src/stories

스토리북을 처음 설치하고 yarn storybook 으로 실행시켜 보면, 이미 작성된 story들을 문서상으로 확인할 수 있다.

이 story들은 기본적으로 src/stories에서 관리된다. *.stories.* infix 가 붙은 파일들은 스토리북 문서 상으로 보여줄 story들이고, 해당 infix 없이 작성된 파일들(대표적으로 .tsx)은 story 안에서 사용될 각종 util이나 컴포넌트를 의미하도록 작성하는 것이 컨벤션이다.

작성된 story들은 문서 좌측의 sidebar에 나타난다. 각 컴포넌트 dropdown 들은 하나의 *.stories.* 파일을 의미하고, 드롭다운 안의 각 컴포넌트들은 해당 파일 안의 story들이다.

이 sidebar 계층은 컴포넌트 title 속성 안에서 / 구분자로 depth를 올리거나 내릴 수 있다. 또는 parameters의 options 속성 중 storySort 값을 수정해 직접 순서를 변경할 수도 있다. (관련 Docs)

2. Story를 작성하자

보통 story를 작성하기 위해서는 template 컴포넌트를 만들어놓고 재사용하는 경우가 많다.

위의 vscode 스크린샷을 보면, Primary / Secondary / Large ... 컴포넌트들이 각각 arg만 다르게 들어가고 있는 것을 볼 수 있다. 이는 story의 타입을 template 컴포넌트의 타입으로 지정한 방법이고, 이외에도 .bind() 메서드를 사용하는 방법도 있다.

// using type
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    backgroundColor: '#ff0',
    label: 'Button',
  },
};

export const Secondary: Story = {
  args: {
    ...Primary.args,
    label: '😄👍😍💯',
  },
};

...
// using .bind()
import type { Story } from '@storybook/react';

const Template: Story = (args: ButtonProps) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = { background: '#ff0', label: 'Button' };

export const Secondary = Template.bind({});
Secondary.args = { ...Primary.args, label: '😄👍😍💯' };

bind 를 사용한 방식에서, 마치 컴포넌트의 props 처럼 args 를 인자로 받고 있다.

Controls addon을 활용하면 굳이 이렇게 각 arg에 따른 story를 하나하나 만들지 않아도, 사용자가 동적으로 값을 변경시켜 가며 컴포넌트의 상태를 확인할 수 있다. 다만 이렇게 하면 사용자가 각 story를 클릭해서 매번 원하는 값을 control 패널에서 입력해야 한다는 귀찮음이 생길 수 있다.

export const Pair: Story = {
  args: {
    buttons: [{ ...ButtonStories.Primary.args }, { ...ButtonStories.Secondary.args }],
    orientation: 'horizontal',
  },
};

반면 args 객체를 잘 활용하면 이런 식으로 여러 컴포넌트에서 필요한 상태 값을 재사용할 수 있다.

Controls 패널

사실 나는 args 객체를 사용할 일은 잘 없었고, 주로 모든 props를 제어 가능한 default story 하나와, 고정된 prop을 받는 view-only stories(ex. primary, secondary 등 각 variant 별 story)를 만들어두었기에 Controls 패널을 사용할 일이 더 잦았다.

import { Button, ButtonProps } from '@mui/material'
import type { Story } from '@storybook/react';

const Template: Story = (props: ButtonProps) => <Button {...props} />;

이런 식으로 story에 타입이 지정된 args를 넘기면, Control 패널에서 해당 타입대로 값을 제어할 수 있게 자동 생성된다.

이를 위해서는 *.stories.* 파일에 story metadata를 작성해주어야 한다.

별도의 lint 설정을 건드리지 않았다면, *.stories.* 파일이 default export하도록 정해져 있을 것이다. 보통 default export로 story metadata를 내보내기 때문인데, 이 metadata는 title, argTypes 등 해당 스토리 파일에 적용되는 여러 정보를 담고 있어 중요한 역할을 한다.

그중 component 속성의 값을 바탕으로 argTypes, 즉 Controls 패널에서 제어 가능한 값들을 자동 생성한다.

Storybook uses this to auto-generate the ArgTypes for your component based on either PropTypes (using react-docgen) or TypeScript types (using react-docgen-typescript).

따라서 metadata.component를 정확하게 작성해주어야 한다. 위처럼 props의 타입을 지정해주는 것으로는 Controls 패널이 활성화되지 않는다. MUI의 경우 기본 MUI 컴포넌트를 component로 지정하면 어째서인지 argTypes를 자동 생성하지 못한다. 해당 컴포넌트를 리턴하는 콜백함수 형태의 default 컴포넌트를 만들어서 metadata.component의 값으로 넘겨주어야 의도대로 작동한다. 방법은 아래 코드와 같다.

import {Button as MuiButton, ButtonProps as MuiButtonProps}

export const Default = (props: MuiButtonProps) => {
  return <MuiButton {...props} />;
};

// not works
export default {
  title: "Button",
  component: Button,
} as Meta;


// works
export default {
  title: "Button",
  component: Default,
} as Meta;

2023년 1월 release된 storybook v7.0은 새로운 형태의 story인 CSFv3을 도입하면서, story를 작성하는 방식이 위와 크게 달라졌습니다. Story 타입은 deprecated 되었고, StoryObj 방식을 사용하여 story를 작성합니다.

Parameters

컴포넌트의 propType을 기반으로 Controls 패널에 값을 자동 생성하는 데는 성공했다. 그런데 이렇게 생성된 값들 중 ReactNode 타입(ex. children) 등 Controls 패널 상에서 값을 입력하기 어려운 prop은 어떻게 하면 좋을까?

이럴 때 feature, addon 등을 제어하는 데 사용되는 Parameters를 활용하면 해당 값을 지워 버리거나, 타입을 바꿔서 적절하게 제어할 수 있도록 수정 가능하다.

// 특정 값 삭제
export default {
  title: "Button",
  component: Default,
  parameters: {
    controls: { exclude: ['children'] },
  },
} as Meta;

// 이름에 background 또는 color가 포함된 prop을 color picker로 제어하도록 지정
export default {
  title: "Button",
  component: Default,
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
      },
    },
  },
} as Meta;

Decorators

만약 feature나 addon이 아니라, 컴포넌트가 렌더링될 때 적용되어야 하는 wrapper function이 있다면 decorators를 활용한다. 즉 러프하게 말해서, decorators는 컴포넌트의 값을 수정할 때 사용한다고 생각하면 쉽다.

예를 들어, Badge 컴포넌트를 보여주는 story를 생각해 보자.

보통 Badge는 anchor element의 한 쪽 귀퉁이에 붙어 사용되므로, 단독으로 UI를 보여주려면 위와 같이 preview 영역의 모서리에 너무 달라붙게 된다. 이 경우 decorator를 활용하면 Badge.stories.* 파일에서만 preview 영역의 여백을 늘릴 수 있다.

export default {
  title: "Components/Badge",
  component: Default,
  decorators: [
    (Story) => (
      <div style={{ margin: "3em" }}>
        <Story />
      </div>
    ),
  ],
} as Meta;

component decorator를 사용해 3em만큼의 여백을 갖는 div 안에서 Badge가 렌더링되도록 수정해보았다.

3. Story를 보여주자

Storybook의 최종 목적은 이렇게 작성한 Story를 보기 좋게 문서화하여 쉽게 공유하고 확인할 수 있도록 하는 것이다. 그냥 컴포넌트 별로 story를 만들어 나열해두는 것보다, addon-docs를 활용하여 보다 깔끔한 문서 형태로 정리하는 편이 좋다. @storybook/addon-docs는 @storybook/addon-essentials에 포함되어 있어 별도로 설치하지 않아도 된다.

Storybook v7.0 (2023.04 release) 부터는 Docs와 Canvas 탭 구분이 사라졌습니다. 이에 해당 파트의 내용을 새 버전에 맞게 수정하였습니다. 이전 버전 (v6.5 이하) 에서는 별도의 config 없이 Docs 탭이 자동 생성됩니다.

🤚 v7.0부터는 Docs / Canvas 구분이 없다

이는 의도된 UI 변경이라고 한다. (불편하다ㅠㅠ)

문서를 auto-generate 해주는 것과, docs addon을 통해 MDX로 더 fancy한 문서를 작성할 수 있는 기능은 v6.5에서도 동일했으나 이 때는 별도의 Docs 탭에서 확인할 수 있었다.

v7.0 업데이트는 전반적으로 zero-configuration을 지향하고 있다. Docs 탭을 없애버린 건 그 일환인 듯하다.

By default, Storybook offers zero-config support for documentation and automatically sets up a documentation page for each story enabled via the tags configuration property.

Docs

Docs를 활용하면 mdx 형태로 깔끔한 문서를 작성할 수 있게 된다. *.stories.* 파일의 story metadata에 tags: ['autodocs'] 를 추가하면 별도의 설정 없이 해당 파일 안의 story들을 바탕으로 Storybook이 Docs를 자동 생성해 준다.

export default {
  title: "Components/Badge",
  component: Default,
  tags: ["autodocs"],
} as Meta;

이 때 문서의 이름은 기본적으로 'Docs' 로 설정된다. 만약 이 이름을 바꾸고 싶다면 main.ts의 config 중 docs 속성의 defaultName을 수정하면 된다. 다만 특정 컴포넌트의 Docs만 이름을 수정하는 것은 지원하지 않는 듯하다. docs의 autodocs 값을 변경하면 모든 .stories 파일에 대해 기본적으로 docs가 생성되게 할 수도 (= autodocs: true) 있다.

const config: StorybookConfig = {
  ...
  docs: {
    autodocs: true,
    defaultName: "Documentation",
  },
 ...
};

또한 이 docs의 default layout 역시 지정할 수 있다.

앞서 main.ts의 config 안에 있었던 docs 속성과 달리, 이번에는 parameters 안의 docs 값을 수정해서 docs의 구성을 변경할 수 있다. preview.ts의 parameters 에서는 모든 stories 파일의 autogen 된 docs에 전역적으로 적용될 layout을 설정할 수 있고, 컴포넌트 별로 다른 layout이 필요하다면 각 컴포넌트의 story metadata 안에서 parameters의 docs 값을 수정해주면 된다.

export default {
  title: "Components/Button",
  component: Default,
  tags: ["autodocs"],
  parameters: {
    docs: {
      page: () => (
        <>
          <Title />
          <Stories />
        </>
      ),
    },
  },
} as Meta;

수정한대로 Controls 대신 Story들이 Title 바로 아래에 놓이게 되었다.

MDX (feat. @stotybook/blocks)

MDX를 사용하면 Storybook이 자동 생성해주는 docs 대신 직접 문서를 작성할 수 있다.

컴포넌트 이름과 일치하는 .mdx 파일이 해당 컴포넌트의 default docs가 된다. 예를 들어, Badge.stories.tsx 파일과 동일 경로에 Badge.mdx 파일이 있다면 이 MDX 파일이 Badge 컴포넌트의 default docs가 된다. default docs는 앞서 main.ts에서 설정한 defaultName으로 sidebar에 표시된다.

Doc Blocks

스토리북은 Docs 작성의 편의성을 높이기 위해 v7.0부터 Doc Blocks를 제공한다.

yarn add @storybook/blocks

Doc Blocks는 Meta, Canvas, Title, ColorPalette 등 스토리북 문서를 구성하는 여러 UI들을 컴포넌트화 해둔 것이다. 각 Block을 MDX 문서 안에서 사용하거나, 자동 생성되는 문서의 레이아웃을 설정하기 위해 parameters의 docs 콜백 안에서 사용할 수도 있다.

TL;DR

  • npx storybook@latest init 으로 스토리북을 최초 설치한다
  • .storybook 경로의 main.ts, preview.ts를 통해 global configutation을 수정할 수 있다.
    • decorators, parameters는 컴포넌트, story 수준에서 덮어씌울 수도 있다.
  • story rendering function의 prop에 타입을 지정하고, 이 story를 metadata의 component로 지정하자
    • 자동으로 argTypes를 인식해 Controls 패널이 활성화된다.
  • autogen 또는 MDX 파일을 통해 Docs를 작성할 수 있다.
    • v7.0부터는 Canvas와 Docs의 탭 구분이 사라졌다.

스토리북의 공식문서를 읽어보는 건 리액트 공식문서를 읽을 때와 많이 달랐다. 리액트 공식문서는 '아~' 하게 되고 점차 배워 나간다는 느낌을 받은 반면, 스토리북 공식문서는 읽는 내내 '그래서 어떻게 하는건데...' 싶은 부분이 많았다. 내용은 많은데 실제로 문서를 작성하는 사람 입장에서는 기능을 소개하기 보다 어떤 목적이고, 어떻게 사용할 수 있는지 practical 한 how-to가 부족하다고 느꼈다.

다음 글에서는

  • toolbar를 사용해서 light / dark 모드를 컨트롤하는 globalType 설정하기
  • decorator를 사용해서 ThemeProvider로 preview 영역 감싸기
  • CSF 3.0에 맞추어 argType이 autogen된 controllable story 작성해보기
    • Omit, Pick으로 prop을 취사선택하거나, mapping으로 제어 타입을 변경해보기
  • MDX로 Docs 영역 작성해보기
  • Docs autogen layout 커스텀해보기

등등 storybook을 사용하며 현실적으로 마주칠 수 있는 문제를 간단하게 직접 구현해볼 것이다.

profile
하루가 모여 역사가 된다

0개의 댓글