※"esModuleInterop"와 "allowSyntheticDefaultImports" 란?
"esModuleInterop": true와 "allowSyntheticDefaultImports": true는 모두 CommonJS 모듈을 ESM import 문법으로 가져오는 과정에서 발생하는 호환성 문제를 해결하기 위해 사용되지만, 각각 하는 역할과 범위가 다릅니다.
간단히 말해, esModuleInterop이 실제 코드(JavaScript)에 개입하여 문제를 해결한다면, allowSyntheticDefaultImports는 타입 검사(TypeScript) 단계에서만 개입하여 오류를 숨겨줍니다.
1. allowSyntheticDefaultImports
이 옵션은 TypeScript가 타입 검사를 수행하는 방식에 영향을 미칩니다.
- 역할: TypeScript에게 CJS 모듈에서 default 내보내기가 없는 경우에도, 해당 모듈을 import * as React from 'react'; 대신 import React from 'react';와 같이 가짜(Synthetic) 기본(Default) 가져오기를 사용하여 가져올 수 있다고 알려줍니다.
- 컴파일 결과: 이 옵션은 실제 JavaScript 출력 코드에는 영향을 미치지 않습니다. 순전히 TypeScript가 다른 모듈을 가져오는 방식에 대한 타입 정보를 추론하고 확인하는 데 사용됩니다.
- 활성화 시: CJS 모듈을 가져올 때, 해당 모듈의 전체 내보내기 객체가 기본 내보내기인 것처럼 취급됩니다.
2. esModuleInterop
이 옵션은 컴파일된 JavaScript 출력 코드에 영향을 미치며, allowSyntheticDefaultImports의 상위 집합으로 간주될 수 있습니다.
- 역할: TypeScript가 CJS 모듈을 ESM 방식으로 가져올 때 발생하는 런타임 동작 불일치를 해결하기 위해 컴파일된 JavaScript 코드에 헬퍼 코드(Interoperability code)를 추가합니다.
- 포함 관계: esModuleInterop를 true로 설정하면, 자동으로 allowSyntheticDefaultImports도 true로 설정됩니다.
- 컴파일 결과:
- import * as React from 'react'; 문을 컴파일할 때, TypeScript는 __importStar 헬퍼 함수를 추가합니다.
- import React from 'react'; 문을 컴파일할 때, TypeScript는 __importDefault 헬퍼 함수를 추가합니다.
- 이 헬퍼 함수들은 CJS 모듈을 가져올 때, module.exports 객체가 default 속성을 가지고 있는지 확인하고, 없으면 module.exports 객체 자체를 default로 래핑하여 ESM의 기본 가져오기(Default Import) 규칙을 따르도록 런타임에서 조정합니다.
즉 "esModuleInterop": true를 사용하는 경우, "allowSyntheticDefaultImports": true는 명시적으로 설정할 필요가 없습니다.
1."esModuleInterop": true의 포함 관계
단순한 하나의 설정이 아니라, 내부적으로 다음 두 가지 설정을 자동으로 활성화하도록 설계되었습니다.
- "allowSyntheticDefaultImports": true
- "importHelpers": true (일부 구문 변환에 필요할 수 있음)
따라서 TypeScript 공식 문서나 일반적인 NestJS/Node.js 환경에서는 다음과 같이 설정하는 것이 표준입니다.
tsconfig 설정
{
"compilerOptions": {
// 이 설정 하나가 "allowSyntheticDefaultImports": true를 포함합니다.
"esModuleInterop": true,
// ... 다른 설정들 ...
}
}
○ "importHelpers" 설정은 뭐야?
"importHelpers": true는 TypeScript 컴파일러 설정 중 하나로, 컴파일된 JavaScript 코드의 크기를 줄이고 중복을 제거하기 위해 사용하는 설정입니다.
TypeScript는 최신 JavaScript 문법(예: async/await, 클래스 상속)을 낮은 버전의 JavaScript(예: ES5)로 변환할 때, 변환된 코드를 보조하기 위한 작은 도우미 함수(Helper Functions)들을 생성합니다.
구문 변환 시 필요한 도우미 함수 예시
| 클래스 상속 | __extends |
| 데코레이터 | __decorate |
| async/await | __awaiter |
이 설정이 true일 때, TypeScript는 이 도우미 함수들을 각 파일마다 반복해서 삽입하는 대신, tslib 패키지에서 해당 함수들을 import하도록 코드를 변경합니다.
- importHelpers: false (기본값): 변환이 필요한 모든 파일의 상단에 도우미 함수 코드가 직접 삽입됩니다. (파일 크기 증가, 중복 발생)
- importHelpers: true: 도우미 함수 코드를 삽입하는 대신, 아래와 같은 구문을 생성합니다.
// (컴파일 결과)
import { __awaiter, __extends } from "tslib";
장점
- 번들 크기 절감: 특히 파일이 많은 대규모 프로젝트에서 도우미 함수가 중복 삽입되는 것을 막아 최종 번들 크기를 크게 줄여줍니다.
- 코드 중복 제거: 모든 파일이 하나의 중앙 라이브러리(tslib)에서 도우미 함수를 참조하게 되어 코드가 깔끔해집니다.
2.esModuleInterop: true 설정
tsconfig의 설정에 esModuleInterop: true 를 지정하게되면 TypeScript는 이제 CommonJS 모듈의 module.exports 전체를 "합성된 Default Export (Synthetic Default Export)"로 간주합니다.
가져오려는 모듈 문제 상황 동작여부
| dayjs (CommonJS 모듈) | import dayjs from 'dayjs'; | 정상 작동 ✅ (CJS 모듈인 dayjs를 ESM의 default import로 가져올 수 있게 함) |
| path (CommonJS 모듈) | import path from 'path'; | 정상 작동 ✅ (CJS 모듈인 dayjs를 ESM의 default import로 가져올 수 있게 함) |
아래 사진처럼 esModuleInterop을 true로 설정하면 컴파일에도 오류가 발생하지 않습니다.


3.esModuleInterop: false설정
TypeScript는 CommonJS 모듈을 가져올 때 엄격한 규칙을 적용합니다.
가져오려는 모듈 문제 상황 해결 방법 (복잡함)
| dayjs (CommonJS 모듈) | import dayjs from 'dayjs'; → 오류 발생! (Default Export가 없다고 판단) | import * as dayjs from 'dayjs'; 또는 const dayjs = require('dayjs');를 사용해야 함. |
| path (CommonJS 모듈) | import path from 'path'; → 오류 발생! (Default Export가 없다고 판단) | import * as path from 'path'; 또는 const path = require('path');를 사용해야 함. |
아래와 같이 This module is declared with 'export =', and can only be used with a default import when using the 'esModuleInterop' flag. 오류가 발생하게 됩니다.


※단 import * as X from 'Y' 식으로 import하면 성공을 하는 그 이유는 아래와 같습니다.

● esModuleInterop:fasle에서 import * as X from 'Y'가 성공하는 이유
import * as path from 'path'; 구문은 모듈이 내보내는 모든 것을 하나의 객체(path)로 묶어 가져오라는 의미입니다.
- 컴파일러의 해석: esModuleInterop: false일 때 TypeScript는 CommonJS 모듈을 만나면, 해당 모듈 전체가 내보내는 객체가 곧 네임스페이스라고 간주합니다.
- 실제 동작: 이 import * as path 구문은 런타임에서 const path = require('path'); 와 거의 동일하게 동작합니다. 즉, path 모듈이 내보낸 module.exports 객체 전체를 path라는 변수에 할당합니다.
- 결론: CommonJS 모듈은 그 자체가 하나의 export 객체이므로, 이를 전체 네임스페이스로 가져오는 import * as 구문은 문법적으로 충돌 없이 정상적으로 처리되는 것입니다.
따라서 import * as 구문은 esModuleInterop: true가 등장하기 전부터 CommonJS 모듈을 TypeScript 환경에서 안전하게 가져오는 표준적인 방법이었습니다.
이로써 node.js 환경을 설정할 때 주의해야하는 import 방식에 대해 알아봤습니다.
모듈의 import 방식의 구분과 왜 이렇게 되는지에 대한 원리를 살펴봤고 ESM과 CJS의 차이를 알기 위해선 아래 포스팅을 참고해주시면 감사하겠습니다.
https://gapal.tistory.com/80
'TypeScript > Nest.js' 카테고리의 다른 글
| NestJS 라이프사이클 (3/3): 우아한 종료와 리소스 정리 (Graceful Shutdown & Resource Cleanup) (0) | 2025.12.01 |
|---|---|
| NestJS 라이프사이클 (2/3): 가드, 파이프, 인터셉터! 요청이 Controller를 통과하는 7단계 (0) | 2025.11.25 |
| Node.js 모듈 시스템 완벽 정리: CJS vs. ESM 파헤쳐보기 (0) | 2025.11.20 |
| NestJS Swagger JWT 인증 설정 방법 | 전역 보안 적용 가이드 (2) | 2025.11.15 |
| Day.js 오류 해결 완벽 가이드 Cannot read properties of undefined (reading '$i') with ESM CommonJS (0) | 2025.11.12 |
