: 팀에서 쓰는 모노레포가 있는데, 내가 따로(모노레포가 아닌 개별 프로젝트) 프로젝트를 생성해서 쓰다가 모노레포로 합쳐야하는 일이 있었다. 그 때, 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를 확인하기 때문에 가능한 것.
: Nope 물론, 실제 node_modules 안에 .pnpm
폴더를 보면 뭔가 파일 자체가 들어있는 것처럼 뭔가가 많다. 하지만, 이는 실제 파일의 하드링크
이다. 일단, 결론부터 말하면 node_modules에는 실제 패키지들에 대한 하드링크
, 심볼릭링크
가 있고, 실제로 플젝에서 import 해서 쓸 때는 lodash 같은 외부 패키지를 참조할 때는 하드링크
를 쓰고, 워크스페이스, 즉, 모노레포 내에서 서로 패키지를 참조할 때는 심볼릭링크
를 참조하면서 시작한다. 결론적으로, 실제 패키지는 다른 곳에 있고, 하드링크, 심볼릭 링크로 일종의 참조를 하는 형태로 관리하는 것. npm, yarn(v1)에서는 실제로 node_modules마다 실제 패키지를 저장했다고 하고, 이에 대한 효율화(그러면 node_modules가 상당히 무거워질거고, 캐싱 개념도 없음)로 pnpm이 나온 것(yarn berry는 방식이 다를뿐 효율화해서 나온 것은 동일함)
** `
외부 패키지 → 하드링크
모노레포 내부 패키지 → 심볼릭 링크
+a
: 하드링크 = Inode을 공유: 하드링크는 원본 파일과 새로운 하드링크 간에 같은 Inode을 공유합니다. 따라서 같은 파일의 서로 다른 이름이라고 생각할 수 있습니다.
: 심볼릭링크 = 심볼릭 링크(Symbolic Link 또는 Symlink)는 리눅스와 유닉스 기반 운영 체제에서 파일이나 디렉터리에 대한 간접적인 참조를 만드는데 사용되는 파일입니다.
심볼릭 링크는 원본 파일이나 디렉터리의 경로를 가지고 있으며, 이 경로를 통해 원본 파일이나 디렉터리에 대한 참조를 제공합니다.
실제 패키지는 OS에 따라 PNPM의 글로벌 스토리지 경로에 저장된다. 프로젝트의 node_modules는 이 글로벌 저장소에서 하드링크를 통해 필요한 패키지를 참조할 뿐인 것이다. 결론적으로 pnpm은 네트워크 요청(실제 모듈 설치 요청) 및 디스크 사용량을 줄이기 위해 로컬 캐싱을 적극 활용한다. 그래서 동일한 패키지를 여러 플젝에서 쓰게될 때 중복해서 다운받지 않고(네트워크 및 디스크 사용 효율화) 로컬 저장소에 보관된 패키지를 사용한다(pnpm-store). 그래서 install을 하더라도 일단은 pnpm-store에 동일한게 있는지 먼저 체크하고, 있으면 하드링크, 심볼릭 링크를 생성해서 그것만 저장하는 식이라, 캐싱의 이점을 그대로 가져와서 효율화한것으로 보면 된다.
: 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