ESM 모듈을 CJS 방식으로 불러오기 (feat. tsconfig.json)

이수빈·2024년 12월 27일
0

Typescript

목록 보기
15/17
post-thumbnail

Node.js 에서 ESM 모듈 사용하기

  • 예전에는 브라우저 환경이 아닌 Node 환경에서 ESM 모듈을 사용하려면 Babel 과 같은 트랜스파일러를 사용해서 코드 변환을 해줘야 했다.

  • 하지만 Node.js 13.2버전 부터는 ESM에 대한 정식지원이 시작되어서 굳이 트랜스파일을 안해줘도 ESM모듈을 사용 할 수 있게 되었다.

Babel 트랜스파일링

  • https://www.daleseo.com/js-babel-node/

  • 바벨은 JS 트랜스파일러이다.

  • 소스 코드를 머신 코드로 바꿔주는 compile과 달리, transpile은 같은 언어를 유지한체 다른 런타임에서 해당 코드가 정상적으로 해석될 수 있도록 형태만 바꿔준다는 차이가 있다.

  • 보통 3가지 => babel core, babel cli, babel preset이 필요함. 자세한건 위의 링크 참고하자

  • 여기서 3가지를 설치 한 후 런타임에 => 트랜스파일링 된 코드를 동시에 실행가능함.

  • .babelrc 이라는 설정파일을 추가하거나, package.json에 preset 설정도 추가 가능함.

  • $ npx babel --presets @babel/env index.js | node

파일 단위로 ES 모듈 적용

프로젝트 단위로 ES모듈 적용

  • package.json 설정을 통해 전체 파일에 적용 하는 방법. 파일 확장자를 일일이 바꾸지 않고 프로젝트 전체에 ES 모듈을 적용하고 싶을 때 적합한 방법이다.
{
  // 생략
  "type": "module"
  // 생략
}

package.json, TSconfig 모듈 해석방법

ref) https://toss.tech/article/commonjs-esm-exports-field

  • 글을 읽으면서 내용을 정리한다.

CJS, ESM 양쪽 모듈해석의 필요성

  • 프로젝트에서 두개의 Module System 을 지원하는 것은 중요하다. => SSR 방식에서는 Node.js를 사용하기 때문에 CJS가 기본방식.

  • Tree-shaking(번들링시 필요하지 않은 모듈이나 코드 삭제) 과정에서도 CJS는 Tree-shaking이 어렵고 ESM은 쉽게 가능하다.

  • CJS는 기본적으로 require / module.exports 를 동적으로 하는 것에 아무런 제약이 없다. => 즉 빌드타임이 아닌 런타임에 모듈간의 관계를 파악하는 것. (모든 파일을 분석해야한다.)

// require
const utilName = /* 동적인 값 */
const util = require(`./utils/${utilName}`);

// module.exports
function foo() {
  if (/* 동적인 조건 */) {
    module.exports = /* ... */;
  }
}
foo();
  • 하지만 ESM은 정직인 구조로 모듈끼리 의존하도록 강제한다. import path에 동적인 값을 사용할 수 없고, export는 항상 최상위 스코프에서만 사용할 수 있다.

  • ESM은 그래서 빌드단계에서 모듈관의 의존관계를 파악 할 수 있고, Tree Shaking이 가능하다.

import util from `./utils/${utilName}.js`; // 불가능

import { add } from "./utils/math.js"; // 가능

function foo() {
  export const value = "foo"; // 불가능
}

export const value = "foo"; // 가능

CJS, ESM 모듈해석 방법

  • 기본적인 모듈 해석방식은 다음과 같다.
  • .js 파일의 Module System은 package.json의 type field에 따라 결정됩니다.
  • type field의 기본값은 "commonjs" 이고, 이 때 .js 는 CJS로 해석됩니다.
  • 다른 하나는 "module" 입니다. 이 때 .js 는 ESM으로 해석됩니다.
  • .cjs 는 항상 CJS로 해석됩니다.
  • .mjs 는 항상 ESM으로 해석됩니다.
  • 이때 package.json에 type : 'module'을 명시하면, 해당 package에 있는 .js파일이 모두 ESM으로 해석된다.

  • 이때 두가지 방법 모두 매끄럽게 제공하려면 => export field에 대해 알아봐야한다.

  • export field는 package.json의 main field와 같은 역할을 한다. 패키지 entry point 를 지정 할 수 있다.

subpath exports

  • 기존 경로는 fileSystem 기준으로 동작한다. => subpath를 지정해서 import path를 커스텀 할 수 있다.
// 디렉토리 구조
/modules
  a.js
  b.js
  c.js
index.js

require("package/a"); // 불가능
require("package/modules/a"); // 가능
// CJS 패키지
{
  "name": "cjs-package",
  "exports": {
    ".": "./index.js",
    "./a": "./modules/a.js",
  },
}
  
// ./a.js가 아니라
// ./modules/a.js를 불러온다.
require("cjs-package/a");

// 에러
// ./b는 exports field에 명시하지 않은 subpath이다.
require("cjs-package/b");
  • 추가적으로 import, require 즉 CJS, ESM 모듈 방식에 따라 특정 조건에 다른 모듈을 제공 할 수도 있다.
{
  "name": "cjs-package",
  "exports": {
    ".": {
      "require": "./dist/index.cjs",
      "import": "./esm/index.mjs"
    }
  }
}

// ESM은 .mjs로 명시해야함 : 해당 package가 CJS 모듈일때 
{
  "exports": {
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.mjs"
    }
  }
}

// CJS는 .cjs로 명시해야함 : 해당 package가 ESM 모듈일 때 
{
  "type": "module"
  "exports": {
    ".": {
      "require": "./dist/index.cjs",
      "import": "./dist/index.js"
    }
  }
}


  • 주의점이 있다. export field는 .으로 하는 상대경로로 작성되어야 한다.
// X
{
  "exports": {
    "sub-module": "dist/modules/sub-module.js"
  }
}

// O
{
  "exports": {
    ".": "./dist/index.js",
    "./sub-module": "./dist/modules/sub-module.js"
  }
}

Typescript Module Resolution

  • Typescirpt 자체의 컴파일러를 통해 module import시 항상 Type Definition을 찾는다.

  • 기존에는 fileSystem 기반으로 모듈을 탐색했다.

  • 하지만 TypeScript 4.7부터 moduleResolution 옵션에 node16 과 nodenext 가 정식으로 추가되었고, node16 과 nodenext 는 filesystem 기반이 아닌 exports field로부터 Type Definition을 탐색한다.

  • 또한, CJS TypeScript( .cts )와 ESM TypeScript( .mts )를 구분한다.

// CJS package
// ESM TS는 mts로 명시해야함
{
  "exports": {
    ".": {
      "require": {
        "types": "./index.d.ts",
        "default": "./index.js"
      },
      "import": {
        "types": "./index.d.mts",
        "default": "./index.mjs"
      }
    }
  }
}

// ESM package
// CJS TS는 cts로 명시해야함
{
  "type": "module",
  "exports": {
    ".": {
      "require": {
        "types": "./index.d.cts",
        "default": "./index.cjs"
      },
      "import": {
        "types": "./index.d.ts",
        "default": "./index.js"
      }
    }
  }
}
  • moduleResolution : 'Node' 와 'NodeNext'는 현재는 도일하다. 아래 링크에 더 많은 옵션들이 있다.

https://www.typescriptlang.org/docs/handbook/modules/theory.html#module-resolution-is-host-defined

module : 'esNext' vs 'nodeNext'의 차이

  • esNext는 TypeScript는 최신 ECMAScript 표준에 따라 모듈을 출력한다. 예를 들어, import와 export 구문이 변환 없이 그대로 출력됨.
{
  "compilerOptions": {
    "module": "esNext",
    "target": "es2022",
    "moduleResolution": "node"
  }
}
  • nodeNext는 Node.js의 ESM과 CommonJS 모듈 해석 방식을 모두 지원하도록 설계된 옵션입니다.
{
  "compilerOptions": {
    "module": "nodeNext",
    "target": "es2022",
    "moduleResolution": "node"
  }
}

ref)
https://www.daleseo.com/js-node-es-modules/

profile
응애 나 애기 개발자

0개의 댓글