$ yh.log
[스터디] npm deep dive - 3장 npm의 대항마 yarn과 pnpm

[스터디] npm deep dive - 3장 npm의 대항마 yarn과 pnpm

npmyarnpnpm패키지 관리JavaScript

작성자 : 오예환 | 작성일 : 2026-01-30 | 수정일 : 2026-01-30 | 조회수 :

들어가며

모든 패키지 관리자는 고유의 장단점이 있다. 이 장에서는 npm의 한계를 살펴보고, 이를 해결하기 위해 등장한 Yarn과 pnpm이 각각 어떤 방식으로 문제를 풀었는지 비교한다. 각 도구가 제공하는 특성을 이해하고, 프로젝트의 환경과 상황에 맞는 패키지 관리자를 선택할 수 있도록 정보를 제공하는 것이 목표이다.

npm의 문제점과 한계

2장에서 npm의 구조와 동작 방식을 깊이 살펴보았다. npm은 JavaScript 생태계의 표준 패키지 관리자이지만, 프로젝트 규모가 커질수록 몇 가지 근본적인 문제가 드러난다.

유령 의존성 (Phantom Dependency)

2장에서 다룬 평탄화(hoisting) 작업의 부작용이다. node_modules를 평탄화하면서, package.json에 직접 선언하지 않은 패키지를 코드에서 import하거나 require할 수 있게 된다.

// package.json에 선언하지 않은 패키지인데도 사용 가능
import something from "phantom-package";
문제설명
의도치 않은 의존직접 선언하지 않은 패키지에 의존하게 됨
갑작스러운 오류다른 의존성이 업데이트/삭제되면 유령 의존성도 사라질 수 있음
재현 불가능한 빌드개발 환경에서는 되지만 CI/CD에서 실패하는 경우 발생
⚠️

패키지 변경에 따라 사용 불가능해질 수 있다.

간접적으로 B 패키지를 사용하고 있었는데, A와 C가 버전업되면서 B에 대한 의존성을 제거했다고 가정하자. 이 경우 평탄화로 끌어올려졌던 B가 node_modules에서 사라지면서, B를 직접 선언하지 않고 사용하던 코드가 갑자기 동작하지 않게 된다.

⚠️

여전히 중첩된 구조가 필요하다.

서로 다른 버전이 필요한 경우가 있다. npm은 최상위 node_modules에 설치할 버전을 하나 선택하고, 나머지 버전들은 중첩된 구조로 설치한다. 특히 node_modules의 크기가 커질수록 호환성을 유지하기 어려워진다.

디스크 I/O 부하

npm은 동일한 패키지라도 프로젝트마다 node_modules개별 복사한다. Node.js는 모듈을 찾기 위해 현재 디렉터리부터 상위로 node_modules를 순회하므로, 구조가 깊어질수록 파일 시스템 접근이 반복되어 I/O 부하가 커진다.

require.resolve.paths("react");
// ["/Users/USER/private/arborist/test/node_modules",
//  "/Users/USER/private/arborist/node_modules",
//  "/Users/USER/private/node_modules",  ...]

실행 위치는 첫 번째 경로이지만, Node.js는 react를 찾을 때까지 여러 디렉터리를 탐색한다. 환경에 따라 탐색 결과가 달라질 수 있어, 실제 서비스에서 의도치 않은 문제를 일으킬 수 있다.

너무 거대한 node_modules

평탄화를 적용하더라도 node_modules의 크기는 매우 크다. 일반적인 React 프로젝트의 node_modules만 해도 수백 MB에 달하는 경우가 흔하다.

  • 불필요한 디스크 공간 사용 — 동일한 패키지가 프로젝트마다 중복 저장된다
  • 빌드 시간 증가 — 파일 수가 많을수록 번들러와 빌드 도구의 탐색 시간이 늘어난다
  • 유효성 검사와 성능 저하node_modules 내부의 무결성을 검증하거나 탐색하는 데 시간이 소요된다

변경에 취약한 락 파일

package-lock.json은 동일한 의존성 트리 재현, 설치 속도 최적화, 변경 사항 추적(diff) 등을 위해 존재한다. 하지만 의존성 하나만 바꿔도 diff가 수백~수천 줄에 달해, 사람이 변경 내용을 리뷰하기란 사실상 불가능에 가깝다.


Yarn: 신속하고 안정적인 패키지 관리자

Yarn의 탄생 배경

Facebook(현 Meta)이 npm 기반으로 프로젝트를 운영하면서 의존성 문제를 해결하기 위해 다방면으로 노력했으나, 근본적인 해결을 위해 자체 패키지 관리자를 구현하기에 이르렀다. 2016년에 Yarn이 처음 공개되었다.

yarn이 지적한 npm의 한계

  • 인터넷이 없는 환경에서는 npm을 사용할 수 없다. node_modules를 압축해서 버전관리에 포함시키는 방법을 사용했으나 의존성에 조금만 변경이 생겨나도 엄청난 양의 변경 사항이 발생해 부하가 심해졌다.
  • 당시 의존성을 완전히 고정하기 위해 shrinkwrap 이라는 명령어가 있었지만 이를 개발자가 의식적으로 실행하지 않으면 누락되기 쉬웠다.
  • 단일 의존성을 업데이트해도 관련되지 않은 의존성까지 유의적 버전 규칙을 기반으로 함께 업데이트되는 문제가 있었다. 이로 인해 변경 사항이 불필요하게 커지면서 관리가 어려워졌다.

Yarn의 버전 역사

버전이름주요 특징
1.xYarn Classicnpm과 동일한 node_modules 구조, 락 파일(yarn.lock) 도입
2.xYarn Berry (초기)Plug'n'Play(PnP) 도입, node_modules 제거 시도
3.xYarn BerryPnP 안정화, 플러그인 시스템 강화
4.xYarn Berry (최신)모든 공식 플러그인 기본 내장, 성능 최적화
6.x~ZPM (차세대)Rust로 재작성, 성능 극대화 (개발 중)
📌

2026년 2월 기준 최신 버전

Yarn Berry의 최신 안정 버전은 4.9.x이다. Yarn Classic(1.x)은 동결(frozen) 상태로, 보안 패치 외에는 더 이상 기능 개선이 이루어지지 않는다.

한편, Yarn 팀은 차세대 버전인 ZPM(v6 이상)을 별도 저장소(yarnpkg/zpm)에서 개발하고 있다. ZPM의 가장 큰 특징은 코어 엔진을 Rust로 재작성하고 있다는 점이다 (Rust 54.9%, TypeScript 27.8%, JavaScript 16.0%). SWC, Turbopack, Biome 등 JavaScript 도구들이 Rust로 재작성되어 성능 향상을 이루고 있는 흐름과 맥을 같이한다.

Yarn의 전체 버전 계보를 정리하면:

  • Classic: v1 이하
  • Berry: v2 ~ v5
  • ZPM: v6 이상 (개발 중)

yarn.lock

Yarn이 생성하는 락 파일이다. npm의 package-lock.json과 유사한 역할을 하지만 몇 가지 차이점이 있다.

  • package.json보다 더 정확한 의존성 정보를 기록 -> 버전 범위가 불명확한 package.json의 한계를 보완하기 위해 설계된 것
# yarn.lock 예시 (Yarn Berry 형식)
"react@npm:^18.2.0":
  version: 18.2.0
  resolution: "react@npm:18.2.0"
  dependencies:
    loose-envify: "npm:^1.1.0"
  checksum: 88e38092da8...
특징yarn.lockpackage-lock.json
형식독자적인 형식 -> YAML 기반 커스텀 형식(yarn berry에서 도입)JSON
자동 수정yarn install 시 자동 갱신npm install 시 자동 갱신
체크섬포함 (무결성 검증)포함 (integrity 필드)
결정적 설치보장npm@5 이후 보장
💡

yarn.lock은 반드시 버전 관리 시스템(Git)에 커밋해야 한다. 이 파일이 있어야 팀원 모두가 동일한 의존성 트리를 재현할 수 있다.

📌

Yarn의 레지스트리 전략: 리버스 프락시에서 CNAME으로

Cloudflare는 전 세계 수백 개 도시에 서버(엣지 노드)를 두고 있는 CDN 서비스이다. 사용자와 물리적으로 가까운 서버에서 데이터를 전달해주므로 응답 속도가 빨라진다.

초기 npm 레지스트리는 Fastly라는 CDN을 통해 패키지를 제공했는데, Yarn 팀은 이 속도가 충분하지 않다고 판단했다. 그래서 Yarn은 Cloudflare를 리버스 프락시로 사용해 자체 레지스트리(registry.yarnpkg.com)를 제공했다.

[Before] Fastly CDN 경유 (느림)
개발자 ─────────────────→ npm 레지스트리 (Fastly)
 
[Yarn] Cloudflare 리버스 프락시 도입
개발자 Cloudflare (캐시) → npm 레지스트리
         캐시 히트 바로 응답
 
[After] npm도 Cloudflare 도입
개발자 Cloudflare npm 레지스트리 빨라짐!
 
 Yarn은 리버스 프락시를 끄고,
  registry.yarnpkg.com을 npm 레지스트리의 CNAME(별칭)으로 전환

npm 자체가 Cloudflare를 도입하면서 속도 차이가 없어졌고, Yarn은 별도 프락시 없이 CNAME(도메인 별칭)으로 npm 레지스트리를 가리키도록 변경했다.

Plug'n'Play (PnP)

📌

이하 내용은 Yarn Berry@4.2.2 기준으로 소개한다. 이후 등장하는 "Yarn"은 모두 Yarn Berry를 의미한다.

Yarn Berry의 가장 혁신적인 기능이다. node_modules 디렉터리를 완전히 제거하고, 의존성 정보를 .pnp.cjs 파일 하나에 담는 방식이다.

기존 방식의 문제

기존 node_modules 방식에서 Node.js가 패키지를 찾는 과정:

  1. 현재 디렉터리의 node_modules에서 탐색
  2. 없으면 상위 디렉터리의 node_modules로 이동
  3. 루트까지 반복 (파일 시스템 I/O가 반복적으로 발생)

이 방식은 탐색이 비효율적이고, 직접 의존성으로 명시하지 않은 패키지도 접근할 수 있는 유령 의존성 문제를 야기한다.

PnP의 동작 원리

yarn start를 실행하면 내부적으로 다음과 동일한 명령이 실행된다:

node -r ./.pnp.cjs index.js
  • -r 플래그: --require의 줄임말로, 프로그램 실행 전에 지정한 모듈을 사전 로드(preload) 하도록 지시한다.
  • 즉, index.js가 실행되기 전에 .pnp.cjs가 먼저 로드되어 모듈 해석 시스템을 세팅한다.

.pnp.cjs란?

PnP 모드의 핵심 파일이다. yarn.lock과 유사하게 의존성 구조가 담겨 있고, 추가로 각 패키지가 실제 어느 위치에 저장되어 있는지까지 명시되어 있다.

.pnp.cjs 의존성 (패키지 이름 실제 위치)
.yarn/cache/ 패키지들이 zip 형태로 저장

그렇다면 require('react')처럼 일반적인 모듈 호출 코드가, 어떻게 zip 파일 내부의 패키지를 찾아갈 수 있을까?

핵심은 require 함수 자체를 변조하지 않는다는 점이다. 대신 require가 모듈 경로를 찾을 때 사용하는 Module._resolveFilename 메서드를 변조한다.

[기존 Node.js]
require('react')
 Module._resolveFilename('react')
 node_modules/react 탐색 (디렉터리 순회)
 
[PnP 모드]
require('react')
 Module._resolveFilename('react')   .pnp.cjs가 변조
 .pnp.cjs 맵에서 즉시 위치 조회
 .yarn/cache/react-xxx.zip 에서 로드

이렇게 탐색을 단일 경로로 한정하면 npm의 두 가지 문제를 동시에 해결할 수 있다:

  1. 비효율적 탐색: 디렉터리를 순회하지 않고 맵에서 즉시 조회
  2. 유령 의존성: .pnp.cjs에 등록된 패키지만 접근 가능하므로 원천 차단

글로벌 캐시

Yarn은 한 번 다운로드한 패키지를 글로벌 캐시 폴더에 저장해 둔다. 이후 같은 패키지가 필요하면 레지스트리에 다시 접속할 필요 없이 캐시에서 가져온다.

 설치:  레지스트리 다운로드 .yarn/berry/cache/ .zip 저장
재설치:   .yarn/berry/cache/ 에서 바로 가져옴 (네트워크 불필요)
  • 패키지들은 모두 .zip 압축 형태로 저장된다
  • 불안정한 레지스트리 접속에 의존하지 않고, 로컬 캐시 우선 전략을 사용한다
  • 설치 과정에서 Fetch → Completed 사이의 네트워크 단계를 생략할 수 있다

비교 정리

항목node_modules 방식PnP 방식
모듈 탐색디렉터리 순회 (느림)맵 조회 (빠름)
디스크 사용량큼 (파일 개별 저장)작음 (zip 압축 저장)
유령 의존성발생 가능원천 차단
설치 속도파일 복사 필요zip 다운로드만으로 완료

PnP모드 끄는법 : .yarnrc.yml 파일에서 nodeLinker: node_modules 설정추가

⚠️

PnP의 한계

PnP는 Node.js의 기본 모듈 해석 방식을 변경하기 때문에, node_modules의 존재를 가정하는 레거시 도구나 라이브러리가 정상 동작하지 않을 수 있다. 이를 위해 Yarn Berry는 기존 방식으로 폴백할 수 있는 옵션을 제공한다.

# .yarnrc.yml - 기존 node_modules 방식으로 폴백
nodeLinker: node-modules

Zero Install

PnP를 기반으로 한 전략으로, 의존성을 Git 저장소에 직접 커밋하는 방식이다.

동작 원리

PnP 모드에서 의존성은 .yarn/cache/ 디렉터리에 zip 파일로 저장된다. 이 zip 파일들은 용량이 상대적으로 작기 때문에, Git 저장소에 커밋해도 크게 부담이 되지 않는다.

프로젝트/
├── .yarn/
   ├── cache/ 의존성 zip 파일들 (Git에 커밋)
   └── releases/ Yarn 바이너리
├── .pnp.cjs 의존성 (Git에 커밋)
├── .yarnrc.yml
└── package.json

장점

장점설명
yarn install 불필요git clone 직후 바로 프로젝트 실행 가능
CI/CD 속도 향상설치 과정 자체가 없으므로 빌드 파이프라인이 빨라짐
네트워크 독립폐쇄적인 환경이나 레지스트리에 접근할 수 없는 환경에서도 동작
완벽한 재현성레지스트리 상태에 의존하지 않으므로 각 버전별 환경을 정확히 재현 가능

단점

단점설명
저장소 크기 제한Git은 하나의 저장소에 약 2GB 크기 제한이 있어, 의존성이 많으면 사용이 어려움
.git 폴더 비대화커밋 히스토리와 내용을 .git에 보관하므로, 관리 대상이 많아질수록 크기가 기하급수적으로 증가
네이티브 패키지 미지원바이너리 파일을 포함하는 네이티브 패키지는 zip으로 관리할 수 없음
💡

폐쇄된 환경에서 설치가 필요하거나, 의존성 변경이 비교적 적고 엄격한 관리를 원하는 프로젝트에 적합하다. 반면, 의존성이 매우 많은 대규모 프로젝트에서는 저장소 크기 문제로 적합하지 않을 수 있다.

플러그인 시스템

Yarn Berry는 플러그인 아키텍처를 채택했다. 핵심 기능 외의 부가 기능을 플러그인으로 분리하여, 필요한 기능만 선택적으로 사용할 수 있다.

플러그인을 통해 가능한 작업:

  • 의존성 버전 결정 방식 변경 — 패키지 해석 로직을 커스터마이징
  • 패키지 fetcher 추가 — 새로운 프로토콜이나 소스에서 패키지를 가져오도록 확장
  • 새로운 명령어 추가yarn CLI에 커스텀 명령어 등록
  • 새로운 이벤트 등록 — 설치 과정의 특정 시점에 훅을 걸 수 있음
# 플러그인 추가 (Yarn 3.x 이전)
yarn plugin import @yarnpkg/plugin-typescript
 
# Yarn 4.x부터는 모든 공식 플러그인이 기본 내장

주요 공식 플러그인:

플러그인기능
plugin-typescriptTypeScript 패키지 설치 시 @types/*를 자동으로 추가
plugin-workspace-tools워크스페이스 관련 고급 명령어 제공
plugin-interactive-tools의존성을 대화형으로 업그레이드
plugin-version워크스페이스 내 버전 관리 자동화

대표적인 커뮤니티 플러그인:

플러그인기능
yarn-plugin-outdated업데이트 가능한 의존성 목록 확인
yarn-plugin-enginesengines 필드 기반 Node.js 버전 호환성 검사
yarn-plugin-licenses설치된 패키지들의 라이선스 정보 확인
yarn-plugin-nolyfill불필요한 polyfill 패키지를 빈 모듈로 대체하여 경량화
📌

Yarn 4.x부터는 모든 공식 플러그인이 기본 번들에 포함되어, 별도로 yarn plugin import를 실행할 필요가 없어졌다.

Yarn이 생성하는 파일과 디렉터리

파일/디렉터리설명Git 커밋 여부
yarn.lock의존성 락 파일O
.yarnrc.ymlYarn 설정 파일O
.pnp.cjsPnP 의존성 맵O
.pnp.loader.mjsESM용 PnP 로더O
.yarn/cache/패키지 zip 캐시Zero Install 시 O
.yarn/releases/Yarn 바이너리O
.yarn/plugins/커스텀 플러그인O
.yarn/unplugged/zip으로 관리 불가한 네이티브 패키지O
.yarn/patches/yarn patch 명령으로 생성한 패키지 패치 파일O
.yarn/sdks/IDE 연동을 위한 SDK (TypeScript, ESLint 등)O
.yarn/install-state.gz설치 상태 캐시 (설치 최적화용)X
💡

PnP 환경에서 IDE SDK가 필요한 이유

TypeScript(tsc)나 ESLint 같은 도구는 내부적으로 자체 모듈 해석 로직을 사용한다. 이 로직은 node_modules 디렉터리가 존재한다고 가정하고, 그 안에서 패키지를 탐색한다.

하지만 PnP 모드에서는 node_modules가 없고, 패키지가 .yarn/cache/zip 파일로 저장되어 있다. Node.js 자체는 .pnp.cjsModule._resolveFilename을 변조해서 정상 동작하지만, TypeScript와 ESLint는 Node.js의 모듈 시스템을 거치지 않고 직접 파일 시스템을 탐색하기 때문에 패키지를 찾지 못한다.

.yarn/sdks/에 저장되는 SDK는 이 도구들에게 PnP의 패키지 위치를 알려주는 패치를 제공한다.

# VS Code용 SDK 설치
yarn dlx @yarnpkg/sdks vscode

pnpm: 디스크 공간 절약과 설치 속도의 혁신

pnpm의 탄생 배경

pnpm(performant npm)은 npm과 Yarn Classic이 가진 한계를 극복하기 위해 만들어졌다. 패키지 설치 시 독창적인 방식을 사용하여 중복 저장을 방지하고, 디스크 사용량을 대폭 줄이는 동시에 설치 속도를 향상시킨다. 대규모 프로젝트나 모노레포 환경에서 특히 유용하다.

문제점npmYarn Classic
디스크 낭비프로젝트마다 패키지 복사npm과 동일
유령 의존성평탄화로 인해 발생npm과 동일
설치 속도느림npm보다 빠르지만 한계 있음

pnpm은 이 문제들을 하드 링크, 심볼릭 링크, 콘텐츠 어드레서블 스토리지라는 세 가지 핵심 개념으로 해결한다.

📌

2026년 2월 기준 최신 버전

pnpm의 최신 버전은 10.x이다. pnpm v10에서는 보안 기본 설정(Security by Default) 이 가장 큰 변화로, preinstall/postinstall 스크립트를 기본적으로 실행하지 않도록 변경되어 공급망 공격(supply chain attack) 벡터를 차단했다.

pnpm-lock.yaml

pnpm이 사용하는 락 파일이다. npm의 package-lock.json, Yarn의 yarn.lock에 대응한다. 기존 락 파일들의 가독성 부족, diff 비교의 어려움, 비대한 크기 등의 문제를 해결하기 위해 간결한 구조로 설계되었다.

특징pnpm-lock.yamlyarn.lock / package-lock.json
의존성 위치importers 섹션 최상단에 위치 → 한눈에 파악 가능구조 내부에 분산
패키지 정보packages 섹션에서 버전과 의존성을 직관적으로 나열중첩된 구조
버전 관리최상위 허용 버전으로 고정버전 범위로 관리
파일 크기간결하여 파싱 속도가 빠름상대적으로 비대
# pnpm-lock.yaml 예시
lockfileVersion: "9.0"
settings:
  autoInstallPeers: true
importers:
  .:
    dependencies:
      react:
        specifier: ^18.2.0
        version: 18.2.0
packages:
  react@18.2.0:
    resolution: { integrity: sha512-... }
    dependencies:
      loose-envify: 1.4.0
💡

pnpm-lock.yaml은 npm의 package-lock.json과 마찬가지로 반드시 Git에 커밋해야 한다. pnpm ci 명령어는 이 파일을 기반으로 정확한 의존성 트리를 재현한다.

글로벌 스토어와 하드 링크

pnpm의 가장 핵심적인 차별점이다. 패키지를 프로젝트마다 복사하는 대신, 글로벌 스토어(~/.pnpm-store)에 한 번만 저장하고 하드 링크로 연결한다.

하드 링크란?

파일 시스템에서 동일한 데이터 블록을 가리키는 또 다른 이름이다. 심볼릭 링크와 달리, 하드 링크는 원본 파일과 완전히 동등한 지위를 가진다.

특성심볼릭 링크하드 링크
원본 삭제 시링크가 깨짐 (dangling link)영향 없음 (데이터 유지)
디스크 사용량경로 문자열만 저장추가 공간 0 (같은 inode)
디렉터리 링크가능불가능 (파일만 가능)
파티션 간가능불가능 (같은 파티션만)

동작 방식

~/.pnpm-store/                    ← 글로벌 스토어 ( 번만 저장)
├── react@18.2.0/
   ├── index.js
   └── ...
└── lodash@4.17.21/
    ├── lodash.js
    └── ...
 
프로젝트A/node_modules/.pnpm/react@18.2.0/
├── index.js  ──────── 하드 링크 ──────── ~/.pnpm-store/react@18.2.0/index.js
└── ...
 
프로젝트B/node_modules/.pnpm/react@18.2.0/
├── index.js  ──────── 하드 링크 ──────── ~/.pnpm-store/react@18.2.0/index.js
└── ...
💡

10개의 프로젝트가 동일한 react@18.2.0을 사용해도, 디스크에는 단 하나의 복사본만 존재한다. pnpm 공식 문서에 따르면, 여러 프로젝트에 걸쳐 사용할 때 최대 70%의 디스크 공간 절약 효과가 있다.

장점설명
디스크 공간 절약동일 패키지는 글로벌 스토어에 한 번만 저장, 프로젝트마다 복사하지 않음
성능 향상상위 폴더를 순회하며 node_modules를 찾는 대신, 하드 링크로 즉시 패키지 접근
일관성 유지와 안전성모든 프로젝트가 동일한 파일(inode)을 참조하므로 버전 불일치가 발생하지 않음

평탄화되지 않은 node_modules

pnpm은 npm처럼 node_modules평탄화하지 않는다. 대신 각 패키지가 정확히 자신의 의존성만 접근할 수 있도록 다음과 같은 전략을 사용한다:

  1. ./node_modules에는 직접 의존성만package.jsondependencies에 선언한 패키지만 위치시킨다
  2. 실제 파일은 .pnpm에 저장 — 직접 의존성의 실제 파일은 .pnpm/ 디렉터리에 하드 링크로 연결한다
  3. 하위 의존성도 .pnpm에 평탄화 — 각 패키지가 필요로 하는 하위 의존성은 ./node_modules/.pnpm/에 평탄화하여 설치한다
  4. 하위 의존성 간 참조는 심볼릭 링크 — 각 패키지의 의존성은 .pnpm/ 내에서 심볼릭 링크로 참조한다
node_modules/
├── .pnpm/ 실제 패키지가 저장되는
   ├── react@18.2.0/
   └── node_modules/
       ├── react/ 하드 링크 (글로벌 스토어 여기)
       └── loose-envify/ 심볼릭 링크 (react의 의존성)
   └── next@14.0.0/
       └── node_modules/
           ├── next/ 하드 링크 (글로벌 스토어 여기)
           └── react/ 심볼릭 링크 (next의 의존성)
├── react -> .pnpm/react@18.2.0/node_modules/react 심볼릭 링크
└── next -> .pnpm/next@14.0.0/node_modules/next 심볼릭 링크
장점설명
유령 의존성 차단package.json에 선언하지 않은 패키지에 접근 불가
엄격한 의존성 관리각 패키지가 자신의 의존성만 볼 수 있음
정확한 의존성 트리평탄화로 인한 버전 충돌 문제 없음
💡

.pnpm/ 안에 node_modules/가 또 있는 이유

위 구조를 보면 .pnpm/react@18.2.0/ 안에 또 node_modules/가 존재한다. 왜 이런 구조가 필요할까?

과거 npm의 평탄화된 node_modules 시스템에서는 선언하지 않은 패키지도 접근할 수 있었다. 일부 패키지들은 이 구조에 의존하여, dependencies에 선언하지 않은 패키지를 직접 require하는 코드를 작성하기도 했다.

[문제 상황]
패키지 A의 코드:
  require('lodash')  ← A는 lodash를 dependencies에 선언하지 않음
                       npm에서는 평탄화 덕분에 우연히 동작했음
 
[pnpm에서]
A가 lodash를 선언하지 않았으므로 엄격 모드에서는 찾을 없음 에러!

pnpm은 이런 잘못된 의존성 참조를 가진 레거시 패키지를 위해, 루트에서 직접 선언한 의존성을 제외한 모든 패키지를 .pnpm/node_modules/에 평탄화하여 배치한다. 이렇게 하면 선언하지 않은 패키지를 참조하는 코드도 .pnpm/node_modules/에서 해당 패키지를 찾을 수 있어 호환성이 유지된다.

node_modules/.pnpm/
├── node_modules/ 모든 패키지가 평탄화된 폴백 영역
   ├── lodash/ 여기서 찾을 있음 (호환성 보장)
   ├── react/
   └── ...
├── react@18.2.0/ 패키지의 격리된 영역
   └── node_modules/
       └── react/
└── ...

즉, 프로젝트 루트의 node_modules/는 엄격하게 관리하면서, .pnpm/node_modules/를 폴백으로 제공하여 레거시 호환성과 엄격한 의존성 관리를 동시에 달성하는 것이다.

콘텐츠 어드레서블 스토리지 (CAS)

pnpm의 글로벌 스토어는 콘텐츠 어드레서블 스토리지(Content-Addressable Storage) 방식으로 관리된다. 파일의 내용(content)을 기반으로 해시값을 생성하고, 이 해시값을 주소(address)로 사용하여 저장한다.

~/.pnpm-store/v3/files/
├── 00/
   ├── 1a2b3c4d5e6f... 파일 내용의 해시가 파일명
   └── 7a8b9c0d1e2f...
├── 01/
   └── ...
└── ff/
    └── ...

CAS의 장점

장점설명
중복 제거내용이 동일한 파일은 해시가 같으므로 한 번만 저장
무결성 보장해시로 파일 내용 검증이 가능하여 변조 감지
버전 간 효율lodash@4.17.204.17.21 사이에 변경되지 않은 파일은 공유
📌

Git도 내부적으로 CAS를 사용한다. Git의 objects/ 디렉터리에 저장되는 blob, tree, commit 객체들은 모두 SHA-1 해시를 기반으로 저장되는데, pnpm의 스토어도 이와 유사한 원리이다.

Copy-on-Write (CoW)

pnpm은 글로벌 스토어의 파일을 프로젝트에 연결할 때 기본적으로 하드 링크를 사용하지만, 파일 시스템이 지원하는 경우 Copy-on-Write(CoW), 즉 reflink을 사용할 수도 있다.

CoW는 파일을 복사할 때 실제 데이터 블록을 즉시 복제하지 않고, 원본과 동일한 디스크 블록을 공유하다가 한쪽이 수정될 때 비로소 해당 블록만 분리 복사하는 기법이다.

[일반 복사 (copy)]
원본 파일 ──── 블록 A, B, C     (3개 블록 사용)
복사 파일 ──── 블록 A', B', C'  (3개 블록 추가 → 총 6개)
 
[Copy-on-Write (reflink)]
원본 파일 ──┐
            ├── 블록 A, B, C    (3개 블록만 사용)
복사 파일 ──┘
 
↓ 복사 파일의 블록 B를 수정하면...
 
원본 파일 ──── 블록 A, B, C     (원본 유지)
복사 파일 ──── 블록 A, B", C    (B만 새로 할당 → 총 4개)
              ↑        ↑
              공유      새 블록
하드 링크 vs Copy-on-Write
비교 항목하드 링크Copy-on-Write (reflink)
동작 방식원본과 같은 inode를 공유원본과 같은 디스크 블록을 공유 (별도 inode)
수정 시 영향한쪽을 수정하면 다른 쪽도 변경수정 시 해당 블록만 분리되어 독립적
안전성의도치 않은 수정이 전파될 수 있음각 파일이 독립적으로 관리되어 안전
디스크 사용추가 공간 없음 (완전 공유)수정 전에는 추가 공간 없음, 수정 시 변경분만 차지
파일 시스템대부분의 파일 시스템에서 지원APFS, Btrfs, XFS 등 일부 파일 시스템만 지원
💡

pnpm은 --package-import-method 옵션으로 파일 연결 방식을 제어한다. 기본값은 auto로, CoW를 먼저 시도하고 실패하면 하드 링크로 폴백한다. 직접 지정하려면 copy, hardlink, clone, clone-or-copy 중에서 선택할 수 있다. macOS(APFS)에서는 CoW가 지원되므로 대부분 자동으로 clone 방식이 적용된다.

심볼릭 링크로 구성된 node_modules

pnpm의 node_modules 구조는 두 단계의 링크로 구성된다:

  1. 프로젝트 루트 node_modules/.pnpm/ 디렉터리의 해당 패키지로의 심볼릭 링크
  2. .pnpm/ 내부의 패키지 파일 → 글로벌 스토어로의 하드 링크
[프로젝트 node_modules]

 심볼릭 링크

[.pnpm 가상 스토어]

 하드 링크

[~/.pnpm-store 글로벌 스토어]

이 구조 덕분에:

  • 프로젝트에서는 require('react')처럼 일반적인 방식으로 패키지를 사용 가능
  • 내부적으로는 디스크 공간을 최소화
  • node_modules를 사용하므로 PnP와 달리 기존 도구와의 호환성 문제가 없음

[장점]

  • 의존성 격리 : 각 패키지는 자체 node_modules를 가지고 있어 다른 의존성과 충돌하지 않는다.
  • 버전관리의 용이성 : 각 패키지 버전이 독립된 공간을 차지하므로 여러 버전을 관리하기가 용이하다.
  • 평탄화된 node_modules의 위험성 회피: 각 의존성은 실제로 자신이 참조하는 의존성에만 접근할 수 있어 평탄화된 node_modules로 인해 발생할 수 있는 문제를 피할 수 있다.

Plug'n'Play 지원

pnpm v9부터 Yarn Berry의 PnP 모드도 지원한다.

# .npmrc
node-linker=pnp

다만, pnpm의 기본 심볼릭 링크 방식이 이미 유령 의존성을 차단하고 디스크 효율성도 뛰어나기 때문에, pnpm에서 PnP를 사용하는 경우는 드물다.


npm, Yarn, pnpm 비교

워크스페이스

세 패키지 관리자 모두 모노레포를 위한 워크스페이스 기능을 지원한다.

항목npmYarn Berrypnpm
설정 위치package.jsonworkspacespackage.jsonworkspacespnpm-workspace.yaml
도입 시기npm@7Yarn 1.xpnpm 초기부터
호이스팅기본 활성화설정 가능기본 비활성화
패키지 간 참조심볼릭 링크심볼릭 링크 / PnP심볼릭 링크
필터 실행--workspaceyarn workspace, foreachpnpm --filter
# pnpm-workspace.yaml 예시
packages:
  - "packages/*"
  - "apps/*"

하위 패키지 스크립트 실행 (필터 실행)

모노레포에서는 루트가 아닌 특정 하위 패키지의 스크립트를 실행해야 하는 경우가 많다. 각 패키지 관리자마다 이를 위한 명령어가 다르다.

npm

# 특정 패키지의 스크립트 실행
npm run build --workspace=packages/ui
 
# 줄임 표현
npm run build -w packages/ui
 
# 모든 워크스페이스에서 실행
npm run build --workspaces
 
# 스크립트가 없는 패키지는 건너뛰기
npm run build --workspaces --if-present

Yarn Berry

# 특정 패키지의 스크립트 실행 (패키지 이름 기준)
yarn workspace @my-app/ui build
 
# 모든 워크스페이스에서 순차 실행
yarn workspaces foreach run build
 
# 모든 워크스페이스에서 병렬 실행
yarn workspaces foreach -p run build
 
# 의존성 순서를 고려하여 실행 (토폴로지 정렬)
yarn workspaces foreach -pt run build

pnpm

# 패키지 이름으로 필터링
pnpm --filter @my-app/ui build
 
# 디렉터리 경로로 필터링
pnpm --filter ./packages/ui build
 
# 글로브 패턴으로 여러 패키지 실행
pnpm --filter "./packages/*" build
 
# 특정 패키지와 그 의존성까지 포함하여 실행
pnpm --filter @my-app/ui... build
 
# 변경된 패키지만 실행 (Git diff 기반)
pnpm --filter "...[origin/main]" build
 
# 모든 워크스페이스에서 실행
pnpm -r build
💡

pnpm의 --filter글로브 패턴, 디렉터리 경로, 패키지 이름, Git diff 기반 변경 감지 등 다양한 방식을 지원하여 세 패키지 관리자 중 가장 유연하다. 특히 ... 접미사로 의존성 그래프를 따라 관련 패키지까지 함께 실행할 수 있어, 대규모 모노레포에서 변경된 부분만 빌드하거나 테스트하는 데 유용하다.

pnpm은 워크스페이스에 필요한 기본 기능을 간결하게 제공하는 한편, 버저닝과 같은 복잡한 기능은 changesets나 Rush같은 서드파티 라이브러리르 사용하는 것을 권장한다. pnpm을 기반으로 한 워크스페이스를 운영하려면 이러한 서드파티 라이브러리를 함께 설치해 사용하는 것이 좋다

명령어 비교

작업npmYarn Berrypnpm
의존성 설치npm installyarn installpnpm install
패키지 추가npm install <pkg>yarn add <pkg>pnpm add <pkg>
개발 의존성 추가npm install -D <pkg>yarn add -D <pkg>pnpm add -D <pkg>
전역 설치npm install -g <pkg>yarn global add <pkg>pnpm add -g <pkg>
패키지 제거npm uninstall <pkg>yarn remove <pkg>pnpm remove <pkg>
스크립트 실행npm run <script>yarn <script>pnpm <script>
클린 설치npm ciyarn install --immutablepnpm install --frozen-lockfile
캐시 정리npm cache cleanyarn cache cleanpnpm store prune
보안 점검npm audityarn npm auditpnpm audit
의존성 업데이트npm updateyarn uppnpm update
대화형 업데이트-yarn upgrade-interactivepnpm update -i

벤치마크 테스트

2024년 11월 기준 (책 기준)

npm은 특정 상황을 제외하면 다른 패키지 관리자에 비해 현저히 느리다. Yarn Classic 또한 대부분의 경우 pnpm이나 Yarn Berry에 비해 느리며, Yarn Classic은 보안 패치 외에는 특별한 기능 개선이 이루어지지 않고 있다.

대부분의 경우 Yarn Berry와 pnpm이 가장 빠른 성능을 보였으며, 그중에서도 Yarn Berry가 가장 뛰어난 성능을 나타냈다. pnpm이 하드 링크나 CoW(Copy-on-Write) 작업이 필요한 반면, Yarn Berry는 필요한 의존성을 다운로드하면서 동시에 미리 준비된 .pnp.cjs 파일에 의존성 그래프를 JSON 형식으로 생성하기만 하면 되기 때문이다.

2026년 기준 최신 벤치마크

📌

2026년 1월 벤치마크 결과 (pnpm 공식 벤치마크 및 커뮤니티 테스트 종합)

2026년 1월 기준 벤치마크(npm 11.x, Yarn 4.x, pnpm 10.x)에서는 상황에 따라 결과가 다소 달라졌다:

Cold Install (캐시 없이 처음 설치):

  • Yarn Berry (PnP 모드) 가 가장 빠름 — node_modules 트리를 생성할 필요가 없기 때문
  • pnpm이 근소한 차이로 2위
  • npm이 가장 느림

Warm Install (캐시가 있는 재설치):

  • pnpm이 가장 빠름 — 글로벌 스토어에서 하드 링크만 생성하면 되므로
  • Yarn Berry가 근소한 차이로 2위
  • npm이 가장 느림

CI/CD 환경:

  • pnpm이 종합적으로 가장 안정적인 성능
  • 글로벌 스토어 재활용이 가능한 환경에서 특히 유리

디스크 사용량:

  • pnpm이 압도적으로 적음 (하드 링크 + CAS)
  • Yarn Berry PnP가 2위 (zip 압축)
  • npm이 가장 많음

종합하면, Yarn Berry(PnP)와 pnpm이 여전히 양강 구도이다. Cold install에서는 Yarn Berry가, 캐시 활용과 디스크 효율에서는 pnpm이 우위에 있다. npm은 많이 개선되었지만 여전히 성능 면에서 뒤처진다.

⚠️

CI/CD에서 pnpm 글로벌 스토어의 한계

pnpm의 Warm Install 속도 이점은 글로벌 스토어(~/.pnpm-store)가 유지되는 환경에서만 유효하다. AWS Amplify, Vercel, Netlify 같은 서비스는 빌드마다 새로운 컨테이너를 생성하므로, 글로벌 스토어가 매번 비어 있는 상태에서 시작한다.

이 경우 pnpm도 패키지를 처음부터 다운로드해야 하므로 Warm Install의 이점이 사라진다. 글로벌 스토어를 활용하려면 CI/CD 플랫폼의 캐시 메커니즘을 별도로 설정해야 한다.

플랫폼캐시 방법
GitHub Actionsactions/setup-nodecache: 'pnpm' 옵션
GitLab CIcache 디렉티브로 스토어 경로 지정
AWS Amplify커스텀 캐시 경로 설정 ($HOME/.pnpm-store)
Vercelpnpm 자동 감지 후 내부 캐시 처리

캐시를 설정하면 두 번째 빌드부터 글로벌 스토어가 복원되어 Warm Install과 유사한 효과를 얻을 수 있다. 반면, 캐시 없이 Cold Install만 비교하면 node_modules 트리를 생성할 필요가 없는 **Yarn Berry(PnP)**가 가장 빠르다. CI/CD에서 pnpm의 강점을 살리려면 글로벌 스토어 캐싱 설정이 필수이다.

Bun의 패키지 관리 성능

Bun은 Zig 언어로 작성된 JavaScript 런타임으로, 자체 패키지 관리자를 내장하고 있다. bun install은 기존 패키지 관리자들과 비교했을 때 압도적인 설치 속도를 보여준다.

Bun이 빠른 이유
  • 네이티브 바이너리: JavaScript/Node.js 기반이 아닌 Zig + C로 작성되어 시스템 수준의 성능 발휘
  • 병렬 다운로드: HTTP 요청을 네이티브 수준에서 극도로 병렬화하여 처리
  • 심볼릭 링크 기반: pnpm과 유사하게 글로벌 캐시에서 하드 링크로 연결
  • 바이너리 락 파일: bun.lockb는 바이너리 형식으로, 파싱과 직렬화가 텍스트 기반 락 파일보다 빠름
벤치마크 비교 (2026년 기준)
시나리오npmYarn Berry (PnP)pnpmBun
Cold Install매우 느림빠름빠름가장 빠름
Warm Install느림빠름빠름가장 빠름
lockfile만 있을 때느림보통빠름가장 빠름
node_modules 재사용보통-빠름가장 빠름
Bun의 한계
한계설명
생태계 호환성Node.js API를 완벽히 구현하지 않아 일부 패키지가 동작하지 않을 수 있음
Windows 지원2025년까지 실험적 수준이었으며, 안정성이 충분히 검증되지 않음
바이너리 락 파일bun.lockb는 사람이 읽을 수 없어 diff 기반 코드 리뷰가 어려움
프로덕션 실적대규모 프로덕션 환경에서의 안정성 검증이 부족
모노레포 지원기본적인 워크스페이스는 지원하지만 pnpm이나 Yarn Berry 수준의 고급 기능은 부족
📌

Bun은 속도 면에서는 독보적이지만, 프로덕션 안정성과 생태계 호환성 측면에서 아직 npm, Yarn, pnpm을 완전히 대체하기는 어렵다. 개인 프로젝트나 프로토타이핑에서 먼저 도입해보고, 프로덕션 적용은 호환성을 충분히 검증한 후 판단하는 것이 좋다.

종합 비교표

항목npmYarn ClassicYarn Berrypnpm
설치 속도느림보통빠름빠름
디스크 효율낮음낮음높음 (PnP)매우 높음
유령 의존성발생발생차단 (PnP)차단
호환성최고높음보통 (PnP 한계)높음
모노레포 지원기본기본강력매우 강력
보안 기본값보통보통보통강력 (v10)
학습 곡선낮음낮음높음보통
시장 점유율1위 (~57%)2위 (~21%)(Berry 포함)3위 (~20%, 급성장)
💡

어떤 패키지 관리자를 선택해야 할까?

  • 입문자 / 소규모 프로젝트: npm (Node.js 기본 내장, 별도 설치 불필요)
  • 대규모 프로젝트 / 모노레포: pnpm 또는 Yarn Berry
  • 디스크 공간이 중요한 환경: pnpm
  • CI/CD 속도 최적화: Yarn Berry (PnP + Zero Install) 또는 pnpm

가장 중요한 것은 어떤 패키지 관리자를 선택하느냐보다, 팀 전체가 하나의 패키지 관리자를 일관되게 사용하는 것이다. 패키지 관리자를 수시로 바꾸는 것이 성능 차이보다 더 큰 비용을 초래한다.