콘텐츠로 이동
Study Note웹 개발 일반

5. 패키지 매니저 — 왜 pnpm인가

node_modules가 우주에서 가장 무거운 물체라는 농담에는 구조적 이유가 있었다

이 장에서 처음 나오는 말4개
패키지Package
npm 저장소에 올라 있는 코드 묶음. package.json이라는 명세서를 가진 폴더 하나가 패키지 하나다.
의존성Dependency
내 프로젝트가 가져다 쓰는 남의 패키지. 그 패키지도 또 의존성을 가진다 — 그래서 몇 개만 설치해도 수백 개가 따라온다.
잠금 파일Lockfile
실제로 설치된 전체 의존성 트리의 정확한 버전을 기록한 파일 (pnpm-lock.yaml). 이게 있어야 어제의 나와 오늘의 CI가 같은 것을 설치한다.
semverSemantic Versioning
주.부.수(2.1.3) 버전 규칙. 주 버전이 오르면 호환이 깨진다는 약속이고, ^2.1.3은 "주 버전만 지키면 올려도 됨"이라는 표기다.

package.json — 프로젝트의 신분증

섹션 제목: “package.json — 프로젝트의 신분증”

JS 프로젝트의 모든 것이 이 파일에서 시작한다.

{
"name": "my-app",
"type": "module",
"packageManager": "pnpm@11.0.0",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "^19.2.0"
},
"devDependencies": {
"typescript": "^6.0.0",
"vite": "^8.0.0"
}
}
  • dependencies vs devDependencies — 실행에 필요한 것 vs 개발·빌드에만 필요한 것. 서버 배포 시 후자는 설치를 생략할 수 있다
  • scripts — 프로젝트의 명령어 사전. pnpm dev는 여기 적힌 vite를 실행하는 것뿐이다. 낯선 레포를 받으면 제일 먼저 여기를 본다
  • packageManager — 이 프로젝트가 쓰는 패키지 매니저와 버전의 선언. CI와 팀원이 같은 도구로 설치하게 고정한다

^19.2.0 같은 범위 표기 때문에 package.json만으로는 오늘과 내일의 설치 결과가 다를 수 있다. 잠금 파일은 마지막 설치에서 실제로 풀린 전체 트리(의존성의 의존성까지)를 정확한 버전으로 못 박는다.

npm이 못 만든 도구라서가 아니다 — 2010년의 설계가 2020년대의 규모를 만난 것이다.

npm은 프로젝트마다 node_modules에 모든 패키지의 실제 복사본을 둔다. 프로젝트 10개가 같은 React를 쓰면 디스크에 React가 10벌이다. 프로젝트 하나의 node_modules가 수백 MB인 게 예사라, 프로젝트가 쌓이면 수십 GB가 된다.

npm은 설치를 빠르게 하려고 의존성의 의존성까지 전부 node_modules 맨 위로 끌어올린다 (호이스팅). 부작용 — 내가 설치한 적 없는 패키지도 import가 된다.

// package.json에는 some-package만 적었는데…
import helper from 'transitive-helper'; // 하위 의존성이 우연히 최상위에 보여 import됨

이게 “유령 의존성(phantom dependency)“이다. 지금은 돌지만, 상위 패키지가 내부 의존성을 바꾸는 순간 내 코드가 영문 모를 이유로 깨진다. 명세에 없는 우연에 기댄 코드다.

pnpm의 해법 — 저장은 한 번, 연결은 링크로

섹션 제목: “pnpm의 해법 — 저장은 한 번, 연결은 링크로”

pnpm(performant npm)은 두 문제를 구조로 푼다.

내용 주소 기반 저장소가 같은 패키지 파일을 한 번만 저장하고, 프로젝트 A와 B의 node_modules에 있는 react가 모두 그 저장소로 링크되는 구조
  • 전역 저장소 + 링크 — 패키지 실체는 머신 전체에서 한 벌만 저장하고, 각 프로젝트에는 링크만 놓는다. 디스크가 절약되고, 이미 받은 패키지의 설치가 매우 빠르다
  • 엄격한 node_modules — package.json에 적은 패키지만 최상위에 보이게 배치한다. 유령 의존성이 구조적으로 import 불가능해진다

이 엄격함이 pnpm의 진짜 가치다. 속도는 다른 도구도 따라잡을 수 있지만, “명세에 적은 것만 쓸 수 있다”는 규율은 구조에서 나온다.

pnpm 10부터 공급망 공격의 주요 경로인 의존성의 설치 스크립트를 기본으로 차단한다. pnpm 11은 검토하지 않은 빌드를 기본 오류로 처리하고, 꼭 필요한 패키지만 allowBuilds에 명시한다. 이 레포의 pnpm-workspace.yaml에 있는 설정이 정확히 그 목록이다.

npmyarnpnpmbun install
위상Node 동봉 기본오래된 대안·워크스페이스 강점이 덱의 권장값런타임과 통합
디스크프로젝트마다 복사복사 (v4는 개선)전역 한 벌 + 링크전역 캐시
유령 의존성허용됨기본 허용구조적으로 차단허용됨
모노레포지원지원가장 성숙 (9장)지원
  • 디렉터리my-app/
    • package.json 처음 볼 파일 — scripts와 의존성
    • pnpm-lock.yaml 이 파일이 있으면 pnpm 프로젝트라는 뜻
    • pnpm-workspace.yaml packages가 있으면 모노레포. 설정만 담을 수도 있음 (9장)
    • 디렉터리node_modules/ 커밋 금지 — 언제든 지우고 다시 만들 수 있어야 한다
      • …
    • 디렉터리src/
      • …

잠금 파일이 곧 패키지 매니저의 표식이다 — package-lock.json이면 npm, pnpm-lock.yaml이면 pnpm, bun.lock이면 Bun. 표식과 다른 도구로 설치하지 않는다.

  • package.json은 명세(범위 표기), 잠금 파일은 실제 설치의 못 박기 — 둘 다 커밋한다
  • npm의 두 구조 문제 — 디스크 낭비(프로젝트마다 복사)와 유령 의존성(호이스팅의 부작용)
  • pnpm의 해법 — 전역 저장소 + 링크로 낭비를, 엄격한 배치로 유령을 잡는다
  • 설치 스크립트 기본 차단 — 공급망 공격 시대의 방어선
  • 잠금 파일이 도구의 표식이다 — 레포의 기존 선택을 따르고, 섞지 않는다