본문 바로가기
카테고리 없음

pnpm 에 대하여

by SuldenLion 2026. 5. 25.
반응형

pnpm — 빠르고, 효율적이며, 엄격한 패키지 매니저

프론트엔드 개발 · 프론트엔드 도구 시리즈 #3
"performant npm"의 약자. 글로벌 저장소와 하드링크로 디스크를 혁명적으로 절약하고, 엄격한 의존성 격리로 幽靈 의존성을 차단합니다. 세 패키지 매니저 중 가장 빠르게 성장 중인 pnpm의 모든 것을 파헤칩니다.


1. pnpm이란 무엇인가

pnpm(performant npm) 은 2017년 Zoltan Kochan이 만든 패키지 매니저입니다. npm과 yarn의 핵심 문제인 중복된 파일 저장느슨한 의존성 격리를 근본적으로 해결합니다.

pnpm의 두 가지 핵심 혁신:

  1. 글로벌 콘텐츠 저장소(Content-Addressable Storage): 같은 패키지는 전체 시스템에 딱 한 번만 저장
  2. 심볼릭 링크 기반 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을 쓸 줄 안다면 당장 전환해도 학습 비용이 거의 없습니다.


 

반응형

댓글