[FE] pnpm 동작 방식

yongkini ·어제
0

FE

목록 보기
9/9
post-thumbnail

pnpm은 패키지를 어디에 저장하고, 플젝에서 이걸 어떻게 가져다 쓸까

분석 계기

: 팀에서 쓰는 모노레포가 있는데, 내가 따로(모노레포가 아닌 개별 프로젝트) 프로젝트를 생성해서 쓰다가 모노레포로 합쳐야하는 일이 있었다. 그 때, package.json에 모든 패키지들이 적혀져 있었는데(당연히 개별 플젝이니까), 이를 모노레포로 합치면서 현재 팀 컨벤션에 맞게 모든 dependencies를 root의 package.json으로 옮기고자 했다. 근데, 옮기려고 보니까 따로 세팅해주는게 없는데 어떻게 개별 플젝에서 root에 있는 package.json의 dependencies의 패키지를 가져올 수 있는걸까? 라는 의문이 들었다. 예를 들어, 모노레포 안에 root package.json이 있고, A, B 프로젝트가 있을 때, A 프로젝트에서 A-1 이라는 패키지를 쓰고, 이를 root package.json에 적어놨다고 해보자. 하지만, B 프로젝트에서는 A-1을 쓰지 않는다. 이런 상황에 root 단위로 pnpm install 을 하고 쓰다보면 문제없이 A에서는 A-1 모듈을 정상적으로 import해서 쓸 수 있게 된다(A 플젝의 package.json에 A-1 모듈을 적은적이 없는데도 혹은 A 플젝으로 접근해서 pnpm install로 A-1을 설치해준 적이 없는데도). 본래도 궁금했어야 하지만, 실질적인 상황을 맞닥뜨리니까 어떻게 되는건지가 궁금하여 알아보게 됐다.

개별 프로젝트는 패키지를 어떤식으로 참조할까?

: 예를 들어, import "A-1" 이렇게 A 프로젝트에 돼있을 때, A플젝은 A-1을 어떻게 찾을까?. 일단 A 플젝 내의 node_modules를 먼저 찾는다. 그 다음에 없으면 스코프 체이닝을 하듯이 상위로 올라가서 찾는다. 그러다가 root node_modules까지 가게된다.

Node.js의 모듈 해석 방식
: Node.js는 require나 import로 패키지를 가져올 때, 현재 디렉토리의 node_modules부터 상위 디렉토리로 올라가며 패키지를 찾는 구조를 가지고 있습니다.

그럼 벌써 답이 나온건가?

: 맞다. 내가 앞서 궁금해던 것의 결론은 A 플젝에 개별적으로 설치를 안해둬도, 스코프 체이닝 하듯이 모듈을 찾아나서기 때문에 최상단 package.json에 적혀있고, 설치를 했다면 참조를 할 수 있게 된다. 그래서 지금 팀 프로젝트 컨벤션 형태처럼 root package.json에 일단 모든 모듈을 설치하되, root와 다른 버전을 쓰고 싶은 독특한 경우에 한해서 각 플젝 package.json에 기입하여 설치한다로 할 수 있는 것이다. 가장 먼저 플젝 내의 node_modules를 확인하기 때문에 가능한 것.

여기서 약간 다른 결의 의문이 들었다. node_modules는 꽤나 무거워보이는데, 여기에 모든 패키지를 실제로 저장하는건가 ?

: Nope 물론, 실제 node_modules 안에 .pnpm 폴더를 보면 뭔가 파일 자체가 들어있는 것처럼 뭔가가 많다. 하지만, 이는 실제 파일의 하드링크이다. 일단, 결론부터 말하면 node_modules에는 실제 패키지들에 대한 하드링크, 심볼릭링크가 있고, 실제로 플젝에서 import 해서 쓸 때는 lodash 같은 외부 패키지를 참조할 때는 하드링크를 쓰고, 워크스페이스, 즉, 모노레포 내에서 서로 패키지를 참조할 때는 심볼릭링크를 참조하면서 시작한다. 결론적으로, 실제 패키지는 다른 곳에 있고, 하드링크, 심볼릭 링크로 일종의 참조를 하는 형태로 관리하는 것. npm, yarn(v1)에서는 실제로 node_modules마다 실제 패키지를 저장했다고 하고, 이에 대한 효율화(그러면 node_modules가 상당히 무거워질거고, 캐싱 개념도 없음)로 pnpm이 나온 것(yarn berry는 방식이 다를뿐 효율화해서 나온 것은 동일함)

** `
외부 패키지 → 하드링크
모노레포 내부 패키지 → 심볼릭 링크

+a

: 하드링크 = Inode을 공유: 하드링크는 원본 파일과 새로운 하드링크 간에 같은 Inode을 공유합니다. 따라서 같은 파일의 서로 다른 이름이라고 생각할 수 있습니다.
: 심볼릭링크 = 심볼릭 링크(Symbolic Link 또는 Symlink)는 리눅스와 유닉스 기반 운영 체제에서 파일이나 디렉터리에 대한 간접적인 참조를 만드는데 사용되는 파일입니다.
심볼릭 링크는 원본 파일이나 디렉터리의 경로를 가지고 있으며, 이 경로를 통해 원본 파일이나 디렉터리에 대한 참조를 제공합니다.

그럼 실제 패키지 파일들은 어디서 들고 있을까 ?

  • Linux/MacOS: ~/.pnpm-store/v6(넘버는 다를 수 있음)
  • Windows: %USERPROFILE%.pnpm-store\v6(넘버는 다를 수 있음)

실제 패키지는 OS에 따라 PNPM의 글로벌 스토리지 경로에 저장된다. 프로젝트의 node_modules는 이 글로벌 저장소에서 하드링크를 통해 필요한 패키지를 참조할 뿐인 것이다. 결론적으로 pnpm은 네트워크 요청(실제 모듈 설치 요청) 및 디스크 사용량을 줄이기 위해 로컬 캐싱을 적극 활용한다. 그래서 동일한 패키지를 여러 플젝에서 쓰게될 때 중복해서 다운받지 않고(네트워크 및 디스크 사용 효율화) 로컬 저장소에 보관된 패키지를 사용한다(pnpm-store). 그래서 install을 하더라도 일단은 pnpm-store에 동일한게 있는지 먼저 체크하고, 있으면 하드링크, 심볼릭 링크를 생성해서 그것만 저장하는 식이라, 캐싱의 이점을 그대로 가져와서 효율화한것으로 보면 된다.

현재 컨벤션의 장단점은 뭘까

  • 장점: root package.json에서 모든 패키지를 관리하기 때문에 버전 통합이 용이하다.
  • 단점: 이번에 내 케이스와는 반대되는 케이스인데, 팀에서 관리하던 프로젝트를 외부로 빼야하는 상황이 생기면 package.json을 일일이 재작성해야한다. 왜? root에 모두 작성해서 개별 플젝에는 내용이 없으므로.

실제 플젝 A만 빌드, 배포한다고 할 때 트리쉐이킹을 해주는가?

: webpack이나 rollup, vite 등은 불필요한 코드를 제거하는 트리 쉐이킹을 해준다. 따라서 vercel에 배포할 때 실제 체크한거지만, 알아서 실제로 import해서 쓰는 패키지만 다운받는걸 볼 수 있다.(build logs)

dependencies:
+ next 15.1.3
+ react 19.0.0
+ react-dom 19.0.0
+ sass 1.82.0
+ zustand 5.0.2

devDependencies:
+ @eslint/eslintrc 3.2.0
+ @svgr/webpack 8.1.0
+ @types/node 20.0.0
+ @types/react 19.0.2
+ @types/react-dom 19.0.0
+ eslint 9.15.0
+ eslint-config-next 15.1.3
+ eslint-config-prettier 9.1.0
+ eslint-plugin-prettier 5.2.1
+ prettier 3.4.2
+ typescript 5.6.2
profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글