package.json의 버전 특수문자들 (~, ^, ...)

package.json의 버전 특수문자들 (~, ^, ...)

풀스택 프리랜서로 일하면서 주력이었던 C#1에서 node 관련 작업으로 전환하게 되었다. 이 과정에서 버전 관리의 중요성을 깊이 깨달았다. 특히, node의 package.json에서 버전을 지정하는 다양한 방법과 그 의미에 대해 더 자세히 알아보게 되었고, 이에 대한 나의 이해와 경험을 공유하고자 한다.

패키지 버전

노드 패키지 버전 포맷

노드는 Semantic Versioning을 사용한다. 중학교 때부터 JetBrains ReSharper를 사용해오며 Calendar Versioning에 익숙한 난 처음에 각각의 구분이 뭘 의미하는지 몰라서 대충 첫 번째가 바뀌면 엄청난 패치, 두 번째가 바뀌면 일반 패치, 세 번째가 바뀌면 소소한 패치 정도로 이해했는데 지금 보니 얼추 맞는 것 같기도 하다. 하지만 정확하지는 않다. 이는 버전 제한자, 버전 범위에서 설명하도록 하겠다.

package.json

노드는 package.json 파일로 패키지를 관리한다. 닷넷의 csproj 파일이랑 같은 역할을 한다고 봐도 무방할 것 같다. 여기에서 프로젝트 이름, 의존성 관리 등 프로젝트의 세부 사항을 관리하게 된다.

버전 제한자, 버전 범위

그리고 package.json에서는 버전 제한자를 사용하여 패키지 매니저2를 통해 의존성을 설치할 때 설치될 버전의 범위를 지정할 수 있다.

문제는 어떤 오픈소스 프로젝트는 ~를 앞에 붙이고, 어떤 건 ^를 앞에 붙이고, 어떤 건 아예 안 붙이고, 어떤 건 >, >=, <, <=를 붙이고… 마지막 부등호들은 직관적으로 이해가 되는데 문제는 ^, ~는 뭐 하는 놈인지 알 수가 없었다.

node-semver

해당 내용은 node-semver 리포지토리의 버전 섹션에서 찾을 수 있었다. 특히 자주 쓰이는 ~(틸드)^(캐럿)에 대해 설명해보자면 다음과 같다.

  • 캐럿(^): 이 제한자는 메이저 버전 내에서 가장 최신의 마이너 버전으로 업데이트를 허용한다. 예를 들어, ^1.2.31.2.3 이상, 2.0.0 미만의 버전을 허용한다. 즉, 메이저 버전 번호가 바뀌지 않는 한 업데이트가 가능하다는 얘기다.

  • 틸드(~): 이 제한자는 마이너 버전 내에서 가장 최신의 패치 버전으로 업데이트를 허용한다. ~1.2.31.2.3 이상, 1.3.0 미만의 버전을 허용한다. 따라서 마이너 버전이 바뀌지 않는 한 업데이트가 가능하다.

>, >=, <, <= 같은 부등호들은 직관적으로 이해가 되지만, ^~는 처음 보면 헷갈릴 수 있다. 이 두 제한자를 제대로 이해하고 사용하면 의존성 관리가 훨씬 수월해진다.

아무것도 지정하지 않으면 앞에 =이 붙어있는 것과 동일하게 처리된다.

주의할 점은, 버전에 >1이라 지정하면 2.0.0 이상의 버전을 의미하지만 >1.0이라 지정하면 1.0.1과 같이 1.0.0보다 큰 버전을 의미한다는 것이다. 즉 자릿수에 따라서 대상 버전 범위가 달라지므로 이를 잘 생각하고 써야 한다.

메이저 버전과 마이너 버전의 차이

소프트웨어 버전 번호는 일반적으로 메이저.마이너.패치 형태로 구성된다. 각각의 부분이 의미하는 바는 다음과 같다.

  1. 메이저 버전(Major Version): 이 숫자가 바뀌면, 그건 보통 큰 변화를 의미한다. 즉, 기존 버전과 호환되지 않는 변경이 발생했다는 거지. 예를 들어, API가 완전히 바뀌거나, 기존 기능이 제거되는 등의 주요 변경이 이루어졌을 때 메이저 버전을 올린다. 메이저 버전이 바뀌면 기존 사용자들은 업그레이드하기 전에 주의 깊게 살펴봐야 한다.

  2. 마이너 버전(Minor Version): 마이너 버전이 바뀌는 건 새로운 기능이 추가되거나 개선이 이루어졌지만, 기존 버전과의 호환성은 여전히 유지되는 경우다. 이러한 변경은 새로운 기능을 도입하거나 기존 기능을 확장할 때 발생한다. 마이너 버전 업데이트는 기존 시스템이나 의존성에 큰 영향을 주지 않으면서도 새로운 기능이나 개선을 제공한다.

  3. 패치 버전(Patch Version): 패치 버전이 바뀔 때는 보통 버그 픽스가 진행되었거나, 보안 문제가 개션되었거나, 마이너 버전을 올리기엔 뭐할 정도로 소소한 기능 개선이 이루어졌을 때이다. 즉, 전체적인 프로그램의 작동에 큰 영향을 미치지 않지만 코드가 변경되었을 때 올라간다고 보면 된다.

예시 (next.js turbopack)

다음 코드는 create-next-app --example with-turbopack으로 생성한 프로젝트의 package.json이다.

{
  "private": true,
  "scripts": {
    "dev": "next dev --turbo",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "latest",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/node": "20.8.10",
    "@types/react": "18.2.33",
    "@types/react-dom": "18.2.14",
    "typescript": "^5.2.2"
  }
}

보면 react, react-dom의 버전은 캐럿 버전으로 지정되었으므로 18.2.0 이상 19.0.0 미만의 버전 중 최신 버전이 설치될 것이다. typescript도 마찬가지로 *5.2.2 이상 6.0.0 미만**의 버전 중 최신 버전이 설치된다.

반면 @types/node를 비롯한 타입 패키지들은 prefix 없이 지정되었다. 또한 patch 버전까지 지정되어 있으므로 정확히 해당 버전만 설치되게 된다.

추가로, 만약 "@types/node": "20.8"로 지정했다면 20.8.0 이상 21.0.0 미만이 설치되게 된다.


  1. ASP.NET, WPF, WinForms, … ↩︎

  2. npm, yarn, pnpm, … ↩︎

댓글

이 블로그의 인기 게시물

C# 남아도는 메모리에도 불구하고 OutOfMemoryException이 발생한다면?

USB를 뒤는 괜찮은데 앞에 꽂으면 인식이 힘들다?

MySQL 데이터 타입과 Java 데이터 타입 비교/매칭