package.json의 ^1.2.3과 ~1.2.3 차이: npm 버전 범위와 0.x의 함정
기본적으로 ^1.2.3은 1.2.3 이상 2.0.0 미만의 정식 버전을, ~1.2.3은 1.2.3 이상 1.3.0 미만의 정식 버전을 허용합니다. SemVer와 npm의 차이, 0.x, 프리릴리스, lockfile을 정리했습니다.
기본 설정에서 npm install <패키지>로 패키지를 설치하면 package.json에 "^1.2.3" 같은 범위가 저장됩니다. 앞의 ^는 어떤 버전까지 설치해도 되는지를 나타냅니다. 비슷하게 생긴 ~와는 허용 범위가 다릅니다.
SemVer는 번호 규칙이고, ^와 ~는 npm 문법이다
Semantic Versioning 2.0.0은 버전을 MAJOR.MINOR.PATCH 형식으로 적고 어느 숫자를 언제 올려야 하는지 정합니다.
1.0.0 이상에서 하위 호환되는 버그 수정만 있다면 PATCH를 올려야 합니다. 하위 호환되는 기능을 추가하거나 공개 API의 기능을 사용 중단 예정으로 표시한다면 MINOR를 올려야 합니다. 하위 호환을 깨는 변경에는 MAJOR 증가가 필요합니다. 이 부분은 명세의 MUST 요구사항입니다.
0.y.z는 초기 개발 단계입니다. 명세는 이 단계에서 무엇이든 바뀔 수 있으며 공개 API를 안정적이라고 보지 말아야 한다고 설명합니다.
SemVer 명세에는 ^와 ~의 의미가 정의되어 있지 않습니다. 둘은 npm이 package.json에서 받는 범위 문법이며, 구체적인 해석은 npm이 사용하는 node-semver 문서에 나옵니다.
범위를 풀어 쓰면
| 표기 | 풀어 쓴 범위 | 기본적으로 허용하는 정식 버전 |
|---|---|---|
^1.2.3 |
>=1.2.3 <2.0.0-0 |
마이너 및 패치 변경 |
~1.2.3 |
>=1.2.3 <1.3.0-0 |
패치 변경 |
^0.2.3 |
>=0.2.3 <0.3.0-0 |
패치 변경 |
~0.2.3 |
>=0.2.3 <0.3.0-0 |
패치 변경 |
^0.0.3 |
>=0.0.3 <0.0.4-0 |
0.0.4 같은 이후 정식 버전은 제외 |
^는 major, minor, patch 가운데 가장 왼쪽에 있는 0이 아닌 자리를 바꾸지 않는 변경을 허용합니다. 그래서 1.x에서는 ^가 ~보다 넓습니다. 하지만 0.x에서는 ^0.2.3과 ~0.2.3이 같은 범위가 됩니다. ^0.0.3은 0.0.4 같은 이후 정식 버전을 받아들이지 않습니다.
~는 마이너 버전까지 적혀 있으면 패치 변경만 허용합니다. 마이너 버전을 생략한 ~1은 >=1.0.0 <2.0.0-0으로 해석됩니다.
상한의 -0과 프리릴리스
SemVer 우선순위에서 프리릴리스는 같은 번호의 정식 버전보다 앞에 옵니다. 프리릴리스 식별자를 비교할 때 숫자 식별자는 숫자가 아닌 식별자보다 우선순위가 낮습니다. 따라서 2.0.0-0은 다른 유효한 2.0.0 프리릴리스보다 앞에 옵니다. <2.0.0-0이라는 상한은 2.0.0-beta도 범위 밖으로 밀어냅니다.
범위 안쪽에 있는 프리릴리스도 기본적으로 제외됩니다. 프리릴리스가 comparator set을 만족하려면 같은 set 안에 동일한 [major, minor, patch]와 프리릴리스 태그를 가진 comparator가 하나 이상 있어야 합니다. node-semver의 includePrerelease 옵션을 켜면 이 기본 제외 규칙이 달라집니다.
다음 결과는 Node.js v24.13.0과 node-semver 7.8.5에서 실행해 확인했습니다.
const semver = require("semver");
for (const range of ["^1.2.3", "~1.2.3", "^0.2.3", "^0.0.3"]) {
console.log(range.padEnd(7), new semver.Range(range).range);
}
console.log(semver.satisfies("1.9.0", "^1.2.3"));
console.log(semver.satisfies("1.9.0", "~1.2.3"));
console.log(semver.satisfies("0.3.0", "^0.2.3"));
console.log(semver.satisfies("1.3.0-beta.1", "^1.2.3"));
console.log(
semver.satisfies("1.3.0-beta.1", "^1.2.3", {
includePrerelease: true,
}),
);
실행 결과:
^1.2.3 >=1.2.3 <2.0.0-0
~1.2.3 >=1.2.3 <1.3.0-0
^0.2.3 >=0.2.3 <0.3.0-0
^0.0.3 >=0.0.3 <0.0.4-0
true
false
false
false
true
범위가 실제 설치 버전을 뜻하지는 않는다
npm 설정 save-prefix의 기본값은 "^"입니다. save-exact를 활성화하면 범위 대신 정확한 버전을 저장합니다.
package.json에 넓은 범위가 적혀 있어도 실제 설치에는 lockfile이 관여합니다. package-lock.json은 생성된 의존성 트리를 정확히 기록하며 저장소에 커밋하도록 만들어진 파일입니다.
인자 없이 npm install을 실행하면 lockfile의 버전이 package.json 범위를 만족하는 동안에는 해당 버전을 사용합니다. 둘이 맞지 않으면 package.json을 기준으로 의존성을 다시 계산하고 lockfile을 갱신합니다.
npm ci는 lockfile이 필요합니다. package.json과 lockfile이 맞지 않으면 lockfile을 수정하지 않고 오류로 끝납니다. package.json이나 lockfile에도 내용을 쓰지 않습니다.
버전 범위가 보장하지 않는 것
^1.2.3이 1.9.0을 받아들이는 이유는 배포자가 1.x 안의 하위 호환성을 유지하고, 호환성을 깨는 변경에는 MAJOR를 올렸다는 전제에 있습니다. 버전 범위는 번호와 프리릴리스 규칙을 비교할 뿐, 실제 API가 호환되는지 검사하지 않습니다.
이 글의 npm 동작은 2026년 10월 1일 npm CLI v12 문서를 기준으로 확인했습니다.
참고한 명세와 문서
- Semantic Versioning 2.0.0 (원문 Markdown)
- node-semver: 범위 문법(
^,~)과 프리릴리스 태그 규칙 - npm CLI v12 문서: package.json, config, npm install, npm ci, package-lock.json