⚡ pnpm CLI & pnpm-lock.yaml — 핵심 명령어 완전 정복
📋 개요
add/update/remove/why/list/dlx/patch/env 까지 pnpm 의 강력한 CLI 와 pnpm-lock.yaml 구조를 시니어 수준으로 이해한다
03. CLI & pnpm-lock.yaml
📋 목차
- pnpm-lock.yaml 구조 해부
- 총정리
- 마무리 퀴즈
- 영철이의 퇴근 일기
- [더 알아보기](#-더 알아보기)
📌 이 문서를 읽기 전에
⏱️ 예상 읽기 시간: 약 28분(전체) / 핵심 파트만: 14분
🗺️ 이 문서의 흐름
[설치/추가/제거 CLI] → [탐색 명령어] → [dlx/patch/env] → [pnpm-lock.yaml 구조]
🎯 이 문서를 다 읽으면 할 수 있는 것
-
pnpm add,pnpm update,pnpm remove의 옵션을 능숙하게 쓸 수 있다 -
pnpm why,pnpm list로 의존성 출처를 추적할 수 있다 -
pnpm patch로 외부 패키지 코드를 안전하게 수정할 수 있다 -
pnpm-lock.yaml구조를 읽고 버전 충돌 원인을 파악할 수 있다
🗺️ 이 문서의 배경 세계관: '영수네 커뮤니티'

- 🐣 영철 ( 신입 ): "영호 님, 저 오늘
@tanstack/react-query를 5버전으로 올렸더니 타입 에러가 10개 넘게 났어요. 다시 4버전으로 내리고 싶은데 어떻게 하는지 모르겠어요. 그리고 왜node_modules에deepmerge가 있는지 아무리 봐도 이유를 모르겠어요 — 저 직접 설치한 적 없는데요. 근데 이거 없애면 뭔가 망가질 것 같아서 손도 못 대고 있어요." - 🦁 영호 ( 리드 ): "영철 님,
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-onlypnpm 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#branchpnpm 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 deepmergepnpm list (pnpm ls)
# 직접 의존성 목록
pnpm list
# 전체 의존성 트리 (깊이 제한)
pnpm list --depth=2
# 특정 패키지 검색
pnpm list react
# 전역 패키지 목록
pnpm list -g
# JSON 출력
pnpm list --jsonpnpm 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 initnpm의 npx 와의 차이:
| 항목 | npx | pnpm 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 와의 차이:
| 항목 | overrides | patch |
|---|---|---|
| 용도 | 간접 의존성 버전 강제 | 패키지 코드 직접 수정 |
| 코드 변경 | 불가 | 가능 |
| 적용 범위 | 버전 범위 선택 | 정확한 버전 |
| 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 | 루트 | 실제 설치 스냅샷 (피어 포함한 완전한 의존성 트리) |
integrity | packages | SHA-512 해시 (무결성 검증) |
specifier | importers | package.json 에 적힌 원래 버전 범위 |
version | importers | lock 이 실제로 고정한 버전 (피어 포함 시 긴 문자열) |
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.yaml 의 importers, packages, snapshots 세 섹션의 역할 차이는?
✅ 정답: importers 는 package.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 도 오늘 처음 제대로 읽어봤다. specifier 와 version 이 분리된 구조가 처음엔 이상하게 느껴졌는데, "요청과 실제 설치를 분리해서 기록한다"는 관점에서 보니 납득이 됐다.
💡 오늘의 교훈: "
pnpm why로 출처를 추적하고,pnpm patch로 급한 버그를 잡고,pnpm-lock.yaml로 버전의 진실을 확인한다 — 이 세 가지가 pnpm 고급 사용자의 무기다."
오늘 점심에 팀원들이랑 삼겹살 먹었다. 영호 리드 님이 내가 pnpm why 를 알았냐고 의외라는 표정을 지으셔서 내심 뿌듯했다. 퇴근 후 헬스장 가서 가볍게 뛰고 왔더니 머리가 맑아졌다.