Package를 선택하는 기준

2024년 10월 25일

npm

npm 은 평면화된 node_modules 를 사용합니다. 평면화된 의존성은 어떠한 문제를 야기할 수 있을까요?
node_modules/
├── express/
│   ├── package.json (depends on debug@^2.6.9)
│   └── node_modules/
│       └── debug/ (version 2.6.9)
의존성 트리를 평면화 하는 알고리즘이 복잡하다.
이로 인해서 일부 패키지들은 다른 프로젝트의 node_modules 폴더 내에 복제되어야 할 수 있습니다.
모듈들이 직접적으로 의존하고 있지 않는 패키지에 접근할 수 있다.
// package.json
{
  "dependencies": {
    "express": "^4.17.1"
  }
}
실제로는 위와 같이 package.json에 의존성의 선언이 이루어져 있다고 가정하겠습니다. 현재 이 프로젝트에서 주입된 의존성은 express 밖에 없는 상황입니다.
const express = require("express");
const debug = require("debug"); // 직접 의존성으로 선언하지 않았는데도 동작함!

debug("my app"); // 작동합니다
하지만 패키지에 의존성이 주입이 되어 있지 않지만 express 내부에 존재하는 node_modules 내부에 존재하는 debug 패키지로 인해서 사용하겠다고 명시되지 않은 패키지가 사용되는 문제가 생깁니다.
이러한 상황에서 발생할 수 있는 문제는 express 업데이트 되어서 내부에서 사용되고 있는 debug 패키지에 변경사항이 생겼을 경우 기존에 프로젝트에서 사용되고 있던 debug는 이를 반영하지 못하기 때문에 기존의 코드는 망가지게 됩니다.
이 뿐만이 아니라 각 프로젝트가 실제로 어떤 의존성을 사용하는지 파악하기 어렵게 되고 암시적 의존성들이 프로젝트 간에 충돌할 수 있으며 의존성 업데이트 시 영향 범위 파악이 어려워진다는 문제를 갖고 있습니다.
npm은 패키지 설치 프로세스가 한 번에 하나씩 순차적으로 설치하는 과정을 갖는다.

Yarn

Yarn은 npm에 비해서 비교적 최신패키지 매니저입니다. 그렇다면 yarn은 npm과 어떠한 차이점이 있는 지에 대해서 알아보겠습니다.
yarn은 npm와 같이 평면화된 node_modules 폴더를 생성하는 동일한 알고리즘을 사용하고 있다.
위에서 설명했던 알고리즘을 사용하고 있기 때문에 근본적으로는 위에서 설명했던 npm이 갖고 동일한, 근본적인 문제를 공유하고 있습니다. 그래서 이러한 문제를 해결하기 위해 yarn berry 를 통해서 문제를 해결하고자 하였습니다.
yarn의 경우, 체크섬이 yarn에 저장됩니다. yarn classic와 yarn berry는 계속해서 잠금 파일을 사용해왔습니다. yarn은 또한 악성 패키지 설치를 방지하며, 불일치가 감지되면 설치가 중단됩니다.
npm에 비해서 패키지를 설치하는 속도가 더 빠르다. 이러한 이유는 yarn은 npm 과 달리 병렬적으로 패키지들을 설치하고 npm에 비해 효율적으로 메모리를 관리하기 때문이다.
yarn berry 의 등장
Plug'n'Play, Zero-installs, 더 나은 의존성 관리

PNPM

pnpm은 하드링크와 심링크를 사용하는 반엄격한 node_modules 구조를 유지한다.
하드 링크는 동일한 파일에 대한 다른 참조입니다. 소프트 링크에서는 새 파일을 만들고 파일의 내용이 다른 경로를 가리킵니다. 심링크는 pnpm이 의존성의 중첩 구조를 만드는 데 사용하는 심볼릭 링크입니다.
PNPM은 모든 모듈에 대한 하드 링크를 포함하는 ".pnpm"이라는 특별한 폴더를 만들었습니다. 의존성을 다운로드할 때 pnpm은 먼저 해당 의존성이 이 저장소에 있는지 확인합니다. 저장소에서 의존성을 찾으면 pnpm은 하드 링크를 생성하여 가져옵니다.
node_modules/
├── .pnpm/
│   ├── express@4.17.1/
│   └── debug@2.6.9/
└── express -> .pnpm/express@4.17.1/
pnpm은 node_modules를 평면화 함으로써 생기는 의존성의 문제를 각 패키지가 자신이 선언한 의존성만 접근을 할 수 있게끔 함으로써 문제를 해결하고자 하였습니다.
위와 같은 방법을 사용할 경우 기존에 require('debug') 와 같이 package.json에 의존성이 주입되지 읺았음에도 불구하고 사용되는 문제를 해결할 수 있습니다.
또한 이러한 접근 방식 때문에 pnpm은 다른 프로젝트에 이미 설치된 동일한 패키지를 재사용합니다. 이것이 pnpm이 제공하는 많은 이점들 중 하나입니다.
yarn와 마찬가지로 체크섬을 사용하고 뿐만 아니라 코드의 무결성을 확인하는 과정을 거쳐서 다른 패키지 매니저에 비해서 보안에 유리합니다.
npm install은 package-lock.json을 사용하여 node_modules 폴더를 생성합니다. Yarn은 yarn.lock 파일과 node_modules 폴더를 생성합니다. PNPM은 평면화된 의존성 트리를 만들지 않습니다.
Pnpm 성능과 디스크 효율성 NPM은 여전히 Yarn과 PNPM에 비해 약간 더 느립니다. Yarn은 동일한 평면화된 node_modules 디렉토리를 사용하지만 NPM과 비교할 만한 속도를 보이며 패키지를 병렬로 설치합니다.
반면에 pnpm은 pnm보다 3배 더 빠르고 효율적입니다.
콜드 캐시와 핫 캐시 모두에서 PNPM이 Yarn보다 빠릅니다. PNPM은 글로벌 저장소에서 단순히 파일을 링크하는 반면, yarn은 캐시에서 파일을 복사합니다. 패키지 버전은 디스크에 한 번만 저장됩니다.
PNPM은 의존성 트리를 평면화하지 않고 이 문제를 해결했습니다. 각 패키지의 의존성은 node_modules 폴더에 그룹화되었고 심링크를 사용하여 의존성들을 함께 그룹화했기 때문에 디렉토리 트리가 평면화되었습니다.
. ├── node_modules/
│ └── .pnpm/
  ├── .npmrc
  ├── package.json
  └── pnpm-lock.yml
pnpm을 사용하여 의존성을 설치하면 package.json 파일이 생성되고, node_modules 폴더도 생성되지만 콘텐츠 주소 지정 저장소 접근방식 때문에 npm과 yarn과는 완전히 다른 구조를 가집니다.
Pnpm은 기존 npm 기능을 기반으로 많은 개선을 이루었습니다. pnpm은 npm의 약점을 제거하면서 모든 장점을 채택하여, pnpm을 두 가지의 장점을 모두 가진 최고의 도구로 만들었습니다.

npm와 yarn을 비교하는 것은 큰 의미가 없다.

npm과 yarn classic 공통의 문제점은 평면화된 node_modules 구조를 생성한다는 것에서 발생되는 문제이다. 이러한 문제로 인해서 두 패키지 모두 유령 의존성 문제, 비효율적인 디스크 공간 사용, pnpm에 비해 느린 설치 속도를 갖고 있을 수 밖에 없다.
그렇기 때문에 오늘 날 새로운 패키지를 사용한다고 할 경우 yarn berry + PnP 의 조합을 사용할 것인지 pnpm 을 사용할 것인지에 대해서 프로젝트의 요구사항등에 맞춰서 선택하여 사용하는 것이 더 권장되는 방식일 것이다.