티스토리 뷰

반응형

안녕하세요. 오늘은 코드와 주석을 기반으로 문서 자동화한 경험을 공유하고자 합니다. 
 
보일러플레이트에서 응집되어 있던 utils, hooks을 용도에 맞게 패키지로 분리하면서, 관리적과 성능적으로는 이점이 있었지만, 실무진 분들이 어떠한 함수를 가져오기 위해 참고할 수 있는 문서가 필요했습니다. 
 
21개가 넘는 패키지들, 총 100개는 넘을 듯한 컴포넌트나 함수들을 개발하는 것을 떠나서 또 문서작업을 해야 한다는 게 굉장히 부담으로 다가왔습니다. 
 
따로 md파일을 생성하여 관리하는 것이 아닌, 명령어만으로 문서가 자동생성 되게 하면 얼마나 편할까요? 
다행히도, 아주 다행이도 이미 잘 개발된 라이브러리를 발견했습니다.
 
바로 MicroSoft 에서 개발한 오픈소스도구인 api-extractor인데요. 해당 라이브러리는 다룬 글이 없고, 이슈 대응에 있어 고민이 되는 부분이 있었지만, 최근까지도 활발하게 이슈가 픽스되고 있고, 해외 레퍼런스도 충분히 있는 것 같아 해당 툴을 사용해 문서 자동화를 진행하게 되었습니다. 
 

TSDoc

설명하기 앞서 코드기반으로 문서를 잘 생성하려면, TSDoc 형식으로 작성이 되어야 합니다. API Extractor는 typescript 코드 구문을 분석하고, TSDoc 형식을 읽어 분석 후 Docmodel를 생성해 주기 때문입니다. 아직 잘 모르신다면, TSDoc Playground를 실행해서 TSDoc 구문이 어떻게 동작하는지 보시면 좋을 것 같습니다.
 
API Extractor 공식문서에서도 주석구문에 대한 정보를 제공하고 있습니다.  어떤 주석을 분석하는지는 아래 경로에서 확인해보시면 됩니다. 

 

미리보기

 
TS Doc 규칙에 맞게 잘 작성된 주석과, 올바르게 타입 된 함수만 있다면 명령어 하나만으로 우측의 md파일이 생성됩니다.  
물론 예쁘게 보이기 위해 커스텀 스크립트를 만든 부분이 있지만, 기본적으로는 명령어 한 줄만으로도 가능합니다. 
 

 
문서 생성 결과 미리보기

 
 

사용 도구 : api-extractor & Docusaurus

사용 도구사용 목적
ms api-extractor각 패키지들에 선언된 코드 주석을 기반으로 지정한 경로에 doc model를(.api.json 파일) 생성해줍니다. 주 목적은 패키지 api 변경 사항을 추적하는 report 파일을 생성해주지만, 저는 doc model 생성을 목적으로 사용하였습니다.
ms api-documentor.api.json 파일들을 읽어 md파일로 생성해줍니다.
meta docusaurus추출한 md파일을 문서에 최적화된 문서 생성 툴 docusaurus를 사용하여 웹사이트를 생성하였습니다.

 
api-extractor는 TypeScript 라이브러리에서 API를 추출하고 문서화하는 데 사용됩니다. 라이브러리의 공개 API를 분석하고, 변경을 추적하기 용이한 기능도 있지만, 저희는 추출된 API 파일을 이용하여 md파일을 생성해 주는 기능만을 사용하고 있습니다.
 
각 패키지의 빌드된 index.d.ts파일을 읽어 docModel이라는 JSON 파일로 출력하고, api-documentor를 사용하여 이를 md 파일로 변환하였습니다. 이렇게 생성된 md 파일을 스크립트를 통해 구조화하고, Docusaurus를 사용하여 웹사이트를 생성하였습니다.
 
초반에는 추출된 API를 @next/mdx를 통해 웹사이트를 구축하였지만, 검색 기능, 소개 페이지, 스타일링 등 많은 기능이 필요하게 되었습니다. 이러한 기능들은 Docusaurus에서 이미 제공하고 있어, Docusaurus를 사용하는 것이 더 효율적이라는 판단 하에 선택하게 되었습니다.
 

API Extractor (@microsoft/api-extractor)

패키지 설치 후 루트경로에 api-extractor.json 파일이 있어야 합니다. 앞서 말했듯 문서 생성 기초 데이터인 docModel 생성만이 목적이기 때문에 apiReport, dtsRollup은 false로 설정하고, docModel을 설정해 줍니다.

mainEntryPointFilePath는 분석의 시작점으로 사용되는 파일 경로입니다. 확장자는 d.ts여야 합니다. 
apiJsonFilePath 은 docModel이 생성될 경로로 따로 지정해주지 않으면  <projectFolder>/temp/<unscopedPackageName>. api.json 경로에 생성됩니다. 
 

{
  "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
  "mainEntryPointFilePath": "<projectFolder>/dist/index.doc.d.ts",
  "compiler": {},
  "apiReport": { "enabled": false },
  "dtsRollup": {
    "enabled": false
  },
  "tsdocMetadata": { "enabled": false },
  "docModel": {
    "enabled": true,
    "apiJsonFilePath": "./apps/docs/api-extractor/<unscopedPackageName>.api.json"
  },
  "messages": {
   ...
  }
}

 

모노레포를 위한 API Extractor

저희는 모노레포로 여러 패키지를 관리하고 있어서 각 패키지에 script를 등록하고, turbo와 같은 관리 도구를 같이 사용하여 동시 스크립트를 돌려 실행합니다. 또 루트경로에 설정한 api-extractor.json 파일을 단일 패키지 루트경로에도 생성 후 extends 해주시면 추후 유지보수가 편합니다. 

  • [root] api-extractor-base.json
  • [package] packages/[example-package]/api-extractor.json 
// [package] packages/[example-package]/api-extractor.json 
{
  "extends": "../../api-extractor-base.json"
}

 
 

API Documenter (@microsoft/api-documenter)

생성된 docModel 파일들을 읽어 md 파일을 생성해 줍니다. 

초기 docModel로 생성 된 md파일

 
문제는 위 생성된 md파일에서 코드블록 밖에서 특정 문자가 들어갈 경우 ( <!-- -->, * 등) docusaurus  실행시 깨지거나 가독성이 좋지 않아 보였습니다.   또 docusaurus에는 브레드 크럼블을 메타태그, 타이틀 정보로 생성이 되어 상단 md구문도 불필요해 보였습니다. 
 
해당 이슈를 한 번에 관리하기 위해 찾아보던 중 api-documenter에서 plugin을 등록할 수 있는 점을 알아냈고, 
제공해 주는 함수 중 MarkdownDocumenterFeature의  onBeforeWritePage를 사용하면 md파일이 생성되기 전에 작성된 로직을 실행하게 할 수 있었습니다. (문서 참조)
 
플러그인에 실행할 부분은 자유롭게 할 수 있지만, 이슈 방지를 위해 아래 문자들은 코드블록 밖에서 제거해 주시면 됩니다.

const REPLACERS = new Map<RegExp, string>([
  [/\\\*/g, '*'],
  [/\\_/g, '_'],
  [/\\`/g, '`'],
  [/\\\[/g, '['],
  [/\\\]/g, ']'],
  [/<!-- -->/g, ''],
  [/<!--[\s\S]*?-->/g, ''],
  [/\{([^}]+)\}/g, '\\{$1\\}'],
])

 
좌측에 우측이 플러그인 실행 후 변환 된 md파일입니다. 해당 파일을 docusaurus에 실행하면 배포했을 때도 예쁘게 보입니다. 

docusaurus 배포

 

카테고라이징을 위한 커스텀 스크립트

기본적으로 api-documentor를 사용하면 md파일이 잘 생성이 되지만, 파일의 폴더 뎁스를 좀 더 분류하고 싶었습니다. 때문에 카테고라이징 하는 부분에 있어서 패키지별, 용도별로 분류를 하기 위해 @category 태그를 추가하였습니다.
주석에만 @category태그를 통해 어떤 폴더에 들어갈 건지 명시를 해주면 추후 스크립트에 해서 해당 폴더명으로 분류가 됩니다.

예를 들어 paginate.ts 파일의 tsdoc 주석 안에서
@category Utils/Array라고 적어주면 스크립트가 돈 후 아래 이미지처럼 분류가 됩니다.

/**
 * @category Utils/Array
*/

 
최종적으로 스크립트까지 실행하고 나면 아래와 같이 폴더 분류가 깔끔하게 됩니다. 

카테고라이징 후

 
 

최종 스크립트 : gen:doc

gen:doc이라는 스크립트 명령어를 치면 아래 파일이 실행이 되는데요. 수정을 하면서 추가되는 스크립트가 많아지고, 자동화와 유지보수를 위해 flow를 통해 흐름을 한눈에 파악할 수 있도록 구분했습니다.

// apps/docs/scripts/generate-api-docs/index.ts
flow(
  // 패키지들을 설치 후 빌드합니다.
  flow(installPackages, awaited(successLog('install packages'))),
  awaited(flow(buildPackages, awaited(successLog('build packages')))),
  // curring 함수에서 타입 정의가 여러 개로 나와서 하나로만 나오게 하는 과정 
  // 카테고리 분석을 위한 카테고리를 메모리에 저장 
  awaited(flow(handleDts, successLog('handle dts'))),
  // api-extractor 실행 > json 파일 생성 
  awaited(flow(buildApiExtractorJson, successLog('build api-extractor.json'))),
  // 생성된 api-extractor json 파일중 parameterName에 개행문자가 포함된 경우 제거
  awaited(
    flow(handleApiExtractorJson, successLog('handle api-extractor.json')),
  ),
  // api-documenter 실행 > md 파일 생성 
  awaited(flow(buildApiDocs, awaited(successLog('build api docs markdown')))),
  // 테이블 그룹화 및 메모리에 저장 된 카테고리 리턴
  awaited(flow(handleIndexMarkdown, successLog('handle index markdown'))),
  // 카테고리를 통한 폴더 분류 작업
  awaited(
    flow(
      handleApiDocFolderStructure,
      successLog('handle api-doc folder structure'),
    ),
  ),
  awaited(flow(cleanDts, successLog(`index.doc.d.ts file has been cleaned.`))),
)()

 
 

Docusaurus + Algoria

이후 Docusaurus에서 검색엔진은 문서에서 추천하는 Algoria를 사용해 무료로 사용하고 있습니다. 배포를 시키면 인덱싱을 해두어 검색리스트에 반영이 되는 구조입니다. 자세한 과정은 공식문서(Search)를 참조해 주세요.

 
 

Github Action + Changesets + Vercel 배포

저희는 패키지 배포 관리를 Changesets으로 하고 있습니다. 배포 후 Changesets에서 제공해 주는 outputs 상태를 통해 npm 패키지에 성공적으로 퍼블리시가 되면 그때 composite action으로 분리한 generate documentation action을 실행해 주어 불필요한 액션을 방지했습니다. 
 
Generate Documentation 액션 과정이 길어지면서 action을 분리할 방법을 찾던 중 composite action에 대해 알게 되었는데 굉장히 유용하게 썼습니다. 관심 있으신 분들을 위해 공식문서를 첨부합니다.

// .github/workflows/release.yml
 ....
 
      - name: Generate documentation
        if: steps.changesets.outputs.published == 'true'
        uses: ./.github/composite-actions/gen-docs
composite action

 
 

Algolia Crawler Github Action

참고로 Algolia Search도 크롤러 action이 있어서 해당 부분도 적용해 배포 시에 크롤러가 실행되게끔 하였습니다.  설정법은 문서를 참조해 주세요.
 

마무리

이 문서 자동화 플로우를 완성한 이후로는 이슈나 업데이트 등으로 패키지 버전을 올릴 때 GitHub Action을 통해 gen:doc 명령어 하나로 최신 문서가 자동으로 생성되고, Docusaurus를 통해 웹사이트가 빌드되며, Vercel을 통해 배포까지 자동화되었습니다. 이제는 새 기능 추가나 수정 사항 반영 후 번거로운 문서 작업 없이도 최신 상태의 문서가 항상 유지가 되어 유지보수가 매우 편안해진 경험이었습니다. 
 
물론 레퍼런스가 많지는 않고, Docusaurus와 같이 빌드하며 이슈도 많이 발생했고, 또 커스텀이 생각보다 많이 되었고 쓰여진 글을 보니 꽤 복잡해보이지만, 커스텀은 대부분 깔끔하게 보이기 위한 저희의 욕심이었기 때문에 해당 부분은 너무 큰 목적이 아니라면 간단한 공식문서만큼 러닝커브는 그리 크지 않았다고 생각합니다. 
 
저는 문서 자동화를 진행하면서 새롭게 알게된 것들이 너무 많고, 로직이 복잡해지면서 적용했던 함수형 프로그래밍에 대해서도 좀 더 익숙해지는 시간이었습니다. 다음에는 이 부분에 대해 다뤄봐도 재밌을 것 같아요. 
 
긴 글 읽어주셔서 감사합니다.
 
 
 
 
 
Reference

 
 
 
 

반응형