pnpm — 빠르고, 효율적이며, 엄격한 패키지 매니저
프론트엔드 개발 · 프론트엔드 도구 시리즈 #3
"performant npm"의 약자. 글로벌 저장소와 하드링크로 디스크를 혁명적으로 절약하고, 엄격한 의존성 격리로 幽靈 의존성을 차단합니다. 세 패키지 매니저 중 가장 빠르게 성장 중인 pnpm의 모든 것을 파헤칩니다.
1. pnpm이란 무엇인가
pnpm(performant npm) 은 2017년 Zoltan Kochan이 만든 패키지 매니저입니다. npm과 yarn의 핵심 문제인 중복된 파일 저장과 느슨한 의존성 격리를 근본적으로 해결합니다.
pnpm의 두 가지 핵심 혁신:
- 글로벌 콘텐츠 저장소(Content-Addressable Storage): 같은 패키지는 전체 시스템에 딱 한 번만 저장
- 심볼릭 링크 기반 node_modules: 선언된 의존성만 접근 가능한 엄격한 구조
# 세 개의 프로젝트가 같은 버전의 lodash를 사용할 때
npm/yarn:
project-a/node_modules/lodash → 3.8MB 복사본
project-b/node_modules/lodash → 3.8MB 복사본
project-c/node_modules/lodash → 3.8MB 복사본
총: 11.4MB
pnpm:
~/.pnpm-store/lodash@4.17.21 → 3.8MB (한 번만 저장)
project-a/node_modules/lodash → 하드링크 (0바이트 추가)
project-b/node_modules/lodash → 하드링크 (0바이트 추가)
project-c/node_modules/lodash → 하드링크 (0바이트 추가)
총: 3.8MB
2. 설치 방법
Corepack (권장)
# Node.js 16.10+ 내장 Corepack 사용
corepack enable
corepack prepare pnpm@latest --activate
pnpm --version
독립 설치
# curl (macOS/Linux)
curl -fsSL https://get.pnpm.io/install.sh | sh -
# PowerShell (Windows)
iwr https://get.pnpm.io/install.ps1 -useb | iex
# npm으로 설치 (역설적이지만 가능)
npm install -g pnpm
# Homebrew (macOS)
brew install pnpm
packageManager 필드 (권장)
{
"name": "my-app",
"packageManager": "pnpm@9.0.0"
}
3. 핵심 동작 원리 — 글로벌 저장소와 하드링크
글로벌 콘텐츠 저장소
pnpm은 패키지를 다운로드할 때 운영체제의 글로벌 저장소에 저장합니다.
# 저장소 위치 확인
pnpm store path
# /Users/username/.local/share/pnpm/store/v3 (macOS)
# C:\Users\username\AppData\Local\pnpm\store\v3 (Windows)
# 저장소 상태 확인
pnpm store status
# 사용하지 않는 패키지 정리
pnpm store prune
저장소는 파일 콘텐츠의 해시값을 키로 사용하는 CAS(Content-Addressable Storage) 구조입니다.
~/.pnpm-store/v3/files/
00/
abc123def456... ← 파일 내용의 SHA-512 해시
01/
...
같은 내용의 파일은 버전이나 패키지 이름에 상관없이 딱 한 번만 저장됩니다.
하드링크(Hard Link)
저장소의 파일들은 node_modules에 하드링크로 연결됩니다.
~/.pnpm-store/files/00/abc123... (inode 5001)
↕ 하드링크 (같은 inode)
my-project/node_modules/.pnpm/lodash@4.17.21/node_modules/lodash/lodash.js
하드링크는 실제 파일의 별칭입니다. 파일이 복사되지 않으므로 추가 디스크 공간을 쓰지 않습니다.
다른 드라이브나 파티션에 프로젝트가 있다면 하드링크 대신 복사본을 사용합니다. 그래도 같은 드라이브 내 다른 프로젝트들 사이에서는 공유됩니다.
node_modules 구조
pnpm의 node_modules는 다른 패키지 매니저와 다른 독특한 구조입니다.
node_modules/
├── .pnpm/ ← 실제 패키지 파일 (가상 저장소)
│ ├── lodash@4.17.21/
│ │ └── node_modules/
│ │ └── lodash/ ← 글로벌 저장소에 하드링크
│ └── react@18.2.0/
│ └── node_modules/
│ ├── react/ ← 글로벌 저장소에 하드링크
│ └── loose-envify@1.4.0/ ← react의 의존성
├── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash/ (symlink)
└── react -> .pnpm/react@18.2.0/node_modules/react/ (symlink)
node_modules 최상위에는 package.json에 직접 선언한 패키지의 심볼릭 링크만 존재합니다. 모든 실제 파일은 .pnpm 디렉토리에 있고, 각 패키지의 의존성은 그 패키지 전용 폴더에 격리됩니다.
4. 엄격한 의존성 격리 — 유령 의존성 차단
npm의 호이스팅(Hoisting) 문제
npm과 yarn(classic)은 의존성 트리를 평탄화(flatten)합니다.
package.json: { "dependencies": { "express": "^4.18.0" } }
npm install 후 node_modules/:
express/
accepts/ ← express의 의존성인데 최상위에 있음
body-parser/ ← express의 의존성
debug/ ← express의 의존성
...
문제는 accepts, body-parser 같은 패키지를 내 코드에서 직접 require할 수 있다는 것입니다.
// package.json에 선언하지 않았지만 동작함 (유령 의존성!)
const accepts = require('accepts');
이는 위험합니다. express가 accepts 의존성을 제거하거나 버전을 바꾸면 내 코드가 예고 없이 깨집니다.
pnpm의 엄격한 격리
pnpm에서는 같은 코드가 오류를 발생시킵니다.
const accepts = require('accepts');
// Error: Cannot find module 'accepts'
// 'accepts'는 내 package.json에 없습니다!
package.json에 선언한 패키지만 node_modules 최상위(심볼릭 링크)에 존재하므로, 유령 의존성은 구조적으로 불가능합니다.
shamefully-hoist — 호환성 옵션
일부 레거시 도구는 유령 의존성에 의존합니다. 이를 위한 탈출구가 있습니다.
# .npmrc
shamefully-hoist=true # npm 방식처럼 모든 패키지를 최상위로 호이스팅
하지만 이 옵션은 의존성 격리의 이점을 모두 포기하므로, 꼭 필요한 경우에만 사용하고 장기적으로는 정식 의존성 선언으로 해결해야 합니다.
5. 핵심 CLI 명령어
pnpm의 CLI는 npm과 매우 유사합니다. 대부분의 명령어를 npm → pnpm으로 바꾸기만 하면 됩니다.
설치
# 의존성 설치 (package.json 기준)
pnpm install
pnpm i # 단축형
# 패키지 추가
pnpm add react react-dom
pnpm add -D typescript eslint # devDependencies
pnpm add -O sharp # optionalDependencies
pnpm add react@18.0.0 # 정확한 버전
# 전역 설치
pnpm add -g serve
pnpm add --global create-next-app
제거 및 업데이트
# 제거
pnpm remove axios
pnpm rm -D eslint # devDependencies에서 제거
# 업데이트
pnpm update # 모든 패키지를 SemVer 범위 내 최신으로
pnpm update react # 특정 패키지
pnpm update --latest # 모든 패키지를 최신으로 (범위 무시)
# 인터랙티브 업데이트
pnpm update --interactive
pnpm up -i # 선택 UI로 업데이트
# 오래된 패키지 확인
pnpm outdated
실행
# 스크립트 실행
pnpm run dev
pnpm dev # run 생략 가능
pnpm test
pnpm build
# 패키지 실행 (npx 대응)
pnpm dlx create-next-app my-app
pnpm dlx prettier --write .
pnpm exec jest # 로컬 바이너리 실행
정보 및 진단
# 설치된 패키지 목록
pnpm list
pnpm ls --depth=0 # 직접 의존성만
# 의존성 트리
pnpm why react # react가 왜 설치되었는지 추적
pnpm list --depth=3 # 3단계까지 트리 출력
# 패키지 정보
pnpm info react
pnpm info react version
# 전역 패키지
pnpm list -g
6. .npmrc 설정
pnpm은 npm과 동일한 .npmrc 파일을 사용합니다.
# .npmrc
# 의존성 격리 설정
shamefully-hoist=false # 기본값: 유령 의존성 차단
strict-peer-dependencies=false # peer dep 불일치를 오류 대신 경고
# 저장소 설정
store-dir=~/.pnpm-store # 글로벌 저장소 위치 (기본값)
# 레지스트리
registry=https://registry.npmjs.org/
@mycompany:registry=https://npm.mycompany.com/
//npm.mycompany.com/:_authToken=${NPM_TOKEN}
# 자동화
auto-install-peers=true # peer dependencies 자동 설치
resolution-mode=highest # 의존성 버전 선택 전략
pnpm-workspace.yaml
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- '!**/__tests__/**' # 테스트 디렉토리 제외
7. pnpm Workspaces — 모노레포
pnpm의 워크스페이스는 모노레포 관리에서 가장 강력한 도구 중 하나입니다.
프로젝트 구조
my-monorepo/
├── pnpm-workspace.yaml
├── package.json (root)
├── pnpm-lock.yaml (루트에서 통합 관리)
├── .npmrc
├── apps/
│ ├── web/
│ │ └── package.json { "name": "@myapp/web" }
│ └── docs/
│ └── package.json { "name": "@myapp/docs" }
└── packages/
├── ui/
│ └── package.json { "name": "@myapp/ui" }
└── config/
└── package.json { "name": "@myapp/config" }
// root package.json
{
"name": "my-monorepo",
"private": true,
"scripts": {
"dev": "pnpm -r --parallel run dev",
"build": "pnpm -r run build",
"test": "pnpm -r run test",
"lint": "pnpm -r run lint"
}
}
워크스페이스 명령어
# 특정 패키지에 명령 실행
pnpm --filter @myapp/web dev
pnpm -F @myapp/web build
# 패턴 매칭
pnpm --filter "./apps/**" build # apps/ 아래 모든 패키지
pnpm --filter "...@myapp/ui" # @myapp/ui와 이를 의존하는 모든 패키지
# 재귀 실행 (모든 패키지)
pnpm -r run build
pnpm --recursive run test
# 병렬 실행
pnpm -r --parallel run dev
# 의존성 순서에 따라 (topological)
pnpm -r --stream run build # 의존 관계 순서 보장
로컬 패키지 참조
// apps/web/package.json
{
"name": "@myapp/web",
"dependencies": {
"@myapp/ui": "workspace:*", // 버전 무관, 항상 로컬
"@myapp/config": "workspace:^1" // workspace 버전 범위
}
}
# 로컬 패키지 연결
pnpm add @myapp/ui --workspace
Catalog — 버전 카탈로그 (pnpm v9+)
여러 패키지에서 동일한 의존성 버전을 중앙에서 관리합니다.
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
catalog:
react: ^18.2.0
typescript: ^5.3.0
eslint: ^8.57.0
// packages/ui/package.json
{
"dependencies": {
"react": "catalog:" // catalog에 정의된 버전 사용
},
"devDependencies": {
"typescript": "catalog:"
}
}
8. 성능 벤치마크
pnpm의 공식 벤치마크에 따른 설치 시간 비교입니다.
상황: React 기반 앱, 의존성 1,000개 이상
npm yarn pnpm
캐시 없음: 46.1s 43.3s 22.1s
캐시 있음 (최초): 14.2s 6.4s 3.1s
캐시 있음 (재설치): 7.8s 1.7s 1.6s
lockfile만 있음: 16.3s 8.3s 4.2s
디스크 절약 효과
# 현재 저장소에서 절약된 공간 확인
pnpm store status
# Packages in the store: 2,847
# Files in the store: 142,831
# Saved: 4.2 GB
9. npm, yarn과의 마이그레이션
npm → pnpm
# 1. 기존 node_modules 제거
rm -rf node_modules
# 2. package-lock.json을 pnpm-lock.yaml로 변환
pnpm import # package-lock.json 또는 yarn.lock 자동 감지
# 3. 설치
pnpm install
# 4. package-lock.json 삭제 (이제 pnpm-lock.yaml이 담당)
rm package-lock.json
yarn → pnpm
# yarn.lock → pnpm-lock.yaml 변환
pnpm import
# yarn.lock 삭제
rm yarn.lock
# 설치
pnpm install
CI/CD 설정 (GitHub Actions)
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm' # pnpm 캐시 자동 처리
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm test
10. 결론
pnpm은 2024년 현재 세 패키지 매니저 중 가장 빠른 성장세를 보이고 있습니다. Vue.js, Vite, Turborepo, Nx 등 주요 오픈소스 프로젝트들이 pnpm으로 전환했습니다.
pnpm을 선택해야 하는 상황:
- 디스크 공간 효율이 중요한 환경 (CI, 공유 서버)
- 모노레포 프로젝트 (pnpm workspace + catalog)
- 유령 의존성을 구조적으로 차단하고 싶을 때
- 대규모 팀에서 의존성 문제 디버깅 비용을 줄이고 싶을 때
주의할 상황:
- Yarn PnP를 사용하는 팀과의 협업
- 극히 일부 레거시 패키지의 호이스팅 의존성 (shamefully-hoist로 해결 가능)
선택 기준 권장
| 빠른 설치 + 디스크 절약 | pnpm ✓ |
| 모노레포 (Turborepo 연동) | pnpm ✓ |
| PnP, Zero-Installs 필요 | Yarn Berry |
| 생태계 호환성 최우선 | npm |
한 줄 요약: pnpm은 npm API를 유지하면서 디스크 효율과 의존성 정확성을 극대화한 실용적인 선택입니다. 이미 npm을 쓸 줄 안다면 당장 전환해도 학습 비용이 거의 없습니다.
댓글