pnpm 파헤치기

pnpm 파헤치기

pnpm 써요!

소개

안녕하세요! 프론트엔드 개발자 김동규입니다. 오늘 공유드릴 내용은 JavaScript의 런타임 환경인 Node.js 진영의 패키지 관리자인 pnpm에 대해 다루고 있어요. 이번 포스트를 통해 pnpm이라는 패키지 관리자에 대해 충분한 이해를 가질 수 있었으면 좋겠습니다.

npm에 대해 먼저 알아보자!

npm이란?

npm은 Node.js의 기본 패키지 관리자로 2010년 1월에 세상에 공개되어 현재까지 많은 개발자들의 사랑을 받고 있습니다. npm은 현 패키지 관리자들의 모티브가 되었는데요. 프로젝트에 대한 메타정보, 그리고 설치한 패키지의 의존성 및 버전을 관리하는 package.json 파일과 설치한 모듈들이 위치하는 node_modules 폴더의 시초는 npm에서 시작되었습니다. 그동안 수동으로 모듈들을 받아 사용했던 당시에는 정말 혁명적인 도구가 아닐수가 없었습니다.

npm 강점기 종료의 이유는?

200만개 이상의 거대한 패키지 레지스트리를 가지고 있고 오래전부터 견고한 인프라와 생태계가 구축되어 있는 npm이 있음에도 불구하고 새로운 패키지 관리자들이 탄생해야 했던 이유는 뭘까요?

node_modules는 깊다

위의 사진은 npm의 node_modules를 지적할 때 많이 사용되곤 하는데요. node_modules라는 폴더는 무려 블랙홀보다 깊다고 합니다😆.

첫째, 유령 의존성

npm의 구조

왼쪽의 파일 구조는 npm의 초창기 package 관리 방법인데요. Nested Dependency Structure 라고 불리기도 합니다. 버전 2이하의 npm은 패키지들을 전역에 받아 사용하여 node_modules 폴더가 없는 형태를 갖고 있는데요. 이 구조의 단점은 중복된 모듈만큼 다시 설치된다는 점입니다. 서비스를 만드는 입장에서 이러한 단점은 엄청난 디스크 낭비를 경험할 수 밖에 없습니다.

이러한 npm의 단점을 고치기 위해 2014년 12월 12일 한 이슈를 시작으로 Flat Dependency Structure 구조를 npm 3버전부터 제공하기 시작합니다. 이는 오른쪽 파일 구조인데요.

해당 관리 방법은 모든 의존성을 검토하고, 여러 패키지에서 공유할 수 있는 버전이 있다면 이를 최상위 node_modules에 설치합니다. 그리고 이미 설치되어 있는 모듈의 다른 버전을 사용해야 한다면 필요한 패키지의 node_modules에 설치합니다.

왼쪽에서 오른쪽 구조로 바뀌어 디스크 공간 절약과 설치 속도 향상이라는 의미있는 결과를 얻을 수 있었습니다. 하지만 이 방법은 npm의 가장 큰 실수로 남아버렸는데요. D 모듈은 다른 모듈들에 의존되어 설치되는 모듈에 불과했는데 이제는 최상위 node_modules까지 올라오는 바람에 package.json에 명시하지 않은 모듈을 불러와 사용할 수 있게 되었습니다.

이러한 문제를 “유령 의존성(ghost dependency)”이라고 부릅니다.

둘째, 거대한 용량과 검증 불가 상태

npm으로 설치되는 node_modules 폴더 내부는 수십, 수백개의 패키지가 서로를 의존하는 구조 속에서 더욱 깊어지고 복잡해집니다. 이렇게 불어난 node_modules의 용량 때문에 형상 관리 도구를 사용할 때 해당 폴더를 제외합니다.

또한, package.json에서 의존하고 있는 모듈들이 node_modules에서 문제없이 설치됐고 관리되고 있는지 검증하기에 너무 까다롭습니다. package.json에 명시된 모듈이 10개만 되더라도 검증이 아닌 도전에 가까워집니다.

다음은 next 공식 문서에서 제공해주는 create-next-app을 사용해서 생긴 11개의 의존성을 가진 프로젝트의 node_modules 개수입니다.

node_modules의 항목 개수

npm은 많은 단점들을 가지고 있지만 그 중에서도 개발자들이 가장 불편해한 단점 2가지를 소개해드렸습니다. 이러한 단점들을 극복하기 위해서 다른 패키지 관리자들이 세상에 등장하게 되었는데요. 이제 Node.js 진영에서 널리 사용되고 있는 있는 npm, yarn, pnpm 중 pnpm에 대해 소개드리도록 하겠습니다.

pnpm에 대해 알아보자!

pnpm이란?

npm은 패키지 관리자로 npm의 단점들을 극복하고자 2017년 7월 28일 오픈소스로 공개되었는데요. npm보다 2배 빠르고 node_modules 관리가 더욱 효율적이고 모노레포 기능을 지원합니다.

pnpm의 원리

content-addresable store

pnpm은 'Content-addressable store'라는 개념의 구조를 node_modules에 적용하였는데요. 이것은 각 모듈을 고유하게 식별할 수 있는 방법을 의미합니다. 기본적으로 pnpm은 각 패키지를 설치할 때 패키지의 식별자로 파일 시스템에 대한 해시를 사용합니다. 이것은 패키지가 실제로 어떻게 설치되었는지에 상관없이 같은 내용을 갖는 패키지는 항상 동일한 식별자를 갖도록 보장합니다. is-odd/LICENSE.md와 is-even/LICENSE.md는 서로 다른 폴더에 존재하지만 서로 같은 내용을 가지고 있어 동일한 해시값을 갖고 있게 됩니다.

이는 곧 모든 버전의 dependencies가 해당 폴더에 물리적으로 한번만 저장되므로 상당한 디스크 공간을 절약할 수 있게 됩니다.

pnpm을 좀 더 파헤쳐보자

npm과 동일하게 CNA(create-next-app)을 사용하여 생긴 프로젝트의 node_modules는 다음과 같습니다.

node_modules with pnpm

pnpm node_modules의 항목 개수

pnpm을 통해 설치한 11개의 의존성을 가진 프로젝트는 npm보다 무려 288개나 감소된 폴더를 갖고 있습니다.

흠...이상하네요. 눈에 보이는 모듈들만이 전부인 것일까요? npm에서 보이던 288개의 모듈들은 어디로 숨어버린 걸까요?

위에서 보이는 사진속의 모듈들은 실재하는 것이 아니라 **심볼릭 링크(윈도우의 바로가기 같은 기능)**를 통해 참조되고 있을 뿐입니다. 각 폴더의 맨 오른쪽에 보이는 화살표가 다른곳과 링크되었다는 것을 의미합니다.

그렇다면 실제 원본 모듈들은 전부 어디로 간거죠?

.pnpm 폴더를 주목해봅시다.

.pnpm 폴더 내부

.pnpm 폴더 안에는 실제 원본 모듈들과 하드 링크로 연결된 모듈들이 들어있습니다. 이 폴더 안에 들어있는 모든 모듈들은 플랫 폴더 구조로 저장되어 있기 때문에 .pnpm/<이름>@<버전>/node_modules/<이름> 으로 지정된 경로로 각종 모듈들을 찾아낼 수 있습니다.

그리고 .pnpm이 하드링크로 연결된 실제 원본 모듈들의 위치는 다음과 같이 찾아낼 수 있습니다.

# zsh
pnpm store path # /Users/po4tion/Library/pnpm/store/v3

해당 명령어를 통해 나온 주소로 이동해볼까요?

pnpm store

알아볼 수 없는 문자들이 잔뜩 존재합니다. 바로 이곳이 pnpm의 Content-addressable store입니다. 신기하지 않나요?

pnpm은 중복되어 의존되는 모듈들이나 같은 모듈이지만 버전만 다르더라도 Content-addressable store 폴더 하위로 모두 집결하고 이것을 참조하여 가져다가 쓰기만 하니, 버전 충돌이 일어날 일도 없고 더욱 안전해졌습니다. 더군다나 pnpm의 store는 전역으로 관리되기 때문에 새로운 프로젝트를 생성하고 설치할 때 이미 설치된 적이 있다면 store로부터 빠르게 설치가 가능해집니다.

여기서 이러한 궁금증이 하나 생기는데요!

pnpm은 왜 굳이 .pnpm 폴더를 만들어 그 아래로 라이브러리와 버전을 관리하는 것일까요? 그냥 바로 node_modules 밑에서 관리하면 안되는 것일까요?

답변을 드리자면 예, 안됩니다.

만약에 .pnpm이라는 폴더 없이 node_modules 하위에서 라이브러리들을 관리한다면 npm의 유령의존성 문제가 다시 발생하게 됩니다. 이유는 Node.js가 가지고 있는 module resolution(모듈 해석법)때문입니다.

# zsh
node
Welcome to Node.js v20.11.1.
Type ".help" for more information.
> require.resolve.paths("next")
[
  '/Users/po4tion/Library/pnpm/store/v3/repl/node_modules',
  '/Users/po4tion/Library/pnpm/store/v3/node_modules',
  '/Users/po4tion/Library/pnpm/store/node_modules',
  '/Users/po4tion/Library/pnpm/node_modules',
  '/Users/po4tion/Library/node_modules',
  '/Users/po4tion/node_modules',
  '/Users/node_modules',
  '/node_modules',
  '/Users/po4tion/.node_modules',
  '/Users/po4tion/.node_libraries',
  '/Users/po4tion/.nvm/versions/node/v20.11.1/lib/node',
  '/Users/po4tion/.node_modules',
  '/Users/po4tion/.node_libraries',
  '/Users/po4tion/.nvm/versions/node/v20.11.1/lib/node'
]

위의 명령어를 통해 확인해보면 Node.js가 어떤 방식으로 사용할 모듈들을 찾는지 알 수 있는데요. node_modules 기준으로 1 depth만 확인합니다. .pnpm 폴더는 이러한 모듈 해석법을 회피하기 위해서는 필수적인 존재입니다. package.json에 명시되어 있지 않은 의존성들이 node_modules 아래로 존재했다면 개발자는 해당 모듈을 사용할 수 있게 되고 혼란을 일으킬 것이 분명합니다.

pnpm은 언제 사용하면 좋을까?

크기가 작은 프로젝트라면 npm이나 pnpm이나 뭘 사용해도 큰 상관은 없습니다. 그러나 대용량의 프로젝트라면 수많은 IO 작업이 일어날테니 파일 시스템을 기반으로 돌아가는 npm을 선택하기 보다는 심볼릭 링크를 사용하여 중복된 모듈들이 실제로 다시 설치되는 일이 없는 pnpm을 선택하는게 좋다고 말씀드릴 수 있습니다.

또한 pnpm의 묘미는 뭐니뭐니해도 바로 모노레포 구조입니다. 모노레포 라이브러리로 유명한 Vercel의 Turborepo는 pnpm 사용을 강력히 추천하고 있는데요. 그 이유는 무엇일까요?

다들 눈치채셨겠지만 중복 모듈 설치가 없고 모노레포의 root 경로에서 생성되는 node_modues의 .pnpm폴더에서 여러개의 서비스들이 공통으로 사용하는 모듈들을 참조하여 사용할 수 있기 때문입니다. 이러한 관리 방법은 공통적으로 사용되는 모듈의 버전을 일일이 관리할 필요가 없고 빌드 속도 향상(패키지의 복사본을 만들지 않아도 됨)에 큰 이점을 얻을 수 있습니다.

다음 패키지 관리자로 기대되는 후보

Bun

강력한 정적 타입 시스템을 갖고 있는 컴파일 시스템 언어인 Zig로 만들어진 차세대 자바스크립트 런타임 환경인데요. Bun은 패키지 관리자로서도 기능을 제공하고 있습니다.

Bun

마무리

사람은 트렌드에 쉽게 휩쓸려가는 동물이라고 합니다. 개발자들도 다르지 않다고 생각되는데요. 주변의 개발자들이 뭐가 좋다고 하는 소리를 듣고 아! 좋은건가 보네, 한번 써보자! 라는 생각을 품는 것은 좋습니다. 하지만 그저 사용만 하는 것이 아닌 왜 좋은지를 정확히 파악해야 적재적소에 사용할 수 있다는 것을 상기하여야 합니다.