[pnpm] 패키지 매니저 pnpm에 대해서 알아보기
나는 기본적으로 npm으로 코딩을 시작해서 npm으로 패키지등을 설치했었다. yarn으로 패키지를 설치하면 빠르다던가 그런 이야기가 있었지만, npm도 속도가 나쁘지 않은 것 같아서 계속적으로 사용하고 있었다. 그러다가 pnpm에 대해서 이야기가 많이 나오는 것을 보았다. yarn보다 빠르다길래 npm을 능가하고 yarn보다 빠르겠구나 생각해서 접해보게 되었다.
Package Manager
: 개발에 필요한 패키지를 설치, 업데이트, 수정, 삭제하는 작업을 편리하고 안전하게 수행하기 위해 사용되는 도구이다.
pnpm
npm에 앞에 p문자만 붙었다. 여기서 앞에 붙은 p는 performant의 '승진, 성능 기준에 맞는' 이라는 뜻을 가지고 있다. 그래서 고성능, 효율적인 뜻이라고 보면 좋을 듯하다.
용량의 차이
1) npm
npm은 하나의 패키지를 설치하면 의존하는 모든 패키지를 node_modules 폴더에 다운로드된다. 패키지가 중복되면 하나만 설치해서 공유한다. 버전이 다르면 폴더 이름이 같아서 설치할 수 없으므로, node_modules에 하나의 버전만 적용한다. 그리고 다른 버전은 사용하는 패키지 하위의 node_modules에 다운로드한다.
예를 들어, express를 설치했을 때 node_modules 폴더에 다운로드 한다. 의존하는 패키지(cookie, msw)도 이 디렉토리에 위치한다. msw에도 cookie를 사용하는데 express가 사용하는 것과 버전이 다르다. 이 경우 각 버전의 패키지를 각 각 분리해서 다운로드한다.
node_modules
- express
- cookie # express가 사용하는 버전
- msw
- node_modules
- cookie # msw가 사용하는 버전
npm은 프로젝트마다 사용하는 패키지를 자신의 node_modules 폴더에 관리한다. 의존 패키지가 같으면 하나의 폴더에 관리해 용량을 약간 줄일 수 있기는 하다. 그러나 프로젝트 간에 중복 패키지까지는 관리하지 못한다.
2) pnpm
pnpm으로 패키지를 설치하면 디스크의 세 군데 위치를 건드린다.
2-1) 가상 저장소
express를 추가한다. 일반 파일로 node_modules에 설치되는 것이 아니라, 심볼릭 링크로 설치된다.
ls -l ./node_modules
express -> .pnpm/express@4.18.2/node_modules/express
원격 저장소의 파일을 이 폴더에 직접 저장하지 않았다. node_modules/.pnpm 을 가리키는 링크만 있다. pnpm는 이곳을 가상 저장소(virtual-store)라고 부른다.
2-2) node_modules
ls -l ./node_modules/.pnpm
express@4.18.2
cookie@0.5.0
# ...
가상 저장소에는 설치한 패키지와 버전의 이름으로 폴더가 있다. 설치한 express와 이 패키지의 package.json에 기록된 의존 패키지들의 이름과 버전을 폴더로 구성한 것이다.
다른 버전의 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은 추가한 패키지를 node_modules에 직접 설치하지 않고 가상 저장소를 가리키는 심볼릭 링크로 구성한다. 하지만 패키지 폴더와 더불어 심볼릭 링크 파일도 추가해 용량이 더 늘었다.
# npm 으로 설치한 용량
du -shL ./node_modules
44M
# pnpm 으로 설치한 용량
du -shL ./node_modules
58M
2-3) store
pnpm이 용량을 줄일수 있는 비결은 바로 스토어(Content-addressable store)라고 불리는 장소이다. 터미널에 아래와 같이 쳐보면
pnpm store path
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 스토어에 원본 패키지를 유지한 채 각 프로젝트 폴더에서 이를 하드 링크로 구성하기 때문에 용량의 변화가 없다.
정리
- 가상 저장소(virtual store): 저장소의 하드 링크를 저장한다.
- 스토어(Content-addressable store): 패키지 파일 원본을 저장한다.
- node_modules: 가상 저장소의 심볼릭 링크를 저장한다.
💡 pnpm은 디스크의 세 위치를 사용하면서 용량을 줄인다.
폴더 구조
1) npm
npm2까지는 관련 패키지들을 묶어 폴더를 관리했다.
node_modules
- express
- cookie
- msw
- cookie
이것은 두 가지 문제가 존재한다.
- 노드가 모듈을 불러올 때 경로가 길 경우 윈도우 환경에서는 불러오지 못한다. cookie처럼 중첩된 패키지를 노드가 불러오는 경우가 그렇다. cookie안에도 의존성이 있고 그 안의 패키지도 중첩된다면 경로 문자가 길어지게 되기 때문이다.
- 용량도 급격히 늘어난다. 같은 버전의 cookie를 사용하더라도 의존하는 패키지 폴더에 각 각 복사해야 하기 때문이다.
이러한 문제를 개선하려고 npm3부터는 평탄한 구조의 node_modules 폴더를 사용한다.
- 모든 패키지가 node_moudles 하위에 위치하기 때문에 긴 경로로 인한 문제를 예방할 할 수 있다.
- 같은 버전의 패키지를 여러 번 사용하더라도 node_modules 바로 아래 위치하기 때문에 폴더는 하나만 생성된다. 용량도 줄일 수 있다.
pnpm은 이것도 세 가지 문제를 지적한다.
- 평탄화되면서 모듈별로 비의존 패키지에 접근할 수 있다. 가량 cookie 코드에서는 express나 msw 패키지에 접근할수 있다. require('../express) 이런 식이다.
- npm이나 yarn이 패키지 의존성을 분석해 평탄하게 만드는 것은 상당히 복잡한 알고리즘이라고 한다. 생각해보면 중복 패키지를 걸러내고, 같지만 버전이 다른 패키지도 발라내야 할 것 같다.
- 일부는 패키지의 node_modules 안에 복사해야한다. 같은 패키지라도 버전이 다를 경우다. 같은 cookie라도 버전이 다를 경우 해당 패키지 가령 msw 폴더 하위의 node_modules 안에 복사해야 한다. 일관성이 부족하다.
2) 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가 node_modules 폴더의 패키지를 중복으로 저장하지 않는다. 예를 들어,
sample_1, sample_2, sample_n라는 프로젝트가 있다고 가정하자. 각 프로젝트가 pkg_1을 포함하고 있다. pkg_1의 크기가 2MB라고 할때, npm이나 yarn 의 경우는 아래와 같이 각 프로젝트별로 node_modules 폴더 안에 pkg_1을 포함하기에 총 6MB의 디스크를 차지하게 된다.
반면, pnpm의 경우엔 별도의 저장소가 있다.(.pnpm_store) 각 프로젝트에서는 pkg_1에 대한 바로가기를 만듭니다. (심볼릭링크) 따라서 4MB(2MB*2)의 크기만큼 중복된 저장 공간을 효율화 할 수 있다. 프로젝트가 많을수록 이 효과는 더욱 커지게 된다.
결론
pnpm은 두 가지 측면에서 지금의 npm보다 성능이 좋다.
- 디스크 용량을 적게 사용한다. 패키지를 한 번만 다운로드해 놓은 저장소를 관리하면서 각 사용처에서는 하드 링크로 연결해서 사용한다.
- 평탄하지 않은 node_modules 폴더를 유지하면서 발생할 수 있는 문제를 해결한다. 연관된 패키지만 묶기 때문에 관련 없는 패키지로의 접근을 차단할 수 있다. 모든 패키지를 일관적인 폴더 구조로 유지할 수 있다
👇🏻 참고 사이트
https://jeonghwan-kim.github.io/2023/10/20/pnpm
https://devscb.tistory.com/135