PNPM vs Yarn Berry: 의존성 관리 전략과 성능 비교 분석
Content-addressable storage와 Plug'n'Play 방식을 통한 차세대 패키지 매니저 선택 가이드
본 글은 PNPM 공식 문서와 릴리스 노트를 바탕으로 작성되었습니다.
PNPM v10에서 가장 중요한 변경사항은 자동 호이스팅의 완전한 제거입니다. 이전 버전에서 개발자 경험(DX)을 위해 ESLint, Prettier 등의 도구를 자동으로 호이스팅하던 정책이 보안 강화를 위해 폐지되었습니다.
이 글에서는 호이스팅 메커니즘의 변화와 Symlink 기반 의존성 격리 전략, 그리고 프로젝트 마이그레이션 방법을 살펴봅니다.
PNPM은 npm, Yarn과 달리 Symlink 기반의 엄격한 의존성 격리를 구현합니다.
npm/Yarn의 Flat 구조:
node_modules/
├── react/
├── react-dom/
├── lodash/ # 직접 의존성 아니어도 접근 가능
├── axios/ # Phantom dependency
└── ...
PNPM의 Symlink 구조:
node_modules/
├── .pnpm/
│ ├── react@18.0.0/
│ │ └── node_modules/
│ │ └── react/
│ ├── react-dom@18.0.0/
│ │ └── node_modules/
│ │ ├── react-dom/
│ │ └── react/ → ../../react@18.0.0/node_modules/react
│ └── lodash@4.17.21/
│ └── node_modules/
│ └── lodash/
├── react/ → .pnpm/react@18.0.0/node_modules/react
└── react-dom/ → .pnpm/react-dom@18.0.0/node_modules/react-dom
1. 단일 저장소 (Single Source of Truth)
모든 패키지는 .pnpm/ 디렉토리에만 저장
→ 디스크 공간 절약 (같은 버전은 한 번만 저장)
2. Symlink를 통한 접근 제어
package.json에 명시된 의존성만 node_modules/ 루트에 symlink 생성
→ 명시되지 않은 의존성은 require() 불가능
3. Phantom Dependency 방지
// package.json에 lodash 없음
import _ from 'lodash'; // [주의] Error: Cannot find module 'lodash'
// npm/Yarn에서는 동작함 (다른 패키지가 설치했으면)
// PNPM에서는 명시적으로 설치 필요PNPM v9 이하에서는 엄격한 격리 원칙에도 불구하고, 개발 도구에 대한 예외를 두었습니다.
이유:
node_modules 루트에서 도구를 찾음@types/* 패키지를 루트에서 탐색자동 호이스팅된 패키지:
node_modules/
├── .pnpm/ # 실제 저장소
├── eslint/ # 자동 호이스팅
├── prettier/ # 자동 호이스팅
├── @types/node/ # 자동 호이스팅
├── @types/react/ # 자동 호이스팅
└── typescript/ # 자동 호이스팅v9의 내부 규칙:
// PNPM v9 내부 로직 (simplified)
const autoHoistPatterns = [
'*eslint*',
'*prettier*',
'@types/*',
'typescript'
];실제 동작:
# package.json
{
"devDependencies": {
"eslint": "^8.0.0",
"eslint-plugin-react": "^7.0.0"
}
}
# pnpm install 후 node_modules 구조
node_modules/
├── .pnpm/
│ ├── eslint@8.0.0/
│ └── eslint-plugin-react@7.0.0/
├── eslint/ → .pnpm/eslint@8.0.0/node_modules/eslint
└── eslint-plugin-react/ → .pnpm/eslint-plugin-react@7.0.0/...
# VSCode ESLint 확장이 node_modules/eslint를 찾음 [권장]PNPM v10은 모든 자동 호이스팅을 제거했습니다.
Before (v9):
pnpm install
node_modules/
├── .pnpm/
├── eslint/ → .pnpm/eslint@8.0.0/... # 자동 호이스팅
└── prettier/ → .pnpm/prettier@3.0.0/... # 자동 호이스팅After (v10):
pnpm install
node_modules/
└── .pnpm/
├── eslint@8.0.0/
└── prettier@3.0.0/
# 루트에 아무것도 없음!
# VSCode ESLint 확장: [주의] Cannot find module 'eslint'이제 모든 호이스팅은 package.json에 명시적으로 선언해야 합니다.
{
"name": "my-app",
"pnpm": {
"public-hoist-pattern": [
"*eslint*",
"*prettier*",
"@types/*",
"typescript"
]
}
}설정 후:
pnpm install
node_modules/
├── .pnpm/
├── eslint/ → .pnpm/eslint@8.0.0/... # 명시적 호이스팅
├── @types/
│ └── node/ → ../.pnpm/@types+node@20.0.0/...
└── typescript/ → .pnpm/typescript@5.0.0/...보안 강화:
// 공격 시나리오 (v9 이하)
// 악의적인 패키지가 auto-hoist된 eslint를 악용
// 1. 공격자가 배포한 패키지
// node_modules/malicious-pkg/index.js
const eslint = require('eslint'); // auto-hoist로 접근 가능!
// eslint 설정을 변조하거나 정보 탈취v10에서는 원천 차단:
// node_modules/malicious-pkg/index.js
const eslint = require('eslint'); // [주의] Error: Cannot find module
// package.json에 eslint가 명시되지 않으면 접근 불가# package.json의 packageManager 필드 업데이트
{
"packageManager": "pnpm@10.0.0"
}
# 또는 직접 설치
npm install -g pnpm@latestv10으로 업그레이드 후 pnpm install을 실행하면:
pnpm install
# IDE에서 오류 발생
Error: Cannot find module 'eslint'
Error: Cannot find module 'prettier'
Error: Cannot find module '@types/node'원인:
node_modules/eslint를 찾을 수 없음@types/*를 찾을 수 없음IDE 플러그인이 필요로 하는 패키지:
ESLint 확장 → eslint, eslint-plugin-*
Prettier 확장 → prettier
TypeScript → @types/*, typescript
Stylelint 확장 → stylelint, stylelint-*
프로젝트 도구:
# package.json 확인
grep -E "(eslint|prettier|typescript|@types)" package.json
# 출력 예시:
"eslint": "^8.0.0",
"eslint-plugin-react": "^7.0.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
"@types/react": "^18.0.0"기본 템플릿:
{
"pnpm": {
"public-hoist-pattern": [
// ESLint 관련
"*eslint*",
"eslint-*",
// Prettier
"*prettier*",
// TypeScript
"@types/*",
"typescript",
// 기타 IDE 도구
"*stylelint*"
]
}
}모노레포의 경우 (루트 package.json):
{
"pnpm": {
"public-hoist-pattern": [
"*eslint*",
"*prettier*",
"@types/*",
"typescript",
// 빌드 도구
"*webpack*",
"*vite*",
"*rollup*"
]
}
}# node_modules 삭제 후 재설치
rm -rf node_modules
pnpm install
# 호이스팅 확인
ls -la node_modules/
# 출력 예시:
eslint -> .pnpm/eslint@8.0.0/node_modules/eslint
prettier -> .pnpm/prettier@3.0.0/node_modules/prettier
@types/
node -> ../.pnpm/@types+node@20.0.0/node_modules/@types/node1. 문제 발생:
# v10으로 업그레이드 후
pnpm install
pnpm dev
# VSCode에서 오류
ESLint: Cannot find module 'eslint'
TypeScript: Cannot find type definitions for 'node'2. 해결:
// package.json
{
"name": "my-next-app",
"dependencies": {
"next": "^14.0.0",
"react": "^18.0.0"
},
"devDependencies": {
"eslint": "^8.0.0",
"eslint-config-next": "^14.0.0",
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
"@types/react": "^18.0.0"
},
"pnpm": {
"public-hoist-pattern": [
"*eslint*",
"@types/*",
"typescript"
]
}
}3. 재설치:
rm -rf node_modules
pnpm install
# 확인
pnpm dev
# [권장] 정상 동작증상:
VSCode ESLint: Cannot find module 'eslint'
해결:
node_modules 확인:
ls node_modules/eslint
# 심볼릭 링크가 있어야 함증상:
{
"pnpm": {
"public-hoist-pattern": ["*"] // 모든 패키지 호이스팅
}
}문제점:
해결:
{
"pnpm": {
"public-hoist-pattern": [
// 필요한 것만 명시적으로
"*eslint*",
"@types/*"
]
}
}증상:
packages/app-a → eslint@8.0.0
packages/app-b → eslint@9.0.0
→ 어떤 버전이 호이스팅될지 불명확
해결:
# .npmrc 또는 .pnpmfile.cjs에서 버전 통일 강제
# 또는 각 워크스페이스마다 명시적 설정package.json 대신 .npmrc에 설정할 수도 있습니다:
# .npmrc
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=@types/*
public-hoist-pattern[]=typescript장점:
.gitignore에 추가)단점:
절대 사용 금지:
{
"pnpm": {
"shamefully-hoist": true // [주의] v10의 보안 강화 무효화
}
}이유:
대신 사용:
{
"pnpm": {
"public-hoist-pattern": ["필요한", "패키지만"]
}
}Before (v9):
// 악의적 패키지가 auto-hoist된 패키지 악용 가능
const eslint = require('eslint'); // 자동으로 접근 가능After (v10):
// package.json에 명시되지 않으면 접근 불가
const eslint = require('eslint'); // [주의] Error팀원이 프로젝트를 처음 볼 때:
{
"pnpm": {
"public-hoist-pattern": [
"*eslint*", // ESLint 사용 중
"@types/*" // TypeScript 사용 중
]
}
}→ 한눈에 프로젝트에서 사용하는 도구를 파악 가능
모노레포에서:
packages/app-a → lodash@4.17.0
packages/app-b → lodash@4.17.21
v9: auto-hoist로 어떤 버전이 올라올지 불명확
v10: 명시적 설정으로 제어 가능
PNPM v10의 자동 호이스팅 제거는 단기적으로 설정 추가라는 불편함을 가져오지만, 장기적으로는 더 안전하고 예측 가능한 의존성 관리를 제공합니다. 특히 대규모 프로젝트나 모노레포 환경에서 명시적 호이스팅은 보안과 유지보수성을 크게 향상시킵니다.
핵심 권장사항:
public-hoist-pattern에 명시shamefully-hoist는 절대 사용하지 말 것package.json에서 중앙 관리참고 자료:
Content-addressable storage와 Plug'n'Play 방식을 통한 차세대 패키지 매니저 선택 가이드