TypeScript 4.7의 Node.js ESM 지원과 마이그레이션 경험
배경
회사 내부 라이브러리를 TypeScript로 관리하는데, 그동안 CommonJS 빌드만 제공했다. TypeScript 4.7이 출시되면서 Node.js ESM 지원이 개선되어 이참에 ESM으로 전환하기로 결정했다.
package.json 변경
가장 먼저 package.json에 "type": "module"을 추가했다. 그리고 exports 필드로 진입점을 명확히 정의했다.
{
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}
tsconfig.json 설정
module과 moduleResolution 설정이 핵심이었다.
{
"compilerOptions": {
"module": "ES2022",
"moduleResolution": "node16",
"target": "ES2022",
"lib": ["ES2022"]
}
}
moduleResolution: "node16"을 설정하면 TypeScript가 package.json의 exports 필드를 읽어 모듈을 올바르게 해석한다.
파일 확장자 명시
ESM에서는 import 시 파일 확장자를 명시해야 한다. 이게 가장 번거로웠다.
// Before
import { helper } from './utils/helper';
// After
import { helper } from './utils/helper.js';
.ts 파일에서 .js 확장자를 쓰는 게 어색했지만, TypeScript 컴파일러가 컴파일 후 경로를 기준으로 하기 때문에 이렇게 작성해야 했다.
마주친 문제들
일부 의존성 패키지들이 ESM을 제대로 지원하지 않아 dynamic import로 우회했다.
const { default: pkg } = await import('legacy-package');
__dirname과 __filename이 없어져서 import.meta.url로 대체했다.
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
결과
마이그레이션 후 번들 사이즈가 약간 줄었고, tree-shaking이 더 잘 동작했다. 하지만 생태계 전반의 ESM 지원이 아직 완벽하지 않아 일부 불편함은 감수해야 했다. 새 프로젝트라면 ESM을 권장하지만, 레거시 프로젝트는 신중히 판단할 필요가 있다.