Storybook 프로젝트에서 TypeScript로 컴포넌트의 props 문서화 편하게 하기

Minjun Kim·2019년 11월 23일
18

TypeScript 를 사용하면 JavaScript의 불편함을 해결해줄 수 있고, IDE를 더욱 적극적으로 활용 할 수 있게 해줍니다. 특히, 리액트 컴포넌트를 TypeScript 로 작성하면, PropTypes 를 완전히 대체 할 수 있고 훨씬 유용하고 편합니다.

아직 TypeScript 를 사용해본적이 없다면, 타입스크립트 기초 연습 블로그 포스트를 한번 훑어보는 것을 권장드립니다. TypeScript를 완벽하게 숙지하고 있지 않아도, 이 강의를 진행하는 것에는 큰 지장은 없습니다. 한번 그냥 따라해보세요. 사실 TypeScript 별거 없습니다.

TypeScript 환경 설정

자, 우리가 만든 Storybook 프로젝트에 TypeScript 환경 설정을 해봅시다!

우선 다음 패키지들을 설치해주세요.

yarn add --dev babel-preset-react-app react-docgen-typescript-loader typescript
# 또는 npm install --save-dev babel-preset-react-app react-docgen-typescript-loader typescript

babel-preset-react-app는 create-react-app 에서 사용하는 babel preset 과 동일합니다. TypeScript를 적용 할 때 우리는 babel-loader 를 사용 할 것입니다. 참고로 babel-loader는 Storybook 프로젝트에 이미 설치가 되어있습니다.

react-docgen-typescript-loader는 컴포넌트의 props 에서 사용된 TypeScript 타입들을 추출하여 문서로 만들어주는 도구입니다. 우리가 이전에 PropTypes 를 작성하여 DocsPage 에서 보여줬었던 것 처럼 말이지요.

typescript는 프로젝트에서 TypeScript를 사용하기 위하여 필수적으로 설치해야하는 패키지입니다.

설치하고 나서 프로젝트 루트 디렉터리에 tsconfig.json 파일을 작성해주세요.

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react"
  },
  "include": ["src"]
}

tsconfig.json은 TypeScript 컴파일러의 환경설정을 정의하는 파일입니다. 위 설정은 CRA의 TypeScript 템플릿으로 새 프로젝트를 만들었을 때 사용하는 tsconfig.json 와 동일합니다.

이제 webpack 설정을 커스터마이징 해 주어야 합니다. .storybook 경로에 webpack.config.js 파일을 생성하면 Storybook의 기본 webpack 설정을 커스터마이징하여 사용 할 수 있습니다. (참고 링크)

.storybook 경로에 webpack.config.js 파일을 만들어서 다음과 같이 코드를 작성해주세요.

.storybook/webpack.config.js

module.exports = ({ config, mode }) => {
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    use: [
      {
        loader: require.resolve('babel-loader'),
        options: {
          presets: [['react-app', { flow: false, typescript: true }]]
        }
      },
      require.resolve('react-docgen-typescript-loader')
    ]
  });
  config.resolve.extensions.push('.ts', '.tsx');
  return config;
};

ts 및 tsx 확장자에 대하여 babel-loader 와 react-docgen-typescript-loader 를 사용하도록 설정해주었습니다.

그 다음에는 .storybook/config.js 파일을 열어서 tsx 파일도 Storybook에서 처리하도록 코드를 수정해주세요.

.storybook/config.js

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

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

마지막으로, src 디렉터리에 typings.d.ts 파일을 만들어서 다음과 같이 입력을 해주세요.

src/typings.d.ts

declare module '*.mdx';

이 파일의 용도는 우리가 추후 ts 파일에서 mdx 확장자로 이루어진 파일을 불러오게 될 때 모듈이 없다는 에러를 방지하는 것 입니다.

이제 TypeScript 환경 설정이 끝났습니다.

@storybook/preset-typescript 를 사용하면 이 작업을 더욱 간소화 할 수도 있습니다. 하지만, babel-loader를 사용하고자 한다면 preset 기능을 쓸 수는 없습니다. 우리는 나중에 아이콘을 리액트 컴포넌트 형태로 사용하기 위하여 babel을 사용 할 것이므로, 직접 TypeScript환경을 직접 설정하는 방식으로 진행하겠습니다.

TypeScript로 컴포넌트 작성하기

이제, 기존에 작성했던 Hello.js 컴포넌트의 파일이름을 Hello.tsx 로 변경해주세요. TypeScript를 사용하는 리액트 컴포넌트의 확장자는 .tsx 입니다.

그리고 나서 컴포넌트 파일에서 PropTypes 를 제거하고 TypeScript 로 props 의 타입을 명시해주겠습니다.

src/Hello/Hello.tsx

import React from 'react';

type HelloProps = {
  /** 보여주고 싶은 이름 */
  name: string;
  /** 이 값을 `true` 로 설정하면 h1 태그로 렌더링합니다. */
  big?: boolean;
  /** Hello 버튼 누를 때 호출 할 함수 */
  onHello?: () => void;
  /** Bye 버튼 누를 때 호출 할 함수 */
  onBye?: () => void;
};

/**
 * 안녕하세요 라고 보여주고 싶을 땐 `Hello` 컴포넌트를 사용하세요.
 *
 * - `big` 값을 `true`로 설정하면 **크게** 나타납니다.
 * - `onHello` 와 `onBye` props로 설정하여 버튼이 클릭했을 때 호출 할 함수를 지정 할 수 있습니다.
 */
const Hello = ({ name, big, onHello, onBye }: HelloProps) => {
  return (
    <div>
      {big ? <h1>안녕하세요, {name}!</h1> : <p>안녕하세요, {name}!</p>}
      <div>
        <button onClick={onHello}>Hello</button>
        <button onClick={onBye}>Bye</button>
      </div>
    </div>
  );
};

Hello.defaultProps = {
  big: false
};

export default Hello;

TypeScript를 사용하여 컴포넌트의 props 의 타입을 선언 할 때에는 Type Alias 또는 Interface 를 사용합니다. props 의 타입을 명시 할 때 둘 중 아무거나 써도 상관 없습니다. 여기서 ? 표시는 해당 props 는 생략을 해도 된다는 것을 의미합니다.

타입을 선언하고 나서는 props 가 해당 타입이란것을 명시하기 위하여 파라미터쪽에 다음과 같이 입력하면 됩니다.

const Hello = ({ name, big, onHello, onBye }: HelloProps) => {

그 다음엔, Hello.stories.js 파일의 이름을 Hello.stories.tsx 로 변경해주세요. 파일 내용은 바꿀 필요 없습니다.

이제, Storybook을 종료 후 다시 켜주세요.

Storybook 서버가 구동 될 때 오류가 나지 않았고, Storybook 페이지가 제대로 보여지는 것을 확인해보세요.

여기까지 잘 따라오셨으면, 이제 여러분들은 Storybook을 활용하는 기본적인 방법을 모두 숙지하신 겁니다. 이제 디자인 시스템을 만들어볼 준비가 끝났습니다. 다음 강의, 확인해주세요~

profile
CEO @ Chaf Inc. 사용자들이 좋아하는 프로덕트를 만듭니다.

3개의 댓글

comment-user-thumbnail
2020년 2월 19일

Storybook 5.3 이상을 사용하신다면 설정을 아래와 같이 하시면 될 것 같습니다.

.storybook/main.js

const path = require('path');

module.exports = {
  stories: ['../src/**/*.stories.(js|mdx|tsx)'],
  addons: ['@storybook/addon-actions', '@storybook/addon-links','@storybook/addon-knobs/register','@storybook/addon-docs/preset'],

  webpackFinal: async (config, { configType }) => {
    config.module.rules.push({
      test: /\.(ts|tsx)$/,
      use: [
        {
          loader: require.resolve('babel-loader'),
          options: {
            presets: [['react-app', { flow: false, typescript: true }]]
          }
        },
        require.resolve('react-docgen-typescript-loader')
      ]
    });
    config.resolve.extensions.push('.ts', '.tsx');
    return config;
  },
};
1개의 답글
comment-user-thumbnail
2020년 5월 19일

가령 아래와 같이 interface를 정의 했다고 했을때 addon docs에서 props 리스트를
iconName, color만 나오도록 할 수는 없을까요? SVGProps<SVGSVGElement>에 정의되어 있는
props까지 리스트업을 해주다보니 너무 많아 오히려 보기 어렵게되네요;

export interface ISvgIcon extends SVGProps<SVGSVGElement> {
  iconName: (keyof typeof Icons);
  color?: string;
}
답글 달기