Nodejs

ESM에서 alias path 사용하기 (module-alias)

아홉번째태양 2023. 8. 13. 12:31

module-alias

최근 한 프로젝트의 모듈 시스템을 ESM으로 업데이트하다가 ESM에서는 기존에 alias path를 사용하던 방식이 먹히지 않는 것을 확인했다. CJS 프로젝트에서는 module-alias라는 라이브러리를 이용해서, 사용중인 alias path들을 빌드 경로와 직접 맵핑해주는 방식을 사용해왔다.

예를들어,

// alias.ts
import { join } from 'node:path';
import moduleAlias from 'module-alias';

const DIST_PATH = join(__dirname, '..', 'types');

moduleAlias.addAliases({
  types: DIST_PATH,
});
// index.ts
import ./alias.ts;

...

하지만 이 코드는 CJS와 ESM이 모듈을 읽는 방식이 다르기 때문에 ESM으로 설정하는 순간 무용지물이 되며, 실행을 해보면 아래와 같은 에러가 발생한다.

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/me/gitove/src/types/commit' imported from /Users/me/gitove/dist/commands/commit/questions.js
    at new NodeError (node:internal/errors:400:5)
    at finalizeResolution (node:internal/modules/esm/resolve:308:15)
    at moduleResolve (node:internal/modules/esm/resolve:945:10)
    at defaultResolve (node:internal/modules/esm/resolve:1153:11)
    at nextResolve (node:internal/modules/esm/loader:163:28)
    at file:///Users/me/gitove/node_modules/.pnpm/esm-module-alias@2.0.3/node_modules/esm-module-alias/index.js:34:12
    at nextResolve (node:internal/modules/esm/loader:163:28)
    at ESMLoader.resolve (node:internal/modules/esm/loader:842:30)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:424:18)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:77:40) {
  code: 'ERR_MODULE_NOT_FOUND'
}

Node.js v18.13.0
 ELIFECYCLE  Command failed with exit code 1.

 

모듈을 언제 읽어오는가?

CJS에서는 모듈을 런타임에 읽는다. 그래서 다음과 같은 상황이 발생할 수 있다.

// a.js
console.log('a1');
console.log(require('./b').b);
console.log('a2');
exports.a = 1;

// b.js
console.log('b1');
console.log(require('./a').a);
console.log('b2');
exports.b = 2;
$ node a.js 
a1
b1
undefined
b2
2
a2

아직 a가 export되지 않았기 때문에 b.js에서 require('./a').a을 찍어보면 undefined라고 나온다.

반면에, ESM에서는 런타임이 아닌 실행전에 코드를 분석하면서 모듈의 경로들을 먼저 읽어낸다. 즉, module-alias처럼 런타임 중 가장 먼저 실행이 되도록 배치하여 이후 다른 모듈의 경로를 읽을 때 alias path를 인지하도록 하는 방법은 애초에 불가능해진다.

 

"-r" 플래그를 사용할 수는 없을까?

보통 ESM에서 이런 유사한 문제가 발생하는 경우 흔하게 사용되는 해법 중 하나는 node를 실행할 때 -r 옵션을 주고 필요로하는 모듈을 먼저 실행하는 것이다.

-r, --require=...                 module to preload (option can be repeated)

$ node --experimental-specifier-resolution=node -r module-alias/register --no-warnings dist/index.js",

하지만, 이 방법에도 한가지 문제가 있다.

module-alias가 내부적으로 require문을 사용하고 있기 때문에 다음과 같은 에러가 발생한다.

[Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/me/gitove/dist/config/alias.js not supported.
Instead change the require of alias.js in null to a dynamic import() which is available in all CommonJS modules.] {
  code: 'ERR_REQUIRE_ESM'
}

이에 대해 esm이라는 라이브러리를 추가로 설치하거나, ts-node에 내장된 esm 모듈을 또 추가로 -r 옵션과 함께 전달해주는 방식들도 소개되기는 하는데, 내 경우에는 잘 되지 않았다.

 

esm-module-alias

그래서 찾아보니 esm-module-alias라는, 같은 문제를 먼저 겪으셨던 고마운 개발자분께서 만든 라이브러리가 있었다.

사용방법은 module-alias와 유사하다.

import generateAliasResolver from 'esm-module-alias';

const aliases = {
  types: 'dist/types',
};
export const resolve = generateAliasResolver(aliases);

하지만 마찬가지로 ESM에서는 프로그램 실행 이전에 설정을 적용하기 위해 코드상에서 import해 오는 것이 아니라, 이 경우에는 -l 플래그를 이용해 기본 파일로더를 교체한다.

--loader, --experimental-loader=...      use the specified module as a custom loader

$ node --loader=./dist/config/loader.js --experimental-specifier-resolution=node --no-warnings dist/index.js

 

 

참고자료

ESM 삽질기
Stackoverflow - ESM does not resolve module-alias
npm - esm-module-alias