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", "scripts": { "dev": "vite", "build": "vite build" }, "dependencies": { "react": "^19.2.0" }, "devDependencies": { "typescript": "^6.0.0", "vite": "^8.0.0" }}dependenciesvsdevDependencies— 실행에 필요한 것 vs 개발·빌드에만 필요한 것. 서버 배포 시 후자는 설치를 생략할 수 있다scripts— 프로젝트의 명령어 사전.pnpm dev는 여기 적힌vite를 실행하는 것뿐이다. 낯선 레포를 받으면 제일 먼저 여기를 본다packageManager— 이 프로젝트가 쓰는 패키지 매니저와 버전의 선언. CI와 팀원이 같은 도구로 설치하게 고정한다
잠금 파일은 왜 필요한가
섹션 제목: “잠금 파일은 왜 필요한가”^19.2.0 같은 범위 표기 때문에 package.json만으로는 오늘과 내일의 설치 결과가 다를 수 있다.
잠금 파일은 마지막 설치에서 실제로 풀린 전체 트리(의존성의 의존성까지)를 정확한 버전으로
못 박는다.
npm의 구조적 문제 두 가지
섹션 제목: “npm의 구조적 문제 두 가지”npm이 못 만든 도구라서가 아니다 — 2010년의 설계가 2020년대의 규모를 만난 것이다.
문제 1 — 디스크 낭비
섹션 제목: “문제 1 — 디스크 낭비”npm은 프로젝트마다 node_modules에 모든 패키지의 실제 복사본을 둔다.
프로젝트 10개가 같은 React를 쓰면 디스크에 React가 10벌이다.
프로젝트 하나의 node_modules가 수백 MB인 게 예사라, 프로젝트가 쌓이면 수십 GB가 된다.
문제 2 — 유령 의존성
섹션 제목: “문제 2 — 유령 의존성”npm은 설치를 빠르게 하려고 의존성의 의존성까지 전부 node_modules 맨 위로 끌어올린다
(호이스팅). 부작용 — 내가 설치한 적 없는 패키지도 import가 된다.
// package.json에는 some-package만 적었는데…import helper from 'transitive-helper'; // 하위 의존성이 우연히 최상위에 보여 import됨이게 “유령 의존성(phantom dependency)“이다. 지금은 돌지만, 상위 패키지가 내부 의존성을 바꾸는 순간 내 코드가 영문 모를 이유로 깨진다. 명세에 없는 우연에 기댄 코드다.
pnpm의 해법 — 저장은 한 번, 연결은 링크로
섹션 제목: “pnpm의 해법 — 저장은 한 번, 연결은 링크로”pnpm(performant npm)은 두 문제를 구조로 푼다.
- 전역 저장소 + 링크 — 패키지 실체는 머신 전체에서 한 벌만 저장하고, 각 프로젝트에는 링크만 놓는다. 디스크가 절약되고, 이미 받은 패키지의 설치가 매우 빠르다
- 엄격한 node_modules —
package.json에 적은 패키지만 최상위에 보이게 배치한다. 유령 의존성이 구조적으로 import 불가능해진다
이 엄격함이 pnpm의 진짜 가치다. 속도는 다른 도구도 따라잡을 수 있지만, “명세에 적은 것만 쓸 수 있다”는 규율은 구조에서 나온다.
보너스 — 공급망 방어
섹션 제목: “보너스 — 공급망 방어”pnpm 10부터 공급망 공격의 주요 경로인 의존성의 설치 스크립트를 기본으로 차단한다.
pnpm 11은 검토하지 않은 빌드를 기본 오류로 처리하고, 꼭 필요한 패키지만 allowBuilds에 명시한다.
이 레포의 pnpm-workspace.yaml에 있는 설정이 정확히 그 목록이다.
도구별 비교
섹션 제목: “도구별 비교”| npm | yarn | pnpm | bun install | |
|---|---|---|---|---|
| 위상 | Node 동봉 기본 | 오래된 대안·워크스페이스 강점 | 이 덱의 권장값 | 런타임과 통합 |
| 디스크 | 프로젝트마다 복사 | 복사 (v4는 개선) | 전역 한 벌 + 링크 | 전역 캐시 |
| 유령 의존성 | 허용됨 | 기본 허용 | 구조적으로 차단 | 허용됨 |
| 모노레포 | 지원 | 지원 | 가장 성숙 (9장) | 지원 |
| 하고 싶은 것 | npm | pnpm |
|---|---|---|
| 의존성 설치 | npm install | pnpm install |
| 패키지 추가 | npm install react | pnpm add react |
| 개발용 추가 | npm install -D vite | pnpm add -D vite |
| 스크립트 실행 | npm run dev | pnpm dev (run 생략 가능) |
| 일회성 실행 | npx shadcn init | pnpm dlx shadcn init |
pnpm dlx는 설치 없이 패키지를 한 번만 받아 실행한다 —
프로젝트 생성기(create-next-app)나 CLI 도구를 쓸 때 만난다.
낯선 레포를 받았을 때
섹션 제목: “낯선 레포를 받았을 때”디렉터리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. 표식과 다른 도구로 설치하지 않는다.
5장 요약
섹션 제목: “5장 요약”package.json은 명세(범위 표기), 잠금 파일은 실제 설치의 못 박기 — 둘 다 커밋한다- npm의 두 구조 문제 — 디스크 낭비(프로젝트마다 복사)와 유령 의존성(호이스팅의 부작용)
- pnpm의 해법 — 전역 저장소 + 링크로 낭비를, 엄격한 배치로 유령을 잡는다
- 설치 스크립트 기본 차단 — 공급망 공격 시대의 방어선
- 잠금 파일이 도구의 표식이다 — 레포의 기존 선택을 따르고, 섞지 않는다
참고 자료
섹션 제목: “참고 자료”- pnpm 공급망 방어 — 설치 스크립트 차단과 pnpm 11의 기본 보호
- pnpm
allowBuilds— 허용·차단 형식과 기본 동작