pnpm 완벽 가이드: 빠르고 효율적인 패키지 매니저

pnpm 완벽 가이드: 빠르고 효율적인 패키지 매니저

이 글의 핵심

pnpm은 디스크 공간을 크게 절약하고 설치 속도가 빠른 차세대 패키지 매니저입니다. Content-addressable 스토리지로 중복 패키지를 제거하고, Symlink로 효율적으로 관리합니다. 엄격한 node_modules 구조로 Phantom Dependencies 문제를 원천적으로 방지합니다.

pnpm이란?

pnpm(performant npm)은 2017년 출시된 차세대 패키지 매니저로, npm과 yarn의 단점을 해결하고 성능과 효율성을 극대화한 도구입니다.

핵심 특징

  1. 디스크 공간 절약

    • Content-addressable 스토리지 사용
    • 중복 패키지 제거
    • 평균 70% 디스크 공간 절약
  2. 빠른 설치 속도

    • npm 대비 3배 빠른 설치
    • yarn v1 대비 2배 빠름
    • 병렬 다운로드 및 설치
  3. 엄격한 의존성 관리

    • Phantom Dependencies 방지
    • Flat node_modules 구조 회피
    • 명시적 의존성만 접근 가능
  4. 모노레포 최적화

    • 워크스페이스 기본 지원
    • 효율적인 패키지 간 링크
    • 필터링 및 선택적 실행

npm vs yarn vs pnpm 비교

항목npmyarn v1yarn v3 (Berry)pnpm
설치 속도느림보통빠름매우 빠름
디스크 사용량많음많음PnP 시 적음매우 적음
node_modules 구조FlatFlatPnP (옵션)Nested + Symlink
Phantom Dependencies발생발생방지방지
모노레포 지원제한적WorkspacesWorkspacesWorkspaces
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
// 프로젝트에서 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

트러블슈팅

# 개발자 모드 활성화 또는
# 관리자 권한으로 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

관련 리소스

다음 단계

  1. Turborepo 통합

    • 캐싱 및 병렬 실행 최적화
    • Remote caching 구성
  2. Nx 통합

    • 의존성 그래프 시각화
    • Affected 명령어 활용
  3. Changesets 도입

    • 버전 관리 자동화
    • Changelog 생성

pnpm은 빠르고 효율적이며 안정적인 패키지 매니저입니다. 디스크 공간 절약과 설치 속도 향상으로 개발 생산성을 크게 높일 수 있습니다.