- node.js 는 모듈을 import 할 때 사용하는 독자적인 규칙이 있다.
- 이 알고리즘을 module resolution algorithm 이라고 부른다.
- 이 알고리즘은 퍼포먼스와 관련해서 비판이 있다.
- npm, yarn classic 은 node.js 의 module resolution 을 그대로 사용한다.
- yarn berry 와 pnpm 은 node.js 의 module resolution 을 사용하지 않는다.
모듈 해석 알고리즘은 아래와 같은 단계를 따른다
-
코어 모듈(Core Modules) 인가?
먼저 Node.js는 요청된 모듈이 코어 모듈인지 확인한다.
코어 모듈은 Node.js 자체에 내장된 모듈이며, 경로를 지정하지 않고 직접 사용할 수 있다.node.js cli 에서는 아래와 같이 1순위로 import 할 수 있다.
node:prefix 를 붙여서 require 캐시를 건너 뛸 수도 있다. 참고로node:prefix 는 virtual module 이라고 한다. 시스템이 너무 당연하게 제공하는 코어 모듈을 서드파티 모듈과 구분하기 위한 수단이다.require('path'); require('node:path')
코어 모듈의 목록은 아래의 코드로 구할 수 있다.
require('module').builtinModules-
파일 모듈(File Modules) 인가?. 코어 모듈이 아니면 Node.js는 파일 시스템에서 모듈 파일을 찾는다.
이때 파일명에 확장자가 없으면.js,.json확장자를 가진 파일을 찾는다.즉 아래와 같은
test.js나console.log('file module')
test.json을.{ "type": "file module" }
node.js cli 에서 아래와 같이 2순위로 import 할 수 있다.
require('./hello/test'); // test.js 또는 test.json
-
디렉토리 모듈(Directory Modules) 인가?
파일 모듈이 아니면 Node.js는 요청된 모듈이 디렉토리인지 확인한다.
모듈이 디렉토리인 경우, 해당 디렉토리에 위치한package.json파일의main필드가 가리키는 파일을 찾는다.
즉 아래와 같은package.json이 있다면.
{
"main": "index.js"
}아래와 같은 index.js 를 3순위로 읽어들인다.
console.log('file module')-
index.js 파일이 있는가?. 만약
package.json파일이 없거나package.json에main필드가 없다면, Node.js는 해당 디렉토리에 위치한index.js파일을 찾는다.즉, 아래와 같은 디렉토리를 패키지로 간주하고 그 안의
index.js를 4순위로 읽어들인다.. └── app └── index.js
-
노드 모듈(Node Modules) 탐색 위의 모든 과정에서 모듈을 찾지 못하면, Node.js는 부모 디렉토리에서 시작하여 루트 디렉토리까지 이동하면서
node_modules디렉토리를 찾는다.
모듈 이름과 일치하는 디렉토리를 찾으면 해당 디렉토리에서 모듈을 찾아 사용한다.
이 절차가 바로 논란이 되는 부분이다. 터미널에서 node.js 를 켜고, repl 에 require.resolve.paths('module-b') 를 입력해보자.
node_modules 를 가질 수 있는 후보 목록이 표시된다. (PC의 설정에 따라 표시되는 내용이 다름)
저 후보 중에 한 곳에 패키지가 설치되어 있으면 되는 것이다. 이 기법을 호이스팅(hoisting)이라고 한다.
$ cd packages/module-a
$ node
Welcome to Node.js v16.14.0.
Type ".help" for more information.
> require.resolve.paths('packages/module-a')
[
'/Users/cinos81/study/hello-monorepo/packages/module-a/repl/node_modules',
'/Users/cinos81/study/hello-monorepo/packages/module-a/node_modules',
'/Users/cinos81/study/hello-monorepo/packages/node_modules',
'/Users/cinos81/study/hello-monorepo/node_modules',
'/Users/cinos81/study/node_modules',
'/Users/cinos81/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/cinos81/.node_modules',
'/Users/cinos81/.node_libraries',
'/Users/cinos81/.nvm/versions/node/v20.8.0/lib/node',
'/Users/cinos81/.node_modules',
'/Users/cinos81/.node_libraries',
'/Users/cinos81/.nvm/versions/node/v20.8.0/lib/node'
]이 단계에 대해 널리 알려진 단점은 아래와 같다
-
의존성 탐색 알고리즘이 비효율적이다.
node.js에서
require()함수를 실행하면 모듈을 찾을 때까지 상위node_modules디렉터리를 순회한다.이것이 호이스팅이다. 이 때 느린 디스크 I/O 동작이 경로의 깊이만큼 발생한다.
경로의 깊이가 블랙홀보다 깊어질 수 있다고 하여 node_modules black hole 이라 부르기도 한다.
-
저장 공간과 설치 시간이 낭비된다.
node_modules디렉터리는 저장 공간을 많이 차지한다. 그만큼 설치에 시간이 많이 걸린다.심지어 프로젝트마다 새로 설치된다.
-
유령 의존성(phantom dependency)
의존성 중복 방지를 위해 호이스팅 기법을 이용하는데 이것은 의도치 않은 side effect을 발생시킨다.
아래 그림에서 package-1은 B(1.0)을 설치한 적이 없지만 require('B')가 작동한다.
require('B')를 사용하는 경우 B(1.0)을 의존하던 패키지를 제거하면 B를 찾지 못하는 오류가 발생한다.조금 과장되게 표현을 하자면, lodash.js 를 삭제했는데 react.js 코드에서 에러가 날 수도 있다는 뜻이다.
이후에 등장한 yarn berry 와 pnpm 은 각자의 방식으로 node.js 의 모듈 해석을 새로 구현했다.
node.js 의 모듈 해석의 단점이 새로운 패키지 매니저의 탄생을 유발했다고 해도 과언이 아니다.

