Performant NPM - PNPM
과거 웹 서비스는 텍스트와 이미지를 통해 정보를 전달하는 단순한 역할만 하였기 때문에 서비스의 규모가 그렇게 크지는 않았다.
이후 시간이 갈수록 HTML과 JavaScript, CSS와 같은 웹 기술의 발전으로 웹에서 제공되는 서비스의 규모가 점차 커졌지만, 이 역시도 2000년대 초반의 2세대 웹에서는 HTML이나 JavaScript의 파일 크기도 상대적으로 작았으며, 서비스를 유지하는 데 큰 무리가 없었다.
하지만 JavaScript의 계속되는 발전에 웹 서비스의 규모도 점차 커져갔으며, 관리되는 리소스나 HTML, 그리고 JavaScript 파일의 규모와 개수도 서비스의 규모와 비례하여 점차 증가해 갔다. 이때부터는 단순히 정보를 출력해주는 문서가 아닌 복잡한 처리가 가능한 하나의 애플리케이션에 가까웠다. 이에 따라 과거에 자주 사용되던 JavaScript의 순차적 로드 방식
은 각 JavaScript 파일 간 스코프(scope)가 구분되지 않는 문제로 인해 서로 다른 JavaScript 파일들을 오염시키거나 충돌이 일어나는 경우 많아지면서 CJS(Common JS), AMD(Asynchronous Module Definition), UMD(Universal Module Definition), ESM(ECMAScript Module) 과 같은 JavaScript의 모듈화가 등장하기 시작
했다.
이렇게 JavaScript의 모듈화는 Node.js와 NPM, 그리고 ES6의 등장으로 더욱 활발해졌으며, 이 모듈을 관리하는 패키지 매니저인 NPM은 모듈화 또는 패키지화에 큰 변화를 주었다. 하지만 Node.js와 NPM 역시 빠른 속도로 발전해 나가는 JavaScript 생태계를 따라잡기에는 역부족이었을까?
단일 모듈의 개수들과 서로 종속된 모듈들의 개수는 폭발적으로 증가하였고 이에 따라 성능, 모듈이 차지하는 디스크의 용량과 종속된 모듈 간의 호환성, 그리고 보안 측면인 문제가 발생하기 시작했고 이를 해결하기 위해 YARN
, PNPM
과 같은 개선된 패키지 매니저가 나오기 시작했다.
이 중 NPM과 YARN은 이미 널리 알려진 패키지 매니저이기에 이 포스팅에서는 최근 많은 주목을 받는 Performant NPM 이라 불리는 PNPM을 소개해 보고자 한다.
22년 회사에서 세미나 후 자료는 준비했지만 게으른 탓에 이제서야 포스팅을하게 된다.
Package Manager의 지속적인 발전
PNPM의 장점
은 곧 기존 패키지 매니저들이 가지고 있는 단점을 해결한 것이기에 PNPM을 소개하기 전 근본의 NPM을 넘어 새로운 패키지 매니저가 왜 자꾸 나오는지 알아보자.
최초의 패키지 매니저는 NPM으로 10년이 넘는 시간 동안 존재해 왔다. 이렇게 오랜 시간 자리매김하고 있는 NPM을 넘어 왜 자꾸만 YARN
, PNPM
과 같은 새로운 패키지 매니저가 등장하는 것일까?
이 질문에 정확한 답변은 존재하지 않는다. 언제나 그렇듯 JavaScript의 생태계에서는 A보다 나은 B가 나오기 마련이기 때문이다. 하지만 굳이 꼽자면 몇 가지 이유를 들 수 있겠다.
1. node_modules
과거 NPM@2 까지의 node_modules의 구조는 단순하였다.
A → B → C
라는 패키지가 있다면 종속 깊이에 따라 순차적으로 종속된 모듈을 설치해 나가는 단순한 방식이었다. 이런 방식은 서로 종속된 모듈 간 분리가 명확했기에 일관성이 유지되었고 각 모듈 간의 오염과 충돌은 적었다.
하지만 종속된 모듈이 많아지면서 디렉토리 계층 구조의 깊이는 더 깊어져 갔고 이로 인한 문제점(Windows Max Path 260자)과 종속성 그래프를 생성하는 시간 역시 길어져만 갔다.
Node.js를 개발한 Ryan Dahl
은 Design Mistakes in Node
에서 node_modules에 대해 모듈 간 종속성 관리, 모듈 간 문제 해결 알고리즘을 매우 복잡하게 만든다고도 하였으며, 아래와 같이 말하기도 하였다.
It's my fault and I'm very sorry.
Unfortunately it's impossible to undo now.
이런 문제점은 고질적으로 설계 자체의 문제로 Ryan Dahl
은 Deno를 개발하기도하였다. Deno
에 대해서는 [Deno] Node.js의 대안!! Deno 알아보기를 참고하도록 하자.
2. 디스크 효율
우리가 webpack, parcel과 같은 번들링 도구로 애플리케이션을 번들링 시 명시된 devDependencies는 포함되지 않는다. 하지만 로컬 PC와 같은 우리의 개발 환경에서는 모든 종속된 모듈들이 디스크의 용량을 차지
한다. 무심코 지나갔을 수 있지만 규모가 큰 애플리케이션의 경우 종속된 모듈의 용량은 은근히 크다. 물론 현 시대의 하드웨어에 무리가 갈 정도로 침범하진 않지만 말이다.
특히 과거의 NPM인 경우 중복되는 종속성이 여러 모듈에 종속된 NPM 도플갱어로 인해 모듈이 많으면 많을수록 폭발적으로 용량이 증가한다. 이러한 문제점은 디스크 효율성도 떨어뜨리지만, 종속된 모듈이 증가하기 때문에 종속성 그래프를 생성하는 시간 역시 길어지는 이유 중 하나이다.
3. 호이스팅(Hoisting)
위에서 node_modules를 설명할 때 NPM@2의 종속성 구조는 매우 단순하다고 하였다. 하지만 이와 연관되어 NPM 도플갱어가 발생하고 또 이에 따라 디스크 효율성이 떨어지는데 NPM은 이를 해결하기 위해 NPM@3부터 동작 방식을 변경하였다.
바로 종속성을 설치할 때 연관 관계에 따라 모듈을 디렉토리 계층 구조의 최상위로 호이스팅(Hoisting)하는 방식이다. 이 방식은 현재까지도 유지되고 있으며, 우리가 node_modules 디렉토리를 보았을 때 하나로 나열된 것을 확인해 볼 수 있다.
NPM은 호이스팅으로 인해 NPM@3부터는 위에서 설명된 node_modules
와 디스크 효율
이 좋아졌지만, 보안적 측면에서 문제
가 생기기 시작했다. Chrome V8 자체는 매우 좋은 보안 샌드박스였지만 Node.js는 사용자가 액세스할 수 있는 모든 것에 접근할 수 있으며, 이는 보안 측면에서 매우 취약하다. 특히나 모든 종속성 구조를 최상위로 호이스팅 한다면 사용자뿐만 아니라 코드 또는 커맨드와 같은 다른 방식으로도 접근할 수 있게 되고 수정까지 가능하게 된다.
이러한 문제로 과거에는 아래와 같은 보안적 이슈가 존재하기도 했었다.
Linux OS에서 sudo NPM 명령을 통한 시스템 파일의 소유권 변경
NPM package event-stream에 악성 패키지를 내포한 비트코인 탈취 시도
이 문제는 YARN 역시 동일한데 현재는 lock
파일을 통해 패키지에 대한 무결성 검증으로 많이 안정화되었다.
4. Mono Repo
과거 Github에서는 1 Project 당 1 Repository
방식인 Multi Repo 방식을 많이 사용하였다. 하지만 점차 core
, cli
, ui
등 하나의 패키지와 연관된 기능들이 많아지고 이에 비례하여 증가하는 레포지토리의 관리는 너무나 비효율적이었다. lint
와 같은 각 프로젝트 별 공통된 패키지를 여러 레파지토리에 설치를 해줘야 하니 말이다.
그러면서 YARN의 workspace와 Lerna 같은 Mono Repo 도구가 나오면서 하나의 레포지토리 안에 여러 패키지를 관리
하는 메커니즘이 나오기 시작하였다. 현재는 대부분 프로젝트에서 Mono Repo
구조로 개발을 진행하고 있으며, 큰 효과를 보고 있다.
하지만 Mono Repo는 하나의 레포지토리 안에서 여러 패키지를 관리하다 보니 종속된 모듈들이 오히려 문제가 되기 시작했다.
예를 들어 하나의 레파지토리 안에 서로 다른 A, B, C의 패키지가 존재한다면 각각의 패키지에서 공통으로 사용되는 Lint를 최상위 패키지로 설치는 하지만 결국 Lint에 종속된 특정 모듈은 B 패키지에 있는 모듈 중에서도 종속될 가능성이 있는 것이다. 이로 인해 안 그래도 복잡한 알고리즘을 가진 종속성 모듈을 더욱 복잡하게 만드는 경우이기도 하다.
큰 카테고리로 패키지 매니저가 발전하는 이유 4가지인 node_modules
, 디스크 효율성
, 호이스팅(Hoisting)
, Mono Repo
에 대해서 알아보았다. 사실 이 내용들은 PNPM에서 해결된 문제점이기도 하고 PNPM이 최근 많은 관심을 받는 이유이기도 하다.
서론이 너무 길었다. 이제 본론으로 들어가 Performant NPM
인 PNPM에 대해 알아보도록 하자.
Performant NPM - PNPM
PNPM은 Performant Node Package Manager
를 의미하는 NPM의 대체 패키지 관리자이다.
PNPM의 주요 목적은 모든 패키지를 중앙 집중식 저장 형태로 관리하고 하드 링크와 심볼릭 링크를 통해 프로젝트에서 패키지를 참조함으로써 빠른 속도
와 프로젝트 간 동일 버전의 종속성 유지
및 디스크 공간 절약
을 목적으로 두고 있다.
현재 오픈 소스의 PNPM 활용
최초 필자가 PNPM을 접하게 된건 vue의 Github에서이다. 하지만 이외에 많은 이름있는 프로젝트에서 PNPM을 사용하고 있다.
2022 state of js - Monorepo tools 부분
2022 state of js - MonoRepo Tools 부분을 보면 PNPM의 인기를 확인할 수 있다.
PNPM의 특징
PNPM의 종속성 관리는 NPM의 기본 메커니즘을 따라가지만, 별도의 중앙 집중화된 관리(virtual-store) 와 하드 링크, 심볼릭 링크가 핵심적이다. 이 개념 하나로 빠른 성능과 디스크의 효율성을 높였다.
또한 진화된 NPM@3 방식의 flat node_modules
구조가 아닌 반대로 NPM@2의 non-flat node_modules
구조를 따라감으로써 전반적인 보안적 취약점이 해결되었다.
PNPM의 특징은 크게 4가지로 나누어 볼 수 있다.
1. 성능
YARN은 NPM보다 빠른 성능을 보이지만 사실상 규모가 큰 시나리오에서는 큰 차이를 나타내지 못한다. 하지만 PNPM은 기타 패키지 관리자보다 최대 2배 빠른 속도
를 보인다.
PNPM의 벤치 마킹에 따른 성능 비교 분석을 보게 되면 여러 케이스에서 PNPM이 다른 패키지 매니저보다 우수한 성능을 나타내는 것을 볼 수 있다.
PNPM 벤치마킹에 따른 성능 비교
2. 디스크 공간의 효율적 사용
NPM과 YARN은 모듈을 설치할 경우 node_modules 경로에 모두 설치되고 만약 같은 모듈을 사용하는 100개의 프로젝트가 있다면 똑같이 100개 프로젝트에 같은 모듈을 설치해야 한다. 이렇게 설치되면 디스크 사용량은 폭발적으로 증가
할 것이고 100번의 설치 과정을 걸쳐야 하는 번거로움이 있지만(또는 복사하거나), PNPM의 경우 모든 종속된 모듈은 특정 디스크 위치(.pnpm-store)에 저장된다. 이후 프로젝트에서 특정 패키지가 필요한 경우 해당 패키지를 프로젝트에 하드 링크(.pnpm)되어 추가 디스크 공간을 사용하지 않기 때문에 디스크 사용이 효율적이고 패키지의 재사용 효율이 높다.
또한 다른 버전의 종속된 모듈이 필요하다면 모든 100개의 프로젝트를 업데이트할 필요 없이 중앙에 관리되는 패키지만 업데이트가 되며 이 한 번의 업데이트로 같은 패키지를 사용하는 프로젝트에서 같은 버전의 패키지를 사용할 수 있다.
3. Non-Flat node_modules
위에서 NPM@2
의 메커니즘은 단순하다고 하였으며, non-flat node_modules
구조로써 얻는 이점들이 있다고 설명하였다. 하지만 NPM@3
부터는 루트 경로로 호이스팅을 하는 flat node_modules
구조를 사용하였다.
NPM과 YARN의 경우 모든 종속성 설치할 때 모든 패키지는 모듈 디렉토리의 로트로 호이스팅을 하고 결과적으로 소스 코드를 통해 종속되지 않은 모듈에도 접근할 수 있다. 하지만 PNPM은 Flat node_modules의 문제를 해결하기 위한 유일한 방법이 아니라고 설명하고 오히려 non-flat node_modules
를 지향하고 NPM@2
의 메커니즘을 사용하였다.
그렇다면 PNPM도 NPM@2
의 문제점을 가지고 있을까?
이에 대한 답은 아니오
이다. PNPM은 NPM@2
의 non-flat node_modules
즉 루트로 호이스팅을 하지 않는 대신 종속된 모든 모듈은 하드 링크 방식과 심볼릭 링크 방식을 사용함으로써 NPM@2
의 문제를 해결하였는데 이런 하드링크와 심볼릭 링크 방식으로 종속성 수와 종성 그래프의 깊이에 관계없이 유지된다.
4. 보안
위에서 설명했듯 PNPM은 애초부터 종속된 모듈들을 루트로 호이스팅을 하지 않기 때문에 호이스팅으로 인한 보안적 문제(모듈에 대한 엑세스)
는 없을 뿐더러 YARN과 동일하게 PNPM에는 설치된 모든 패키지의 체크섬이 포함된 파일이 존재하여 코드가 실행되기 전 설치된 모든 패키지의 무결성을 확인
한다.
5. Mono Repo
초창기 Lerna는 Mono repo로서 사용자도 많으며 유명 프로젝트에서 많이 사용되었지만, 많은 것을 스스로 알아야 했고 현재도 그렇다. 버전업도 많이 진행되고 개선되었으며, 과거보다는 안정되었지만, 소문에 의하면 메인 개발자의 번아웃으로 유지보수에 어려움을 겪는다고 한다.
Lerna 이외에 YARN을 사용하여 Mono Repo를 구축할 수도 있다. YARN Classic(YARN 1)
은 17년도 Lerna와 같은 Mono Repo를 지원하기 시작하였고, YARN Berry(YARN 2)
는 YARN Classic
의 개념을 기반으로 개선된 Mono Repo를 지원한다.
PNPM의 경우에도 역시나 Mono Repo를 지원하며, YARN Berry와 유사하다. 즉, PNPM은 Lerna와 YARN을 대체할 수 있는 Mono Repo를 제공한다.
Lerna에 관한 내용은 아래 내용을 참고하자.
Lerna를 활용한 Mono-Repo 구축 완벽 가이드 - 개념 정리
Lerna를 활용한 Mono-Repo 구축 완벽 가이드 - 예제를 통한 완벽 파악
설치
PNPM의 설치 방식에는 독립 실행형 설치
방법과 [Corepack](https://nodejs.org/api/corepack.html)
, NPM을 통한 설치
를 제공한다.
독립 실행형
Windows PowerShell
|
POSIX systems - curl
|
POSIX systems - wget
|
Corepack
Node.js v16.13부터는 패키지 관리자를 위해 Corepack을 출시하였다. v16에서는 실험적 기능으로 PNPM과 YARN을 지원
하고, 해당 기능을 활성화하면 PNPM을 사용할 수 있다.
|
이후 최신 버전의 PNPM을 사용하려면 아래와 같은 명령어를 사용한다.
|
NPM
PNPM도 NPM과 동일한 패키지 매니저이지만 NPM을 통해서 설치할 수 있다.
|
Node.js 지원 버전
PNPM은 2023년 09월 기준으로 v8까지 출시된 상태이고 최소 버전은 PNPM v5이다. v5의 경우 Node.js v12 이상부터 지원하며 v7은 Node.js v18까지 지원한다.
(22년도 해당 포스팅을 준비할 때에는 v7까지 출시된 상태였고 최소 버전은 v4였으며, v4의 경우 Node.js v10 이상부터 지원이었다. 이런 과정을 보면 하상 Node.js 버전업에 맞게 업데이트를 꾸준히 해야겠다는 생각을 다시끔 한다.)
Node.js | PNPM 5 | PNPM 6 | PNPM 7 | PNPM 8 |
---|---|---|---|---|
Node.js v12 | ✔️ | ✔️ | ❌ | ❌ |
Node.js v14 | ✔️ | ✔️ | ✔️ | ❌ |
Node.js v16 | ? | ✔️ | ✔️ | ✔️ |
Node.js v18 | ? | ️✔️ | ✔️ | ✔️ |
Node.js v20 | ️? | ️ ? | ✔️ | ✔️ |
실행
PNPM의 실행 방식은 YARN과 비슷하며 크게 다를 것이 없다.
PNPM의 CLI는 PNPM cli 공식 문서에서 확인 할 수 있다. 몇 가지 가장 많이 사용하는 CLI를 알아보자.
pnpm install
aliases
:i
PNPM install
은 프로젝트의 모든 종속성을 설치한다.
|
pnpm add pnpm add
는 패키지와 패키지가 의존하는 모든 패키지를 설치한다. 기본적으로 모든 새로운 패키지는 프로덕션 종속성(dependencies)으로 설치된다.
|
Command | Description |
---|---|
pnpm add |
dependencies 설치 |
pnpm add -D |
devDependencies 설치 |
pnpm add -O |
optionalDependencies 설치 |
pnpm add -g |
global 설치 |
pnpm add |
버전 지정 설치 |
pnpm update
aliases
:up
,upgrade
PNPM update
는 지정된 범위에 따라 패키지의 버전을 업데이트한다.
|
Command | Description |
---|---|
pnpm update | 모든 종속성 업데이트 |
pnpm update |
특정 패키지를 특정버전으로 업데이트 |
pnpm update “@babel/*” | 패키지 scope 내 모든 종속성 업데이트 |
pnpm update ! |
특정 패키지를 제외한 모든 종속성 업데이트 |
pnpm remove
aliases
:rm
,uninstall
,un
PNPM remove
는 프로젝트 내 패키지를 제거한다.
|
Command | Aliases | Description |
---|---|---|
pnpm remove –recursive | -r | Mono Repo의 Workspace 내에서 사용되는 경우 모든 작업 공간의 패키지에서 종속성 제거 |
pnpm remove –global | -g | 전역 패키지 제거 |
pnpm remove –save-dev | -D | devDependencies 에서 종속성 제거 |
pnpm remove –save-optionval | -O | optionalDependencies 에서 종속성 제거 |
pnpm remove –save-prod | -P | dependencies 에서 종속성 제거 |
pnpm remove –filter |
Filtering을 통한 특정 하위 집합으로 제거 |
NPM의 대체로 가능한가?
확실히 성능적인 부분과 디스크 효율적인 부분으로 따지면 PNPM이 승자이며, 하나의 프로젝트에 여러 그룹이 진행하거나 여러 프로젝트를 mono Repo로 관리를 한다면 PNPM은 유용하다.
그렇다고 쉽사리 현재 진행되고 있는 프로젝트 또는 진행 될 프로젝트에 반영하기는 쉽지 않을 것이다. 언제나 그렇듯 JavaScript의 생태계에서는 A보다 좋다는 B가 나와도
근본이 있기 때문이며, 여기서 근본은 NPM이기 때문이다.
수십 년간 Node.js와 NPM은 유지가 되어왔고 이를 개선한 새로운 오픈 소스가 출시해도 NPM과 YARN 역시 이에 뒤처지지 않게 발전해 왔으며, 문제점도 해결해 왔다. 물론 가장 효율적인 방법으로 초기 설계부터 진행해 온 것과 그렇지 않은 것의 차이는 있지만 NPM을 유지한다고 문제가 생기는 것은 아니다.
다만 현재 본인이 겪고 있는 문제점이 PNPM으로 해결이 된다면 PNPM을 사용
하는 것이 옳다고 생각한다.
PNPM이 NPM보다 우월하다는 관점에서 보기보다는 NPM이 과거에 어떤 문제가 있었고 왜 패키지 매니저가 발전
해 왔는지 알아보았다. 무심코 우리는 패키지를 설치 및 업데이트와 같이 단순하게 사용을 해왔지만, 이러한 단순한 도구가 왜 계속 발전해 왔는지 파악할 수 있었으며, PNPM이라는 새로운 패키지 매니저에 대해 배워보았다.
개인적으로는 NPM이 아닌 PNPM을 사용하려고 노력하고 있지만 PNPM 역시 오픈 소스이기 때문에 문제 발생에 대한 두려움도 있기에 실무적으로 프로덕션이 필요한 경우에는 사용을 자제하고 있지만 확실히 빠른 속도로 개발적 효율은 향상했다고 느껴진다.
앞으로 또 어떤 패키지 매니저가 나올지 모르지만, 다양한 내용을 알아 둔다면 나중에 분명 도움이 되는 날이 올 것으로 생각한다.