OTTER-LOG

tsdoc, storybook으로 문서화하기

tsdoc, storybook으로 문서화하기
by otter2023년 2월 7일에 최종수정되었습니다.
잘못된 내용이 있으면 댓글을 달아주세요.

얼마전부터 진행하고 있는 over-ui 의 프로젝트는 거의 끝나가고 있습니다. over-ui 는 예를 들면 Select 와 같은 컴포넌트들을 모아둔 React ui component 라이브러리 입니다. 이러한 라이브러리를 사용자들이 사용할 때에, 쉽게 이해할 수 있으려면 문서화의 과정이 중요하다고 생각했습니다.

그런데 문서화를 진행하던 중 커다란 문제점을 만났습니다. 😂

지금까지의 문서화의 문제점

지금까지 문서화를 작업했던 부분은 README 파일에 위와 같은 usage 를 개별적으로 적어두는 방식이었습니다. 그런데, 이와 같은 부분은 다음과 같은 문제점이 있었습니다.

  • 컴포넌트의 모든 api 를 일일이 적어주어야 했습니다.

    컴포넌트에서 사용할 수 있는 props 의 양이 상당했습니다. 또한, 합성 컴포넌트 방식으로 진행되었기에 하나의 컴포넌트 안의 하위 컴포넌트도 다수 존재했습니다. 이를 모두 파악해 직접 적어주는 방식은 비효율적이라 생각했습니다.

  • usage 가 너무 많았습니다.

    한 컴포넌트에서 5개 이상의 usage 가 존재했고, 이러한 usage 를 코드로만 보여주고 있었습니다. 사용자의 입장에서 이러한 문서를 읽기 힘들 것이라 판단했습니다. 그리고, README 를 이용해 이를 작성하는 것은 개발자 입장에서도 불편했습니다.

  • 사용자가 이를 직접 테스트해볼 수 있는 환경이 제공되지 않습니다.

    코드 샌드박스등을 이용할 수도 있었지만, MD 파일에서 링크 등으로 개별적으로 연결해 주어야 했습니다. 또 코드 샌드박스를 관리하는 일에도 비용이 발생한다고 생각했습니다.

문제를 해결방안 생각하기

위의 문제점을 정리하고 해결 방안을 생각해 보았습니다.

  • 컴포넌트의 api를 추출해 내야 한다.

여러 라이브러리를 검색하던 중, typedoc 을 알게 되었고 typedoc 을 통해 문서화 또는 마크다운으로 변환이 가능하다는 것을 알게 되었습니다. 사용자의 편리성을 위해 tsdoc 을 작성하고 있었으므로 해당 라이브러리를 이용하기로 결정했습니다.

  • 개발자는 여러 usage를 효율적으로 작성하고, 정리할 수 있어야 한다.
  • 사용자가 테스트를 해보는 환경을 제공해야 한다.

위의 두가지 부분에 대해서는, 개발자를 위한 친절한 UI 컴포넌트 라이브러리 만들기를 참고해 스토리북에 문서화를 통합는 방법을 생각했습니다. 저희는 이미 스토리북을 통해 UI 테스트를 진행하고 있어서 빠른 적용이 가능했고 별개의 usage 를 구분해서 적용할 수 있다는 점도 좋았습니다.

typdoc으로 api 문서 생성하기

저희는 tsdoc 을 통해, 해당 컴포넌트의 타입들에 대한 설명을 아래와 같이 적용하고 있었습니다.

export type ToggleRenderProps = { /** * children을 render할때 사용할 수 있는 renderProps 입니다. @example * ```tsx * <Toggle>{({ pressed }) => (pressed ? 'pressed' : 'notPressed')}</Toggle> * // pressed 값에 따라, 다른 컴포넌트, string, 아이콘을 조건부 렌더링할 수 있습니다. * ``` */ pressed: boolean; disabled: boolean; }; export type ToggleProps = { /** * 외부에서 선언한 상태를 이용할때 사용합니다. * `defaultPressed`와 함께 사용할 수 없습니다. * `onPressedChange` 를 함께 사용해야합니다. * * @example * ```tsx * const [state, setState] = React.useState(false); * return <Toggle pressed={state} onPressedChange={setState}/> * ``` */ pressed?: boolean; /** * `Toggle`의 `pressed` 상태의 초기값을 지정합니다. * `pressed`와 함께 사용할 수 없습니다. * `onPressedChange`와 함께 사용할 수 있습니다. * @defaultValue false */ defaultPressed?: boolean; /** * `Toggle`의 상태가 바뀔때 실행되는 함수입니다. */ onPressedChange?: (pressed: boolean) => void; /** * 토글을 `disabled`하는 prop입니다. * @defaultValue false */ disabled?: boolean; children?: React.ReactNode | ((props: ToggleRenderProps) => React.ReactNode); };

typedoc-plugin-markdown 플러그인을 통해, 다음과 같은 markdown 문서를 추출해 낼 수 있습니다.

markdown 형태로 추출되기 때문에, 이를 그대로 복사해서 사용할 수 있었습니다. 사실 typedoc 만을 통해 그대로 문서화된 파일을 배포할 수도 있지만, 저희는 실제 코드의 사용례를 같이 보여주고 싶어 typedocapi 를 추출해 내는 형태로만 이용했습니다.

React docgen을 사용하지 않은 이유

"react-docgen-typescript-plugin": "^1.0.5", "react-remove-scroll": "^2.5.5", "rollup": "^3.3.0", "rollup-plugin-peer-deps-external": "^2.2.4", "storybook-addon-react-docgen": "^1.2.43",

그런데, 사실 react-docgen-typescript-plugin 이라는 스토리북 addon 이 이미 존재합니다. 이를 간단히 storybook 설정 파일에 적용해주면, 아래와 같이 proptable 을 추출할 수 있습니다.

그런데, 이 라이브러리는 실제 컴포넌트 이름과 설정해주는 디스플레이 이름이 다를 경우 props 들을 가지고 오지 못하는 문제점이 있었습니다. 저희의 컴포넌트들은 대다수 합성 컴포넌트 형태를 취하고 있었고 실제 컴포넌트 이름과, display 이름이 다른 경우가 많았습니다. 해당 라이브러의 깃허브 이슈를 확인해 보니 해당 부분의 문제점이 해결되지 않아 사용하지 못했습니다.

( 수정 시점에 확인해 보니 해당 이슈가 해결되었습니다! )

Storybook 에 문서화 통합하기

그리고, 문서화는 스토리북에 통합하기로 했습니다. 이미, storybook 을 사용하고 있어서 빠른 통합이 가능했기 때문입니다. 또한 mdx 포맷을 사용하기로 결정했습니다. 포맷은 jsx 를 가지고 와서 사용할 수 있다는 장점이 존재했고 스토리북과 같이 사용한다면, 스토리를 불러올수도 있었기 때문입니다.

// toggle.stories.tsx // import mdx from './toggle.mdx'; export default { title: 'over-ui/Toggle', parameters: { docs: { page: mdx, // mdx 문서를 이용하는 설정을 진행해줍니다. }, controls: { expanded: true }, }, } as Meta;

그리고 mdx 문서를 아래와 같이 적용해 주었습니다.

## with emotion `emotion`을 사용한다면 다음과 같이 스타일링할 수 있습니다. `data-pressed`, `data-disabled`를 통해 스타일링을 간편히 할 수 있습니다. <Canvas> <Story id="over-ui-toggle--styled" /> // 작성된 story를 불러오는 기능을 추가했습니다. </Canvas> ```tsx const StyledToggle = styled(Toggle)` padding: 10px 15px; &[data-pressed='on'] { background-color: green; } &[data-disabled='true'] { background-color: red; } &:focus { border: 2px solid blue; } border: none; font-size: 12px; `;


위와 같이 진행하면 아래와 같이 문서화가 완료됩니다.


![](http://res.cloudinary.com/ddzuhs646/image/upload/v1675772623/blog/8936e902-a5db-4030-9a46-7db7a96d53ef/8936e902a5db40309a467db7a96d53ef.png)


사용자는 이 문서에서 `show code` 를 통해 실제 코드를 바로 확인해 볼 수 있습니다.  그리고 **이 문서에서 바로 클릭과 같은 인터렉션을 통해 어떤 점이 바뀌는지 테스트**해볼 수 있습니다.


작성하는 사람의 입장에서도 생각보다 시간이 오래 걸리지 않았습니다. 이미, `storybook` 을 통해 `ui` 테스트를 진행하고 있었기도 했으며, 시간이 없다면 `storybook` 의 `actions` 등의 기능은 적용하지 않아도 위와 같은 문서화가 가능했기 때문입니다.


```typescript
export const Styled = () => {
  return (
    <div style={{ display: 'flex', gap: '10px' }}>
      <StyledToggle>styled Toggle</StyledToggle>
      <StyledToggle disabled>disabled Toggle</StyledToggle>
      <StyledToggle defaultPressed={true}>pressed Toggle</StyledToggle>
    </div>
  );
};

const StyledToggle = styled(Toggle)`
  padding: 10px 15px;

  &[data-pressed='on'] {
    background-color: green;
  }
  &[data-disabled='true'] {
    background-color: red;
  }
  &:focus {
    border: 2px solid blue;
  }
  border: none;
  font-size: 12px;
`;

// 위의 문서화를 거치기 위해 쓴 코드는 이정도 입니다.
// 물론 mdx에, 추가적은 내용은 적어주었지만요!
// 이미 UI 테스트를 하기위해 작성한 부분이었으므로, 새롭게 추가하는 코드는 없었습니다.

마무리

문서화를 하는 것은 중요하다고 생각합니다. 이번에도, 팀원이 작성한 컴포넌트를 제가 사용할 수도 있었고 제가 작성한 코드를 팀원이 사용할 수도 있었습니다. 문서화가 없었다면, 해당 컴포넌트를 제대로 이용하기 힘들 것이라는 생각이 들었습니다.

다만, 문서화를 편하게 하는 방법을 생각하는 것도 좋다고 생각합니다. 사실 이 과정을 거치면서 그냥 api 손으로 일일이 써줄걸.. 하는 생각도 들었습니다. 효율을 뽑아내려는 방법에는 역시 시간이 걸리나보다 했습니다. 다만, 마무리하고 나니 이후의 문서작업을 간단히 마무리할 수 있어 좋았습니다.

Ref