Changesets로 패키지 릴리즈 자동화하기
여러 프로젝트에서 재사용되는 코드는 매번 바닥부터 작성하는것보단, 이왕이면 라이브러리로 배포하는게 편할 수 있다. 언어마다 다르지만, node생태계에선 npm이라는 라이브러리 저장소가 존재하고, 만든 라이브러리는 여기에 쉽게 배포할 수 있다.
이 글에선 패키지를 만드는 방법과 changesets를 사용하여 changelog관리 및 배포 과정을 자동화하는 방법을 다룬다.
패키지 만들기
node에서는 라이브러리 대신 패키지라는 용어를 사용한다. 패키지마다 package.json
파일을 포함시키고, 여기에 패키지에 대한 정보를 작성해야 한다. 패키지를 만들 때 필요한 최소한의 프로퍼티는 다음과 같다.
name: 패키지 이름
main / exports: 모듈의 entry point(둘 다 작성되어 있으면 exports가 우선)
type: 모듈 타입
version: 패키지 버전 (semantic versioning spec을 따른다)
files: npm배포시 포함할 파일
scripts: 스크립트 작성시 사용
dependencies, peerDependencies: 의존성 정보
프로퍼티에 대한 설명은 node docs, npm docs에 대부분 나와있다. 대부분이라고 말한 이유는, 개발 과정에서 추가로 사용되는 번들러, 바벨, 린트 등이 configuration을 위해 package.json
내에서 커스텀 프로퍼티를 사용할 수 있기 때문이다. 예를들면 다음과 같다.
parcel은
source
,targets
등의 필드를 사용할 수 있다. (참고)eslint는
eslintConfig
필드를 사용할 수 있다. (참고)browserslist는
browserslist
필드를 사용할 수 있다. (참고)
따라서 패키지 개발시 추가로 사용하는 도구가 무엇인지, 해당 도구의 어떤 기능을 사용하는지에 따라 package.json
에 다양한 프로퍼티를 작성하게 될 수 있다.
로그인
npm에 배포하려면 우선 로그인을 해야한다. npm아이디와 비밀번호를 입력해서 로그인할 수 있다.
npm login
로그인된 유저네임을 확인하고 싶다면 다음 명령어를 입력한다.
npm whoami
npm에 업로드되는 파일
패키지를 npm에 배포하기 위해서는 다음 명령어를 입력하면 된다.
npm publish
패키지에는 기본적으로 package.json
, README.md
등 패키지 관련 파일이 모두 포함된다. 만약 package.json
에 files
필드가 있다면 여기에 지정한 파일만 포함된다.
// 예시
// package.json
{
...,
files: ["dist", "lib"]
}
.gitignore
파일과 비슷하게, 패키지에 포함시키지 않을 파일은 .npmignore
에 포함시킬 수 있는데, .gitignore
파일만 있는 경우에는 여기서 버전 관리에 포함시키지 않으려고 작성해둔 파일은 패키지에도 포함되지 않는다. 보통 빌드 결과물로 나오는 dist, lib등의 디렉토리는 .gitignore
에 포함시키니까, 신경쓰지 않으면 패키지 배포시에도 누락되도록 설정될 수 있다. 참고로 .npmignore
의 문법은 .gitignore
와 동일하다.
패키지에 포함될 파일을 확인하는 용도로 사용할 수 있는 명령어도 있다.
npm pack --dry-run
npm pack은 배포할 패키지의 압축파일을 만드는 명령어이고, --dry-run옵션은 실험삼아 명령어를 실행해보고자 할 때 쓴다. 아래는 실행 예시이다.
main vs. exports
둘 다 라이브러리의 entry point를 명시하는데 사용한다.
// package.json
{
name: "pkg",
main: "dist/index.js",
...
}
위와 같이 작성하면 pkg
라는 라이브러리를 require/import할 때 dist/index.js
가 사용된다.
main과 exports의 역할은 같지만 다음과 같은 차이가 있다.
exports가 더 최신스펙이다.
main은 하나의 entry point만 작성할 수 있다.
exports는 여러개의 entry point를 작성할 수 있고, exports에 명시되지 않은 entry point는 consumer가 import/require할 수 없다.
main과 exports가 둘 다 있다면, exports가 우선시된다.
exports는 환경별(cjs/esm등) entry point를 정의할 수 있다.
node docs에선 main은 node10 이하를 지원할 때, 그 외에는 exports를 쓰는것을 권장한다.
main을 사용했을 때는 entry point에서 상대경로를 통해 다른 파일에도 접근할 수 있는데, exports는 그렇지 않다는 점이 다르다.
// pkg 패키지 구조
// .
// ㄴdist
// ㄴindex.js
// ㄴpath
// ㄴindex.js
// { main: "./dist/index.js" } 로 작성한 경우
import mod from "pkg"; // dist/index.js로 해석
import mod from "pkg/path"; // dist/path/index.js로 해석
// { exports: "./dist/index.js" } 로 작성한 경우
import mod from "pkg"; // dist/index.js로 해석
import mod from "pkg/path"; // 정의되지 않은 entry point라 에러
exports로는 조건에 따라 다른 entry point를 매핑할 수도 있다.
{
type: "module",
exports: {
".": {
"import": "./index.js", // esm 사용시
"require": "./index.cjs", // cjs 사용시
}
}
}
type이 module이면 .js
는 모두 esm으로 취급되기 때문에, require에 대한 entry point를 .js
로 작성하면 안되고, .cjs
로 작성해야 한다. 반대로 type이 commonjs인 경우엔 .js
는 모두 cjs로 취급되기 때문에, import에 대한 entry point는 .mjs
로 작성해야 한다.
더 자세한 내용은 여기를 참고하면 된다.
배포 전 build
개발 과정에서 번들러를 이용해서 빌드하는 경우엔, 빌드 전에 output directory를 한번 비워주는게 좋다. 다음 릴리즈에 삭제되는 파일이 있을 수도 있기 때문에 이 과정을 건너 뛰는 경우엔, 의도하지 않은 파일이 배포에 포함될 수 있다.
// 예시
scripts: {
"clean": "rm -rf dist",
"build": "yarn clean && rollup -c"
}
Changesets
패키지는 계속해서 버전이 업데이트 될 수 있는데, 새 릴리즈를 배포할 때 마다 changelog와 release정보를 작성하여 버전 히스토리를 관리하는 경우가 많다. 근데 이걸 모든 버전에 대해서 배포할 때 마다, 바뀐 정보를 비교해가며 수동으로 작성하는 건 정말 귀찮다. 이를 해결하기 위해 changesets
라는 패키지를 사용할 수 있다.
changesets
는 버전 및 changelog를 함께 관리하는 도구이다. 비슷한 다른 도구를 찾아보니 semantic-release
라는 도구를 발견했는데 버전에 대한 설명이 커밋 메세지와 결합되고, 모노레포 지원이 없다는게 단점이라고 한다(찾아보니 이런 리포지토리가 있긴하다). changesets
에선 changelog의 경우 커밋과 버전 관리를 분리하므로(이유는 여기에서 확인할 수 있다.) 커밋 메세지와 별개로 변경 사항에 대한 설명을 작성할 수 있다.
배포 및 버전관리를 자동화하기 위한 GitHub Actions및 오류 상황을 감지하기 위한 bot도 지원한다.
설치 방법
yarn add -D @changesets/cli
yarn changeset init
성공적으로 설치했다면 루트 디렉토리에 .changeset/config.json
이 생성된다. public하게 배포할거라면 config.json
에서 access
필드를 public
으로 바꿔줘야 한다. (참고)
자동화를 적용하고 새 버전을 릴리즈할 때의 과정은 다음과 같다.
1. 코드 변경
이전 버전과 새 버전 사이에 커밋이 여러개 있을 수 있고, 모노레포 환경에서 여러 패키지를 변경할 수도 있다. 코드를 변경하고 새 버전을 릴리즈할 준비가 되었다면 다음 단계로 넘어간다.
2. changeset생성
yarn changeset
을 사용하여 changeset을 생성한다. changeset은 .changeset/{random-name}
으로 생성되는 마크다운 파일로, package.json과 changelog를 업데이트하기 위한 정보를 포함한다.
cli에서 changesets이 바뀐 패키지를 감지하여 업데이트할 패키지 목록을 보여주며, 선택한 패키지마다 semver spec을 따르는 bump type을 선택할 수 있다.
선택 후 summary를 작성하면 changeset이 생성된다. frontmatter에는 패키지별 bump type이, content엔 작성했던 summary가 들어있다. 예를들어 다음과 같은 파일이 생성된다.
---
'@scopeName/packageName1': patch
'@scopeName/packageName2': minor
---
summary
이 파일을 기반으로 changelog
및 package.json
의 버전이 업데이트된다. cli로 생성했지만 직접 마크다운을 수정해도 상관없다.
3. changeset파일을 타겟 브랜치에 반영
changeset을 커밋하고 PR혹은 push를 통해 타겟 브랜치에 반영한다.
4. 버전 및 changelog업데이트
changeset파일 내용을 기반으로 package.json
의 버전과 changelog
가 자동으로 업데이트된다. 이후 changeset파일을 지우고, 업데이트 내용과 함께 새 커밋으로 반영한다.
5. 새 패키지 배포
4번의 변경사항을 기반으로 새 패키지를 배포해야 하는 경우를 감지하고 npm에 배포한다.
GitHub workflow 작성시 주의사항
위 4~5번 과정을 자동화하기 위해 GitHub workflow를 작성할 수 있다. 예시 코드는 여기와 각 액션별 리포지토리 참고하여 작성한게 전부라 설명할 것이 거의 없어서 링크로 대체하였다.
그리고 주의할 사항이 몇 가지 있다.
npm token
CI환경에서 npm배포를 하려면 npm token이 필요하다. 이 링크를 참고해서 read-write권한의 토큰을 발급받으면 된다. 발급받고 레포지토리 secret으로 등록해준다.
yarn@^4를 사용한다면
패키지 매니저로 yarn@^4를 사용한다면, changesets action docs의 예제대로 yarn version
스크립트를 실행할 경우 오류가 발생한다. 이 이슈에 이유 및 해결방법이 설명되어있다. 요약하자면 yarn version
이 기존에는 package.json
의 scripts에 작성한 version스크립트를 실행했는데, v4에선 yarn version
이라는 yarn 자체에서 제공하는 명령어를 수행하도록 변경되었기 때문이다.
package.json
의 version script를 실행하라는 의미로 yarn run version으로 바꿔도 되는데, 나는 헷갈려서 아예 다른 이름으로 작성했다.
GitHub을 사용한다면
GitHub의 경우 GitHub Actions중에 PR을 작성할 수 있도록 하기 위해, 레포지토리 Settings → Actions → General에서 Workflow permissions섹션에서 PR권한을 허용해줘야 한다.
더 풍부한 changelog작성하기
@changesets/changelog-github
을 사용하면 changelog에 changeset를 추가한 사람과 PR, 커밋에 대한 링크도 포함시켜준다.
(출처)
로컬에서 패키지를 설치하고
yarn add -D @changesets/changelog-github
.changesets/config.json
을 수정해준다.
{
"changelog": ["@changesets/changelog-github", { "repo": "<org>/<repo>" }],
}
더 자세한 내용은 여기를 참고하면 된다.