pnpm 완벽 가이드: 빠르고 효율적인 패키지 매니저
이 글의 핵심
pnpm은 디스크 공간을 크게 절약하고 설치 속도가 빠른 차세대 패키지 매니저입니다. Content-addressable 스토리지로 중복 패키지를 제거하고, Symlink로 효율적으로 관리합니다. 엄격한 node_modules 구조로 Phantom Dependencies 문제를 원천적으로 방지합니다.
pnpm이란?
pnpm(performant npm)은 2017년 출시된 차세대 패키지 매니저로, npm과 yarn의 단점을 해결하고 성능과 효율성을 극대화한 도구입니다.
핵심 특징
-
디스크 공간 절약
- Content-addressable 스토리지 사용
- 중복 패키지 제거
- 평균 70% 디스크 공간 절약
-
빠른 설치 속도
- npm 대비 3배 빠른 설치
- yarn v1 대비 2배 빠름
- 병렬 다운로드 및 설치
-
엄격한 의존성 관리
- Phantom Dependencies 방지
- Flat node_modules 구조 회피
- 명시적 의존성만 접근 가능
-
모노레포 최적화
- 워크스페이스 기본 지원
- 효율적인 패키지 간 링크
- 필터링 및 선택적 실행
npm vs yarn vs pnpm 비교
| 항목 | npm | yarn v1 | yarn v3 (Berry) | pnpm |
|---|---|---|---|---|
| 설치 속도 | 느림 | 보통 | 빠름 | 매우 빠름 |
| 디스크 사용량 | 많음 | 많음 | PnP 시 적음 | 매우 적음 |
| node_modules 구조 | Flat | Flat | PnP (옵션) | Nested + Symlink |
| Phantom Dependencies | 발생 | 발생 | 방지 | 방지 |
| 모노레포 지원 | 제한적 | Workspaces | Workspaces | Workspaces |
| CI 캐싱 | 보통 | 좋음 | 좋음 | 매우 좋음 |
| 학습 곡선 | 낮음 | 낮음 | 높음 | 낮음 |
벤치마크 (100개 패키지 기준)
npm install: 45초
yarn install: 35초
pnpm install: 15초 (캐시 있을 시 5초)
디스크 사용량:
npm: 1.2GB
yarn: 1.1GB
pnpm: 400MB
설치 및 초기 설정
pnpm 설치
# npm으로 설치
npm install -g pnpm
# Homebrew (macOS)
brew install pnpm
# Chocolatey (Windows)
choco install pnpm
# Scoop (Windows)
scoop install pnpm
# Standalone script (권장)
curl -fsSL https://get.pnpm.io/install.sh | sh -
# PowerShell (Windows)
iwr https://get.pnpm.io/install.ps1 -useb | iex
# 버전 확인
pnpm --version
기본 설정
# 전역 스토어 위치 확인
pnpm store path
# 캐시 정보
pnpm store status
# Node.js 버전 관리 활성화
pnpm env use --global lts
# 자동 완성 설치 (bash)
pnpm completion bash > ~/.pnpm-completion.sh
echo 'source ~/.pnpm-completion.sh' >> ~/.bashrc
# 자동 완성 설치 (zsh)
pnpm completion zsh > "${fpath[1]}/_pnpm"
기본 사용법
패키지 관리
# 패키지 설치
pnpm install # 또는 pnpm i
# 패키지 추가
pnpm add lodash # dependencies
pnpm add -D typescript # devDependencies
pnpm add -O react # optionalDependencies
pnpm add -P express # peerDependencies (package.json만)
# 글로벌 패키지 설치
pnpm add -g typescript
# 특정 버전 설치
pnpm add lodash@4.17.21
pnpm add react@^18.0.0
# 패키지 제거
pnpm remove lodash # 또는 pnpm rm, pnpm uninstall
# 패키지 업데이트
pnpm update # 모든 패키지
pnpm update lodash # 특정 패키지
pnpm update --latest # 최신 버전으로 (--save 옵션 필요 시)
# 패키지 정보
pnpm list # 설치된 패키지 목록
pnpm list --depth 0 # 최상위만
pnpm why lodash # 왜 설치되었는지
pnpm outdated # 업데이트 가능한 패키지
npm 명령어 호환성
# npm → pnpm 변환
npm install → pnpm install
npm install lodash → pnpm add lodash
npm uninstall lodash → pnpm remove lodash
npm run dev → pnpm dev
npm test → pnpm test
pnpm의 동작 원리
Content-Addressable 스토리지
~/.pnpm-store/
└── v3/
└── files/
└── 00/
└── abc123...
├── lodash@4.17.21 (실제 파일)
├── react@18.2.0
└── typescript@5.0.0
프로젝트 A/node_modules/
├── .pnpm/
│ ├── lodash@4.17.21/ → ~/.pnpm-store/.../abc123
│ └── react@18.2.0/ → ~/.pnpm-store/.../def456
└── lodash → .pnpm/lodash@4.17.21/node_modules/lodash
프로젝트 B/node_modules/
├── .pnpm/
│ └── lodash@4.17.21/ → ~/.pnpm-store/.../abc123 (재사용!)
└── lodash → .pnpm/lodash@4.17.21/node_modules/lodash
Symlink 구조
// 프로젝트에서 lodash 사용
import _ from 'lodash';
// Node.js 모듈 해석 과정
// 1. node_modules/lodash 확인 (symlink)
// 2. .pnpm/lodash@4.17.21/node_modules/lodash로 이동
// 3. 실제 파일은 ~/.pnpm-store에서 hardlink로 참조
Phantom Dependencies 방지
// npm/yarn (Flat 구조)
node_modules/
├── express/ # 직접 설치
├── lodash/ # express의 의존성
└── body-parser/ # express의 의존성
// package.json에 없어도 사용 가능 (문제!)
import _ from 'lodash'; // ✅ 작동 (Phantom Dependency)
// pnpm (Nested 구조)
node_modules/
├── express/ → .pnpm/express@4.18.0/node_modules/express
└── .pnpm/
├── express@4.18.0/
│ └── node_modules/
│ ├── express/
│ ├── lodash/ # express만 접근 가능
│ └── body-parser/
// package.json에 없으면 에러 (정확!)
import _ from 'lodash'; // ❌ 에러: Cannot find module 'lodash'
워크스페이스 (Monorepo)
프로젝트 구조
my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── packages/
│ ├── app/
│ │ ├── package.json
│ │ └── src/
│ ├── ui/
│ │ ├── package.json
│ │ └── src/
│ └── utils/
│ ├── package.json
│ └── src/
└── pnpm-lock.yaml
워크스페이스 설정
# pnpm-workspace.yaml
packages:
# 모든 packages 하위 디렉터리
- 'packages/*'
# 특정 디렉터리
- 'apps/*'
- 'libs/*'
# 제외
- '!**/test/**'
// 루트 package.json
{
"name": "my-monorepo",
"private": true,
"scripts": {
"dev": "pnpm -r --parallel dev",
"build": "pnpm -r build",
"test": "pnpm -r test"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
워크스페이스 명령어
# 모든 워크스페이스에서 실행
pnpm -r install # --recursive
pnpm -r build
pnpm -r test
# 병렬 실행
pnpm -r --parallel dev
# 특정 워크스페이스만
pnpm --filter app install
pnpm --filter ui build
pnpm --filter utils test
# 필터 패턴
pnpm --filter "./packages/*" build
pnpm --filter "!app" test # app 제외
pnpm --filter "...app" build # app과 의존성들
pnpm --filter "app..." build # app에 의존하는 것들
# 변경된 패키지만
pnpm --filter="[origin/main]" test
# 특정 워크스페이스에서 명령어 실행
cd packages/app
pnpm install lodash
# 또는 루트에서
pnpm --filter app add lodash
워크스페이스 간 의존성
// packages/app/package.json
{
"name": "app",
"dependencies": {
"ui": "workspace:*", // 항상 워크스페이스 버전
"utils": "workspace:^1.0.0" // 버전 범위 지정
}
}
// packages/ui/package.json
{
"name": "ui",
"version": "1.2.0",
"dependencies": {
"utils": "workspace:~1.0.0"
}
}
실전 모노레포 구성
# 프로젝트 생성
mkdir my-monorepo && cd my-monorepo
pnpm init
# 워크스페이스 설정
cat > pnpm-workspace.yaml << EOF
packages:
- 'packages/*'
EOF
# 패키지 생성
mkdir -p packages/{app,ui,utils}
# 각 패키지 초기화
cd packages/app && pnpm init && cd ../..
cd packages/ui && pnpm init && cd ../..
cd packages/utils && pnpm init && cd ../..
# 공통 의존성 설치 (루트)
pnpm add -D -w typescript @types/node
# 특정 패키지에 의존성 추가
pnpm --filter app add react react-dom
pnpm --filter ui add react
pnpm --filter utils add lodash
# 워크스페이스 의존성 추가
pnpm --filter app add ui --workspace
pnpm --filter app add utils --workspace
고급 기능
패키지 오버라이드
// package.json
{
"pnpm": {
"overrides": {
"foo": "^1.0.0",
"bar@^2.1.0": "3.0.0",
"baz>qux": "^1.0.0"
}
}
}
Peer Dependency 자동 설치
// .npmrc
auto-install-peers=true
strict-peer-dependencies=false
공유 워크스페이스 Lockfile
# shared-workspace-lockfile 옵션
echo "shared-workspace-lockfile=true" >> .npmrc
# 워크스페이스마다 독립적인 lockfile
echo "shared-workspace-lockfile=false" >> .npmrc
선택적 의존성 설치
# 프로덕션 의존성만
pnpm install --prod
# 옵션널 의존성 제외
pnpm install --no-optional
# Dev 의존성 제외
pnpm install --production
패치 패키지
# 패키지 수정을 위한 준비
pnpm patch lodash@4.17.21
# → /tmp/abc123/lodash 에 압축 해제됨
# 파일 수정 후...
# 패치 생성
pnpm patch-commit /tmp/abc123/lodash
# → patches/lodash@4.17.21.patch 생성
// package.json
{
"pnpm": {
"patchedDependencies": {
"lodash@4.17.21": "patches/lodash@4.17.21.patch"
}
}
}
pnpm 설정 (.npmrc)
# 프로젝트 루트에 .npmrc 파일 생성
# 스토어 위치 커스터마이징
store-dir=~/.pnpm-store
# 호이스팅 설정
hoist=true
hoist-pattern[]=*eslint*
hoist-pattern[]=*prettier*
# 심볼릭 링크 대신 하드 링크 사용
link-workspace-packages=true
# Peer Dependencies 엄격 모드
strict-peer-dependencies=true
# 레지스트리 설정
registry=https://registry.npmjs.org/
@mycompany:registry=https://npm.mycompany.com/
# 네트워크 설정
network-concurrency=16
fetch-retries=5
fetch-timeout=60000
# 로그 레벨
loglevel=info
# 진행 표시
progress=true
# 자동 설치
auto-install-peers=true
# 공개 호이스팅 패턴
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
CI/CD 통합
GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm test
- name: Build
run: pnpm build
GitLab CI
# .gitlab-ci.yml
image: node:18
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
before_script:
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm config set store-dir .pnpm-store
- pnpm install --frozen-lockfile
stages:
- test
- build
test:
stage: test
script:
- pnpm test
build:
stage: build
script:
- pnpm build
artifacts:
paths:
- dist/
Docker 통합
# Dockerfile
FROM node:18-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
FROM base AS dependencies
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod
FROM base AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM base AS production
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
CMD ["node", "dist/index.js"]
Jenkins Pipeline
// Jenkinsfile
pipeline {
agent any
environment {
PNPM_HOME = "$HOME/.local/share/pnpm"
PATH = "$PNPM_HOME:$PATH"
}
stages {
stage('Setup') {
steps {
sh 'npm install -g pnpm'
sh 'pnpm install --frozen-lockfile'
}
}
stage('Test') {
steps {
sh 'pnpm test'
}
}
stage('Build') {
steps {
sh 'pnpm build'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'pnpm deploy'
}
}
}
}
Turborepo와 함께 사용
# Turborepo 설치
pnpm add -D -w turbo
# turbo.json 설정
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false
}
}
}
// package.json
{
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"dev": "turbo run dev --parallel"
}
}
# Turborepo로 실행
pnpm build # 캐시 활용, 의존성 순서대로
pnpm test # 변경된 패키지만 테스트
pnpm dev # 모든 패키지 병렬 실행
마이그레이션 가이드
npm에서 pnpm으로
# 1. pnpm 설치
npm install -g pnpm
# 2. package-lock.json 변환
pnpm import
# 3. node_modules 제거
rm -rf node_modules
# 4. pnpm으로 설치
pnpm install
# 5. 기존 lockfile 제거
rm package-lock.json
yarn에서 pnpm으로
# 1. pnpm 설치
npm install -g pnpm
# 2. yarn.lock 변환
pnpm import
# 3. node_modules 제거
rm -rf node_modules
# 4. pnpm으로 설치
pnpm install
# 5. 기존 lockfile 제거
rm yarn.lock
# 6. .yarnrc 제거 (있다면)
rm .yarnrc
점진적 마이그레이션
# 1. 일부 프로젝트만 pnpm 사용
cd project-a
pnpm install
# 2. 다른 프로젝트는 기존 매니저 유지
cd ../project-b
npm install
# 3. 모든 프로젝트에서 테스트 후 전환
베스트 프랙티스
1. .npmrc 설정 최적화
# 공유 lockfile 사용 (모노레포)
shared-workspace-lockfile=true
# 디스크 공간 최적화
store-dir=~/.pnpm-store
# 성능 최적화
network-concurrency=16
fetch-retries=3
# 보안
strict-peer-dependencies=true
auto-install-peers=false
2. 스크립트 구성
{
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "pnpm -r --parallel dev",
"build": "pnpm -r --filter='...{./packages/app}' build",
"test": "pnpm -r --workspace-concurrency=4 test",
"lint": "pnpm -r lint",
"clean": "pnpm -r exec -- rm -rf dist node_modules"
}
}
3. 의존성 버전 관리
# 정확한 버전 사용
pnpm add -E react
# 워크스페이스 프로토콜
"dependencies": {
"ui": "workspace:*"
}
# Renovate/Dependabot 설정
# .renovaterc.json
{
"extends": ["config:base"],
"rangeStrategy": "bump",
"packageRules": [
{
"matchPackagePatterns": ["*"],
"rangeStrategy": "pin"
}
]
}
4. 캐시 관리
# 스토어 정리 (사용하지 않는 패키지)
pnpm store prune
# 특정 패키지 제거
pnpm store remove lodash
# 캐시 상태 확인
pnpm store status
트러블슈팅
Symlink 권한 문제 (Windows)
# 개발자 모드 활성화 또는
# 관리자 권한으로 PowerShell 실행
# Symlink 생성 권한 부여
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" -Name "AllowDevelopmentWithoutDevLicense" -Value 1
Phantom Dependency 해결
# 에러 발생 시
Error: Cannot find module 'lodash'
# 해결: package.json에 명시적으로 추가
pnpm add lodash
Hoisting 문제
# .npmrc에 추가
hoist-pattern[]=*
public-hoist-pattern[]=*types*
Peer Dependency 경고
# 자동 설치 활성화
auto-install-peers=true
# 또는 수동 설치
pnpm add react --save-peer
성능 최적화 팁
1. 캐시 활용
# GitHub Actions
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'pnpm'
2. Frozen Lockfile
# CI에서 항상 사용
pnpm install --frozen-lockfile
# 로컬 개발
pnpm install
3. 선택적 빌드
# 변경된 패키지만
pnpm --filter="[origin/main]" build
# 특정 패키지와 의존성
pnpm --filter="...app" build
4. 병렬 실행 제한
# 동시 실행 제한 (메모리 부족 방지)
pnpm -r --workspace-concurrency=2 build
관련 리소스
다음 단계
-
Turborepo 통합
- 캐싱 및 병렬 실행 최적화
- Remote caching 구성
-
Nx 통합
- 의존성 그래프 시각화
- Affected 명령어 활용
-
Changesets 도입
- 버전 관리 자동화
- Changelog 생성
pnpm은 빠르고 효율적이며 안정적인 패키지 매니저입니다. 디스크 공간 절약과 설치 속도 향상으로 개발 생산성을 크게 높일 수 있습니다.