
요약
의존성 관리를 편하게 하기 위해 패키지 매니저를 사용합니다.
npm: 디스크 I/O 작업 많아 상대적으로 느리고, 호이스팅 전략으로 인해 의존성 충돌이 발생할 수 있습니다.
Yarn Berry: 병렬로 패키지를 설치하며 PnP 방식으로 패키지를 압축 및 캐싱해 Zero-install 전략을 활용할 수 있습니다.
pnpm: 패키지를 해시 기반으로 관리하고, 심볼릭 링크를 활용해 디스크 공간을 절약하며, 비평탄화 구조로 의존성을 명확하게 관리합니다.
패키지 매니저는 자바스크립트 프로젝트에서 패키지 관리를 위해 사용되는 도구입니다. 여기서 패키지는 라이브러리를 의미하며, 여러 라이브러리들을 문제없이 설치하고 관리하는 역할을 합니다.
가장 큰 이유는 패키지 관리를 통해 의존성 문제를 해결하기 위해서입니다. 패키지 관리가 필요한 이유는 패키지의 의존성을 효율적으로 관리하고, 일관성 있고 안정적인 개발 환경을 유지하기 위해서입니다.
프로젝트에서 사용하는 라이브러리들이 또 다른 라이브러리(의존성)에 의존하는 경우가 많습니다. 심지어 동일한 라이브러리를 사용하더라도 버전이 다를 수 있습니다. 만약 패키지 매니저가 없다면, 개발자가 한땀 한땀 의존성을 확인하고, 적절한 버전의 라이브러리를 찾아 설치 및 관리해야 할 것입니다.
이는 매우 비효율적이고, 의존성 충돌 문제가 발생할 가능성도 큽니다.
의존성이 제대로 관리되지 않으면 일관된 개발 환경을 유지하기 어렵습니다. 예를 들어, 몇 달 전에 설치한 라이브러리를 재설치하는 경우, 과거와 현재 버전이 맞지 않을 수 있습니다. 이로 인해 기존에 잘 동작하던 코드가 비정상적으로 동작할 수 있습니다.
이러한 문제들을 해결하기 위해 패키지 매니저가 등장했습니다.
각 패키지 매니저는 문제 해결 방식이나 성능 최적화 관점이 다릅니다.
npm은 기본적인 패키지 관리 기능을 제공하는 반면, Yarn은 성능 향상을 위해 병렬 설치를 지원하고, pnpm은 디스크 공간 절약에 중점을 두어 동일한 패키지를 여러 번 설치하지 않도록 관리합니다.
앞서 언급했듯이, npm은 Node.js 설치시 기본으로 제공되어 가장 범용적으로 사용되는 패키지 매니저로, package.json 파일을 기반으로 프로젝트의 의존성 트리를 구성합니다.
정리하면, package.json에 정의된 패키지들을 순차적으로 탐색하면서 node_modules 폴더에 설치합니다.
범용적이지만 해당 패키지 매니저는 문제가 있습니다.
각 패키지가 독립적으로 종속성을 관리하기 때문에, 서로 다른 패키지가 동일한 패키지의 다른 버전을 요구할 경우, 같은 패키지가 중복 설치될 수 있습니다.
예를 들어, A 패키지와 B패키지가 lodash의 동일한 버전을 사용해도, 각각 패키지 마다 동일한 버전의 lodash가 설치됩니다. 이로 인해 중복된 패키지들이 쌓이며, 용량이 커질 수 있습니다.
또 다른 문제로 각 패키지들이 의존된 패키지를 디스크 I/O 작업이 발생 하면서 탐색 및 처리하다보니 느립니다.
이 문제를 해결하기 위해 npm은 끌어올리기(hoisting)이라는 개념을 도입했습니다.
출처: toss tech node_modules로부터 우리를 구원해 줄 Yarn Berry
중복된 패키지를 최상위 레벨로 이동시켜 설치하는 방식입니다. 이를 통해 여러 패키지가 동일한 종속성을 요구할 경우, 하나의 상위 폴더에 패키지를 모아 중복을 방지하고, 디스크 사용량을 줄이는 효과가 있습니다.
하지만 끌어올리기 또 다른 문제가 발생했습니다.
유령 의존성(phantom Dependency)으로 패키지에서 직접 의존하고 있지 않은 라이브러리를 참조(import) 할 수 있는 현상입니다.
이해를 돕기 위해 다시 이미지를 확인하면
이미지의 오른쪽 트리와 같이 중복을 제거하여 동일 레벨에 라이브러리 들이 배치되어 있는데 D 라이브러리에는 B(2.0)의 라이브러리가 존재합니다. 그럼에도 불구하고 호이스팅으로 인해 B(1.0)의 라이브러리를 참조하는 오류가 발생할 수 있습니다.
이러한 방식이 근본적으로 한계가 있어, 최근에는 다른 관점으로 개선된 yarn과 pnpm 같은 패키지 매니저들이 더 많이 사용되는 추세인것으로 보여집니다.
Yarn(Yet Another Resource Negotiator)은 Facebook이 개발한 패키지 매니저로, npm의 의존성을 설치 속도와 안정성 문제를 개선하기 위해 만들어졌습니다.
초기 yarn은 package.json 파일을 분석해 패키지들을 캐시 공간에 압축(zip)하여 설치하고 node_modules 폴더에 압축 해제후 복사하는 방식으로 동작했습니다. 그래도 npm과는 다르게 병렬로 설치하다보니 속도는 빨랐지만 복사가 되어 중복이 생기다보니 디스크 공간의 사용은 거의 동일했습니다.
npm도 버전업 되면서 병렬처리 급으로 속도가 올라 왔다고 합니다.
yarn 개발자들은 최적화 목적으로 yarn berry버전에서 PnP 기능을 추가 했습니다.
특징으로는 node_modules 폴더를 사용하지 않으며, 중앙 저장소(.yarn/cahche/...)에 zip 파일 형태로 저장됩니다. ㅠnode_modules를 사용하지 않기 때문에 프로젝트 최상단 경로에 pnp.cjs 파일을 생성해 각 패키지의 위치 정보를 기록하여 패키지에 접근합니다. 또한 zip 파일 내부의 내용을 직접 잠조할수 있어 압축 해제가 필요하지 않습니다.
의존성 구조를 구축할 때 더 이상 폴더를 순회할 필요가 없습니다.
// .pnp.cjs
/* react 패키지 중에서 */
["react", [
/* npm:17.0.1 버전은 */
["npm:17.0.1", {
/* 이 위치에 있고 */
"packageLocation": "./.yarn/cache/react-npm-17.0.1-98658812fc-a76d86ec97.zip/node_modules/react/",
/* 이 의존성들을 참조한다. */
"packageDependencies": [
["loose-envify", "npm:1.4.0"],
["object-assign", "npm:4.1.1"]
],
}]
]],
.pnp.cjs 파일을 메모리에 올려두고 위치 정보를 알 수 있어 탐색에 있어 npm과는 다르게 디스크 I/O가 적어 성능향상 되고 node_modules 폴더에 실제 파일이 없기 때문에 디스크 공간도 절약할 수 있습니다.
Yarn은 Berry 버전부터 새로운 캐싱 방식을 도입하여, 캐시 폴더에 패키지를 .zip 형식으로 압축해 저장하기 시작했습니다. 이로 인해 패키지 크기가 줄어들어 디스크 용량을 절약할 수 있게 되었습니다.
패키지 용량과 파일 수가 적어지면서 Zero-install이라는 전략이 가능해졌습니다. Zero-install은 설치된 의존성을 Git에 포함하여, 프로젝트를 처음 클론한 환경에서도 별도 설치 없이 바로 사용할 수 있는 방식입니다.
npm과 yarn의 한계를 극복하기 위해 만들어진,
디스크 공간을 효율적으로 사용하고 설치 속도를 극대화하는 패키지 매니저입니다.
pnpm의 가장 큰 특징은 모든 패키지 버전을 전역 저장소에 한 번만 저장한다는 점입니다.
~/.pnpm-store/ (Content-addressable)
└── files/
├── e4e3570f12... -> react@1 패키지의 해시 값에 대응
└── f8d162891c... -> lodash 패키지의 해시 값에 대응
새로운 패키지가 저장되면, 그 패키지는 실제 파일의 내용을 기반으로 한 고유한 해시로 저장됩니다. 이 해시가 pnpm의 전역 저장소(~/.pnpm-store)에서 패키지 데이터를 구분하는 역할을 합니다.
파일 자체는 전역적으로 한 번만 저장되고 추후 프로젝트는 이 파일을 하드 링크를 통해 연결되게 됩니다.
각 패키지의 내용을 기반으로 고유한 해시값을로 관리하는 이유는 내용이 같으면 해시값도 같다는 특성을 이용해 중복 관리와 중복체크에 있어 효율적 입니다.
pnpm이 패키지를 프로젝트의 node_modules에 직접 복사하지 않고, 대신 하드 링크를 사용하여 pnpm의 전역 저장소(~/.pnpm-store)에 있는 패키지지와 동일한 파일을 가리키게 합니다. 이로 인해, 디스크 공간을 절약하고 파일 접근 속도를 높여줍니다.
noode_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ ├── bar -> <store>/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
├── foo@1.0.0
│ └── node_modules
│ ├── foo -> <store>/foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
└── qar@2.0.0
└── node_modules
pnpm은 비평탄화(Non-flat) 전략을 채택하여, 의존성을 명확히 분리 관리합니다. 이는 의존성 관리의 효율성과 정확성을 높이며, 기존 npm의 문제였던 유령 의존성(phantom dependencies) 문제를 방지합니다.
pnpm은 node_modules 내부에 .pnpm 디렉토리를 생성하여 실제 패키지를 저장합니다. 각 패키지의 의존성은 해당 패키지의 node_modules 디렉토리에서 관리됩니다.
node_modules
└── .pnpm
├── foo@1.0.0
│ └── node_modules
│ ├── foo
│ └── dependency-of-foo
└── bar@1.0.0
└── node_modules
├── bar
└── dependency-of-bar
pnpm은 의존성을 효율적으로 관리하기 위해 하드 링크와 소프트 링크를 함께 사용합니다. 먼저, 중앙 저장소(~/.pnpm-store)에 패키지 파일을 저장한 뒤, 프로젝트의 node_modules/.pnpm에 이 파일들을 하드 링크로 연결하여 디스크 공간을 절약합니다. 이후, 프로젝트의 node_modules에서는 .pnpm 디렉토리 내의 패키지들을 소프트 링크로 참조하며, 의존성 트리를 따라 각 패키지의 종속성을 연결합니다.
pnpm 공식 사이트에서 제공하는 패키지 매니저 기능 비교표 입니다.

아무 생각 없이 사용하던 npm, 그리고 빠르다고만 알고 있었던 Yarn과 pnpm. 이번 기회에 각자의 의존성 관리 메커니즘을 이해하게 되었습니다. 동작 방식만 보면 pnpm이 가장 효율적으로 느껴졌고, Yarn의 Zero-install 전략도 매력적이었습니다.
https://velog.io/@wns450/sdfbg4sz
https://toss.tech/article/node-modules-and-yarn-berry
https://velog.io/@seobbang/%ED%8C%A8%ED%82%A4%EC%A7%80-%EB%A7%A4%EB%8B%88%EC%A0%80-npm-yarn-pnpm-yarn-berry
https://pnpm.io/ko/motivation