pnpm

처음 들었을 때 "p"가 무슨 뜻일지 궁금했다. 홈페이지에서 바로 찾을 수 없었고 인터넷에 검색해도 잘 나오지 않았다.

performant npm

'승진, 성능 기준에 맞는' 이런 뜻인데 '고성능 npm'이라고 이해했다. 회사 동료의 제안으로 사용하게 된 이 패키지 매니져가 이름처럼 빠르고 성능도 좋았다. npm보다 기능이 부족하거나 사용상 혼란스러운 부분없이 자연스럽게 도구를 바꿀 수 있었다.

pnpm의 구조와 간단한 사용법을 정리한다.

용량: npm

pnpm이 용량을 적게 사용하는 것은 패키지 관리 방식의 차이 때문이다. 먼저 npm의 패키지를 관리 방법을 이해한 뒤 둘을 비교하겠다.

npm은 하나의 패키지를 설치하면 의존하는 모든 패키지를 node_modules 폴더에 다운로드 한다. 패키지가 중복되면 하나만 설치해서 공유한다. 버전이 다르면 폴더 이름이 같아서 설치할 수 없는데 이 때는 node_modules에 하나의 버전만 남기고 나머지 버전은 사용하는 패키지 하위의 node_modules에 다운로드 하는 방식을 취한다.

express를 설치한다면 node_modules 폴더에 다운로드 한다. 의존하는 패키지(이중 cookie도 포함)도 이 디렉토리에 평탄하게 위치할 것이다. msw를 하나 더 추가하면 의존하는 모든 패키지를 마찬가지로 node_modules 폴더에 설치한다. msw도 cookie를 사용하는데 express가 사용하는 것과 버전이 다르다. 이 경우 각 버전의 패키지를 각 각 분리해서 다운로드 할 것이다.

node_modules
  - express
  - cookie # express가 사용하는 버전
  - msw
    - node_modules
      - cookie # msw가 사용하는 버전

이처럼 npm은 프로젝트마다 사용하는 패키지를 자신의 node_modules 폴더에 관리한다. 의존 패키지가 같으면 하나의 폴더에 관리해 용량을 약간 줄일 수 있기는 하다. 그러나 프로젝트 간에 중복 패키지까지는 관리하지 못한다. 프로젝트마다 express를 사용한다면 각 node_modules 하위에 이 패키지를 매번 설치할 것이다.

용량: pnpm

pnpm으로 패키지를 설치하면 디스크의 세 군데 위치를 건드린다.

먼저 node_modules. 설치한 패키지 express를 추가한다. 일반 파일이 아니라 심볼릭 링크다.

ls -l ./node_modules

express -> .pnpm/express@4.18.2/node_modules/express

원격 저장소의 파일을 이 폴더에 직접 저장하지 않았다. node_modules/.pnpm 을 가리키는 링크만 있다.

pnpm은 이곳을 가상 저장소(virtual-store)라고 부른다.

ls -l ./node_modules/.pnpm

express@4.18.2
cookie@0.5.0
# ...

가상 저장소에는 설치한 패키지와 버전의 이름으로 폴더가 있다. 설치한 express와 이 패키지의 package.json에 기록된 의존 패키지들의 이름과 버전을 폴더로 구성한 것이다. express에서 cookie 0.5.0 버전을 사용한다.

다른 버전의 cookie을 사용하는 msw를 프로젝트에 추가하면 가상 저장소는 이렇게 바뀐다.

ls -l ./node_modules/.pnpm

express@4.18.2
cookie@0.5.0 # express에서 사용하는 패키지
msw@1.3.2
cookie@0.4.2 # msw에서 사용하는 패키지
# ...

cookie 패키지가 각 버전별로 설치되었다.

pnpm은 추가한 패키지를 npm처럼 node_modules에 직접 설치하지 않고 가상 저장소를 가리키는 심볼릭 링크로 구성한다. 하지만 이것만으로는 디스크 용량을 줄이지는 못한다. 패키지 폴더와 더불어 심볼릭 링크 파일도 추가해 용량이 더 늘었다.

# npm 으로 설치한 용량
du -shL ./node_modules
44M

# pnpm 으로 설치한 용량
du -shL ./node_modules
58M

pnpm이 용량을 줄일수 있는 비결은 바로 스토어(Content-addressable store)라고 불리는 장소다.

pnpm store path

~/Library/pnpm/store/v3

pnpm의 스토어 경로를 확인했다. 사용자 폴더의 이 위치다.

숫자와 해시값으로 된 폴더를 발견했다.

ls ~/Library/pnpm/store/v3/files

00 05 0a 0f 14 19 1e 23 28 2d 32 37 3c 41 46 4b 50 55 5a 5f 64 69

파일을 하나씩 열어보니 내가 그동안 설치했던 패키지들이다.

~/Library/pnpm/store/v3/files/0c # epxress@4.18.2
~/Library/pnpm/store/v3/files/dc # msw@1.3.2
~/Library/pnpm/store/v3/files/19 # cookie 0.5.0
~/Library/pnpm/store/v3/files/34 # cookie 0.4.2

pnpm은 스토어에 패키지를 다운로드하고 각 프로젝트에서 하드 링크로 가져다 사용한다.

ls -li ~/Library/pnpm/store/v2/files/19/{package.json 해시값}
12238952 # inode가 같다

ls -li node_modules/.pnpm/cookie@0.4.2/node_modules/cookie/package.json
12238952 # inode가 같다

하드 링크는 소프트 링크와 달리 inode 값이 같다. 파일을 다시 만들지 않고 같은 inode를 가리키기 때문에 하드 링크를 여러 개 만들더라도 디스크 용량을 더 차지하지 않는다. pnpm 스토어에 원본 패키지를 유지한 채 각 프로젝트 폴더에서 이를 하드 링크로 구성하기 때문에 용량의 변화가 없다.

정리하면 pnpm은 디스크의 세 위치를 사용하면서 용량을 줄인다.

  • 저장소(Content-addressable store): 패키지 파일 원본
  • 가상 저장소(virtual store): 저장소의 하드 링크
  • node_modules: 가상 저장소의 심볼릭 링크

폴더 구조: npm

pnpm은 평탄하지 않은 node_modules를 갖는다. 이것을 이해하려면 npm이 어떻게 패키지를 폴더로 관리했는지 이력을 알아야 한다.

npm2까지는 관련 패키지들을 묶어 폴더를 관리했다. 이런 식이다.

node_modules
  - express
    - cookie
  - msw
    - cookie

이것은 두 가지 문제가 있다.

  1. 노드가 모듈을 불러올 때 경로가 길 경우 윈도우즈 환경에서는 불러오지 못한다. cookie처럼 중첩된 패키지를 노드가 불러오는 경우가 그렇다. cookie안에도 의존성이 있고 그 안의 패키지도 중첩된다면 경로 문자가 길어지게 되기 때문이다.

  2. 용량도 급격히 늘어난다. 같은 버전의 cookie를 사용하더라도 의존하는 패키지 폴더에 각 각 복사해야 하기 때문이다.

이러한 문제를 개선하려고 npm3부터는 평탄한 구조의 node_modules 폴더를 사용한다.

  • 모든 패키지가 node_moudles 하위에 위치하기 때문에 긴 경로로 인한 문제를 예방할 할 수 있다.

  • 같은 버전의 패키지를 여러 번 사용하더라도 node_modules 바로 아래 위치하기 때문에 폴더는 하나만 생성된다. 용량도 줄일 수 있다.

pnpm은 이것도 세 가지 문제를 지적한다.

  1. 평탄화되면서 모듈별로 비의존 패키지에 접근할수 있다. 가량 cookie 코드에서는 express나 msw 패키지에 접근할수 있다. require('../express) 이런 식이다. 물론 이렇게 코드를 작성하지 않겠지만 말이다.

  2. npm이나 yarn이 패키지 의존성을 분석해 평탄하게 만드는 것은 상당히 복잡한 알고리즘이라고 한다. 생각해보면 중복 패키지를 걸러내고, 같지만 버전이 다른 패키지도 발라내야 할 것 같다. 복잡함이 어느 정도인지는 잘 모르겠다.

  3. 일부는 패키지의 node_modules 안에 복사해야한다. 같은 패키지라도 버전이 다를 경우다. 같은 cookie라도 버전이 다를 경우 해당 패키지 가령 msw 폴더 하위의 node_modules 안에 복사해야 한다. 일관성이 부족하다.

폴더 구조: pnpm

pnpm은 평탄하지 않은 node_modules를 유지하면서 위 세 가지 문제를 해결한다. 바로 심볼링 링크를 활용한다.

패키지를 설치하면 먼저 가상 스토어 node_modules/.pnpm 에 패키지 하드 링크를 만든다. 모든 패키지들이 버전과 함께 평탄하게 유지된다.

ls -l ./node_modules/.pnpm

express@4.18.2
cookie@0.5.0
cookie@0.4.2
msw@1.3.2

그리고나서 node_modules에는 의존성에 따라 패키지별로 그룹지어 심볼릭 링크를 만든다.

ls -li ./node_modules

express -> .pnpm/express@4.18.2/node_modules/express
msw -> .pnpm/msw@1.3.2/node_modules/msw

node_modules에는 package.json의 dependency 객체에 선언한 패키지의 폴더만 있다. 이 폴더는 가상 스토어를 가리킨다. 노드는 심볼릭 링크의 실제 경로를 실행하기 때문에 경로가 길어질 가능성이 없다. 그렇기 때문에 긴 경로 문제도 발생하지 않을 것이다.

그럼 가상 스토어의 msw는 종속성인 cookie를 어떻게 해결할까?

ls -li node_modules/.pnpm/express@4.18.2/node_modules

cookie -> ../../cookie@0.5.0/node_modules/cookie

가상 스토어에 있는 express 패키지는 자체 node_modules 폴더를 가진다. 이것도 가상 스토어를 가리키는 심볼릭 링크다. 노드는 이 심볼릭 링크의 원래 주소, 즉 가상 스토어의 경로를 사용한다.

정리하면 pnpm은 비평탄한 node_modules를 유지한다. 그러면서 평탄한 구조의 문제를 심볼릭 링크로 해결한다.

결론

pnpm은 두 가지 측면에서 지금의 npm보다 성능이 좋다.

  1. 디스크 용량을 적게 사용한다. 패키지를 한 번만 다운로드해 놓은 저장소를 관리하면서 각 사용처에서는 하드 링크로 연결해서 사용한다.

  2. 평탄하지 않은 node_modules 폴더를 유지하면서 발생할수 있는 문제를 해결한다. 연관된 패키지만 묶기 때문에 관련 없는 패키지지로의 접근을 차단할 수 있다. 평탄 작업을 위한 복잡한 작업도 필요없다. 모든 패키지를 일관적인 폴더 구조로 유지할 수 있다.

참고