mimizae 님의 블로그
pnpm이란? 본문

pnpm의 등장
pnpm은 "Performant NPM"의 약자로, npm과 yarn의 비효율을 개선하기 위해 성능과 효율성을 강조한 패키지 매니저이다!
🤔 npm과 yarn의 비효율?
npm과 yarn은 가장 널리 쓰이는 패키지 매니저이지만, 프로젝트 단위로 모든 의존성을 복사해서 보관한다는 구조적 한계를 가지고 있다.
이 구조는 프로젝트 수가 많아질수록 여러 가지 문제를 만들어낸다.
예를 들어, 로컬 PC에 React 프로젝트가 100개 있다고 가정해보자.
모든 프로젝트가 동일하게 react@18.2.0을 사용하고 있더라도, npm이나 yarn은 각 프로젝트의 node_modules 디렉토리 안에 React 라이브러리를 매번 새로 다운로드하고 저장한다.
결국 내용은 완전히 동일한 파일임에도 불구하고, 하드 디스크에는 React 파일이 100번 복사되어 존재하게 된다...!!! 😮💨
이러한 중복은 단순히 디스크 용량만 낭비하는 문제가 아니다. 프로젝트를 새로 생성하거나 의존성을 설치할 때마다 npm과 yarn은
- 패키지를 레지스트리에서 다운로드
- 압축 해제
- 파일을 node_modules에 복사
이 과정이 모든 프로젝트마다 반복된다. 프로젝트가 늘어날 수록 그대로 누적되며 설치 시간 증가와 디스크 I/O 부담으로 이어진다.
즉, npm과 yarn은 같은 의존성을 여러 프로젝트에서 사용하더라도 이를 공유하지 못하고 매번 새로 복사하는 방식을 사용하고 있으며, 이것이 pnpm이 등장하게 된 배경 중 하나이다.
그럼, pnpm은 어떻게 동작하길래 이런 비효율을 개선하는 걸까?
content-addressable file store 방식
pnpm의 핵심은 content-addressable file store라는 저장 방식에 있다.
이 방식은 말 그대로, 패키지를 프로젝트마다 따로 저장하는 대신 하나의 공용 저장소(전역 저장소)에 한 번만 저장하고 이를 여러 프로젝트에서 공유하는 구조이다.
pnpm은 사용자의 홈 디렉토리 아래에 ~/.pnpm-store라는 글로벌 저장소를 두고, 모든 패키지를 이곳에 관리한다!! 그리고 동일한 패키지라면, 아무리 많은 프로젝트에서 사용하더라도 단 한 번만 설치한다.
이때 중요한 개념이 바로 content-addressable이다. pnpm은 파일 이름이나 경로를 기준으로 패키지를 관리하지 않는다.
대신, 각 의존성 파일의 내용을 기준으로 해시(hash) 값을 생성하고, 이 해시 값을 통해 파일을 식별한다. 내용이 완전히 같은 패키지라면, 자연스럽게 같은 해시 값을 갖게 되고, pnpm은 이를 같은 패키지로 인식한다.
즉, 버전과 내용이 동일한 패키지는 여러 번 설치되지 않고, 글로벌 저장소에 하나만 존재하게 된다!!!!!!!!!!!

그렇다면 각 프로젝트의 node_modules는 어떻게 구성될까?!
pnpm은 실제 파일을 복사하지 않는다. 대신, 필요한 패키지를 가리키는 심볼릭 링크(symbolic link)를 생성해 node_modules에 연결한다. 프로젝트 입장에서는 마치 해당 패키지가 자신의 node_modules에 있는 것처럼 보이지만, 실제 파일은 모두 글로벌 저장소를 바라보고 있는 셈이다.
예를 들어, lodash를 사용하는 프로젝트가 100개 있다고 가정해보자!
npm이나 yarn을 사용한다면, lodash 패키지는 각 프로젝트의 node_modules에 100번 복사된다. 반면 pnpm을 사용하면 lodash는 ~/.pnpm-store에 딱 한 번만 설치되고, 100개의 프로젝트는 모두 그 lodash를 가리키는 심볼릭 링크만을 갖게 된다.
결과적으로 디스크 용량은 훨씬 적게 사용되고, 설치 속도 역시 크게 개선된다!!!
만약 특정 프로젝트만 다른 버전의 lodash를 사용한다면 어떻게 될까? 이 경우에도 기존 패키지가 덮어써지는 일은 없다!!!! pnpm은 다른 버전의 lodash를 새로운 해시 값으로 인식(내용이 달라졌으니...)하고, 글로벌 저장소에 해당 버전만 추가로 저장한다.
덕분에 각 프로젝트는 서로 영향을 받지 않으며, 버전 충돌이나 호환성 문제 없이 독립적으로 의존성을 관리할 수 있다.
pnpm의 장점
pnpm은 앞서 살펴봤듯, 디스크 공간을 효율적으로 사용할 수 있는 장점 말고도 많은 장점을 가지고 있다.
특히 기존 npm이나 yarn과 비교했을 때, pnpm은 설치 프로세스 자체를 최적화하여 npm 대비 약 2배 이상 빠른 성능을 보여준다.
이러한 속도 차이는 pnpm의 설치 방식에서 비롯된다.
npm은 의존성 관계를 고려하여 패키지를 하나씩 순차적으로 설치하는 반면, pnpm은 독립적인 패키지들을 동시에 병렬로 설치할 수 있다.
불필요하게 복잡한 설치 순서를 계산하지 않기 때문에, 전체 설치 시간이 크게 단축된다.
예를 들어, 프로젝트에 react, lodash, axios라는 서로 독립적인 패키지를 설치한다고 가정해보자!!
npm의 경우, react를 다운로드하고 설치한 뒤에야 lodash 설치가 시작되고, 그 다음에야 axios 설치가 진행된다. 결국 전체 소요 시간은 각 패키지 설치 시간이 모두 더해진 값이 된다.
반면 pnpm은 react, lodash, axios의 다운로드와 설치를 동시에 시작한다. 따라서 전체 설치 시간은 세 패키지 중 가장 오래 걸리는 하나의 설치 시간에 가깝게 수렴하게 된다. 패키지가 많아질수록 이 차이는 더욱 크게 체감된다.

💭 참고) 사진의 지표 설명 (lower is better)
1. clean install (클린 설치)
- 상황: 완전히 새로운 환경에서 프로젝트를 처음 시작하는 경우
- 조건: node_modules 폴더, package-lock.json과 같은 lock 파일, 그리고 패키지 매니저의 캐시까지 아무것도 없는 상태에서 install 명령어를 실행한다.
- 의미: 패키지 매니저의 순수한 다운로드 및 설치 성능을 측정하는 가장 기본적인 지표이다.
2. with lockfile (CI 서버 환경)
- 상황: Jenkins, GitHub Actions와 같은 CI/CD 서버에서 빌드를 실행하는 경우와 가장 유사함
- 조건: package-lock.json이나 pnpm-lock.yaml 같은 lock 파일은 있지만, 캐시나 node_modules는 없는 상태이다.
- 의미: lock 파일을 통해 정해진 버전의 패키지들을 얼마나 빨리 설치하는지 측정한다.
3. with cache, with lockfile (동료 개발자 환경)
- 상황: 팀 동료가 Git에서 프로젝트를 클론받아 처음 install 명령어를 실행하는 경우
- 조건: lock 파일이 있으며, 이전에 다른 프로젝트를 통해 설치했던 패키지들이 캐시에 남아있는 상태
- 의미: 캐시를 얼마나 효율적으로 활용하여 설치 속도를 높이는지 측정한다.
4. with cache, with lockfile, with node_modules (반복 설치)
- 상황: 이미 설치가 완료된 프로젝트에서 install 명령어를 다시 실행하는 경우
- 조건: lock 파일, 캐시, node_modules가 이미 모두 존재하는 완벽한 상태이다.
- 의미: 변경 사항이 없을 때, 얼마나 빠르게 "변경 없음"을 확인하고 작업을 마치는지를 측정한다. (이상적으로는 0초에 가까워야 한다.)
5. update (의존성 업데이트)
- 상황: 라이브러리 버전을 올리는 등 package.json을 수정한 후 install을 실행하는 경우
- 조건: 기존 설치 상태에서 package.json의 버전 정보만 변경된다.
- 의미: 변경된 의존성을 얼마나 빠르고 정확하게 감지하고 업데이트 하는지를 측정한다.
마지막으로 pnpm은 완벽하게 격리된 의존성 트리를 보장한다.
각 프로젝트는 자신이 명시적으로 의존하는 패키지에만 접근할 수 있으며, 의존성에 포함되지 않은 패키지를 우연히 참조하는 일이 발생하지 않는다. 이로 인해 흔히 말하는 phantom dependency(유령 의존성) 문제를 원천적으로 방지할 수 있고, 의존성 구조가 명확하고 예측 가능해진다!!!
💭 참고 - phantom dependency(유령 의존성)이란?
phantom dependency란?
phantom dependency(유령 의존성)란 package.json에 명시하지 않았는데도, 우연히 사용이 가능한 의존성을 말한다.
즉, '아니 나는 설치한 적 없는데 왜 import가 되지...?' 하는 무서운 상황을 의미... 😨
왜 이런 일이 생길까?
이 현상은 주로 npm이나 yarn의 node_modules 구조 때문에 발생한다.
npm과 yarn은 의존성을 설치할 때, 여러 패키지를 상위 node_modules에 평평하게(flat) 설치하려고 하기 때문에 이런 무서운 상황이 발생한다.
내 프로젝트
├─ node_modules
│ ├─ react
│ ├─ lodash
│ └─ axios
└─ src
axios가 내부적으로 follow-redirects라는 패키지에 의존한다고 가정해보자.
원래라면:
- follow-redirects는 axios의 의존성
- 내 프로젝트가 직접 사용하는 패키지는 아님!!!
하지만 npm과 yarn은 구조를 평평하게 만들면서 follow-redirects를 내 프로젝트의 node_modules 최상위에 올려버릴 수도 있다.
그 결과…
// package.json에는 없는데?...
import followRedirects from 'follow-redirects';
이 코드가 에러 없이 동작해버린다... 🤯
이게 왜 문제일까?
1. 코드가 의도한 대로가 아닌... 우연히 동작한다
로컬 개발 환경에서는 아무 문제 없이 실행되던 코드가, 다른 개발자의 PC나 CI 환경, 혹은 배포 서버에서는 갑자기 에러를 발생시키는 일이 흔하게 발생한다.
npm과 yarn의 node_modules 구조는 설치 순서나 패키지 버전, 환경에 따라 달라질 수 있기 때문에, 어떤 환경에서는 존재하던 의존성이 다른 환경에서는 존재하지 않을 수 있다. 그 결과, 동일한 코드임에도 실행 결과가 달라지는 불안정한 상황이 만들어진다.
2. 의존성 관계가 명확하지 않다
실제로는 axios가 내부적으로 사용하는 follow-redirects에 의존하고 있으면서도, 프로젝트의 package.json에는 해당 패키지가 전혀 명시되어 있지 않다.
코드를 처음 보는 개발자는 이 의존성을 알아채기 어렵고, 어떤 패키지가 실제로 필요한지 한눈에 파악하기도 힘들어진다. 시간이 지날수록 의존성 구조는 점점 불투명해지고, 유지보수 비용은 자연스럽게 증가하게 된다.
3. 패키지 업데이트 시 갑자기 깨진다
예를 들어 axios를 업데이트했을 때, 내부 의존성이 변경되거나 제거된다면, 지금까지 문제없이 동작하던 코드가 갑자기 깨질 수 있다. package.json에는 명시되지 않았기 때문에, 왜 에러가 발생하는지 원인을 추적하기도 어렵다.
흔히 “어제까지 잘 되던 코드가 왜 갑자기 안 되지?... 😇”
라는 상황이 바로 이런 phantom dependency에서 비롯된다.
그럼 pnpm은 왜 안전할까?
위에서 알아보았듯이, pnpm은 엄격한 node_modules 구조를 사용한다.
프로젝트는 자신이 명시한 의존성만 접근 가능하며 다른 패키지의 의존성은 직접 import 불가하다!!!
즉... 아래 예시는 바로 에러가 날 것!
// package.json에 없으면
import followRedirects from 'follow-redirects'; // ❌ 에러
덕분에 의존성 구조가 명확해지며, 유령 의존성이 원천 차단될 수 있다.
pnpm 사용 방법
pnpm의 기본적인 사용법은 npm이나 yarn과 거의 동일하다. 기존에 npm이나 yarn을 사용해봤다면, 별도의 학습 없이도 바로 사용할 수 있다.
- 프로젝트의 모든 의존성을 설치할 때는 다음과 같이 실행한다.
$ pnpm install // 전체 설치
- 특정 패키지를 새로 추가하고 싶다면 add 명령어를 사용한다.
$ pnpm add <pkg> : pkg(패키지) 설치
- 이 외에도 프로젝트 실행이나 개발 서버 구동과 같은 스크립트 명령어는 npm과 동일하게 사용할 수 있다.
$ pnpm <cmd> // ex) pnpm start, pnpm dev .. etc
참고 자료
[패키지 매니저] npm, yarn, pnpm, yarn-berry
Javascript 패키지 매니저 pnpm, pnpm이란?, pnpm사용이유, pnpm 설치방법, pnpm 사용방법
'FE' 카테고리의 다른 글
| Tanstack Query 초기 세팅 (0) | 2026.01.29 |
|---|---|
| swagger-typescript-api 적용기 (0) | 2026.01.28 |
| 에러 핸들링을 하는 방법(Suspense와 ErrorBoundary) (0) | 2026.01.02 |
| CI/CD란 뭘까? (0) | 2025.12.26 |
| 페이지네이션에 대해서 (0) | 2025.12.21 |