PNPM vs Yarn Berry: 의존성 관리 전략과 성능 비교 분석
Content-addressable storage와 Plug'n'Play 방식을 통한 차세대 패키지 매니저 선택 가이드
Yarn은 Facebook(현 Meta)이 npm의 한계를 극복하기 위해 2016년 출시한 JavaScript 패키지 매니저입니다. 많은 사람들이 Yarn을 "빠른 npm"으로 생각하지만, **Yarn의 진정한 목적은 속도가 아니라 프로젝트 관리의 안정성과 재현성(reproducibility)**입니다.
2020년 Yarn 2.0(Yarn Berry) 출시와 함께 Plug'n'Play(PnP) 방식이 도입되면서, Yarn은 node_modules를 제거하고 의존성 관리를 근본적으로 재설계했습니다. 이 글에서는 Yarn Classic과 Berry의 차이, PnP 메커니즘, 실전 활용 전략을 다룹니다.
1. 비결정적 설치 (Non-deterministic Installs)
# 개발자 A의 머신
npm install express
# node_modules/express@4.17.1
# 개발자 B의 머신 (다음 날)
npm install express
# node_modules/express@4.17.2 (새 패치 버전 릴리스)package.json에 ^4.17.0으로 명시되어 있으면, npm은 설치 시점에 따라 다른 버전을 설치할 수 있었습니다.
2. 느린 설치 속도
# npm (2016)
npm install lodash
# 1. 네트워크 요청
# 2. 압축 파일 다운로드
# 3. 압축 해제
# 4. node_modules 복사
# 평균 5-10초3. 보안 취약점
npm은 패키지 설치 시 보안 검증이 부족했고, 체크섬 검증도 선택적이었습니다.
1. yarn.lock: 결정적 의존성
# yarn.lock
express@^4.17.0:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==Yarn은 프로젝트에 yarn.lock 파일을 생성하여, 모든 머신에서 동일한 버전을 설치하도록 보장했습니다.
2. 병렬 다운로드 및 캐싱
# Yarn Classic
yarn add lodash react axios
# 3개 패키지를 병렬로 다운로드
# 글로벌 캐시에 저장: ~/.yarn/cache
# 평균 2-3초 (npm 대비 3배 빠름)3. 체크섬 검증
Yarn은 모든 패키지의 무결성을 yarn.lock의 integrity 필드로 검증합니다.
| 특징 | Yarn Classic (v1) | Yarn Berry (v2+) |
|---|---|---|
| node_modules | 사용 (hoisted) | PnP 모드: 제거node_modules 모드: 선택 가능 |
| 의존성 저장 | 디렉토리 구조 | ZIP 파일 (.yarn/cache) |
| 의존성 조회 | Node.js 기본 해석기 | .pnp.cjs 매핑 파일 |
| 버전 관리 | 전역 설치 필요 | 프로젝트별 버전 (yarn set version) |
| 디스크 사용량 | 높음 (중복 저장) | 낮음 (ZIP 압축 + 전역 캐시) |
| 설치 속도 | 빠름 | 매우 빠름 (Zero-Install 가능) |
| 호환성 | 높음 | 낮음 (PnP 비호환 패키지 존재) |
my-project/
├── node_modules/
│ ├── express/ # Hoisted
│ ├── react/ # Hoisted
│ └── lodash/ # express의 의존성이지만 루트에 Hoisted
├── package.json
└── yarn.lockHoisting 문제:
lodash가 package.json에 없어도 import lodash from 'lodash' 가능 (Phantom Dependency)my-project/
├── .yarn/
│ ├── cache/
│ │ ├── express-npm-4.18.2-e0b5ea00d5-a7e37eafeb.zip
│ │ ├── react-npm-18.2.0-1eae08fee2-88e38092da.zip
│ │ └── lodash-npm-4.17.21-6382451519-eb835a2e51.zip
│ ├── releases/
│ │ └── yarn-4.5.3.cjs # 프로젝트별 Yarn 실행 파일
│ └── unplugged/ # Native 모듈용
├── .pnp.cjs # 의존성 맵
├── .pnp.loader.mjs # ESM 로더
├── .yarnrc.yml # 설정 파일
├── package.json
└── yarn.lock1. I/O 오버헤드
# node_modules 방식 (Yarn Classic/npm)
$ ls -la node_modules | wc -l
157832 # 15만 개 이상의 파일
# 설치 시간: 압축 해제 + 디스크 쓰기
$ time npm install
real 0m32.451s2. 비효율적인 의존성 조회
// require('express')를 실행하면 Node.js는:
// 1. ./node_modules/express 확인
// 2. ../node_modules/express 확인
// 3. ../../node_modules/express 확인
// ... (최상위 디렉토리까지 반복)평균적으로 require() 호출당 여러 번의 파일 시스템 조회가 발생합니다.
1. .pnp.cjs: 의존성 맵
// .pnp.cjs (간소화된 예시)
const packageRegistry = new Map([
["express", [
["npm:4.18.2", {
packageLocation: "./.yarn/cache/express-npm-4.18.2-a7e37eafeb.zip/node_modules/express/",
packageDependencies: new Map([
["body-parser", "npm:1.20.1"],
["cookie", "npm:0.5.0"],
]),
}],
]],
["react", [
["npm:18.2.0", {
packageLocation: "./.yarn/cache/react-npm-18.2.0-88e38092da.zip/node_modules/react/",
packageDependencies: new Map([
["loose-envify", "npm:1.4.0"],
]),
}],
]],
]);
// Yarn이 제공하는 커스텀 resolver
function require(moduleName) {
const pkg = packageRegistry.get(moduleName);
return loadFromZip(pkg.packageLocation);
}2. 동작 원리
// 기존 방식 (node_modules)
require('express');
// Node.js가 파일 시스템을 순회하며 찾음 (느림)
// PnP 방식
require('express');
// 1. .pnp.cjs의 packageRegistry에서 O(1) 조회
// 2. ZIP 파일 경로 직접 접근
// 3. ZIP에서 모듈 로드 (느림 → 해결: .yarn/unplugged)3. 성능 비교
# 벤치마크: Express 프로젝트 (53개 의존성)
npm (v8): 32.4초
Yarn Classic (v1.22): 14.2초
Yarn Berry (node_modules): 12.8초
Yarn Berry (PnP): 2.3초 # 14배 빠름
Yarn Berry (PnP + cache): 0.4초 # 80배 빠름1. Phantom Dependency 방지
// package.json에 lodash 미선언
// Yarn Classic/npm: 동작 (잘못됨)
import _ from 'lodash'; // OK (express가 가져옴)
// Yarn Berry PnP: 에러 (올바름)
import _ from 'lodash';
// Error: 'lodash' is not declared in package.json2. 디스크 공간 절약
# 10개 프로젝트에서 동일한 의존성 사용 시
npm/Yarn Classic: 10 × 300MB = 3GB
Yarn Berry PnP: 1 × 180MB (ZIP) = 180MB # 17배 절약3. Zero-Install
# .yarn/cache를 Git에 커밋
git add .yarn/cache
git commit -m "Add dependencies to repository"
# 다른 개발자가 클론 후
git clone https://github.com/user/project
cd project
yarn # 설치 없이 바로 사용 가능 (0.1초)1. 네이티브 모듈 비호환
# node-gyp로 빌드되는 패키지
node-sass, bcrypt, sqlite3, canvas 등
# 해결: unplugged 모드
# .yarnrc.yml
packageExtensions:
"node-sass@*":
unplugged: true2. 동적 require() 미지원
// 동작하지 않음
const moduleName = 'express';
require(moduleName); // Error: Cannot resolve dynamic require
// 해결: package.json에 명시적 선언
{
"dependencies": {
"express": "^4.18.0"
}
}3. IDE 지원 부족 (과거)
VS Code, WebStorm 등은 2021년 이후 PnP를 공식 지원합니다.
// .vscode/settings.json
{
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"eslint.nodePath": ".yarn/sdks"
}
// SDK 생성
yarn dlx @yarnpkg/sdks vscodeNode.js 16.9+ 버전에 포함된 패키지 매니저의 버전 관리 도구입니다.
# Node.js에 포함된 Yarn 확인
which yarn
# /usr/local/bin/yarn (Corepack shim)
# Corepack 활성화
corepack enable
# 프로젝트별 Yarn 버전 설정
# package.json
{
"packageManager": "yarn@4.5.3"
}
# Yarn 명령 실행 시 자동으로 4.5.3 다운로드 및 사용
yarn install프로젝트별 버전 설정:
# Yarn Berry로 업그레이드
yarn set version berry
# 또는 특정 버전
yarn set version 4.5.3
# .yarn/releases/yarn-4.5.3.cjs 생성
# .yarnrc.yml 생성
yarnPath: .yarn/releases/yarn-4.5.3.cjs
# 다시 Classic으로 되돌리기
yarn set version classic프로젝트 A (Yarn Berry):
cd project-a
cat .yarnrc.yml
# yarnPath: .yarn/releases/yarn-4.5.3.cjs
yarn --version # 4.5.3프로젝트 B (Yarn Classic):
cd project-b
cat package.json
# "packageManager": "yarn@1.22.19"
yarn --version # 1.22.191. 프로젝트 준비
# 1단계: Yarn Berry 설치
yarn set version berry
# 2단계: PnP 모드 활성화 (.yarnrc.yml 자동 생성)
# nodeLinker: pnp # 기본값
# 3단계: 의존성 재설치
rm -rf node_modules
yarn install
# 4단계: IDE SDK 설정
yarn dlx @yarnpkg/sdks vscode2. TypeScript 설정
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "node",
"types": ["node"]
},
"ts-node": {
"transpileOnly": true,
"require": ["./.pnp.cjs"] // PnP 로더
}
}3. ESLint 설정
# ESLint PnP 플러그인 설치
yarn add -D eslint-plugin-pnp
# .eslintrc.js
module.exports = {
extends: ['plugin:pnp/recommended'],
};4. 호환성 문제 해결
# .yarnrc.yml
# 비호환 패키지를 node_modules 모드로 설치
packageExtensions:
"react-native@*":
peerDependencies:
react: "*"
"node-sass@*":
unplugged: true
# 또는 전역적으로 node_modules 모드 사용
nodeLinker: node-modules장점:
yarn install 생략 → 빌드 시간 단축설정:
# .yarnrc.yml
enableGlobalCache: false # 프로젝트별 캐시 사용
# .gitignore
.yarn/*
!.yarn/cache # 캐시는 커밋
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions고려사항:
# 대용량 프로젝트의 캐시 크기
du -sh .yarn/cache
# 180MB (소규모)
# 2.5GB (모노레포)
# Git LFS 사용 권장 (대규모 프로젝트)
git lfs install
git lfs track ".yarn/cache/**/*.zip"디렉토리 구조:
my-monorepo/
├── packages/
│ ├── web/
│ │ ├── package.json # "name": "@myapp/web"
│ │ └── src/
│ ├── mobile/
│ │ ├── package.json # "name": "@myapp/mobile"
│ │ └── src/
│ └── shared/
│ ├── package.json # "name": "@myapp/shared"
│ └── src/
├── package.json
└── yarn.lock루트 package.json:
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "yarn workspaces foreach -A run build",
"test": "yarn workspaces foreach -A run test"
}
}패키지 간 의존성:
// packages/web/package.json
{
"name": "@myapp/web",
"dependencies": {
"@myapp/shared": "workspace:*" // 같은 모노레포의 패키지
}
}Workspace 명령어:
# 특정 workspace에 패키지 추가
yarn workspace @myapp/web add react
# 모든 workspace에서 스크립트 실행
yarn workspaces foreach run build
# 의존성 그래프 시각화
yarn workspaces list --json테스트 환경: React 프로젝트 (134개 의존성, macOS M1)
| 패키지 매니저 | Cold Install | Warm Install (캐시) |
|---|---|---|
| npm v8 | 32.4s | 18.2s |
| Yarn Classic | 14.2s | 8.7s |
| pnpm v7 | 11.3s | 4.2s |
| Yarn Berry (node_modules) | 12.8s | 5.1s |
| Yarn Berry (PnP) | 2.3s | 0.4s |
| Yarn Berry (Zero-Install) | - | 0.1s |
10개 프로젝트 (각 134개 의존성):
npm: 3.2GB (100%)
Yarn Classic: 3.1GB (96.8%)
pnpm: 680MB (21.2%) # 하드링크
Yarn Berry: 520MB (16.2%) # ZIP 압축Turborepo + Yarn Berry Workspace (50개 패키지):
# Cold build
Yarn Classic: 145s
Yarn Berry: 92s # 1.6배 빠름
# Warm build (캐시)
Yarn Classic: 78s
Yarn Berry: 23s # 3.4배 빠름증상:
Error: Cannot find module 'some-package'해결:
# .yarnrc.yml - 특정 패키지만 unplugged
packageExtensions:
"some-package@*":
unplugged: true
# 또는 node_modules 모드로 전환
nodeLinker: node-modules증상:
Cannot find module '@/components/Button' or its corresponding type declarations.해결:
# SDK 재생성
yarn dlx @yarnpkg/sdks vscode
# VS Code 재시작
# Cmd+Shift+P → "TypeScript: Select TypeScript Version" → "Use Workspace Version"증상:
Module not found: Can't resolve 'react/jsx-runtime'해결:
// package.json
{
"packageManager": "yarn@4.5.3",
"dependencies": {
"react": "^18.2.0"
}
}# .yarnrc.yml
nodeLinker: node-modules # Next.js는 PnP 비호환 (2024년 기준)GitHub Actions:
# .github/workflows/ci.yml
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn' # Yarn Berry 자동 인식
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: yarn install --immutable # yarn.lock 수정 금지Zero-Install 사용 시:
- name: Install dependencies
run: |
# .yarn/cache가 Git에 있으므로 즉시 사용 가능
yarn --version[권장] 모노레포 프로젝트
[권장] CI/CD 시간 최적화가 중요한 경우
[권장] 디스크 공간이 제한적인 환경
[권장] 의존성 보안이 중요한 경우
[주의] React Native 프로젝트
[주의] 레거시 프로젝트
[주의] 네이티브 모듈 의존성이 많은 경우
Yarn은 단순히 "빠른 npm"이 아닙니다. Yarn Classic은 결정적 의존성 관리를, Yarn Berry는 PnP를 통한 근본적인 패러다임 전환을 목표로 합니다.
핵심 요약:
마이그레이션 전략:
# 1단계: 기존 프로젝트에서 테스트
yarn set version berry
yarn install
npm run build # 정상 동작 확인
# 2단계: 호환성 문제 해결
# .yarnrc.yml에서 nodeLinker: node-modules 사용
# 3단계: PnP 전환 (선택)
# nodeLinker: pnp
# IDE SDK 설정
# 팀원 교육
# 4단계: Zero-Install (선택)
# enableGlobalCache: false
# .yarn/cache 커밋Yarn Berry는 초기 학습 곡선이 있지만, 장기적으로 프로젝트 관리의 안정성과 성능을 크게 향상시킵니다. 특히 모노레포 환경에서는 Yarn Berry + Workspace의 조합이 현재 가장 강력한 도구입니다.
Content-addressable storage와 Plug'n'Play 방식을 통한 차세대 패키지 매니저 선택 가이드