Skip to content

Instantly share code, notes, and snippets.

@shoveller
Last active August 22, 2025 03:52
Show Gist options
  • Select an option

  • Save shoveller/56e5a84560f48ceabeda221bb74350f1 to your computer and use it in GitHub Desktop.

Select an option

Save shoveller/56e5a84560f48ceabeda221bb74350f1 to your computer and use it in GitHub Desktop.
node.js 의 모듈 해석 알고리즘(module resolution algorithm)

요약

  • node.js 는 모듈을 import 할 때 사용하는 독자적인 규칙이 있다.
  • 이 알고리즘을 module resolution algorithm 이라고 부른다.
  • 이 알고리즘은 퍼포먼스와 관련해서 비판이 있다.
  • npm, yarn classic 은 node.js 의 module resolution 을 그대로 사용한다.
  • yarn berry 와 pnpm 은 node.js 의 module resolution 을 사용하지 않는다.

모듈 해석 알고리즘(module resolution algorithm)

모듈 해석 알고리즘은 아래와 같은 단계를 따른다

  1. 코어 모듈(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
  1. 파일 모듈(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
  2. 디렉토리 모듈(Directory Modules) 인가?
    파일 모듈이 아니면 Node.js는 요청된 모듈이 디렉토리인지 확인한다.
    모듈이 디렉토리인 경우, 해당 디렉토리에 위치한 package.json 파일의 main 필드가 가리키는 파일을 찾는다.
    즉 아래와 같은 package.json 이 있다면.

{
  "main": "index.js"
}

아래와 같은 index.js 를 3순위로 읽어들인다.

console.log('file module')
  1. index.js 파일이 있는가?. 만약 package.json 파일이 없거나 package.json 에 main 필드가 없다면, Node.js는 해당 디렉토리에 위치한 index.js 파일을 찾는다.

    즉, 아래와 같은 디렉토리를 패키지로 간주하고 그 안의 index.js 를 4순위로 읽어들인다.

    .
    └── app
        └── index.js
  2. 노드 모듈(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'
]

이 단계에 대해 널리 알려진 단점은 아래와 같다

  1. 의존성 탐색 알고리즘이 비효율적이다.

    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 의 모듈 해석의 단점이 새로운 패키지 매니저의 탄생을 유발했다고 해도 과언이 아니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment