본문으로 건너뛰기
Previous
Next
pnpm 완벽 가이드: 빠르고 효율적인 패키지 매니저

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을 시작하는 방법은 여러 가지가 있지만, Standalone script를 사용하는 것이 가장 권장됩니다. 이 방법은 pnpm을 자체적으로 관리하여 Node.js 버전 변경 시에도 영향을 받지 않습니다.

pnpm 설치

npm으로 pnpm을 설치할 수도 있지만, 이는 역설적으로 npm에 의존하게 됩니다. Standalone script는 pnpm을 독립적으로 설치하여 시스템 경로에 추가합니다. 설치 후에는 npm과 거의 동일한 명령어를 사용할 수 있습니다.

macOS와 Linux에서는 curl 스크립트를, Windows에서는 PowerShell 스크립트를 사용합니다. 설치가 완료되면 새 터미널을 열어 pnpm --version으로 확인하세요.

# 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 install 실행 시 중앙 스토어(~/.pnpm-store)가 자동으로 생성됩니다. 이 디렉터리에 모든 패키지가 저장되며, 각 프로젝트는 심볼릭 링크로 참조합니다.

기본 설정

# 전역 스토어 위치 확인
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 [email protected]
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...
                ├── [email protected] (실제 파일)
                ├── [email protected]
                └── [email protected]

프로젝트 A/node_modules/
├── .pnpm/
│   ├── [email protected]/ → ~/.pnpm-store/.../abc123
│   └── [email protected]/ → ~/.pnpm-store/.../def456
└── lodash → .pnpm/[email protected]/node_modules/lodash

프로젝트 B/node_modules/
├── .pnpm/
│   └── [email protected]/ → ~/.pnpm-store/.../abc123 (재사용!)
└── lodash → .pnpm/[email protected]/node_modules/lodash
// 프로젝트에서 lodash 사용
import _ from 'lodash';

// Node.js 모듈 해석 과정
// 1. node_modules/lodash 확인 (symlink)
// 2. .pnpm/[email protected]/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 [email protected]

# → /tmp/abc123/lodash 에 압축 해제됨
# 파일 수정 후...

# 패치 생성
pnpm patch-commit /tmp/abc123/lodash

# → patches/[email protected] 생성
// package.json
{
  "pnpm": {
    "patchedDependencies": {
      "[email protected]": "patches/[email protected]"
    }
  }
}

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

관련 리소스

실전 사례: 모노레포 마이그레이션

대규모 프로젝트를 npm에서 pnpm으로 마이그레이션하면 CI 빌드 시간을 절반으로 줄이고 디스크 사용량을 70% 이상 절약할 수 있습니다. 실제 모노레포 마이그레이션 과정을 살펴보겠습니다.

npm → pnpm 마이그레이션 단계

마이그레이션은 생각보다 간단합니다. 기존 package-lock.json을 pnpm-lock.yaml로 변환하고, npm 명령어를 pnpm으로 교체하면 됩니다. 대부분의 경우 추가 설정 없이 동작하며, 문제가 발생하면 .npmrc로 쉽게 해결할 수 있습니다.

# 1. 기존 node_modules 및 lock 파일 제거
rm -rf node_modules packages/*/node_modules
rm package-lock.json

# 2. pnpm 설치
npm install -g pnpm

# 3. pnpm으로 설치 (package-lock.json이 있으면 자동 변환)
pnpm install

# 4. 스크립트 확인
pnpm run build
pnpm run test

# 5. CI 설정 업데이트
# .github/workflows/ci.yml
# - run: npm ci
# + run: pnpm install --frozen-lockfile

Turborepo와 통합하여 빌드 속도 10배 향상

Turborepo는 pnpm 워크스페이스와 완벽하게 통합되어 증분 빌드와 원격 캐싱을 제공합니다. 한 번 빌드된 패키지는 코드가 변경되지 않는 한 다시 빌드하지 않습니다.

# Turborepo 설치
pnpm add -Dw turbo

# turbo.json 설정
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "cache": false
    },
    "lint": {
      "cache": false
    }
  }
}

# 변경된 패키지만 빌드 (10배 빠름)
pnpm turbo run build --filter="[origin/main]"

# 전체 빌드 (캐시 활용)
pnpm turbo run build

Turborepo는 의존성 그래프를 분석하여 병렬 실행 가능한 작업을 동시에 실행합니다. CI에서 원격 캐시를 활성화하면 팀원이 빌드한 결과를 공유하여 중복 빌드를 제거할 수 있습니다.

실제 성능 개선 사례

# 마이그레이션 전 (npm)
- 의존성 설치: 3분 20초
- 전체 빌드: 8분 40초
- 디스크 사용: 12GB (10개 프로젝트)
- CI 총 시간: 15분

# 마이그레이션 후 (pnpm + Turborepo)
- 의존성 설치: 45초 (4.4배 빠름)
- 전체 빌드: 1분 30초 (5.7배 빠름, 캐시 미사용)
- 증분 빌드: 20초 (26배 빠름, 캐시 사용)
- 디스크 사용: 3.5GB (70% 절약)
- CI 총 시간: 2분 30초 (6배 빠름)

주의사항

1. Phantom Dependencies 주의

pnpm은 엄격한 의존성 구조로 Phantom Dependencies를 방지합니다. 이는 장점이지만, 기존 npm 프로젝트에서 암묵적으로 사용하던 패키지는 에러가 발생할 수 있습니다. 이런 경우 package.json에 명시적으로 추가하세요.

일부 도구나 빌드 시스템은 Symlink를 지원하지 않을 수 있습니다. Windows에서는 개발자 모드를 활성화하거나 관리자 권한이 필요할 수 있습니다.

3. Hoisting 제어

특정 패키지를 루트로 끌어올리려면 .npmrc에서 hoist-pattern을 설정하세요. 기본적으로는 엄격한 구조를 유지하는 것이 좋습니다.

다음 단계

  1. Turborepo 통합

    • pnpm 워크스페이스와 Turborepo를 함께 사용하면 모노레포의 빌드 속도를 10배 이상 향상시킬 수 있습니다. 원격 캐싱으로 팀 전체의 생산성을 높이세요.
  2. Nx 통합

    • Nx는 의존성 그래프 시각화와 affected 명령어로 변경된 부분만 선택적으로 빌드/테스트할 수 있습니다. 대규모 모노레포에 적합합니다.
  3. Changesets 도입

    • Changesets으로 모노레포의 버전 관리와 릴리스를 자동화할 수 있습니다. 각 변경사항을 추적하고 semantic versioning을 자동으로 적용합니다.

pnpm은 빠르고 효율적이며 안정적인 패키지 매니저입니다. 디스크 공간 절약과 설치 속도 향상으로 개발 생산성을 크게 높일 수 있으며, 특히 모노레포 환경에서 그 진가를 발휘합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. pnpm을 활용한 효율적인 패키지 관리 완벽 가이드. npm/yarn 대비 장점, 설치부터 워크스페이스, 모노레포 구성, CI/CD 통합, 디스크 공간 절약 원리까지 실전 예제로 학습합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

pnpm, Package Manager, Node.js, npm, yarn, Monorepo, Workspace, DevOps 등으로 검색하시면 이 글이 도움이 됩니다.