ESM에서 alias path 사용하기 (module-alias)
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