Changesets로 패키지 릴리즈 자동화하기

·

6 min read

여러 프로젝트에서 재사용되는 코드는 매번 바닥부터 작성하는것보단, 이왕이면 라이브러리로 배포하는게 편할 수 있다. 언어마다 다르지만, 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.jsonfiles필드가 있다면 여기에 지정한 파일만 포함된다.

// 예시
// 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

이 파일을 기반으로 changelogpackage.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>" }],
}

더 자세한 내용은 여기를 참고하면 된다.