스토리북 제대로 활용하기

devstone·2021년 12월 12일
7

React Study

목록 보기
5/7
post-thumbnail

사용 이유

  1. 복잡한 로직 없이 독립적인 환경에서 컴포넌트를 개발할 수 있습니다.
  2. 재사용을 위한 컴포넌트들을 story에서 조합해 테스트할 수 있습니다.
  3. 컴포넌트들을 문서화 할 수도 있고 디자인 시스템에 적용해 피그마의 컴포넌트들과 동기화할 수 있습니다 (실제 저희 회사에서 이런 방식으로 디자인 시스템을 적용하고 있습니다)

초기세팅

초기세팅부터 실행까지 정말 5분도 걸리지 않는 ... 매우 간단합니다

// 초기세팅
npx sb init

// 추가적으로 내가 설치해준 것 
yarn add --dev react-docgen-typescript-loader 
  • 두 번째 명령어는 아래와 같이 story북 실행했을 때 컴포넌트 테이블 만들기 위해 설치했습니다

실행

  • 명령어 실행 후 localhost:6006 포트에서 실행할 수 있습니다. (심지어 6006번 포트가 사용중이면 자동으로 다른 포트에서 실행시켜 줍니다!)
yarn storybook
or
npm run storybook 

폴더구조

크게 .storybook 폴더와 stories 폴더가 생성되는데, .storybook 폴더 내부의 파일들은 전역적인 설정 관련된 것들을, stories 폴더 내부에는 예시가 되는 컴포넌트와 스토리들이 있습니다!

  • .storybook/main.js → stories를 위한 config 설정들
    • addon : 필요한 확장 프로그램을 찾아서 npm 설치한 이후에 여기에 추가하여 사용할 수 있습니다!
    const path = require('path');
    // storybook을 위한 config설정들 
    module.exports = {
      // stories 파일이 어디에 있는지 경로 설정 
      stories: ['../stories/**/*.stories.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'],
      // addons 세팅 , 유니버스엔 하얀 컴포넌트들이 많아서 다크모드를 적용시켜줌 
      addons: [
        '@storybook/addon-links',
        '@storybook/addon-essentials',
        'storybook-addon-styled-component-theme/dist/preset',
    		'storybook-dark-mode',
      ],
      webpackFinal: async (config) => {
        // node_mules폴더와 styles 폴더 안의 모듈을 인식할 수 있게 함
        config.resolve.modules = [path.resolve(__dirname, '..'), 'node_modules', 'styles'];
    
        // 절대 경로 설정
        config.resolve.alias = {
          ...config.resolve.alias,
          '@components': path.resolve(__dirname, '../components'),
          '@assets': path.resolve(__dirname, '../public/assets'),
        };
    
        return config;
      },
    };
  • .storybook/preview.js → 모든 story들에 글로벌하게 적용될 포맷 세팅
    // 모든 stort들에 글로벌하게 적용될 포맷을 세팅
    
    // npx sb init을 통해 세팅된 값 
    // controls - 개발자가 코드를 수정하지 않아도 storybook에서 동적으로 인터렉션 가능하도록 . 
    export const parameters = {
      // Global 하게 argType에 on 으로 시작하는 이벤트 핸들러 함수들을 모두 허용하는 정규식을 적어주면, Action 탭에서 이벤트가 발생하는 것을 감지할 수 있음
      actions: { argTypesRegex: "^on[A-Z].*" },
      // 해당 데이터타입을 가진 속성을 만났을 때 정규표현식을 통해 데이터타입에 따라 storybook은 이들을 적절하게 테스팅할 수 있도록 매칭해줄 것. 
      controls: {
        matchers: {
          color: /(background|color)$/i,
          date: /Date$/,
        },
      },
    }
    
    // 글로벌 스타일 적용시키고 싶다면 여기서 적용시켜줄 수 있음 (추후 decorator같은 것들)
  • 스토리를 작성할 위치
    • 컴포넌트가 위치하는 폴더 안에 같이 위치시킵니다.
    • 스토리들은 development-only로, production bundle 에는 포함되지 않습니다.

스토리 만들기

스토리는 쉽게 말해 하나의 컴포넌트가 실행 가능한 하나의 케이스를 의미합니다. 특정한 props를 넘겼을 때, 그 자체가 하나의 스토리가 되고, 그렇게 다양한 스토리들을 정의하면 우리는 스토리북에서 모든 스토리들을 직관적으로 확인할 수 있습니다.

  • 기본 꼴
// 어떤 이름으로 스토리북에 올릴 것인지, 어떤 설정으로 렌더링할지 정의
export default {
	title: 스토리북에 올릴 component폴더 계층 구조,
	component: 스토리를 만들 컴포넌트 이름
}

export const 스토리이름 = () => 해당스토리에서 테스트할 인자가 담긴 컴포넌트

Button.tsx

  • 일단 하나의 컴포넌트를 만듭니다.
import React from 'react';
import styled from '@emotion/styled';
interface ButtonProps {
    label: string;
    size?: 'small' | 'large';
    onClick?: () => void;
}

export default function Button({label, size, ...props}:ButtonProps) {
    return (
        <Styled.Button size={size}>
            <div className="button" role="button" {...props}>
                {label}
            </div>
        </Styled.Button>
    )
}

const Styled = {
    Button:styled.div<{size: 'small'|'large'}>`
        display: flex; 
        justify-content: center;
        align-items: center;
        width: ${({ size }) => (size === 'small' ? '48px' : '76px')};
        border-bottom: 1px solid #FFFFFF;
        .button{
            padding-top: 11px;
            padding-bottom: 11px;
            cursor: pointer;
            color: #FFFFFF;
            text-align: center;
            font-family: JejuMyeongjo;
            font-size: 14px;
        }
    `,
}

Component Story Format

  • 위에서 만든 컴포넌트를 바탕으로 스토리를 작성해보겠습니다.
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import Button  from './Button';

// 어떤 컴포넌트의 story인지, 어떤 설정으로 렌더링할지 정의 
export default {
  title: 'stories/Button',
  component: Button,
} as ComponentMeta<typeof Button>;

// 기본 포맷을 정해두고 bind로 제어 
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

// 각각이 새로운 스토리들
// export const Small = () => <Button size="small" label="button" />; 얘와 같음
export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  label: 'Button',
};

간단한 컴포넌트에 대해 스토리를 작성하는 것은 이게 전부이지만, 추가적으로 알아두면 좋을 것들에 대해 정리해보았습니다.

스토리북 내부에 스타일 먹이기 (Decorator)

스토리북에 보여질 때에 어떻게 보여질 지 스타일을 적용시킬 수 있습니다. 다양한 범위에 같은 스타일을 중복 적용 시키면 중복으로 적용되므로 이 점도 유의하시면 좋을 것 같습니다.

  • 지금 스토리가 너무 여백 없이 붙어 있으니 보여질 스토리들에 여백을 줘보도록 하겠습니다.
  1. 하나의 스토리범위
    //Button.stories.tsx
    Small.decorators = [(Story) => <div style={{ margin: '3em' }}><Story/></div>];
  1. stories.tsx의 범위
    //Button.stories.tsx
    export default {
      title: 'stories/Button',
      component: Button,
      decorators: [
        (Story) => (
          <div style={{ margin: '3em' }}>
            <Story/>
          </div>
        ),
      ],
    } as ComponentMeta<typeof Button>;
  1. 모든 stories.tsx의 범위 (ThemeProvider 이용)
    //preview.js 
    import React from 'react';
	// styled-component를 사용해도 무방합니다. 저는 next와 함께 사용해서 emotion을 사용하였습니다.
    import { ThemeProvider } from '@emotion/react';
    const theme = {
      margin: '10px',
    }
    
    export const decorators = [
      (Story) => (
        <ThemeProvider theme={theme}>
          <Story />
        </ThemeProvider>
      ),
    ];
  • 이런 방식들로 보여질 스토리들에 스타일을 적용시킬 수 있습니다!

정적 메타데이터 부여하는 Parameters

Parameters를 통해 스토리북 내부에서 우리가 코드를 변형시키지 않고 테스트 해볼 수 있는 옵션들을 제공할 수 있습니다.

  • 이렇게 백그라운드를 선택할 수 있는 옵션을 만들어보겠습니다.

  1. 하나의 스토리 범위
    //Button.stories.tsx
    Small.parameters = {
      backgrounds: {
        values: [
          { name: 'black', value: '#000000' },
          { name: 'green', value: '#0f0' },
        ],
      },
    };
  1. stories.tsx 범위
    //Button.stories.tsx
    export default {
      title: 'stories/Button',
      component: Button,
      decorators: [
        (Story) => (
          <div style={{ margin: '3em' }}>
            <Story/>
          </div>
        ),
      ],
      parameters: {
        backgrounds: {
          values: [
            { name: 'black', value: '#000000' },
            { name: 'green', value: '#0f0' },
          ],
        },
      },
    } as ComponentMeta<typeof Button>;
  1. 모든 stories.tsx 범위
    //preview.tsx
    export const parameters = {
      actions: { argTypesRegex: "^on[A-Z].*" },
      controls: {
        matchers: {
          color: /(background|color)$/i,
          date: /Date$/,
        },
      },
    	// 추가 
      backgrounds: {
        values: [
          { name: 'black', value: '#000000' },
          { name: 'green', value: '#0f0' },
        ],
      },
    }

Rename stories

말 그대로, 스토리북에 보여질 이름을 바꾸는 설정입니다.

//Button.stories.tsx

Small.storyName = 'I am small button';

배포 후 테스트하기

스토리북에서는 컴포넌트를 수정했을 때, 그 컴포넌트가 제대로 만들어진 컴포넌트인지 테스트 할 수 있는 기능을 제공해줍니다. 이를 위해선 스토리북을 배포해야하는데, 스토리북을 아래 사이트에서 배포하고 나면 기존 브랜치에서 새로운 브랜치를 따서 pr을 날렸을 때 해당 pr에서 변경된 사안이 제대로 테스트 되었는지 눈으로 확인할 수 있도록 해줍니다.

  1. 해당 사이트에서 깃허브로 연동
  2. 스토리북을 사용하고 있는 프로젝트 선택
  3. 이후 아래와같이 뜨는 명령어를 해당 파일에서 실행 → 배포 url 나옴

  • 기존 브랜치에서 새로운 브랜치를 따서 pr을 날렸을 때 해당 pr에서 변경된 사안이 제대로 테스트 되었는지 직접 확인할 수 있습니다!

  • 그리고 아래와 같이 변경 사항이 어떻게 적용되었는지에 대해서도 직접 확인하고 해당 pr을 머지할 수 있습니다.

Reference

레퍼런스는 이 링크를 통해 확인하실 수 있습니다!

profile
개발하는 돌멩이

1개의 댓글

comment-user-thumbnail
2022년 5월 10일

안녕하세요 : ) 글 잘봤습니다!!
궁금한게 있는데 그럼 storybook에 작성한 Button등의 컴포넌트를 export해서
작성한 Button을 특정 컴포넌트에 import해서 사용하는 형태가 맞을까요?!

답글 달기