ABOUT ME

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

Designed by Tistory.