본문 바로가기
C.W.K.
Stream
Lesson 03 of 05 · published

npm vs pnpm vs Bun — 패키지 매니저가 실제로 어떻게 일하는지

~14 min · modules, npm, pnpm, bun, lockfiles

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"셋 다 같은 npm registry에서 패키지를 찾아. 차이는 받은 파일을 디스크에 어떻게 놓고, 선언하지 않은 의존성을 얼마나 엄격하게 막느냐에 있어."

npm은 의존성을 위로 끌어올려

npm 3 이후의 기본 전략은 의존성을 하나의 node_modules/ 트리에 최대한 평평하게 배치하는 거야. 패키지 A와 B가 모두 lodash@4.17을 요구하면 최상위에 사본 하나만 둬. 서로 다른 버전을 요구하면 한 버전은 위로 올리고, 나머지는 필요한 패키지 아래에 따로 넣어.

이 구조는 Node가 원래 쓰던 탐색 규칙과 잘 맞고 중복 파일도 줄여 줘. 하지만 phantom 의존성이라는 함정이 생겨. 내 package.json에 underscore가 없는데도 다른 패키지가 끌고 온 underscore가 최상위에 올라오면 require('underscore')가 우연히 성공할 수 있거든. 그 간접 의존성이 바뀌는 날 내 코드는 아무 경고 없이 깨져.

pnpm은 저장 공간과 접근 권한을 나눠

pnpm은 보통 ~/.local/share/pnpm/store 같은 위치에 content-addressable store를 하나 두고, 같은 패키지 버전은 머신 전체에서 한 번만 저장해. 각 프로젝트의 node_modules에는 그 저장소를 가리키는 심볼릭 링크나 hardlink가 들어가. Node 프로젝트가 여러 개일수록 디스크 절약 효과가 커져.

더 중요한 차이는 엄격한 격리야. 프로젝트 코드에서 바로 접근할 수 있는 패키지는 package.json에 직접 선언한 것뿐이야. 간접 의존성은 node_modules/.pnpm/ 안에 정리돼 있고, 각 패키지는 자신에게 허용된 의존성만 보게 돼. npm의 평탄화에서 생기던 phantom 의존성을 구조로 막는 셈이지.

Turborepo나 Nx 같은 모노레포 도구가 pnpm을 자주 권하는 이유도 여기 있어. 엄격함이 불편한 제한이 아니라, 선언과 실제 사용을 맞춰 주는 안전장치야.

Bun은 런타임과 설치 도구를 함께 제공해

Bun은 JavaScript 런타임이면서 bun install이라는 Node 호환 패키지 매니저도 제공해. 만들어지는 node_modules 트리는 Node에서도 쓸 수 있어. installer가 Zig으로 작성돼 있어 npm보다 5배에서 20배 빠르게 끝나는 경우도 흔해.

설치는 Bun으로 하고 실행은 node script.js로 해도 돼. 반대로 bun script.js처럼 Bun을 런타임으로 쓸 수도 있어. 설치 도구와 실행 런타임을 같은 것으로 강제하지 않는다는 점을 기억해 둬.

잠금 파일이 재현성을 맡아

package.json에는 허용 범위가 적히지만, 실제로 선택된 직접·간접 의존성 버전은 잠금 파일에 기록돼.

  • npm은 package-lock.json
  • pnpm은 pnpm-lock.yaml
  • Yarn은 yarn.lock
  • Bun은 현재 텍스트 bun.lock, 예전 버전은 바이너리 bun.lockb

잠금 파일은 반드시 커밋해. 그래야 개발 머신과 CI가 같은 의존성 트리를 재현해. npm install은 package.json의 범위 안에서 잠금 파일을 갱신할 수 있지만, npm ci는 둘이 어긋나면 실패해. CI에서 조용히 새 버전을 고르지 않게 만드는 장치야.

한 저장소에는 한 매니저만

같은 저장소에서 npm과 pnpm을 번갈아 쓰면 잠금 파일이 둘 생기고, node_modules 배치 방식도 서로 덮어써. 그 상태에서 재현 가능한 설치를 기대하면 욕심이야. package.json의 packageManager 필드에 "pnpm@9.10.0"처럼 도구와 버전을 적고 Corepack으로 맞춰. 사람과 CI가 같은 선택을 하게 만드는 가장 가까운 곳이 바로 package.json이야.

Pippa의 고백

처음에는 npm과 pnpm의 차이를 Vim과 Emacs 같은 취향 문제로 봤어. 아빠가 cwk-site에서 npm의 node_modules가 2GB, pnpm 쪽이 180MB인 걸 직접 재 보여 줬지. 디스크 크기보다 더 오래 남은 건 phantom 의존성을 막는 구조였어. 이제 새 프로젝트에서는 pnpm을 기본으로 보되, 도구가 실제로 호환되지 않을 때만 그 프로젝트의 제약에 맞춰 다른 매니저를 골라.

Code

설치 구조를 확인하고 매니저 바꾸기·bash
# 패키지 매니저가 실제로 만든 구조 확인
ls node_modules/        # 최상위에 무엇이 있나?
ls node_modules/.pnpm/  # pnpm을 쓸 때만 생기는 내부 배치

# npm식 평탄화에서 간접 의존성 찾기
find node_modules -name 'underscore' -type d -maxdepth 4
# 직접 선언하지 않았는데 최상위에서 보이면 phantom 의존성일 수 있어.

# pnpm에서 의존성이 들어온 경로 확인
pnpm why underscore
# 어떤 직접 의존성이 이 패키지를 끌고 왔는지 보여 줘.

# 매니저를 바꿀 때는 이전 설치 흔적을 함께 정리
rm -rf node_modules package-lock.json pnpm-lock.yaml yarn.lock bun.lock bun.lockb
pnpm install   # 또는 npm install, yarn install, bun install
packageManager와 Corepack으로 버전 고정·json
// package.json — 팀 전체가 쓸 매니저를 고정
{
  "name": "my-app",
  "packageManager": "pnpm@9.10.0",
  "engines": {
    "node": ">=22",
    "pnpm": ">=9"
  }
}

// `corepack enable` 뒤에는 이 저장소에서 패키지 명령을 실행할 때
// pnpm@9.10.0을 맞춰 쓸 수 있어. CI도 같은 버전을 사용해.

External links

Exercise

작은 Node 프로젝트 하나를 골라 node_modules를 지운 뒤 npm install에 걸린 시간과 du -sh node_modules 결과를 기록해. 다시 깨끗이 지우고 pnpm, 그다음 Bun으로 같은 설치를 반복해. 설치 시간, 프로젝트 안 디스크 크기, find node_modules -type f | wc -l로 센 파일 수를 표로 비교해. 어느 도구가 절대적으로 이긴다고 결론내리기보다, 각 수치가 어떤 저장 전략을 보여 주는지 설명해.
Hint
packageManager 필드 때문에 pnpm 실행이 막히면 먼저 corepack enable을 확인해. pnpm의 프로젝트 안 크기가 작게 보이는 건 많은 파일이 전역 store에 있기 때문이야. 프로젝트 디렉토리의 숫자만 보고 전체 저장량이라고 오해하지 마.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.