DEV BOX

'26/10/06 UPDATE

TOOLS / NPM / FIELD NOTES

package.json의 ^0.2.3은 왜 0.3.0으로 올라가지 않을까

npm의 ^는 '같은 메이저'가 아니라 맨 왼쪽의 0이 아닌 자리를 고정합니다. 그래서 ^0.2.3은 0.3.0을 받지 않습니다. 가짜 저장소에서 npm 11.19.0으로 실제 설치한 결과로 범위마다 무엇이 들어오는지, 잠금 파일·npm outdated·npm update가 각각 무엇을 바꾸는지, 0.x 의존성을 다음 마이너로 올리는 순서를 정리합니다.

package.json에 "some-lib": "^0.2.3"이 적혀 있고, 저장소에는 이미 0.3.0이 올라와 있습니다. ^는 "호환되는 새 버전을 받아 오라"는 뜻이라고 배웠으니 npm update를 하면 0.3.0이 들어올 것 같습니다. 그런데 설치되는 것은 0.2.9입니다. 이 글을 읽고 나면 ^가 버전의 어느 자리를 고정하는지 설명할 수 있고, 0.x 패키지를 다음 마이너로 올려야 할 때 어떤 명령이 무엇을 바꾸는지 구분해 쓸 수 있습니다.

예제는 2026-10-05에 macOS 27.0.1에서 Node.js v24.21.0과 npm 11.19.0(npm에 들어 있는 semver 7.8.5)으로 실행했습니다. 인터넷 저장소 대신 내 컴퓨터(127.0.0.1)에 작은 가짜 npm 저장소를 띄우고, 가짜 패키지 boxlab-fmt의 버전 아홉 개(0.0.3, 0.0.4, 0.2.3, 0.2.9, 0.3.0, 1.2.3, 1.9.0, 1.10.0-beta.1, 2.0.0)를 올려 두었습니다. 패키지 이름과 버전은 모두 설명을 위해 만든 것입니다.

^는 '같은 메이저'가 아니라 '맨 왼쪽의 0이 아닌 자리'를 고정한다

흔히 ^1.2.3을 "1.x 중 최신"이라고 외웁니다. 1.x에서는 맞는 말이지만 규칙 자체는 조금 다릅니다. npm이 버전 범위를 해석할 때 쓰는 node-semver는 캐럿 범위를 "[메이저, 마이너, 패치] 중 맨 왼쪽의 0이 아닌 자리를 바꾸지 않는 변경만 허용한다"고 정의합니다.

그래서 ^1.2.3은 첫 자리 1이 고정되어 >=1.2.3 <2.0.0-0이 됩니다. ^0.2.3은 첫 자리가 0이므로 그다음 자리인 2가 고정되어 >=0.2.3 <0.3.0-0입니다. 0.3.0은 처음부터 범위 밖입니다. ^0.0.3은 패치까지 고정되어 >=0.0.3 <0.0.4-0, 사실상 0.0.3 하나만 허용합니다. 상한 끝에 붙은 -0은 2.0.0-beta처럼 상한 버전의 프리릴리스도 들어오지 못하게 하는 표시입니다.

개발 노트

^는 어느 자리를 고정할까

맨 왼쪽에서 처음 나오는 0이 아닌 자리가 그대로 남습니다

  • ^1.2.3 · 메이저 고정

    >=1.2.3 <2.0.0-0

    • 첫 자리 1이 0이 아님
    • 1.x.x 안에서 마이너·패치 허용
  • ^0.2.3 · 마이너 고정

    >=0.2.3 <0.3.0-0

    • 첫 자리가 0이라 둘째 자리 2를 고정
    • 패치만 올라감 · 0.3.0은 범위 밖
  • ^0.0.3 · 패치까지 고정

    >=0.0.3 <0.0.4-0

    • 0.0.3 하나만 해당
    • 0.0.4도 받지 않음

변환식은 npm이 쓰는 node-semver 7.8.5가 출력한 그대로입니다. 끝의 -0은 상한 버전의 프리릴리스까지 막는 표시입니다.

^는 늘 '메이저 고정'이 아닙니다. 메이저가 0이면 마이너를, 마이너도 0이면 패치를 고정합니다.

0.x에서는 물결표 ~와 결과가 같아진다는 점도 알아 두면 좋습니다. ~0.2.3도 >=0.2.3 <0.3.0-0으로 풀렸습니다. 1.x 이상에서는 다릅니다. ~1.2.3은 <1.3.0-0까지라 이 저장소에서 1.2.3만 골랐고, ^1.2.3은 1.9.0을 골랐습니다.

실제로 설치해 보면

범위 문자열만 계산해 본 것이 아니라, 범위마다 새 프로젝트를 만들고 가짜 저장소를 가리키게 한 뒤 실제로 npm install을 실행해 node_modules에 들어온 버전을 읽었습니다. "^1.2.3"은 1.9.0, "^0.2.3"은 0.2.9, "^0.0.3"은 0.0.3이었습니다. 0.3.0이 설치된 것은 ">=0.2.3 <1.0.0"처럼 범위를 직접 넓혔을 때와 "^0.3.0"을 적었을 때뿐이었습니다.

개발 노트

같은 저장소, 범위마다 설치된 버전

가짜 패키지 boxlab-fmt의 9개 버전 중 npm install이 고른 것

  • "^1.2.3"

    -> 1.9.0

    • 1.x 중 가장 높은 정식 버전
    • 2.0.0과 1.10.0-beta.1은 제외
  • "^0.2.3"

    -> 0.2.9

    • 0.3.0이 있어도 0.2.x에 머묾
  • "~0.2.3"

    -> 0.2.9

    • 0.x에서는 ^와 같은 결과
  • ">=0.2.3 <1.0.0"

    -> 0.3.0

    • 0.x 마이너까지 열어 둔 범위
    • 깨질 수 있는 변경도 함께 받음

게시된 버전: 0.0.3 0.0.4 0.2.3 0.2.9 0.3.0 1.2.3 1.9.0 1.10.0-beta.1 2.0.0. 2026-10-05 npm 11.19.0으로 로컬 저장소에서 설치했습니다.

새 프로젝트마다 범위 하나만 바꿔 npm install을 실행한 결과입니다. ^0.2.3은 0.3.0을 건너뛰었습니다.
실행 결과. ranges.mjs: ^1.2.3은 >=1.2.3 <2.0.0-0으로 1.9.0, ^0.2.3은 >=0.2.3 <0.3.0-0으로 0.2.9, ^0.0.3은 >=0.0.3 <0.0.4-0으로 0.0.3, ~0.2.3은 >=0.2.3 <0.3.0-0으로 0.2.9. satisfies("1.10.0-beta.1", "^1.2.3")은 false. install.mjs: "^1.2.3" 1.9.0, "^0.2.3" 0.2.9, "^0.0.3" 0.0.3, ">=0.2.3 <1.0.0" 0.3.0.
npm에 들어 있는 semver로 범위를 풀어 보고, 같은 범위로 실제 npm install을 실행한 출력입니다. 값은 그대로 두고 일부 줄과 열만 골라 옮겼습니다.

같은 출력에 프리릴리스도 하나 있습니다. 1.10.0-beta.1은 숫자로는 1.9.0보다 크지만 ^1.2.3에 들지 않았습니다. node-semver는 범위 안에 같은 [메이저, 마이너, 패치]를 가진 프리릴리스 비교식이 있을 때만 프리릴리스 버전을 받아들입니다. 베타를 써 보고 싶다면 ^1.10.0-beta.1처럼 범위에 직접 적어야 합니다. 라이브러리 코드에서 semver.satisfies()를 쓸 때는 includePrerelease 옵션으로 이 동작을 끌 수 있고, 실행해 보니 그때는 true가 나왔습니다.

왜 0.x에서는 마이너까지 묶어 둘까

유의적 버전(Semantic Versioning 2.0.0) 명세 4항은 메이저 0인 0.y.z를 초기 개발 단계로 보고, 이때는 무엇이든 언제든 바뀔 수 있으며 공개 API를 안정적이라고 여기면 안 된다고 적고 있습니다. 같은 명세의 FAQ는 초기 개발 중에는 0.1.0에서 시작해 릴리스마다 마이너를 올리는 방식을 가장 단순한 방법으로 제시합니다.

그러니 0.x 패키지에서는 0.2에서 0.3으로 가는 마이너 변경이 1.x에서의 메이저 변경과 비슷한 무게를 가질 수 있습니다. npm의 캐럿은 이 관례에 맞춰 0.x에서는 패치만, 0.0.x에서는 그것조차 열지 않습니다. "0.3.0이 왜 안 들어오지"는 버그가 아니라 깨질 수 있는 변경을 자동으로 받지 않도록 막아 둔 결과입니다.

잠금 파일이 있으면 범위를 넓혀도 그대로다

범위만 보고 끝내면 반쯤 맞습니다. 실제 프로젝트에는 package-lock.json이 있고, npm install 문서는 잠금 파일이 있으면 설치가 그 파일을 따른다고 적고 있습니다. 먼저 "0.2.3"으로 고정해 설치한 뒤 범위를 "^0.2.3"으로 바꾸고 다시 npm install을 실행했습니다. 잠긴 0.2.3이 새 범위 안에 들기 때문에 잠금 파일과 node_modules는 0.2.3 그대로였습니다.

lock.mjs 실행 결과. "0.2.3"으로 설치하면 잠금 파일과 node_modules 0.2.3. 범위를 "^0.2.3"으로 바꿔 install해도 0.2.3 그대로. npm outdated: boxlab-fmt Current 0.2.3, Wanted 0.2.9, Latest 2.0.0. npm update 뒤 0.2.9. npm install boxlab-fmt@^0.3.0 뒤 0.3.0.
잠금 파일이 있는 프로젝트에서 범위, npm outdated, npm update, 새 범위 설치를 차례로 실행한 출력입니다.

이때 npm outdated가 상황을 가장 잘 보여 줍니다. Current는 0.2.3, Wanted는 0.2.9, Latest는 2.0.0이었습니다. Wanted는 package.json 범위를 만족하는 최고 버전이고, Latest는 저장소에서 latest 태그가 붙은 버전입니다. 두 열이 다르다면 범위 때문에 못 올라가고 있다는 뜻입니다. npm update를 실행하자 0.2.9까지만 올라갔고 package.json의 ^0.2.3은 바뀌지 않았습니다. npm update 문서도 기본적으로 package.json의 범위 값은 수정하지 않는다고 설명합니다.

0.x 의존성을 다음 마이너로 올리는 순서

0.3.0으로 가는 일은 명령 하나로 자동으로 일어나지 않습니다. 범위를 바꾸는 결정이 필요합니다. 실제 패키지라면 먼저 변경 기록에서 0.3.0의 깨지는 변경을 확인합니다(여기서 쓴 가짜 패키지에는 변경 기록이 없습니다). 그다음 새 범위로 설치합니다. npm install boxlab-fmt@^0.3.0을 실행하자 package.json은 ^0.3.0, 잠금 파일과 node_modules는 0.3.0이 되었습니다. 그 뒤로는 다시 0.3.x 안에서만 npm update가 움직입니다.

개발 노트

0.x 의존성은 어떻게 올릴까

잠금 파일, 범위, 명령이 각각 무엇을 바꾸는지

  1. npm install

    lock 0.2.3 -> 0.2.3

    • 잠금 파일의 0.2.3이 범위 안이면
    • 그대로 설치
  2. npm outdated

    0.2.3 | 0.2.9 | 2.0.0

    • Current | Wanted | Latest
    • Wanted = 범위 안 최고 버전
  3. npm update

    lock 0.2.9

    • 범위 안에서만 올림
    • package.json은 그대로
  4. 범위를 바꿔 설치

    npm install boxlab-fmt@^0.3.0

    • 변경 기록을 읽은 뒤
    • 0.3으로 옮기는 결정

4단계 뒤 package.json은 ^0.3.0, 잠금 파일과 node_modules는 0.3.0이 되었습니다. 2026-10-05 npm 11.19.0 실행 결과입니다.

npm update는 범위를 넘지 않습니다. 0.x에서 마이너를 올리는 일은 범위를 직접 바꾸는 결정입니다.

반대로 ">=0.2.3 <1.0.0"처럼 0.x 전체를 열어 두면 편해 보이지만, 초기 개발 단계의 깨지는 변경까지 다음 npm update에 섞여 들어옵니다. 실험 결과에서도 이 범위만 0.3.0을 받아 왔습니다.

기억할 한 줄은 이것입니다. ^는 맨 왼쪽의 0이 아닌 자리를 고정합니다. 0.x 의존성이 올라가지 않는다면 npm outdated에서 Wanted와 Latest를 비교하고, 다음 마이너가 필요하면 변경 기록을 읽은 뒤 범위를 직접 바꿔 설치하세요.

끝 · END OF NOTE목록으로
COMMENTS BOX

이 기록에 대화를 더해 주세요.

궁금한 점, 다른 접근, 직접 해 본 결과를 나눠 주세요.

최신순

로그인 상태 확인 중…

댓글을 불러오는 중…