pnpm 패키지 매니저
TL;DR
- pnpm 패키지 매니저의 핵심 개념을 빠르게 파악할 수 있다.
- 배경과 이유를 통해 왜 필요한지 맥락을 이해할 수 있다.
- 특징과 상세 내용을 통해 실무 적용 포인트를 확인할 수 있다.
1. 개념
pnpm 패키지 매니저의 핵심 정의와 문제 공간을 간단히 정리한다.
2. 배경
이 주제가 등장한 기술적·조직적 배경과 기존 접근의 한계를 설명한다.
3. 이유
왜 지금 이 방식을 채택해야 하는지, 기대 효과와 트레이드오프를 함께 정리한다.
4. 특징
핵심 동작 방식, 장단점, 적용 시 주의점을 빠르게 훑을 수 있도록 요약한다.
5. 상세 내용
pnpm 패키지 매니저
작성일: 2026-02-09 수정일: 2026-02-26 카테고리: Build / Package Manager 포함 내용: pnpm, npm, yarn, Content-Addressable Store, Phantom Dependencies, Monorepo, workspace, 보안, 역사, Corepack, Catalogs
1. pnpm이란?
개념
pnpm = Performant npm
└── "빠르고 디스크 효율적인" 패키지 매니저
└── npm/yarn의 문제점을 해결하기 위해 등장
패키지 매니저 기초 설명
┌─────────────────────────────────────────────────────────────────┐
│ 패키지 매니저가 뭔가요? (완전 기초) │
│ │
│ 비유: 레고 블록 조달 시스템 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 프로그래밍 = 레고로 집을 짓는 것 │ │
│ │ 패키지 = 다른 사람이 만든 레고 블록 세트 │ │
│ │ 패키지 매니저 = 레고 블록 배달/관리 서비스 │ │
│ │ │ │
│ │ 직접 만들기: 벽돌 하나하나 직접 제작 → 시간 낭비 │ │
│ │ 패키지 사용: "벽돌 10개 주세요" → 자동 배달 │ │
│ │ │ │
│ │ npm/yarn/pnpm 모두 이 "배달 서비스" 역할 │ │
│ │ 차이점: 배달 방식과 보관 방식이 다름 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 개념들: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ package.json = 주문서 (어떤 블록이 필요한지 목록) │ │
│ │ node_modules = 배달된 블록 보관 창고 │ │
│ │ lock 파일 = 정확한 블록 버전 기록표 │ │
│ │ registry = 블록을 파는 중앙 시장 (npmjs.com) │ │
│ │ 의존성(dependency) = 내가 쓰는 블록이 필요로 하는 블록 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 등장 배경
npm/yarn의 문제점
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 기존 방식 (npm/yarn): │
│ │
│ 프로젝트 A 프로젝트 B 프로젝트 C │
│ └── node_modules/ └── node_modules/ └── node_modules/ │
│ └── lodash@4 └── lodash@4 └── lodash@4 │
│ └── react@18 └── react@18 └── react@18 │
│ └── axios@1 └── axios@1 └── axios@1 │
│ └── ... └── ... └── ... │
│ │
│ 문제점: │
│ ├── 같은 패키지가 프로젝트마다 중복 설치 │
│ ├── 디스크 공간 낭비 (프로젝트당 수백 MB ~ 수 GB) │
│ ├── 설치 시간 증가 │
│ └── 네트워크 대역폭 낭비 │
│ │
│ 실제 사례: │
│ "10개 프로젝트 × 500MB = 5GB 디스크 사용" │
│ "같은 lodash가 10번 복사됨" │
│ │
└─────────────────────────────────────────────────────────────────┘
Flat node_modules의 문제
┌─────────────────────────────────────────────────────────────────┐
│ │
│ npm v3+, yarn의 "Flat" 구조: │
│ │
│ 의도: 중첩 깊이 줄이기 (Windows 경로 길이 제한 등) │
│ │
│ node_modules/ │
│ ├── A/ ← 직접 의존성 │
│ ├── B/ ← A의 의존성인데 최상위로 끌어올림 │
│ ├── C/ ← B의 의존성인데 최상위로 끌어올림 │
│ └── ... │
│ │
│ 부작용: Phantom Dependencies 발생 │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 패키지 매니저의 역사 - 누가 왜 만들었나?
3.1 npm의 탄생 (2010)
┌─────────────────────────────────────────────────────────────────┐
│ npm의 탄생 이야기 │
│ │
│ 시대: 2009-2010년 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Ryan Dahl이 Node.js를 만듦 (2009년 5월) │ │
│ │ → 서버에서 JavaScript를 실행할 수 있게 됨! │ │
│ │ │ │
│ │ 문제: 코드를 공유할 방법이 없음 │ │
│ │ ├── ZIP 파일 다운로드 │ │
│ │ ├── GitHub에서 직접 복사 │ │
│ │ └── 버전 관리? 그런 건 없었음 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Isaac Z. Schlueter (Yahoo 개발자): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "이거 너무 불편하다. 패키지 매니저를 만들자!" │ │
│ │ │ │
│ │ 2010년 1월 12일: npm 첫 공개 릴리스 │ │
│ │ 2011년: Node.js에 기본 포함됨 │ │
│ │ 2014년: npm Inc. 설립 (상업화) │ │
│ │ 2020년: GitHub(Microsoft)에 인수됨 │ │
│ │ │ │
│ │ 현재: 130만+ 패키지, 월 750억+ 다운로드 │ │
│ │ → 세계 최대 소프트웨어 레지스트리 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ npm이 만든 3가지 핵심 개념: │
│ ├── 1. package.json: 프로젝트의 "주문서" │
│ ├── 2. 중앙 레지스트리: npmjs.com이라는 "시장" │
│ └── 3. Semantic Versioning: 1.2.3 (주.부.패치) 버전 체계 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 left-pad 사건 (2016년 3월) - npm 생태계의 충격
┌─────────────────────────────────────────────────────────────────┐
│ left-pad 사건 - 11줄의 코드가 인터넷을 멈추다 │
│ │
│ 2016년 3월 22일, 세계적인 사건 발생: │
│ │
│ 배경: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Azer Koculu라는 개발자가 'kik'이라는 npm 패키지를 소유 │ │
│ │ → Kik 메신저 회사가 상표권 주장 │ │
│ │ → npm이 Kik 회사 편을 들어 패키지명을 빼앗음 │ │
│ │ → 분노한 Koculu가 자신의 273개 패키지를 전부 삭제! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 문제: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 삭제된 패키지 중 'left-pad' = 문자열 왼쪽 채우기 함수 │ │
│ │ 고작 11줄짜리 코드 │ │
│ │ │ │
│ │ 그런데... 이 패키지를 의존하는 프로젝트: │ │
│ │ ├── Babel (JavaScript 컴파일러) │ │
│ │ ├── React 개발 도구 │ │
│ │ ├── Webpack │ │
│ │ └── Facebook, Netflix, Spotify 등 수천 개 프로젝트 │ │
│ │ │ │
│ │ 결과: 전 세계적으로 빌드가 깨짐 │ │
│ │ "11줄의 코드가 인터넷을 멈췄다" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 이 사건이 드러낸 npm의 구조적 문제: │
│ ├── 1. 레지스트리 연결 없으면 빌드 불가 │
│ ├── 2. Lock 파일이 없어서 설치 재현 불가 │
│ ├── 3. 누구나 패키지를 삭제할 수 있었음 │
│ └── 4. 의존성 체인의 취약성이 명백해짐 │
│ │
│ → 이 사건이 yarn과 pnpm 탄생의 직접적 촉매가 됨 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.3 yarn의 등장 (2016년 10월) - Facebook의 해결책
┌─────────────────────────────────────────────────────────────────┐
│ yarn - Facebook이 만든 이유 │
│ │
│ Facebook 내부 상황 (2016년): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ JavaScript 코드베이스가 거대해지면서: │ │
│ │ │ │
│ │ 문제 1: 비결정적 설치 │ │
│ │ → npm install을 2번 실행하면 다른 결과가 나옴 │ │
│ │ → "내 컴퓨터에선 되는데?" 문제가 빈번 │ │
│ │ │ │
│ │ 문제 2: 엄청난 용량 │ │
│ │ → React Native의 68개 의존성 → 121,358개 파일 생성 │ │
│ │ → Babel 업데이트 한 번에 80만 줄 diff │ │
│ │ │ │
│ │ 문제 3: 느린 순차 설치 │ │
│ │ → npm은 패키지를 하나씩 순서대로 다운로드 │ │
│ │ → 대규모 프로젝트에서 수 분씩 걸림 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 인물: │
│ ├── Sebastian McKenzie: Babel 창시자, yarn 프로젝트 시작 │
│ ├── Christoph Nakazawa: Facebook 런던, JS 도구 팀장 │
│ └── 협력: Google, Exponent(Expo), Tilde(Ember.js) │
│ │
│ yarn이 가져온 혁신: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. yarn.lock: 정확한 버전 고정 → 재현 가능한 설치 │ │
│ │ 2. 병렬 다운로드: 동시에 여러 패키지 다운로드 │ │
│ │ 3. 오프라인 캐시: 한 번 다운로드하면 오프라인 설치 가능 │ │
│ │ 4. 체크섬 검증: 패키지 변조 방지 │ │
│ │ │ │
│ │ 결과: 설치 시간 수 분 → 수 초로 단축 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 하지만 yarn도 해결하지 못한 것: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Flat node_modules 구조 유지 (npm과 동일) │ │
│ │ Phantom Dependencies 여전히 발생 │ │
│ │ 디스크 공간 낭비 (프로젝트별 복사) │ │
│ │ │ │
│ │ → 이것이 pnpm이 탄생한 이유! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3.4 pnpm의 탄생 (2017) - 근본적 해결책
┌─────────────────────────────────────────────────────────────────┐
│ pnpm - Zoltan Kochan의 근본적 해결책 │
│ │
│ Zoltan Kochan의 불만: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "yarn이 나왔을 때 기대했지만 실망했다." │ │
│ │ "빠르긴 한데, 근본적인 문제를 안 고쳤다." │ │
│ │ "Flat node_modules가 진짜 문제인데 왜 아무도 안 고치지?" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 선구자: Alexander Gugel의 'ied' (2015) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 심볼릭 링크 기반 의존성 관리 실험 │ │
│ │ → 이 아이디어에서 pnpm이 영감을 받음 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 타임라인: │
│ ├── 2016년 1월: Rico Sta. Cruz가 초기 커밋 │
│ ├── 2017년 6월: pnpm v1 정식 릴리스 (Zoltan Kochan 주도) │
│ ├── 2020년: Yarn Classic 유지보수 모드 진입 → pnpm 급성장 │
│ ├── 2021년: Vue.js, Vite가 pnpm으로 전환 │
│ ├── 2025년 1월: pnpm v10 "보안 기본값" 마일스톤 │
│ └── 2026년 현재: 다운로드 수 매년 2배 성장 중 │
│ │
│ pnpm의 핵심 철학: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "문제의 근본을 고치자" │ │
│ │ │ │
│ │ npm/yarn: Flat 구조의 부작용을 땜질로 해결 │ │
│ │ pnpm: Flat 구조 자체를 버리고 새로운 구조 도입 │ │
│ │ │ │
│ │ = Content-Addressable Store + 심볼릭 링크 │ │
│ │ = 디스크 절약 + Phantom Dependencies 원천 차단 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3.5 Yarn Berry (v2+)와 PnP - 그리고 실패
┌─────────────────────────────────────────────────────────────────┐
│ Yarn Berry의 급진적 실험과 생태계의 거부 │
│ │
│ Yarn Classic이 유지보수 모드에 들어가며 (2020): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Mael Nison(arcanis)이 Yarn을 완전히 새로 작성 │ │
│ │ → Yarn Berry (v2+) │ │
│ │ → 핵심 기능: Plug'n'Play (PnP) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ PnP의 아이디어: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "node_modules 폴더 자체를 없애버리자!" │ │
│ │ │ │
│ │ 방법: │ │
│ │ 1. 패키지를 ZIP 파일로 .yarn/cache/에 저장 │ │
│ │ 2. .pnp.cjs 파일로 모든 경로를 매핑 │ │
│ │ 3. Node.js의 fs 모듈을 덮어씌워서 ZIP에서 직접 읽기 │ │
│ │ │ │
│ │ 장점: 설치 거의 즉시, Zero-Install 가능 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 실패했나: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ - Node.js 표준이 아님 (런타임 패치 필요) │ │
│ │ - 수많은 라이브러리/도구와 호환 안 됨 │ │
│ │ - Webpack, Jest, IDE 플러그인 등이 깨짐 │ │
│ │ - Facebook 자체도 Yarn Classic에 머무름 │ │
│ │ - 2024년 기준 주요 오픈소스 프로젝트 중 PnP 사용 = 0 │ │
│ │ │ │
│ │ 교훈: 너무 급진적인 변화는 생태계가 따라오지 않는다 │ │
│ │ │ │
│ │ 반면 pnpm은: │ │
│ │ + node_modules 유지 (호환성 보장) │ │
│ │ + Node.js 표준 그대로 동작 │ │
│ │ + 기존 도구와 거의 100% 호환 │ │
│ │ → "현실적인 혁신"이 승리함 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4. pnpm의 해결책
Content-Addressable Store
┌─────────────────────────────────────────────────────────────────┐
│ │
│ pnpm 방식: │
│ │
│ 비유: 도시 중앙 도서관 시스템 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ npm/yarn = 각 집(프로젝트)마다 책(패키지)을 구매 │ │
│ │ pnpm = 중앙 도서관(store)에 책 1권, 각 집은 대출카드 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ~/.pnpm-store/ ← 글로벌 저장소 (딱 1곳) │
│ └── v3/ │
│ └── files/ │
│ └── 00/ │
│ └── 01/ │
│ └── ... ← 해시 기반 파일 저장 │
│ │
│ 프로젝트 A 프로젝트 B │
│ └── node_modules/ └── node_modules/ │
│ └── .pnpm/ └── .pnpm/ │
│ └── lodash → 하드링크 └── lodash → 하드링크 │
│ └── react → 하드링크 └── react → 하드링크 │
│ │
│ 핵심: │
│ ├── 패키지 파일은 store에 1번만 저장 │
│ ├── 프로젝트에서는 하드 링크로 참조 │
│ ├── 디스크 사용량 대폭 감소 (70-90%) │
│ └── 설치 속도 향상 (이미 store에 있으면 링크만) │
│ │
└─────────────────────────────────────────────────────────────────┘
심볼릭 링크 vs 하드 링크
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 심볼릭 링크 (Symbolic Link): │
│ ├── "바로가기" 같은 개념 │
│ ├── 원본 파일을 가리키는 포인터 │
│ └── 원본 삭제되면 깨짐 │
│ │
│ 하드 링크 (Hard Link): │
│ ├── 같은 파일 데이터를 가리키는 또 다른 이름 │
│ ├── 원본과 동등한 존재 │
│ ├── 원본 삭제해도 데이터 유지 │
│ └── 디스크 공간 추가 사용 없음 │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 심볼릭 링크 = "3층 302호에 가세요" (안내문) │ │
│ │ → 302호가 사라지면 안내문은 무의미 │ │
│ │ │ │
│ │ 하드 링크 = 같은 방에 문이 2개 있는 것 │ │
│ │ → 문 하나를 막아도 다른 문으로 들어갈 수 있음 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ pnpm: │
│ ├── 패키지 폴더 → .pnpm/ : 심볼릭 링크 │
│ └── 파일 → store : 하드 링크 │
│ │
└─────────────────────────────────────────────────────────────────┘
5. Phantom Dependencies 차단
문제 설명
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Phantom Dependency (유령 의존성): │
│ │
│ 비유: 친구의 친구 물건을 마음대로 쓰는 것 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 내가 A한테 빌린 도구 → 정당한 사용 │ │
│ │ A가 갖고 있는 B의 도구를 내가 마음대로 사용 → 위험! │ │
│ │ → A가 B와 절교하면 나도 그 도구를 못 씀 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ package.json: │
│ dependencies: { "express": "4.18" } │
│ │
│ express가 내부적으로 "accepts" 패키지 사용 │
│ │
│ npm/yarn (flat node_modules): │
│ node_modules/ │
│ ├── express/ │
│ └── accepts/ ← express의 의존성이지만 최상위에 노출됨 │
│ │
│ 내 코드에서: │
│ import accepts from 'accepts' // 동작함! (하지만 위험) │
│ │
│ 문제: │
│ ├── package.json에 없는 패키지를 사용 가능 │
│ ├── express가 accepts 제거하면 내 코드도 깨짐 │
│ └── 의존성 그래프가 불명확해짐 │
│ │
└─────────────────────────────────────────────────────────────────┘
pnpm의 엄격한 구조
┌─────────────────────────────────────────────────────────────────┐
│ │
│ pnpm node_modules 구조: │
│ │
│ node_modules/ │
│ ├── .pnpm/ ← 실제 패키지들 (격리됨) │
│ │ ├── express@4.18.0/ │
│ │ │ └── node_modules/ │
│ │ │ ├── express/ ← 실제 파일 │
│ │ │ └── accepts/ ← express만 접근 가능 │
│ │ └── accepts@1.3.8/ │
│ │ └── node_modules/ │
│ │ └── accepts/ │
│ └── express/ → .pnpm/express@4.18.0/.../express │
│ │
│ 내 코드에서: │
│ import express from 'express' // 정상 │
│ import accepts from 'accepts' // 에러! (올바른 동작) │
│ │
│ → 명시적으로 선언한 의존성만 접근 가능 │
│ → 더 안전하고 예측 가능한 의존성 관리 │
│ │
└─────────────────────────────────────────────────────────────────┘
6. npm vs yarn vs pnpm 비교
기능 비교
| 항목 | npm | yarn (classic) | pnpm |
|---|---|---|---|
| 출시 | 2010 | 2016 | 2017 |
| 저장 방식 | 프로젝트별 복사 | 프로젝트별 복사 | 글로벌 store + 링크 |
| 디스크 사용 | 높음 | 높음 | 매우 낮음 |
| 설치 속도 | 보통 | 빠름 | 가장 빠름 |
| Lock 파일 | package-lock.json | yarn.lock | pnpm-lock.yaml |
| node_modules | flat | flat (PnP 옵션) | 엄격한 중첩 |
| Phantom Deps | 허용 | 허용 | 차단 |
| Monorepo | workspaces | workspaces | 최적화된 workspace |
성능 벤치마크 (일반적인 경우)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 설치 시간 (cold cache): │
│ npm: ████████████████████ 100% │
│ yarn: ████████████████ 80% │
│ pnpm: ████████████ 60% │
│ │
│ 설치 시간 (warm cache - 이미 store에 있음): │
│ npm: ████████████████████ 100% │
│ yarn: ████████████████ 80% │
│ pnpm: ████ 20% ← 링크만 생성 │
│ │
│ 디스크 사용량: │
│ npm: ████████████████████ 100% │
│ yarn: ████████████████████ 100% │
│ pnpm: ██████ 30% ← 공유 store │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 대형 프로젝트들의 pnpm 전환 사례
┌─────────────────────────────────────────────────────────────────┐
│ 왜 대형 오픈소스가 pnpm을 선택했나? │
│ │
│ Vue.js (2021년 10월): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 전환 이유: esbuild v0.13이 플랫폼별 바이너리를 │ │
│ │ optionalDependencies로 변경 │ │
│ │ │ │
│ │ Yarn: 모든 플랫폼 바이너리 다운로드 (~102MB) │ │
│ │ pnpm: 현재 플랫폼 바이너리만 다운로드 (~14.5MB) │ │
│ │ → 7배 차이! │ │
│ │ │ │
│ │ 커뮤니티 투표: 90%가 pnpm 추천 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Vite (Vue와 동시기): │
│ ├── 공식 저장소가 pnpm 모노레포 │
│ ├── 현대 프론트엔드 빌드 도구의 표준 │
│ └── React, Svelte, Lit 생태계 전체에 영향 │
│ │
│ Turborepo + Vercel: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ pnpm workspace + Turborepo 조합이 │ │
│ │ 모노레포의 사실상 표준(de facto standard)이 됨 │ │
│ │ │ │
│ │ pnpm: 의존성 관리 + 워크스페이스 연결 │ │
│ │ Turborepo: 태스크 오케스트레이션 + 캐싱 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 기타 주요 채택: │
│ ├── Prisma (DB ORM) │
│ ├── SvelteKit (공식 템플릿에 포함) │
│ ├── Nuxt 3 (모노레포에 pnpm 권장) │
│ └── VueUse, Slidev 등 Anthony Fu의 프로젝트들 │
│ │
│ 2024년 사용률 (State of Frontend 설문, 6000명+): │
│ ├── npm: 56.6% (Node.js 기본 포함이라 높음) │
│ ├── Yarn Classic: 21.5% │
│ └── pnpm: 19.9% (만족도 93%로 최고) │
│ │
└─────────────────────────────────────────────────────────────────┘
8. pnpm의 보안 장점 - 공급망 공격 방어
8.1 event-stream 사건 (2018)
┌─────────────────────────────────────────────────────────────────┐
│ event-stream 사건과 pnpm의 보안 대응 │
│ │
│ 2018년 11월, 주당 200만 다운로드의 event-stream 패키지: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 공격 과정: │ │
│ │ 1. 공격자가 원래 관리자에게 관리 권한을 양도받음 │ │
│ │ 2. flatmap-stream이라는 악성 의존성을 추가 │ │
│ │ 3. postinstall 스크립트로 악성 코드 실행 │ │
│ │ 4. 비트코인 지갑(Copay) 정보 탈취 │ │
│ │ │ │
│ │ pnpm v10이었다면? │ │
│ │ → postinstall 스크립트 기본 차단! │ │
│ │ → 이 공격은 원천 봉쇄되었을 것 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ npm/yarn = 택배 받자마자 자동으로 상자를 열어줌 │ │
│ │ → 상자 안에 폭탄이 있어도 열어버림 │ │
│ │ │ │
│ │ pnpm v10 = 택배는 받되, 열기 전에 허가를 받아야 함 │ │
│ │ → "이 택배 열어도 되나요?" 물어봄 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8.2 pnpm v10의 보안 기본값 (2025)
┌─────────────────────────────────────────────────────────────────┐
│ pnpm v10 - "보안을 기본값으로" │
│ │
│ 1. Lifecycle Scripts 기본 차단 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ npm/yarn: postinstall 스크립트 자동 실행 (위험!) │ │
│ │ pnpm v10: 기본적으로 차단, 명시적 허용 필요 │ │
│ │ │ │
│ │ // package.json │ │
│ │ "pnpm": { │ │
│ │ "allowedBuilds": ["sharp", "esbuild"] │ │
│ │ } │ │
│ │ → 신뢰하는 패키지만 빌드 스크립트 실행 허용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2. 엄격한 node_modules = 공급망 공격 방어 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phantom Dependency 차단의 보안적 의미: │ │
│ │ │ │
│ │ npm/yarn: 악성 패키지가 다른 패키지의 파일에 접근 가능 │ │
│ │ pnpm: 각 패키지는 자기 의존성에만 접근 가능 │ │
│ │ → 악성 코드의 피해 범위가 제한됨 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 3. 릴리스 나이 필터링 (minimumReleaseAge) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // .npmrc │ │
│ │ minimum-release-age=1440 // 24시간 (분 단위) │ │
│ │ │ │
│ │ → 새로 릴리스된 패키지를 24시간 대기 후 설치 │ │
│ │ → 제로데이 공격을 발견할 시간 확보 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 4. 이상한 출처 차단 (blockExoticSubdeps) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // .npmrc │ │
│ │ block-exotic-subdeps=true │ │
│ │ │ │
│ │ → 의존성이 git URL이나 tarball에서 오는 것을 차단 │ │
│ │ → npm 레지스트리를 통한 패키지만 허용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
9. 주요 명령어
기본 명령어
# pnpm 설치
npm install -g pnpm
# 또는
corepack enable # Node.js 16.13+ 내장
# 버전 확인
pnpm --version
# 패키지 설치
pnpm install # 모든 의존성 설치
pnpm add lodash # 프로덕션 의존성 추가
pnpm add -D typescript # 개발 의존성 추가
pnpm add -g tsx # 전역 설치
# 패키지 제거
pnpm remove lodash
# 스크립트 실행
pnpm run build # npm run build와 동일
pnpm build # run 생략 가능
pnpm test
pnpm start
Store 관리
# store 경로 확인
pnpm store path
# → ~/.local/share/pnpm/store/v3
# store 상태 확인
pnpm store status
# 사용하지 않는 패키지 정리
pnpm store prune
유용한 옵션
# 프로젝트의 모든 의존성 업데이트
pnpm update
# 특정 패키지만 업데이트
pnpm update lodash
# 의존성 트리 확인
pnpm list
pnpm list --depth=2
# 왜 이 패키지가 설치됐는지 확인
pnpm why lodash
10. Monorepo 지원
pnpm-workspace.yaml
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- '!**/test/**' # test 폴더 제외
Monorepo 구조
┌─────────────────────────────────────────────────────────────────┐
│ │
│ my-monorepo/ │
│ ├── package.json ← 루트 package.json │
│ ├── pnpm-workspace.yaml ← workspace 설정 │
│ ├── pnpm-lock.yaml ← 단일 lock 파일 │
│ ├── node_modules/ ← 공유 의존성 │
│ ├── apps/ │
│ │ ├── web/ ← Next.js 앱 │
│ │ │ └── package.json │
│ │ └── api/ ← Express 서버 │
│ │ └── package.json │
│ └── packages/ │
│ ├── ui/ ← 공유 컴포넌트 │
│ │ └── package.json │
│ └── utils/ ← 공유 유틸리티 │
│ └── package.json │
│ │
└─────────────────────────────────────────────────────────────────┘
Workspace 명령어
# 특정 패키지에서 명령 실행
pnpm --filter web build
pnpm --filter @my/utils test
# 모든 패키지에서 명령 실행
pnpm -r run build # recursive
pnpm -r run test
# 의존성 있는 패키지들만 빌드
pnpm --filter web... build # web과 web이 의존하는 모든 패키지
# 워크스페이스 패키지를 의존성으로 추가
pnpm add @my/utils --filter web --workspace
11. pnpm 최신 기능 (2024-2025)
11.1 Corepack - Node.js 공식 인정
┌─────────────────────────────────────────────────────────────────┐
│ Corepack으로 pnpm 관리하기 │
│ │
│ Corepack이란? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Node.js 16.9+에 내장된 패키지 매니저 버전 관리 도구 │ │
│ │ → pnpm이 Node.js에 "공식 인정"받은 것 │ │
│ │ │ │
│ │ 비유: 운영체제에 기본 설치된 앱 스토어 │ │
│ │ npm = 기본 앱, pnpm = 앱 스토어에서 받을 수 있는 앱 │ │
│ │ │ │
│ │ // package.json │ │
│ │ { │ │
│ │ "packageManager": "pnpm@9.15.0" │ │
│ │ } │ │
│ │ │ │
│ │ corepack enable // 활성화 │ │
│ │ pnpm install // 자동으로 9.15.0 버전 사용 │ │
│ │ │ │
│ │ 팀원 모두 같은 pnpm 버전 보장! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.2 Catalogs (pnpm 9.5+) - 모노레포 의존성 통합
┌─────────────────────────────────────────────────────────────────┐
│ Catalogs - 버전을 한 곳에서 관리 │
│ │
│ 문제: 모노레포에서 10개 패키지가 React를 사용 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 기존: 각 package.json에 버전이 흩어져 있음 │ │
│ │ app-a/package.json: "react": "^18.2.0" │ │
│ │ app-b/package.json: "react": "^18.3.0" // 다른 버전! │ │
│ │ lib-c/package.json: "react": "^18.2.0" │ │
│ │ → 버전 드리프트, 머지 충돌, 업그레이드 번거로움 │ │
│ │ │ │
│ │ Catalogs: 버전을 한 곳에서 선언 │ │
│ │ // pnpm-workspace.yaml │ │
│ │ catalog: │ │
│ │ react: ^18.3.0 │ │
│ │ typescript: ^5.5.0 │ │
│ │ │ │
│ │ // 각 package.json │ │
│ │ "dependencies": { "react": "catalog:" } │ │
│ │ → 모든 패키지가 같은 버전 사용 보장! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 기존 = 각 부서가 따로 사무용품 주문 (규격이 다 다름) │ │
│ │ Catalogs = 본사에서 공용 규격을 정해놓고 일괄 주문 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.3 pnpm patch - 의존성 직접 수정
┌─────────────────────────────────────────────────────────────────┐
│ pnpm patch - 라이브러리 버그 직접 고치기 │
│ │
│ 상황: lodash에 버그가 있는데 PR이 머지되려면 한참 걸림 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ pnpm patch lodash // lodash를 임시 폴더에 추출 │ │
│ │ # → 임시 폴더에서 버그 수정 │ │
│ │ pnpm patch-commit /tmp/xxx // .patch 파일 생성 및 등록 │ │
│ │ │ │
│ │ // package.json에 자동 기록 │ │
│ │ "pnpm": { │ │
│ │ "patchedDependencies": { │ │
│ │ "lodash": "patches/lodash.patch" │ │
│ │ } │ │
│ │ } │ │
│ │ → 매번 install할 때 자동으로 패치 적용 │ │
│ │ → 포크 없이 의존성 버그 수정 가능! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 기존 = 차에 결함이 있으면 리콜 올 때까지 기다림 │ │
│ │ pnpm patch = 직접 수리하고 매뉴얼에 기록해둠 │ │
│ │ → 다음에 차를 빌려도 자동으로 같은 수리 적용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.4 pnpm deploy - 프로덕션 배포 최적화
┌─────────────────────────────────────────────────────────────────┐
│ pnpm deploy - Docker 이미지 최적화 │
│ │
│ 모노레포에서 하나의 앱만 배포하고 싶을 때: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ pnpm deploy --filter=my-app /output │ │
│ │ │ │
│ │ → my-app에 필요한 의존성만 추출 │ │
│ │ → Docker 이미지 크기 최소화 │ │
│ │ → 불필요한 devDependencies 제외 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 기존 = 이사할 때 집 전체 짐을 다 가져감 │ │
│ │ pnpm deploy = 새 집에 필요한 것만 골라서 가져감 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
12. 마이그레이션
npm/yarn에서 pnpm으로
# 1. 기존 node_modules 삭제
rm -rf node_modules
rm package-lock.json # 또는 yarn.lock
# 2. pnpm으로 설치
pnpm install
# 3. pnpm-lock.yaml 생성됨 (커밋 필요)
git add pnpm-lock.yaml
git commit -m "chore: migrate to pnpm"
주의사항
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 마이그레이션 시 주의: │
│ │
│ 1. Phantom Dependencies 에러 │
│ → package.json에 누락된 의존성 추가 필요 │
│ → pnpm add <missing-package> │
│ │
│ 2. 일부 패키지 호환성 │
│ → shamefully-hoist 옵션으로 flat 구조 허용 가능 │
│ → .npmrc에 shamefully-hoist=true │
│ → (권장하지 않음, 임시 해결책) │
│ │
│ 3. CI/CD 수정 필요 │
│ → npm install → pnpm install │
│ → npm run → pnpm run │
│ │
└─────────────────────────────────────────────────────────────────┘
13. 설정 (.npmrc)
주요 설정
# .npmrc
# store 경로 변경 (기본: ~/.local/share/pnpm/store)
store-dir=~/.pnpm-store
# 엄격 모드 (기본값, phantom deps 차단)
strict-peer-dependencies=true
# 호환성 모드 (npm처럼 flat 구조, 권장 안 함)
shamefully-hoist=true
# Node.js 버전 관리
use-node-version=18.17.0
# 레지스트리 설정
registry=https://registry.npmjs.org/
# [pnpm v10] 보안 설정
minimum-release-age=1440
block-exotic-subdeps=true
14. 언제 무엇을 쓰면 좋은가? - 실전 선택 가이드
┌─────────────────────────────────────────────────────────────────┐
│ 실전 패키지 매니저 선택 가이드 │
│ │
│ pnpm을 쓰면 좋은 경우: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ + 모노레포를 운영할 때 (최적의 선택) │ │
│ │ + 디스크 공간이 제한적일 때 (CI/CD 러너, 노트북) │ │
│ │ + 보안이 중요한 프로젝트 (금융, 의료) │ │
│ │ + 여러 프로젝트를 동시에 개발할 때 │ │
│ │ + CI/CD 빌드 시간을 줄이고 싶을 때 │ │
│ │ + 새 프로젝트를 시작할 때 (처음부터 pnpm 추천) │ │
│ │ + 의존성을 엄격하게 관리하고 싶을 때 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ npm을 쓰면 좋은 경우: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ + 팀원들이 패키지 매니저에 익숙하지 않을 때 │ │
│ │ + 특별한 요구사항 없이 간단한 프로젝트 │ │
│ │ + Node.js 설치만으로 바로 시작하고 싶을 때 │ │
│ │ + 기존 npm 프로젝트를 유지보수할 때 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ yarn을 쓰면 좋은 경우: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ + 이미 yarn을 쓰고 있는 프로젝트 │ │
│ │ + 팀이 yarn에 익숙한 경우 │ │
│ │ ! 신규 프로젝트라면 pnpm 고려 권장 │ │
│ │ ! Yarn Classic은 유지보수 모드 (신기능 없음) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 판단 플로우차트: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Q: 모노레포인가? │ │
│ │ ├── YES → pnpm (최적의 선택) │ │
│ │ └── NO │ │
│ │ Q: 보안이 중요한가? │ │
│ │ ├── YES → pnpm (Lifecycle 차단, 엄격한 격리) │ │
│ │ └── NO │ │
│ │ Q: 디스크/속도 최적화가 필요한가? │ │
│ │ ├── YES → pnpm │ │
│ │ └── NO │ │
│ │ Q: 팀 학습 비용을 최소화하고 싶은가? │ │
│ │ ├── YES → npm (가장 간단) │ │
│ │ └── NO → pnpm (장기적으로 이점) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
15. 정리
┌─────────────────────────────────────────────────────────────────┐
│ │
│ pnpm = 빠르고 디스크 효율적인 패키지 매니저 │
│ │
│ 핵심 특징: │
│ ├── Content-Addressable Store: 패키지 1번만 저장 │
│ ├── 하드 링크: 디스크 공간 절약 (70-90%) │
│ ├── 엄격한 node_modules: Phantom Dependencies 차단 │
│ ├── Monorepo 최적화: 효율적인 workspace 관리 │
│ ├── 보안 기본값: Lifecycle 차단, 릴리스 나이 필터링 │
│ └── 최신 기능: Catalogs, patch, deploy, Corepack │
│ │
│ 역사적 맥락: │
│ ├── npm (2010): 최초의 Node.js 패키지 매니저 │
│ ├── left-pad 사건 (2016): npm 생태계 취약성 드러남 │
│ ├── yarn (2016): Facebook이 만든 빠른 대안 │
│ ├── pnpm (2017): 근본적 구조 혁신 │
│ └── Yarn Berry PnP (2020): 급진적 실험, 생태계 거부 │
│ │
│ 보안: │
│ ├── event-stream 같은 공급망 공격 방어 │
│ ├── postinstall 스크립트 기본 차단 (v10) │
│ ├── minimumReleaseAge로 제로데이 방어 │
│ └── 엄격한 격리로 악성 코드 피해 범위 제한 │
│ │
│ 선택 기준: │
│ ├── 모노레포/보안/디스크/속도 → pnpm │
│ ├── 간단한 프로젝트/학습 비용 최소화 → npm │
│ └── 기존 yarn 프로젝트 유지보수 → yarn │
│ │
│ 주요 기업 사용: │
│ Vue.js, Vite, Nuxt, Turborepo, Vercel, Prisma, │
│ SvelteKit 등 │
│ │
└─────────────────────────────────────────────────────────────────┘
관련 키워드
pnpm, npm, yarn, 패키지 매니저, node_modules, Content-Addressable Store, 하드 링크, 심볼릭 링크, Phantom Dependencies, 유령 의존성, Monorepo, workspace, pnpm-workspace.yaml, pnpm-lock.yaml, .npmrc, left-pad, Isaac Z. Schlueter, Zoltan Kochan, Sebastian McKenzie, Yarn Berry, PnP, Plug'n'Play, Corepack, pnpm patch, Catalogs, pnpm deploy, Supply Chain Attack, 공급망 공격, event-stream, Lifecycle Scripts, minimumReleaseAge, Turborepo, Vue.js