Opening
사내 프로젝트에서 CesiumJS 를 보다 편하게 사용하기 위해서 만들었던 여러 기능들을 개인적으로 정리해보려다가 Browser Compatibility(브라우저 호환성), Documentation, Versioning, CI/CD, npm publishing, 마지막으로 GitHub Pages Deployment 까지. 생각나는 가능한 모든 자동화를 도입해서 개발 환경을 구축한 기록을 남겨본다.
Package Metadata
일반적인 프론트 프로젝트의 경우는 패키지를 다운 받아 사용하기만 하고 배포하지 않기 때문에 package.json
에 명시되는 metadata 를 신경 쓸 일이 없다. 하지만 npm 패키지 형태로 관리 및 사용하려는 경우는 패키지의 이름부터 버전, exports 등 구체적으로 명시해줘야 하는 metadata 가 많다. Configuring npm 에서 모든 항목을 확인할 수 있다.
1{
2 "name": "@(scope)/(package-name)",
3 "version": "0.0.2",
4 "description": "package description",
5 "keywords": [],
6 "repository": {
7 "type": "git",
8 "url": "(github path)"
9 },
10 "homepage": "(homepage path)",
11 "bugs": {
12 "url": "(bug/issues report page path)"
13 },
14 "license": "MIT",
15 "author": {
16 "name": "(author name)",
17 "url": "(author homepage path)"
18 },
19 "peerDependencies": {
20 "cesium": "^1"
21 },
22}
필수로 명시해야 하는 것은 다음 두 가지가 있다.
- name: 패키지의 이름으로, npm 에 publish 하기 위해서는 사용할 npm 계정에 publish 권한이 있는 scope 를 지정해주어야 한다. 개인 계정의 경우, username 과 같은 scope 는 기본으로 권한이 주어진다.
- version: npm 은 semver 모듈로 패키지의 버전을 판단하기 때문에 형식을 맞춰주어야 한다. 이는 Semantic Versioning 규칙을 따르는 것으로, 이 프로젝트에서는 후술할 Changesets versioning 툴을 사용했다.
Project Configurations
TypeScript - Builds
기본 컴파일러인 tsc
를 사용해도 되지만, tsup
이라는 번들러를 이용했다. (그냥)
1import { defineConfig } from 'tsup';
2
3export default defineConfig({
4 clean: true,
5 dts: true,
6 entry: ['src/index.ts'],
7 esbuildOptions(options) {
8 options.platform = 'neutral';
9 },
10 format: ['cjs', 'esm'],
11 minify: true,
12 outExtension({ format }) {
13 return {
14 js: format === 'cjs' ? '.cjs' : '.js',
15 };
16 },
17 sourcemap: false,
18});
19
Common JS 와 Module JS (ESM) 두 가지 방식으로 컴파일을 진행했고, dts 파일을 제공하기 때문에 minify 를 true 로 설정했다. dist 디렉토리에 생성되는 빌드 결과물을 package.json
에서 제대로 명시해주어야 한다.
1{
2// ...
3 "type": "module",
4 "main": "dist/index.js",
5 "module": "dist/index.js",
6 "files": [
7 "dist"
8 ],
9 "types": "dist/index.d.ts",
10 "exports": {
11 ".": {
12 "types": "./dist/index.d.ts",
13 "import": "./dist/index.js",
14 "require": "./dist/index.cjs",
15 "default": "./dist/index.js"
16 }
17 },
18// ...
19}
- type: 패키지 내에서 JavaScript 의 기본 확장자인
.js
를 어떻게 취급할지를 명시한다. module 이면 esm, common 이면 common js 로 취급한다. - main: 패키지의 기본 primary entry point.
- module: 패키지를 esm 방식으로 접근할 때의 primary entry point.
- files: 패키지가 포함하는 파일들로,
.gitignore
의 반대격. - types: 패키지에 대한 type 을 추론할 수 있는 declaration (dts) 파일.
- exports: main 에서 명시한 entry point 를 상세하게 나눠 명시하는 항목으로, require(Common JS) 로 접근하면
.cjs
파일을 참고하게 하는 것 등이 가능.
TypeScript - Documentation
JavaScript 는 JSDoc 이라는 documentation 양식이 있다. TypeScript 에서도 동일한 양식을 사용한다.
1// TypeScript Documentation Example
27891012
13abstract class Collection<C, I>{
14 ...
15}
TypeScript JSDoc 에서 지원 및 미지원 태그를 확인할 수 있다. TypeScript 는 JSDoc 의 타입 표시를 생략한다. JSDoc 을 잘 작성하면 IDE 내에서 설명을 제공할 수 있을 뿐만 아니라, API 문서까지도 자동화해서 제공할 수 있다.
TypeDoc 은 대표적인 Documentation Tool 중 하나이다. JSDoc 형태의 Comment 들을 정적으로 serve 할 수 있는 html
로 작성해준다. MarkDown(MD) 형식도 지원하기에 API 문서를 어떻게 제공할 지에 따라 선택할 수 있다. typedoc.json
로 Configuration 을 설정할 수 있으며 Typedoc Themes 에서는 사용할 수 있는 스타일 테마 플러그인과 커스텀 테마 작성 방법에 대해 안내하고 있다.
1{
2 "$schema": "https://typedoc.org/schema.json",
3 "entryPoints": ["src/index.ts"],
4 "out": "docs",
5 "name": "Name of this document",
6 "includeVersion": true,
7 "excludePrivate": false,
8 "excludeProtected": false,
9 "excludeExternals": true,
10 "readme": "README.md",
11 "githubPages": true,
12 "categorizeByGroup": true,
13 "navigationLinks": {
14 "GitHub": "Link to your project",
15 "NPM": "Link to your package"
16 }
17}
위 설정대로 local 환경에서 typedoc
을 실행하면 src/index.ts
파일이 포함하고 있는 코드의 JSDoc 에 대한 API Document 가 docs
디렉토리 하위에 생성된다. 그 중 index.html
을 라이브 서버 로 실행해보면 Cesium Utils 와 같이 API Document 가 구성되는 것을 확인해볼 수 있다.
Versioning - Changesets
npm 의 Semantic Versioning 규칙을 따르면서 사용자들에게 패치노트 같은 정보를 제공해주기 위한 versioning 툴을 사용했다. 배포 및 Documentation 자동화를 위한 GitHub Actions 의 핵심 trigger 역할을 하도록 설계해서 이벤트 시점을 조절하는 것이 중요했는데, 이렇게 설계한 것에는 몇 가지 이유가 있다.
- npm publish 는
package.json
의 name 과 version 이 primary key 로 작용한다. 같은 패키지 이름으로 이미 존재하는 버전으로는 다시 publish 할 수 없다. - Changeset 은 Changeset Actions 를 통해
CHANGELOG.md
의 내용을 수정할 수 있는데, Changeset 의 타입(patch/minor/major) 에 따라 package.json
의 version 을 함께 업데이트 하는 Pull Request 를 생성하도록 설정할 수 있다. typedoc
으로 생성한 API Documentation 에는 패키지의 version 을 표시할 수 있는데, 이는 TypeDoc 실행 시점의 package.json
version 을 기준으로 한다.
이런 점들을 고려해서 배포 과정을 자동화하되, 다음과 같은 배포 process 를 설계했다.
- 코드의 수정과 함께 주요 변경점을 담은 changeset 을 만들어 commit 한다.
- Changeset action 이 version update PR 을 생성한다.
- Version Update PR 을 Repository Owner 가 수동으로 승인한다.
CHANGELOG.md
와 package.json
의 변화를 감지해 GitHub Release 를 생성하고, npm 에 publish 한 뒤, API Document 를 업데이트 하고 GitHub Pages 에 Deploy 한다.
이는 배포에 대한 의사 결정 단계를 상정하여 Repository Owner 의 PR 수락 시점을 최종 배포 시점으로 설정한 프로세스이다. 이런 workflow 를 구현하기 위해 Changeset action 에 포함된 npm publish 단계를 생략하고, PR 수락 이후 npm publish 를 진행하도록 구성했다.
1# changesets.yml
2name: Changesets
3
4on:
5 push:
6 branches:
7 - main
8
9env:
10 CI: true
11 HUSKY: 0 # Disable husky hit hooks
12
13jobs:
14 version:
15 timeout-minutes: 15
16 runs-on: ubuntu-latest
17 permissions:
18 contents: write
19 pull-requests: write
20 outputs:
21 has_changes: ${{ steps.changesets.outputs.hasChangesets }}
22 pr_number: ${{ steps.changesets.outputs.pullRequestNumber }}
23 steps:
24 - name: Checkout code repository
25 uses: actions/checkout@v4
26 with:
27 fetch-depth: 0
28
29 # This project uses pnpm as a package manager.
30 - name: Setup pnpm
31 uses: pnpm/action-setup@v4
32
33 - name: Setup node.js
34 uses: actions/setup-node@v4
35 with:
36 node-version: 22
37 cache: 'pnpm'
38
39 - name: Install dependencies
40 run: pnpm install
41
42 - name: Disable Git hooks
43 run: |
44 git config --global user.name "github-actions[bot]"
45 git config --global user.email "github-actions[bot]@users.noreply.github.com"
46
47 # Disable husky git hooks
48 mkdir -p /tmp/empty-hooks
49 git config --global core.hooksPath /tmp/empty-hooks
50
51 - name: Create and publish versions
52 id: changesets
53 uses: changesets/action@v1
54 with:
55 commit: "Release version update"
56 title: "Release version update"
57 # You can publish to npm here
58 publish: "pnpm build"
59 env:
60 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61 # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # required for npm publish
62 HUSKY: 0
63
64 - name: Generate documentation for PR
65 if: steps.changesets.outputs.hasChangesets == 'true'
66 run: |
67 echo "Changesets created a PR #${{ steps.changesets.outputs.pullRequestNumber }}"
68
69 # Get the branch name that changesets created
70 CHANGESET_BRANCH=$(gh pr view ${{ steps.changesets.outputs.pullRequestNumber }} --json headRefName -q .headRefName)
71 echo "Changesets branch: $CHANGESET_BRANCH"
72
73 # Checkout that branch
74 git fetch origin $CHANGESET_BRANCH
75 git checkout $CHANGESET_BRANCH
76
77 # Generate documentation
78 pnpm typedoc
79
80 # Verify docs were generated properly
81 if [ ! -f "./docs/index.html" ]; then
82 echo "Documentation generation failed!"
83 exit 1
84 fi
85
86 # Commit and push the documentation changes
87 git add docs/
88 git commit -m "docs: Update documentation for release" || echo "No documentation changes to commit"
89 git push origin $CHANGESET_BRANCH
90 env:
91 GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}
92
Changeset action 으로 생성한 PR 에 Documentation 변경점을 commit 하는 과정을 추가했다. GitHub Pages Deployment 에는 영향을 주지 않지만, 해당 페이지에 표시되는 정보와 local 소스의 docs 디렉토리 하위 정보를 동기화하는 역할이다.
1# release-and-publish.yml
2name: Release and Publish
3
4on:
5 push:
6 branches:
7 - main
8 paths:
9 - 'CHANGELOG.md'
10 - 'package.json'
11 workflow_dispatch:
12
13jobs:
14 release-and-publish:
15 runs-on: ubuntu-latest
16 if: contains(github.event.head_commit.message, 'Release version update')
17 permissions:
18 contents: write
19 id-token: write
20 steps:
21 - name: Checkout code repository
22 uses: actions/checkout@v4
23 with:
24 fetch-depth: 0
25
26 - name: Setup pnpm
27 uses: pnpm/action-setup@v4
28
29 - name: Setup node.js
30 uses: actions/setup-node@v4
31 with:
32 node-version: 22
33 registry-url: 'https://registry.npmjs.org'
34
35 - name: Install dependencies
36 run: pnpm install
37
38 - name: Get package version
39 id: package-version
40 run: |
41 PACKAGE_VERSION=$(node -p "require('./package.json').version")
42 echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
43
44 - name: Get latest changes from CHANGELOG
45 id: changelog
46 run: |
47 # Extract the changes for the latest version
48 LATEST_CHANGES=$(sed -n "/## ${{ steps.package-version.outputs.version }}/,/## [0-9]/p" CHANGELOG.md | sed '$d' | sed '1d')
49 # Store the changes in a file to preserve newlines
50 echo "$LATEST_CHANGES" > latest_changes.txt
51 echo "changes_file=latest_changes.txt" >> $GITHUB_OUTPUT
52
53 - name: Create GitHub Release
54 uses: softprops/action-gh-release@v2
55 with:
56 tag_name: v${{ steps.package-version.outputs.version }}
57 name: Release v${{ steps.package-version.outputs.version }}
58 body_path: ${{ steps.changelog.outputs.changes_file }}
59 draft: false
60 prerelease: false
61 generate_release_notes: true
62 env:
63 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64
65 - name: Build package for publishing
66 run: pnpm build
67
68 - name: Publish to NPM registry
69 run: pnpm publish --no-git-checks
70 env:
71 NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
72 HUSKY: 0
73
74 deploy:
75 permissions:
76 contents: read
77 pages: write
78 id-token: write
79 concurrency:
80 group: "pages"
81 cancel-in-progress: false
82 needs: release-and-publish
83 environment:
84 name: github-pages
85 url: ${{ steps.deployment.outputs.page_url }}
86 runs-on: ubuntu-latest
87 steps:
88 - name: Checkout
89 uses: actions/checkout@v4
90 - name: Setup Node.js
91 uses: actions/setup-node@v4
92 with:
93 node-version: 22
94 - name: Setup pnpm
95 uses: pnpm/action-setup@v4
96 - name: Install dependencies
97 run: pnpm install
98 - name: Generate documentation
99 run: pnpm typedoc
100 - name: Setup Pages
101 uses: actions/configure-pages@v5
102 - name: Upload artifact
103 uses: actions/upload-pages-artifact@v3
104 with:
105 path: './docs/'
106 - name: Deploy to GitHub Pages
107 id: deployment
108 uses: actions/deploy-pages@v4
109
CHANGELOG.md
와 package.json
이 변경됐을 때, 최근 commit message 가 "Release version update" 라면 두 가지 job 이 실행된다.
-
CHANGELOG.md
에 반영된 이번 업데이트 버전에 대한 변경 사항을 포함한 GitHub Release 를 생성하고, npm publish 를 진행한다. - 앞선 job 이 성공하면, API Documentation 을 최신화하고 GitHub Pages 에 deploy 한다.
NOTE: 이 프로젝트는 husky
와 commitlint
로 commit 메시지의 형식을 제한하고 있어 "Release version update" 라는 메시지가 겹칠 일이 없지만, 같은 환경이 아니라면 조건 설정에 유의해야 한다.
Closing
npm 패키지를 사용만 해보고 직접 올려본 것은 처음인데, 실무를 겪어보면서 생긴 개발 업무 프로세스에 대한 개인적인 convention 을 잘 정리해서 반영하지 않았나 싶다. Branch naming convention, branch protection rules 등 협업을 고려한 프로세스도 기회가 되면 설계해보고 싶다.