TypeScript 4.7의 Node.js ESM 지원과 moduleResolution bundler

배경

회사 프로젝트를 Node.js 16 LTS로 업그레이드하면서 ESM(ECMAScript Modules)으로 전환을 시도했다. TypeScript 4.7이 Node.js ESM을 공식 지원한다고 해서 적용해봤는데, 생각보다 까다로운 부분이 많았다.

문제 상황

기존 CommonJS 프로젝트를 ESM으로 전환하면서 import 경로 해석이 제대로 되지 않았다. 특히 확장자 없는 import가 런타임에서 에러를 발생시켰다.

// 이렇게 작성하면 tsc는 통과하지만 Node.js 실행 시 에러
import { helper } from './utils/helper';

해결 과정

1. package.json 설정

{
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  }
}

2. tsconfig.json 조정

{
  "compilerOptions": {
    "module": "ES2022",
    "moduleResolution": "node16",
    "target": "ES2022",
    "lib": ["ES2022"]
  }
}

moduleResolutionnode16으로 설정하는 게 핵심이었다. 이 옵션은 Node.js의 ESM 해석 방식을 정확히 따른다.

3. import 경로에 확장자 추가

Node.js ESM은 확장자를 명시해야 한다. 번거롭지만 규칙을 따라야 했다.

import { helper } from './utils/helper.js'; // .ts가 아닌 .js

컴파일 후 생성되는 .js 파일을 기준으로 작성해야 한다는 점이 직관적이지 않았다.

결과

ESM 전환 자체는 완료했지만, 기존 라이브러리들의 ESM 지원 여부를 일일이 확인해야 했다. 일부 패키지는 아직 CommonJS만 제공해서 dynamic import로 우회했다.

당분간은 CommonJS와 ESM을 혼용하는 과도기가 계속될 것 같다. 새 프로젝트라면 ESM으로 시작하는 게 낫지만, 기존 프로젝트 마이그레이션은 신중히 판단해야 한다.