본문 바로가기

Development/기타

새로운 취미, npm 패키지 배포 과정 정리

들어가며

요즘 또 새로운 취미가 생겼다. 그건 바로, 정리. 여태 진행해왔던 여러 프로젝트의 코드를 주욱 살펴보면서 반복되는 코드를 어떻게 정리할까를 고민해왔다. 처음에는 GitHub Submodule로 관리해오다가(예전에 이와 관련한 글을 올린 적이 있음), 매번 프로젝트를 생성할 때마다 Submodule을 설정하려니 매우 귀찮음을 느꼈다. 그 귀찮음과 더불어 서브모듈의 코드를 수정해야하는 상황이 빈번하게 발생하니, 결국에는 서브모듈을 사용하지 않게 되었다. 이런 삽질을 계속 해오면서 다음과 같은 생각이 문득 들었다.

아, 그냥 나만의 BoilerPlate를 만들자.

여러 서브모듈을 하나의 프로젝트에 녹이기는 꽤나 머리 아픈 작업이었다. 처음에는 NestJS Monorepo로 BoilerPlate를 만들기로 했다. 그런데 왠걸, libs가 계속 증가하니 오히려 코드를 찾기가 더 어려웠다.

이거 해도해도 끝이 안 보이네.

결국 libs에서 핵심적인 기능들만 최소한으로 추려서 npm으로 배포해야겠다는 생각이 들었다. 그렇게 약 3일 간 총 3개의 패키지를 배포하얐고 패키지 배포 자동화까지 구축하면서 오랜만에 신선한 즐거움을 찾을 수 있었다. 언제 또 패키지를 배포할지는 모르겠으나, 그때도 삽질하지 않기 위해 글로 정리하려 한다.

세세한 내용(이를 테면, tsconfig, prettier, eslint 등)은 다루지 않고, 프로젝트 설정, 버전 관리, 패키지 배포를 중점으로 정리할 예정이다.

1. 프로젝트 설정

npm으로 배포하려면 당연히 npm으로 생성한 프로젝트가 있어야 한다.

npm init -y

그리고 생성된 package.json의 내용을 적절히 수정해주도록 한다.

{
  "name": "패키지 이름"
  "version": "1.0.0",
  "description" : "패키지 설명",
  "main": "빌드한 산출물의 메인이 되는 파일 경로",
  "license" : "MIT",
  "author": "제작자",
  "homepage": "깃헙 Repository 주소#readme",
  "repository": {
    "type": "git",
    "url": "git+깃헙 Repository 주소.git"
  },
  "bugs": {
    "url": "깃헙 Repository 주소/issues"
  },
  "keywords": ["패키지에 대한 검색어들"],
}

단, 위에서 version은 아직 수정하지 말자. 그리고 licenseISC에서 MIT로 변경해주었다. 라이선스에 관련해서도 할 말이 많은데, 이 글에서는 다루지 않겠다. 위의 내용을 실제로 작성한 예시는 다음과 같다.

{
  "name": "@choewy/nestjs-redis",
  "version": "0.0.1",
  "description": "NestJS Redis",
  "main": "dist/libs/index.js",
  "license": "MIT",
  "author": "choewy",
  "homepage": "https://github.com/choewy/nestjs-redis#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/choewy/nestjs-redis.git"
  },
  "bugs": {
    "url": "https://github.com/choewy/nestjs-redis/issues"
  },
  "keywords": [
    "nestjs",
    "nestjs/redis",
    "nestjs/redis-module",
    "nestjs-redis",
    "redis",
    "ioredis"
  ]
}

그리고 본인의 개발환경에 맞는 추가적인 설정(jsconfig.json, tsconfig.json 등)을 해주도록 하자.

2. 버전 관리

버전 체계는 일반적으로 major, minor, patch로 구성된다. 예를 들어 버전이 1.2.3인 경우 각각 매칭되는 구성요소는 다음과 같다.

  • major : 1
  • minor : 2
  • patch : 3

다른 글에 워낙 잘 정리되어 있어서 글을 안 남기려고 했는데, 각각의 개념을 최대한 간략하게 정리하면 다음과 같다.

  • major : 주 버전이라고 하며, 소프트웨어의 주요 변경사항이나 업그레이드를 나타낸다. major가 올라간다는 것은 기존 버전과 호환되지 않는 큰 변화가 존재할 수 있음을 암시한다.
  • minor : 부 버전이라고 하며, 새로운 기능이 추가되거나 기존 기능이 개선되었음을 나타낸다. minor가 올라간다는 것은 주 버전과의 호환성을 유지하면서 새로운 기능이 추가되었거나 개선되었음을 암시한다.
  • patch : 패지 버전이라고 하며, 주로 버그 픽스, 코드 수정 등의 작은 변경 사항은 나타낸다. 주 버전과 부 버전 사이의 패치가 이루어지며 기존 기능의 오류를 수정하거나 안정성을 개선하는데 주로 사용된다.

npm 또한 위와 같은 버전 체계를 따르며, package.jsonversion을 변경하는 명령어를 제공한다.

# PATCH 한 단계 올림
npm version patch

# MINOR 한 단계 올림
npm version minor

# MAJOR 한 단계 올림
npm version major

# 직접 버전 변경
npm version [VERSION]

3. 패키지 배포

작업한 프로젝트를 패키지로 배포하는 곳은 크게 GitHub Packages, npm가 있다. 필자는 npm 패키지로 공개 배포를 하였다.
처음에는 이 둘이 같은줄 알고 꽤나 삽질했다.

"GitHub Package는 배포 성공했는데, 왜 npm에 해당 버전이 없지?"

여담이지만, GitHub Actions으로 npm 배포 후 GitHub Packages로도 배포하는 과정을 추가했는데, 사실상 필자가 배포한 GitHub Packages는 GitHub Repository에서 npm 배포 버전을 알리는 용도로 사용하였다.

아무쪼록 패키지를 배포하는 과정은 매우 간단하다.

  1. npm에 로그인한다.
  2. 배포 준비를 한다.
  3. 명령어로 배포한다.

이제 세세한 내용을 살펴보도록 하자.

3.1. npm 로그인

아래 명령어를 실행하면 npm 홈페이지에서 로그인하라는 메시지와 링크가 출력된다.

npm login

npm notice Log in on https://registry.npmjs.org/
Login at:
https://www.npmjs.com/login?next=/login/cli/여기에_해시값_있음
Press ENTER to open in the browser...

로그인 하라는 링크(https://www.npmjs.com/login?next=/login/cli/여기에_해시값_있음)로 접속해서 로그인해주도록 하자(npm 계정이 없으면 당연히 회원가입을 먼저 해야한다). 로그인이 완료되면 아래 메시지가 한 줄 더 출력된다.

Logged in on https://registry.npmjs.org/.

3.2. 배포 준비

배포하기 전에 가장 중요한 것이 있다.

"무엇"을 "어떤 버전"으로 배포할건데?

즉, 배포하려는 코드가 있어야 하고, 상황에 따라 적절한 버전으로 변경해주어야 한다. 필자는 typescript로 작업했고, 소스맵과 테스트용 코드는 배포하지 않기 위해 tsconfig.json.npmignore을 적절히 수정해주었다. 여기서 .npmignore.gitignore와 같이 npm에 배포할 때 npm으로 올리지 않을 파일의 목록을 작성하면 된다.

# 모든 폴더와 파일을 올리지 않을 거임
*

# 아래 파일은 올릴 거임
!dist/**
!package.json
!LICENSE

# 단, 아래 파일은 안 올릴 거임
dist/tsconfig.tsbuildinfo
dist/libs/main*

최초로 배포할 때에는 이전 버전이 없기 때문에 버전을 변경하지 않아도 되지만, 그 이후부터는 버전 덮어쓰기가 안 되므로 버전을 변경해주어야 배포가 가능하다. 만약, 최초 버전을 1.0.0이 아닌 0.0.1 등의 버전으로 생성하려면 package.json의 을 version을 직접 변경하거나, 아래 명령어로 변경하면 된다.

npm version v0.0.1

npm version으로 버전을 변경하기 전에 git push 먼저 해주도록 하자. npm version 명령어는 현재 git-staged된 내역이 있으면 작동하지 않는다.
npm version으로 버전을 변경한 경우 해당 버전에 맞는 git tag가 로컬에 생성된다. npm version이 생성해주는 tag를 활용해서 무언가를 시도해도 좋을 것 같다.

이제 대망의 패키지 배포를 해보자. npm 유료 플랜이거나 회사나 팀의 조직이 있는 경우 private으로 배포할 수 있다. 그런데, 필자는 그런거 없기 때문에 public으로 배포하였다.

npm publish

위 명령어는 뒤에 --access=public 옵션이 생략되어 있다. 즉, 기본적으로 전체 공개 배포를 한다는 뜻이다. 만약, private으로 배포하려면 --access=private로 옵션을 변경해주면 된다.

여기까지가 npm 배포에 기본적인 내용이었다. 그런데, 위의 과정을 계속 반복하다보면 각각의 버전 사이에 이빨빠진 형태로 배포하게 된다.

인간은 같은 실수를 반복할 수 있다. 필자도 인간이다. 고로 같은 실수를 반복할 수 있다.

  • 버전을 올려놓고 배포는 했는데, 빌드하는 과정을 생략해서 이전 버전의 코드가 배포됨
  • 버전만 올려놓고 배포하는 것을 까마득하게 잊은 후 다시 버전을 올린 후 배포해서 버전 간 이빨 빠짐

위의 과정이 귀찮기도 하고, 뭐 하나 빼먹을 때면 배포가 제대로 안 되는 경우가 있으니 시스템에 맡기기로 했다.

4. 패키지 배포 자동화

패키지 배포를 자동화하기 위해 GitHub Actions를 활용하였다. 먼저, GitHub Actions로 배포하는 과정을 정리해보면 다음과 같다.

  1. master 브랜치에 커밋되면, package.jsonversion에도 변경점이 있는지 확인한다.
  2. 변경점이 있으면 이전 커밋과 현재 커밋의 package.jsonversion을 확인한다.
  3. 이 둘이 서로 다른 경우 현재 커밋을 기준으로 tag를 생성하고 push한다.
  4. 3번 작업이 완료되면 release를 생성한다.
  5. 4번 작업이 완료되면 npm에 배포한다.
  6. 5번 작업이 완료되면 GitHub Packages에 배포한다.

위에도 언급했지만, 사실상 6번은 GitHub Packages로 배포한다기보다도 GitHub Repository에 현재 npm 버전을 보여주기 위한 용도로 이용하였다.

위의 논리대로 동작하는 코드는 다음과 같다.

전체 코드는 맨 아래 마치며에 링크한 GitHub으로 접속하시면 확인할 수 있습니다.

  • master 브랜치에 push 될 때, package.json의 내용이 변경되면 실행
on:
  push:
    branches: [master]
    paths: ['package.json']
  • 이전 커밋과 현재 커밋에서 package.json의 version을 outputs에 저장
jobs:
  diff:
    runs-on: ubuntu-22.04
    outputs:
      previous: ${{ steps.version.outputs.previous }}
      current: ${{ steps.version.outputs.current }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      - id: version
        run: |
          previous=$(git show HEAD^:package.json | grep '"version"' | awk -F '"' '{print $4}')
          current=$(git show HEAD:package.json | grep '"version"' | awk -F '"' '{print $4}')
          echo "previous=v$previous" >> $GITHUB_OUTPUT
          echo "current=v$current" >> $GITHUB_OUTPUT
  • 현재 버전을 기준으로 tag 생성 후 push
  • 이전 커밋과 현재 커밋의 package.json의 version이 다른 경우 실행
jobs:
  diff:
  create-tag:
    needs: diff

    if: needs.diff.outputs.previous != needs.diff.outputs.current
    runs-on: ubuntu-22.04
    outputs:
      tag: ${{ steps.tag.outputs.tag }}
    steps:
      - uses: actions/checkout@v4
      - id: tag
        run: |
          tag=${{ needs.diff.outputs.current }}
          git tag $tag
          git push origin $tag
          echo "tag=$tag" >> $GITHUB_OUTPUT
  • tag가 생성되면 release도 생성
jobs:
  diff:
  create-tag:
  create-release:
    needs: create-tag
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ needs.create-tag.outputs.tag }}
          release_name: ${{ needs.create-tag.outputs.tag }}
          draft: false
          prerelease: false
  • release가 생성되면 npm에 배포
jobs:
  diff:
  create-tag:
  create-release:
  npm-publish:
    needs: create-release
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://registry.npmjs.org
      - env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          npm ci
          npm run build
          npm publish
  • npm에 배포완료 후 GitHub Packages로도 배포
jobs:
  diff:
  create-tag:
  create-release:
  npm-publish:
  github-packages:
    needs: npm-publish
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://npm.pkg.github.com
      - env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          npm ci
          npm run build
          npm publish

처음에는 모든 jobs마다 workflows를 생성했는데, tags, release 트리거가 동작하지 않아서 하나의 workflows로 구성하게 되었다. 이어서 위의 배포 자동화를 더 간편하게 해주는 script는 다음과 같다.

{
  "scripts": {
    "version:patch": "npm version patch && git push origin master",
    "version:minor": "npm version minor && git push origin master",
    "version:major": "npm version major && git push origin master"
  }
}

이제 코드 작업이 끝난 후 master 브랜치에 마음껏 push하다가, 배포를 해야겠다는 마음이 들때 위의 명령어만 실행하면 되는 환경을 구축하였다.

팀이나 회사에서 작업할 때에는 master에 직접 push하는 것은 바람직하지 않다. PR 올리고, 코드리뷰 받고, 테스트 돌리고 master로 merge해야한다. 그리고 개인적으로는 이게 더 좋다. 서로의 실수를 잡아주기도 하고, 더 나은 코드를 작성위해 고민하게 되기 때문이랄까. 그런데, 지금은 필자 혼자 코딩하면서 노는 중이므로 조금 편하게 작업하였다.

마치며

현재 npm으로 배포한 패키지 3개는 모두 아직까지는 나만을 위한 패키지이다.

이 말은 한 이유는 다음과 같다.

문서가 불친절해요. 테스트 코드 없어서 신뢰성 보장이 안 돼요.

해당 프로젝트마다 NOTE.md에 TODO 리스트를 남겨놓았다. 가장 시급한 건 문서화인 듯하다. 그 다음은 전부 테스트 코드를 작성하는 것이 계획이다. 아무쪼록 새로운 취미가 생겨서 코딩하는데 즐거움을 주는 것 같다.

추가적으로 사이드 프로젝트, 공부 등 이것저것 다양한 시도를 같이 해보고 싶으신 분들은 댓글 또는 이메일 남겨주세요. 취미가 개발입니다.