⚡ pnpm CLI & pnpm-lock.yaml — 핵심 명령어 완전 정복

2026년 3월 20일 수정됨

📋 개요

add/update/remove/why/list/dlx/patch/env 까지 pnpm 의 강력한 CLI 와 pnpm-lock.yaml 구조를 시니어 수준으로 이해한다

03. CLI & pnpm-lock.yaml

📋 목차

  1. pnpm-lock.yaml 구조 해부
  2. 총정리
  3. 마무리 퀴즈
  4. 영철이의 퇴근 일기
  5. [더 알아보기](#-더 알아보기)

📌 이 문서를 읽기 전에

⏱️ 예상 읽기 시간: 약 28분(전체) / 핵심 파트만: 14분

🗺️ 이 문서의 흐름

[설치/추가/제거 CLI] → [탐색 명령어] → [dlx/patch/env] → [pnpm-lock.yaml 구조]

🎯 이 문서를 다 읽으면 할 수 있는 것

  • pnpm add, pnpm update, pnpm remove 의 옵션을 능숙하게 쓸 수 있다
  • pnpm why, pnpm list 로 의존성 출처를 추적할 수 있다
  • pnpm patch 로 외부 패키지 코드를 안전하게 수정할 수 있다
  • pnpm-lock.yaml 구조를 읽고 버전 충돌 원인을 파악할 수 있다

🗺️ 이 문서의 배경 세계관: '영수네 커뮤니티'

에러가 너무 많은 영철이와 pnpm why를 알려주는 영호
  • 🐣 영철 ( 신입 ): "영호 님, 저 오늘 @tanstack/react-query 를 5버전으로 올렸더니 타입 에러가 10개 넘게 났어요. 다시 4버전으로 내리고 싶은데 어떻게 하는지 모르겠어요. 그리고 왜 node_modulesdeepmerge 가 있는지 아무리 봐도 이유를 모르겠어요 — 저 직접 설치한 적 없는데요. 근데 이거 없애면 뭔가 망가질 것 같아서 손도 못 대고 있어요."
  • 🦁 영호 ( 리드 ): "영철 님, pnpm why deepmerge 한 번 쳐보세요. 어디서 가져온 건지 의존성 트리로 보여줄 거예요. 버전 내리는 건 pnpm add @tanstack/react-query@4 하거나, pnpm-lock.yaml 에서 직접 버전 확인하고 pnpm update @tanstack/react-query@4 --force 해보세요. pnpm-lock.yaml 이 버전 충돌의 진실이 담긴 파일이에요 — 이걸 읽을 줄 알아야 시니어예요."

🤔 왜 알아야 하는가

npm 과 pnpm 의 명령어는 80% 가 비슷해 보이지만, pnpm 만의 고유한 강력한 기능 을 모르면 절반만 쓰는 것이다.

pnpm why 로 간접 의존성 출처를 추적하고, pnpm patch 로 외부 라이브러리 버그를 직접 패치하며, pnpm env 로 nvm 없이 Node.js 버전을 전환하고, pnpm-lock.yaml 을 읽어 버전 충돌의 원인을 파악하는 능력 — 이것이 pnpm 을 제대로 쓰는 시니어와 그냥 쓰는 주니어를 가른다.


📦 설치 관련 핵심 명령어

pnpm install (pnpm i)

# 기본 설치
pnpm install
 
# CI 환경: lock 파일 기준으로 완전 재현 (lock 파일이 변경되면 에러)
pnpm install --frozen-lockfile
 
# 프로덕션 의존성만 설치 (devDependencies 제외)
pnpm install --prod
 
# 오프라인 설치 (글로벌 스토어에 있는 것만)
pnpm install --offline
 
# lock 파일만 업데이트 (node_modules 는 건들지 않음)
pnpm install --lockfile-only

pnpm add

# 일반 의존성
pnpm add axios
 
# 특정 버전
pnpm add axios@1.6.7
 
# devDependency
pnpm add -D eslint typescript @types/node
 
# peerDependency
pnpm add --save-peer react
 
# optionalDependency
pnpm add --save-optional @next/bundle-analyzer
 
# 전역 설치
pnpm add -g serve
 
# github 레포에서 직접 설치
pnpm add github:user/repo#branch

pnpm update (pnpm up)

# package.json 의 SemVer 범위 내에서 모든 패키지 업데이트
pnpm update
 
# 특정 패키지만
pnpm update axios
 
# SemVer 범위 무시하고 최신 버전으로 (package.json 도 변경)
pnpm update --latest
 
# 특정 패키지를 인터랙티브하게 선택해서 업데이트
pnpm update --interactive
pnpm update -i
 
# 🦁 유용한 패턴: @tanstack 스코프 전체 업데이트
pnpm update "@tanstack/*"

pnpm remove (pnpm rm)

# 패키지 제거
pnpm remove axios
 
# devDependency 에서 제거
pnpm remove -D eslint
 
# 전역 패키지 제거
pnpm remove -g serve

🔍 패키지 탐색 명령어 — why, list, outdated

pnpm why — 의존성 출처 추적

가장 강력한 pnpm 전용 명령어 중 하나. 특정 패키지가 왜 설치되어 있는지, 어떤 패키지의 의존성인지 트리로 보여준다.

pnpm why deepmerge
 
# 출력 예시
youngsu-community@1.0.0 /home/user/projects/youngsu-community
└─┬ @tanstack/react-query@5.17.15
  └─┬ @tanstack/query-core@5.17.15
    └── deepmerge@4.3.1
# 특정 패키지가 어디서 왔는지 상세 트리
pnpm why -r deepmerge  # workspace 전체에서 검색 (모노레포 유용)
 
# JSON 출력
pnpm why --json deepmerge

pnpm list (pnpm ls)

# 직접 의존성 목록
pnpm list
 
# 전체 의존성 트리 (깊이 제한)
pnpm list --depth=2
 
# 특정 패키지 검색
pnpm list react
 
# 전역 패키지 목록
pnpm list -g
 
# JSON 출력
pnpm list --json

pnpm outdated

# 업데이트 가능한 패키지 목록
pnpm outdated
 
# 출력 예시
┌──────────────────────────┬─────────┬────────────┬─────────────────────────────────────────────────────────────┐
 Package Current Latest Details
├──────────────────────────┼─────────┼────────────┼─────────────────────────────────────────────────────────────┤
 @tanstack/react-query 5.17.15 5.28.0 https://tanstack.com/query
 axios (dev)              │ 1.6.5   │ 1.6.7      │ https://axios-http.com                                      │
└──────────────────────────┴─────────┴────────────┴─────────────────────────────────────────────────────────────┘

⚡ pnpm dlx — npx 보다 나은 이유

pnpm dlx 는 npm 의 npx 와 같은 역할 — 패키지를 설치하지 않고 임시로 실행한다. 하지만 더 나은 점이 있다.

# npx 와 동일한 역할
pnpm dlx create-next-app@latest youngsu-community
 
# shadcn/ui 컴포넌트 추가
pnpm dlx shadcn@latest add button
 
# prisma 초기화
pnpm dlx prisma init

npm의 npx 와의 차이:

항목npxpnpm dlx
패키지 캐시매번 새로 다운로드 가능글로벌 스토어 캐시 활용
속도느릴 수 있음캐시된 경우 빠름
보안인터넷에서 즉시 실행동일 (주의 필요)
# 🦁 create-next-app 으로 프로젝트 생성 시
# pnpm 이 설치된 환경에서 --use-pnpm 플래그 사용
pnpm dlx create-next-app@latest my-app --use-pnpm

🩹 pnpm patch — 의존성 수술

외부 라이브러리에 버그가 있지만 수정 PR 이 머지되기까지 기다릴 수 없을 때, pnpm patch 로 로컬에서 패키지를 직접 수정하고 패치 파일로 관리할 수 있다.

패치 생성 과정

# 1단계: 패치할 패키지를 임시 디렉토리로 추출
pnpm patch some-package@1.2.3
 
# 출력:
# You can now edit the package at:
#   /tmp/some-package@1.2.3/
 
# 2단계: 임시 디렉토리에서 파일 수정
# (에디터로 /tmp/some-package@1.2.3/src/index.js 수정)
 
# 3단계: 패치 파일 생성 & 등록
pnpm patch-commit /tmp/some-package@1.2.3

패치 결과물

# pnpm-workspace.yaml (또는 package.json 에 자동 추가됨)
patchedDependencies:
  some-package@1.2.3: patches/some-package@1.2.3.patch
# patches/some-package@1.2.3.patch (자동 생성)
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -10,7 +10,7 @@ function brokenFunction() {
-  return undefined;  // 🐛 버그: undefined 반환
+  return [];         // ✅ 수정: 빈 배열 반환
 }

🦁 영호 리드의 실전 적용 사례

# 영수네 커뮤니티에서 실제로 발생한 상황:
# next-auth 에서 특정 엣지케이스 버그 발견, 공식 수정까지 2주 예상
 
pnpm patch next-auth@4.24.5
# → /tmp/next-auth@4.24.5/ 에서 버그 있는 함수 수정
pnpm patch-commit /tmp/next-auth@4.24.5
 
# 이후 설치할 때마다 patches/next-auth@4.24.5.patch 가 자동 적용됨
# → 공식 패치 릴리즈 후 pnpm patch-remove next-auth@4.24.5 로 제거

overrides 와의 차이:

항목overridespatch
용도간접 의존성 버전 강제패키지 코드 직접 수정
코드 변경불가가능
적용 범위버전 범위 선택정확한 버전
git 추적package.json 변경.patch 파일

🖥️ pnpm env — Node.js 버전 관리

pnpm 은 nvm, fnm 없이도 Node.js 버전을 직접 관리할 수 있다.

# 특정 버전 설치 및 사용
pnpm env use --global 20
 
# LTS 버전 설치
pnpm env use --global lts
 
# 설치된 Node.js 버전 목록
pnpm env list
 
# 원격 사용 가능한 버전 목록
pnpm env list --remote
 
# 특정 버전 제거
pnpm env remove --global 18

.nvmrc 와 연동:

# .nvmrc 에 적힌 버전 자동 사용 (pnpm env 는 .nvmrc 를 직접 읽지 않음)
# package.json 의 engines 필드로 버전 제한하고 engine-strict=true 사용 권장
// package.json
{
  "engines": {
    "node": ">=20.0.0",
    "pnpm": ">=9.0.0"
  }
}
# .npmrc
engine-strict=true
# → 잘못된 Node.js 버전으로 pnpm install 시 즉시 에러

📄 pnpm-lock.yaml 구조 해부

기본 구조

# pnpm-lock.yaml
# 💡 이 파일은 우리가 설치한 모든 패키지의 "정확한 지문(버전, 해시값)"을 영수증처럼 보관합니다.
lockfileVersion: '9.0'
 
settings:
  autoInstallPeers: true
  excludeLinksFromLockfile: false
 
# 🦁 1. importers 구역: "누가(프로젝트/앱), 어떤 버전을 요청했는가?"
importers:
  # 프로젝트 루트(.) 에서 요청한 내용
  .:
    dependencies:
      react:
        specifier: ^18.2.0 # package.json에 적힌 원래 버전 (예: ~버전대 언저리)
        version: 18.2.0    # pnpm이 "이번엔 이 버전으로 고정할게!" 하고 낙장불입한 정확한 버전
      next:
        specifier: ^14.2.0
        # 💡 피어(peer) 의존성이 얽히면 이렇게 이름이 길어집니다. (이게 pnpm의 격리 마법!)
        version: 14.2.0(@babel/core@7.24.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
    devDependencies:
      typescript:
        specifier: ^5.3.3
        version: 5.3.3
 
# 🦁 2. packages 구역: "설치된 패키지들의 기술적인 신상정보"
packages:
  react@18.2.0:
    resolution:
      # 💡 다운로드 받은 파일이 중간에 변조되지 않았는지 확인하는 지문(해시값)입니다.
      integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fgQ==
    engines: {node: '>=0.10.0'}
 
  next@14.2.0(@babel/core@7.24.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
    resolution:
      integrity: sha512-...
    engines: {node: '>=18.17.0'}
    peerDependencies:
      '@babel/core': ^7
      react: ^18.2.1
      react-dom: ^18.2.1
    peerDependenciesMeta:
      '@babel/core':
        optional: true # 설치 안 해도 그만인 선택적 피어
 
# 🦁 3. snapshots 구역: "최종적으로 묶여진 가족사진 (의존성 트리)"
snapshots:
  react@18.2.0:
    dependencies:
      loose-envify: 1.4.0 # react가 동작하려면 이 친구가 무조건 필요해!
 
  next@14.2.0(@babel/core@7.24.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
    dependencies:
      react: 18.2.0
      react-dom: 18.2.0(react@18.2.0)

핵심 필드 해석

필드위치의미
lockfileVersion최상단pnpm lock 파일 포맷 버전
importers루트프로젝트별 선언된 의존성 (specifier: 요청 버전, version: 실제 해석 버전)
packages루트각 패키지의 메타정보 (resolution, engines, peerDependencies)
snapshots루트실제 설치 스냅샷 (피어 포함한 완전한 의존성 트리)
integritypackagesSHA-512 해시 (무결성 검증)
specifierimporterspackage.json 에 적힌 원래 버전 범위
versionimporterslock 이 실제로 고정한 버전 (피어 포함 시 긴 문자열)

npm ci vs pnpm install --frozen-lockfile

# npm 의 CI 명령어
npm ci
 
# pnpm 의 동일한 역할
pnpm install --frozen-lockfile
# → lock 파일이 package.json 과 일치하지 않으면 에러 발생
# → 새 패키지를 설치하거나 lock 파일을 수정하지 않음
# → CI 환경에서 항상 이 옵션 사용 권장

lock 파일 충돌 해결

# merge conflict 발생 시 — pnpm 이 자동으로 재해석해줌
git checkout --theirs pnpm-lock.yaml   # 상대방 버전 선택
pnpm install --lockfile-only            # lock 파일만 재생성
 
# 또는 conflict marker 를 제거하고
pnpm install --merge-git-lockfiles      # git merge 상황에서 자동 병합

🏁 총정리

pnpm 핵심 CLI 치트시트
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
설치:    pnpm i --frozen-lockfile (CI), pnpm i (개발)
추가:    pnpm add 패키지, pnpm add -D 패키지 (devDep)
제거:    pnpm remove 패키지
업데이트: pnpm up, pnpm up --latest, pnpm up -i (인터랙티브)
탐색:    pnpm why 패키지, pnpm list, pnpm outdated
실행:    pnpm dlx 패키지 (npx 대체)
패치:    pnpm patch 패키지@버전 → 수정 → pnpm patch-commit 경로
Node:    pnpm env use --global 20
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📝 마무리 퀴즈

Q1. pnpm why some-package 명령어는 어떤 상황에서 유용하며, npm 에는 같은 기능이 없는 이유는 무엇인가?

정답: pnpm why특정 패키지가 왜 설치되어 있는지 의존성 트리를 역추적하여 보여준다. package.json 에 직접 명시하지 않은 간접 의존성의 출처를 파악할 때 필수적이다. npm 에는 npm explain 패키지명 으로 유사한 기능이 있지만, pnpm 의 격리 구조 덕분에 트리가 더 명확하게 표시된다.

💡 상세 해설:

  • 사용 사례: 보안 audit 에서 취약한 간접 의존성이 나왔을 때 → "어떤 직접 의존성을 업그레이드해야 하는가"를 파악
  • 모노레포에서 -r 플래그로 전체 workspace 범위에서 검색 가능
  • 📌 핵심 기억법: "왜 여기 있어? pnpm why 를 물어봐."

Q2. pnpm-lock.yamlimporters, packages, snapshots 세 섹션의 역할 차이는?

정답: importerspackage.json 에서 선언된 버전 범위(specifier)와 실제 해석된 버전을 프로젝트별로 기록한다. packages 는 각 패키지의 메타 정보(resolution hash, engines, peerDeps)를 저장한다. snapshots 는 피어 의존성 조합까지 포함한 완전한 설치 스냅샷 으로, 같은 패키지가 다른 피어 조합으로 여러 번 나타날 수 있다.

💡 상세 해설:

  • specifier: "^18.2.0" → 요청한 버전 범위 / version: "18.2.0" → 실제 설치된 버전
  • next@14.2.0(react@18.2.0)(react-dom@18.2.0) 처럼 괄호 안에 피어 조합이 명시됨
  • 📌 핵심 기억법: importers = "요청", packages = "정보", snapshots = "실제 설치 상태"

Q3. 영철이의 테스트 타임: 실무 딜레마 (영철의 선택)

영철이가 next-auth 에서 버그를 발견했다. GitHub 에서 이미 fix PR 이 올라와 있지만 아직 릴리즈가 안 됐다. 배포는 3일 후다.

영철이 선택지:
A. 배포 일정을 늦추고 공식 릴리즈를 기다린다
B. next-auth 를 포크해서 자체 버전을 npm 에 배포한다
C. pnpm patch 로 로컬에서 버그를 수정하고 .patch 파일을 관리한다

영호 리드가 권장하는 선택지와 이유는?

정답: C. pnpm patch 를 사용한다. 배포 일정을 늦출 수 없고, 포크 배포는 유지보수 부담이 크기 때문이다. pnpm patch.patch 파일로 변경 사항을 추적할 수 있고, 공식 릴리즈 후 pnpm patch-remove 로 깔끔하게 제거할 수 있다.

💡 상세 해설:

  • A 의 문제: 비즈니스 일정을 기술적 이유로 늦추는 것은 PM 에게 설득력이 없음
  • B 의 문제: 자체 포크는 이후 업데이트를 계속 수동으로 병합해야 함. 영속적인 부채가 됨
  • C 의 장점: .patch 파일이 git 에 커밋되므로 팀 전체가 동일한 수정 사항을 공유. 공식 수정 후 제거 절차가 명확함
  • 주의사항: .patch 파일은 정확한 버전(@1.2.3)에 묶여 있으므로, 패키지 버전이 올라가면 패치를 다시 확인해야 함
  • 📌 핵심 기억법: "패치는 임시 반창고다 — 공식 치료제가 나오면 바로 뗀다."

🐣 영철이의 퇴근 일기

오늘 pnpm why deepmerge 를 쳐봤는데 진짜 신세계였다. 어디서 온 건지 몰랐던 패키지의 출처를 트리로 딱 보여주니까 "아 @tanstack/query-core 가 쓰는 거구나" 하고 바로 알게 됐다. npm 썼을 때는 이런 거 알아내려면 npm ls 결과를 한참 뒤졌는데.

그리고 pnpm patch 는 진짜 강력한 기능이다. 외부 라이브러리 버그를 로컬에서 수정하고 .patch 파일로 관리한다니... 이런 게 있는 줄도 몰랐다. 전에는 비슷한 상황에서 node_modules 안 파일을 직접 건드렸다가 pnpm install 할 때마다 초기화돼서 난감했는데, 이제 해결책이 생겼다.

pnpm-lock.yaml 도 오늘 처음 제대로 읽어봤다. specifierversion 이 분리된 구조가 처음엔 이상하게 느껴졌는데, "요청과 실제 설치를 분리해서 기록한다"는 관점에서 보니 납득이 됐다.

💡 오늘의 교훈: "pnpm why 로 출처를 추적하고, pnpm patch 로 급한 버그를 잡고, pnpm-lock.yaml 로 버전의 진실을 확인한다 — 이 세 가지가 pnpm 고급 사용자의 무기다."

오늘 점심에 팀원들이랑 삼겹살 먹었다. 영호 리드 님이 내가 pnpm why 를 알았냐고 의외라는 표정을 지으셔서 내심 뿌듯했다. 퇴근 후 헬스장 가서 가볍게 뛰고 왔더니 머리가 맑아졌다.


🔗 더 알아보기