본문 바로가기

TECH

Storybook 7.0 사용기 (+ Next.js, Typescript, Styled-component)

마지막으로 스토리북 6.5 버전을 사용했었는데 7.0으로 바뀌고 나서 Type 관련해서 많이 변경된 것 같다. 6.5 > 7.0으로 업데이트를 잘못하면 타입 지옥에 빠질 수도 있을 것 같다. 그러나 Storybook을 처음 적용하는 프로젝트 였기 때문에 7.0으로 설치하고, 변화된 부분을 살펴보았다.

Storybook

설치

  • 스토리북 설치
npx storybook@latest init
  • 스토리북 실행
npm run storybook

config 관련 수정

추가적으로 건들인 부분이었는데 stories에 모든 stories.ts 파일이 묶이는게 아니고, 컴포넌트 내에 stories.ts 파일이 있있으면 했다. 그 이유는 컴포넌트의 Props 수정이 일어날 수 있는데 Stories/해당 컴포넌트.stories.ts 파일에서 찾아서 또 수정하는게 아니라 (까먹고 수정못할 가능성 높음) 해당 컴포넌트 폴더에서 보기 쉽게 하고 싶었기 때문이다.

변경 전
변경 후

config 파일에서 component 폴더 위치로 경로를 변경해주었다.

// .storybook/main.ts

const config: StorybookConfig = {
  stories: [
    '../components/**/*.mdx',
    '../components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
}

추가적으로 stories.tsx 파일에서 docs가 자동적으로 생성되길 바랬기 때문에 config를 하나 더 추가해주었다.

const config: StorybookConfig = {
 // 중략
  docs: {
    autodocs: true,
  },
}

스토리북 코드 작성

- 기존 6.5 ver

Story라는 type을 지정해주고, AddButtonProps라는 type도 import 시켜주었어야 했다. 그리고, Template을 만들어서 bind 를 일일히 해주고, storyName이라는 걸 또 지정해주어야해서 Default에 관련된 코드들이 묶여있지 않았다.

import AddButton from '.';

import type { Story } from '@storybook/react';
import type { AddButtonProps } from './types';

export default {
  title: 'AddButton',
  component: AddButton,
  argTypes: {
    size: {
      description: '버튼의 사이즈를 결정합니다.',
      control: 'select',
      default: 'medium',
      options: ['small', 'medium', 'large'],
    },
    disabled: {
      description: '버튼의 활성화 여부를 결정합니다',
      default: 'false',
    },
  },
};

const Template: Story<AddButtonProps> = (args) => {
  return <AddButton {...args} />;
};

export const Default = Template.bind({});
Default.args = {
  title: '도메인 만들기',
};
Default.storyName = 'Default';

- 바뀐 7.0 ver

Story라는 타입이 deprecated 되어 사용할 수 없게 되었다. 그리고, 이제 더이상 해당 컴포넌트의 type을 import 하지 않아도 된다. 그리고 Story라는 태그를 만들어주어서 default에 관한 것들을 모아서 확인할 수 있다.

render는 만약 설정해주지 않으면, 기본 AddButton 컴포넌트가 랜더링 되고 간혹 컴포넌트에 style로 wrapping이 필요한 경우가 있는데 그럴 때 사용해주면 유용하다 !

import AddButton from '.';

import type { Meta, StoryObj } from '@storybook/react';

const meta: Meta<typeof AddButton> = {
  title: 'AddButton',
  tags: ['autodocs'],
  component: AddButton,
  argTypes: {
    size: {
      description: '버튼의 사이즈를 결정합니다.',
      control: 'select',
      default: 'medium',
      options: ['small', 'medium', 'large'],
    },
    disabled: {
      description: '버튼의 활성화 여부를 결정합니다',
      default: 'false',
    },
  },
};

export default meta;

type Story = StoryObj<typeof AddButton>;

export const Default: Story = {
  render: (args) => (
    <div style={{ width: 70 }}>
      <AddButton {...args}></AddButton>
    </div>
  ),
  args: {
    size: 'medium',
  },
};

+ 추가

여러가지 type의 Story를 만들 수 있는데 필요하지 않은 args 를 제거하고 싶을 때가 존재한다. 

예를 들어, Button 이라는 컴포넌트에서 Default / Positive 로 나눠서 보여주고 싶은 경우에 Default에는 모든 props 노출 Positive 에는 일부 props 를 노출하여 storybook에서 보여주는 경우다. ( 6.5 > 7.0 으로 바뀌면서 이 부분을 제일 찾기 어려웠다 ㅠㅠ )

 

 

- 6.5 ver

 

Default.parameters 를 따로 선언해주서 include / exclude 를 따로 설정해준다.

export const Default = Template.bind({});
Default.args = {
  title: '도메인 만들기',
};
Default.storyName = 'Default';
Default.parameters = {
  controls: {
    include: ['title', 'color'],
  },
};

- 7.0 ver

 

parameters 의 controls 에 exclude 를 사용해주면 된다. (exclude 할게 너무 많아서 include 부분만 추가하고 싶다면, include 로 사용해주면 된다.) 

그냥 기존에 작성했던 모든 부분이 내부로 이동한 것인데 이상한 부분에서 삽질했다..🙃 

export const Default: Story = {
  render: (args) => <Button {...args}>Enter</Button>,
};

export const Positive: Story = {
  render: (args) => <Button {...args}>Enter</Button>,
  args: {
    primary: true,
    buttonTheme: 'positive',
  },
  parameters: {
    controls: {
      exclude: [
        'gap',
        'icon',
        'secondary',
      ],
    },
  },
};

 

Styled-component JSX 적용

Storybook 관련 코드를 작성했는데 스토리북 내에 primary(#4038FD)가 undefined 라고 뜬다.
props로 받은 props.theme.primary가 전달이 잘 되지 않은 것이다.

// FileModalErrorNode.tsx
import S from "./styled.ts"

const FileModalErrorNode: FC<FileModalErrorNodeProps> = ({
  error,
  loading,
}) => {
    // 코드 중략

      return (
     <S.ErrorNodeWrapper>
      //중략
     </S.ErrorNodeWrapper>
    )

}
// styled.ts
const RefetchWrapper = styled.div`
  display: inline-block;
  color: ${({ theme }) => theme.primary};
  cursor: pointer;
`;

styled-component에 있는 ThemeProvider로 전달해준 theme를 storybook 상에서도 인식할 수 있게 해주어야하는데, 이를 인식할 수 있게 @storybook/addon-styling를 설치해준다.

  • Styling Addon 설치
npm install -D @storybook/addon-styling
  • config file 수정
// .story/main.ts

import type { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
  stories: [
    '../components/pure/**/*.mdx',
    '../components/pure/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-styling',
  ],

  ... 중략

  }
  • theme 설정

light / dark 모드에 따라서 ThemeProvider로 전달되는 color 칩이 달라서 하단처럼 제공해주었다.

const lightTheme = {
  colors: lightModeColor,
};

const darkTheme = {
  colors: darkModeColor,
};

export const decorators = [
  withThemeFromJSXProvider({
    themes: {
      light: lightTheme,
      dark: darkTheme,
    },
    defaultTheme: 'light',
    GlobalStyles,
    Provider: ThemeProvider,
  }),
];

storybook 상에서 설정해준 theme에 맞게 해당 color를 확인 할 수 있다

스토리북 사용기

리액트나 넥쩨를 사용하면서 컴포넌트로 모듈화를 하게 되는데, 직접 눈으로 확인할 수 있는 라이브러리가 없다면 다른사람이 만든 컴포넌트를 활용하기 너무 어렵다 ( ㅠㅠ ) 회사에서도 만들어진 컴포넌트를 끌어 써야하는데 직접 UI에 비슷한 디자인이 있는지 확인하거나, 이 컴포넌트가 있는지 매번 여쭤봐야하는 단점이 있어서 스토리북을 도입하게 되었다.

컴포넌트가 너무 많기 때문에 스토리북 코드 작성에 시간을 많이 투자하고 있는데 지금까지는 좋다. (물론 시간 투자를 많이 해야하고, 노가다성 작업이지만 미래를 위해서 지금 힘들기로 했다) 상태관리 툴 ( redux, recoil, zustand 등)의 로직과 연결된 컴포넌트는 스토리북 연결이 어려울 것 같지만 (?) 아직까진 로직이 연결되지 않은 pure한 컴포넌트까진 괜찮을 것 같다 ! ! ⏤ 상태관리 툴과 연결시키는 방법이 있다면 추후에 또 추가할 예정  

 

6.5에 비해 많이 바뀐 것은 없지만 storybook 관련 코드를 사용하는 부분에서 변화가 있었다(typescript 한정인 것 같기도) 6.5에 익숙해있어서 이게 뭐지? 싶은 부분이 있었지만, 훨씬 더 깔끔하게 작성이 되는 부분이 있어서 좋아요... 

 


storybook 공식 문서
@storybook/addon-styling
Storybook 데이터 연결하기