이번에 따로 시간을 내어 알아보기 전 까지는 저는 그냥 yarn을 사용하고 있었습니다. 딱히 별 생각은 없었고 그 당시(2020년 쯤..?)에 그저 npm 보다 빠르고 성능이 좋다고 알려져 있기도 했고, npm보다 yarn이 뭔가 있어보여서 yarn을 사용했던거 같습니다.
근데 시대가 많이 변했는지 yarn berry나 pnpm 같은 새로운 패키지 매니저에 대해 들려오더군요. 이번에 신규 사이드 프로젝트를 진행하게 되면서 프로젝트 세팅을 할 예정인데 pnpm으로 넘어가도 좋은지 알아보기 위해 이렇게 시간을 내어 조사해보게 되었습니다.
😂 딱히 전문가가 아니기 때문에 글이 단순하며 틀린 부분이 있을 수 있습니다. 참고부탁드립니다. 제가 이해하는 내용에 대해서만 설명드리겠습니다.
여러분들은 패키지 매니저의 필수 기능(=기본 기능)은 뭐라고 생각하시나요? 저는 아래 목록은 기본적으로 있어야 하지 않을까 생각합니다.
그 중에서 당연 중요한게 있으니 바로 의존성 관리일 것입니다. 그리고 이는 npm, yarn, yarn berry, pnpm 중 뭘 사용하던 기본적으로 제공되는 기능이라 할 수 있습니다.
그렇다면 npm, yarn, yarn berry, pnpm는 어떤 차이가 있을까요?
결국 의존성 관리 방법에 따라 설치 속도나 디스크 사용량이 차이가 나는 것이기 때문에 각 패키지 매니저에 따라 의존성 관리를 어떻게 하는지 살펴보면 좋을 것입니다.
✍️ 겉으로는 다 비슷해보고 우리같이 사용만 하는 입장에서는 아무거나 잘 되는거 사용하면 되는 입장이긴 하지만, 내부 동작은 매우 다르다는 사실을 알고 있으면 좋겠다.
가장 간단한 의존성에 대해 이해를 해봅시다. 우리는 A(1.0)과 C(1.0)이라는 라이브러리를 설치해서 사용하려고 합니다. 하지만 실제로는 A는 B(1.0)를 필요로 하고, C는 B(2.0)를 필요로 할 수 있겠죠? 이런걸 서로 의존성이 얽혀있다고 합니다.
해당 이미지는 직방 기술 블로그에서 참고.
이랬을 때 처음 npm 버전 2 이하(npm@~2)에서는 단순하게 생각했습니다. 그냥 전부 각각 폴더 구조를 만들어서 사용해버리는 것이지요. 하지만 이랬을 때 너무 중복이 많아지지 않을까요? 지금은 B 버전이 달라서 따로 설치하는게 맞지만 버전이 같았다면 어땠을까요?
참고로 yarn 버전 1은 yarn classic이라고 부르고, yarn 버전 2는 버전 1과는 완전히 독립적인 yarn berry라는 녀석으로 만들어졌습니다.
npm 버전 3 이후와 yarn classic 에서의 의존성 관리는 npm@~2와 같은 중복 문제를 해결하기 위해 hositing 기법을 사용했습니다. hositing이란 위로 올린다는 뜻이죠?
위에 보는 것처럼 A(1.0), B(1.0)은 그냥 설치하면 중복이기 때문에 전부 끌어올려서(hoisting)해서 flat하게 만들어서 사용합니다. 하지만 그냥 끌어올리면 B(1.0)과 B(2.0) 처럼 버전 충돌이 날 수 있겠죠? 이 때는 어쩔 수 없이 이미 B가 있다면 나머지 B는 nested하게 상위 모듈 아래에 둡니다.
하지만 이런 방식에 문제점은 유령 의존성을 만들어낸다는 것인데요.
예를 들어, 우리는 'react-query'를 설치해서 사용해본다고 합시다. 하지만 정말 뜬금없이 'match-sorter' 라는 녀석을 우리는 불러와서 사용할 수 있을 것입니다. 이는 'match-sorter'은 'react-query'의 의존 관계에 있는 패키지이기 때문입니다.
import { useQueryClient } from "react-query";
import {matchSorter} from 'match-sorter' // ??
문제는 이런 유령 의존성에 있는 패키지는 내가 뭣모르고 사용했다가 나중에 어떤 패키지를 지웠더니 뜬금없는 곳에서 사이드 이펙트가 생길 수 있다는 것입니다. 이런 점이 혼란스럽게 만듭니다.
pnpm은 2017년에 세상에 등장했습니다. 알고보니 yarn berry보다 이전이었네요. pnpm의 의존성 관리 기법은 크게 두가지 관점에서 볼 수 있었습니다.
이렇게 사용했을 때, 패키지를 복사해서 사용하는 대신 hard link를 사용해서 빠르며 공간도 덜 차지 합니다. (근데 버전이 다른경우는 어떻게 되는거지…? 따로 설치하나?)
예를 들어, express를 설치하면 원래는 node_moduels에 전부 flat 하게 설치되던게, 이제는 express라는 덩어리가 생성되고 그 안에 flat하게 되는 구조라고 할 수 있습니다.
yarn berry(v2)는 2020년에 세상에 나왔습니다. yarn berry는 기존 의존성 관리 시스템과 많이 다릅니다.
보는 것처럼 node_modules를 생성하지 않습니다. 대신 .yarn/cache 폴더에 의존성 정보가 저장되고, .pnp.cis 파일에 의존성을 찾을 수 있는 정보가 기록됩니다.
또한, Zip 아카이브로 관리된다는 점도 특이한 부분입니다. 이는 스토리지 용량을 아낄 수 있고, 설치가 신속하게 되며, 특히 Zero-install 이라는 기술(?)도 적용해볼 수 있습니다. Zero-install 이란 의존성도 Git 등을 이용하여 버전 관리를 할 수 있는 방식을 말합니다. 이렇게 했을 때 의존성도 버전관리가 되니 브랜치가 바꾸었다고 yarn install을 실행하지 않아도 되고, CI에서 의존성 설치하는 시간도 크게 절약될 수 있다고 합니다.
저는 일단 이번 목표는 pnpm을 사용해보는 것입니다. yarn berry도 물론 흥미롭지만 시간 관계상 나중에 사용해보고 비교해보도록 하겠습니다.
설치는 매우 간단합니다. 하지만 에러가 납니다. 이 때는 그냥 sudo를 붙여주도록 합니다. 아무래도 global 저장소(.pnpm-store)를 만들고 사용할 때 권한이 있어야 봅니다.
npm i -g pnpm
pnpm 간단 사용법... 너무 단순해서 npm, yarn을 쓰다가 넘어가도 사용법에 있어서는 별 문제 없습니다.
pnpm으로 create-next-app 을 실행시켜보도록 합니다. next.js 공식문서에도 pnpm이 있는 것을 보니 pnpm이 점점 많이 사용되는 추세인듯 합니다.
pnpm create next-app
일단 그냥 체감상 실행속도는 확실히 빨라진거 같습니다. 그리고 설치하면서 나오는 로그를 살펴보면 특이한게 보였습니다.
Using pnpm.
Initializing project with template: app
Installing dependencies:
- react
- react-dom
- next
- typescript
- @types/react
- @types/node
- @types/react-dom
- eslint
- eslint-config-next
Downloading registry.npmjs.org/typescript/4.9.5: 11.6 MB/11.6 MB, done
Downloading registry.npmjs.org/next/13.2.1: 10.4 MB/10.4 MB, done
Downloading registry.npmjs.org/@next/swc-darwin-arm64/13.2.1: 27.9 MB/27.9 MB, done
Packages: +266
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
Content-addressable store is at: /Users/ckstn0777/Library/pnpm/store/v3
Virtual store is at: node_modules/.pnpm
dependencies:
+ @types/node 18.14.1
+ @types/react 18.0.28
+ @types/react-dom 18.0.11
+ eslint 8.34.0
+ eslint-config-next 13.2.1
+ next 13.2.1
+ react 18.2.0
+ react-dom 18.2.0
+ typescript 4.9.5
Progress: resolved 278, reused 0, downloaded 266, added 266, done
Done in 18.3s
Success! Created nextjs-boilerplate at /Users/ckstn0777/Documents/nextjs-boilerplate
“Packages are hard linked from the content-addressable store to the virtual store.” -> 이는 pnpm hard link 기능을 사용해서 content-addressable store(global store)를 이용하고 있다는 뜻이라고 보여집니다.
이번에는 아래 공간이 좀 궁금해졌습니다.
Content-addressable store is at: /Users/ckstn0777/Library/pnpm/store/v3
Virtual store is at: node_modules/.pnpm
일단 node_modules를 보니 확실히 다른 점이 눈에 보입니다. 폴더도 단순하게 얼마 없고, 화살표시로 hard link(symlink)가 적용되었습니다. 아마 /Users/ckstn0777/Library/pnpm/store/v3를 가리키고 있는거 같습니다.
이건 제가 이전에 yarn으로 만든 프로젝트에 node_modules 입니다. 확실히 yarn으로 만든 node_modules 구조를 보면 hoist가 되면서 전부 flat 하게 만들어진게 보입니다.
Content-addressable store is at: /Users/ckstn0777/Library/pnpm/store/v3 이 공간을 찾아보니 files랑 tmp 폴더가 존재했습니다. tmp는 일단 아무것도 없었고…
drwxr-xr-x 4 ckstn0777 staff 128 2 26 22:16 .
drwxr-xr-x 3 ckstn0777 staff 96 2 26 22:16 ..
drwxr-xr-x 258 ckstn0777 staff 8256 2 26 22:17 files
drwxr-xr-x 2 ckstn0777 staff 64 2 26 22:17 tmp
files를 들어가보니 이상한 폴더명을 한 폴더들이 여러개 있었다. 그리고 그 중 한개를 들어가보니 알 수 없는 파일들이 가~득 했었습니다. 더 이상 알아보기는 어려웠습니다.
/Users/ckstn0777/Library/pnpm/store/v3 에서 확인한 용량. 269MB
❯ du -hs
269M .
yarn으로 생성한 nextjs 프로젝트에서 확인해본 결과 381MB. 근데 여기에는 뭔가 더 설치를 해서… 좀 더 큰 걸 수도 있습니다.
❯ du -sh
381M .
근데 사실 당연히 큰 차이는 없을 거 같은게 지금은 아직 처음이라 전역 저장소를 사용하나 그냥 node_modules를 사용하나 별 차이는 없을 거 같습니다. 나중에 프로젝트가 점점 많아지면 그 때서야 전역 저장소가 더 이득이겠죠?