Node.js 모듈 확장자 완벽 정리: .js, .cjs, .mjs부터 TypeScript .cts, .mts까지
Node.js 애플리케이션 개발 시 CommonJS(CJS)와 ES Modules(ESM)가 혼용되면서 .js, .cjs, .mjs 및 TypeScript의 .ts, .cts, .mts 확장자에 대한 정확한 동작 규칙을 이해하는 것이 필수적입니다. 각 확장자별 해석 메커니즘과 설정 방법을 정리합니다.
1. Node.js 모듈 시스템의 2가지 축: CommonJS vs ES Modules
Node.js 런타임은 모듈을 로딩하고 실행하는 두 가지 시스템을 탑재하고 있습니다.
- CommonJS (CJS): Node.js의 전통적인 동기식 모듈 시스템입니다. require() 함수로 모듈을 동기적으로 불러오며, module.exports 객체를 통해 기능을 내보냅니다. 탑레벨(Top-level) await 구문을 사용할 수 없습니다.
- ES Modules (ESM): ECMAScript 정식 표준 비동기 모듈 시스템입니다. import 및 export 구문을 사용하며, 모듈 구문 분석이 정적(Static)으로 수행됩니다. Strict Mode가 기본 적용되며 탑레벨 await 구문을 지원합니다.
2. JavaScript 모듈 확장자의 동작 원리 (.js, .cjs, .mjs)
Node.js 런타임은 파일 확장자와 프로젝트의 package.json 설정을 조합하여 모듈의 구동 방식을 판별합니다.
(1) .js 확장자와 package.json의 "type" 필드
.js 확장자는 자체만으로 CJS인지 ESM인지 결정되지 않으며, 해당 파일이 위치한 가장 가까운 상위 package.json 파일의 "type" 필드값에 따라 해석됩니다.
- "type": "commonjs" (또는 "type" 필드 생략 시): 해당 디렉터리 내의 모든 .js 파일을 CommonJS로 처리합니다.
- "type": "module": 해당 디렉터리 내의 모든 .js 파일을 ES Modules로 처리합니다.
(2) .cjs 확장자
package.json 내의 "type" 필드 설정과 상관없이 해당 파일이 명시적으로 CommonJS 모듈임을 런타임에 알립니다.
- "type": "module"로 지정된 프로젝트 내부에서 일부 레거시 스크립트나 동기식 설정 파일을 CJS로 동작시켜야 할 때 활용됩니다.
(3) .mjs 확장자
package.json 내의 "type" 필드 설정과 상관없이 해당 파일이 명시적으로 ES Modules임을 런타임에 알립니다.
- 기본 CJS 프로젝트 내에서 비동기 모듈 로딩이나 탑레벨 await를 사용해야 하는 독립 모듈을 작성할 때 활용됩니다.
3. TypeScript 모듈 확장자의 동작 원리 (.ts, .cts, .mts)
TypeScript 4.7 버전부터 도입된 Node16 및 NodeNext 모듈 해석 전략(moduleResolution)에 따라 컴파일 결과물 파일 확장자가 다르게 생성됩니다.
(1) .ts 확장자
일반적인 TypeScript 소스 파일입니다. 컴파일 타임에 tsconfig.json의 "module" 설정과 package.json의 "type" 필드 조합에 따라 최종적으로 .js 파일로 컴파일됩니다.
(2) .cts 확장자
CommonJS 모듈로 변환됨을 보장하는 TypeScript 소스 파일입니다.
- 컴파일 시 자바스크립트 파일인 .cjs 파일과 타입 선언 파일인 .d.cts 파일로 최종 변환됩니다.
- 코드 내부에서 CJS의 require() 구문 및 module.exports 타깃으로 컴파일됩니다.
(3) .mts 확장자
ES Modules로 변환됨을 보장하는 TypeScript 소스 파일입니다.
- 컴파일 시 자바스크립트 파일인 .mjs 파일과 타입 선언 파일인 .d.mts 파일로 최종 변환됩니다.
- 코드 내부에서 ESM의 import 및 export 구문으로 컴파일되며, 탑레벨 await 구문을 사용할 수 있습니다.
4. 확장자 및 모듈 해석 규칙 종합 비교표
| 확장자 | 모듈 체계 | package.json 영향 | 주요 로딩 구문 | 컴파일 후 생설물 (TS 기준) |
| .js | CJS 또는 ESM | 영향 받음 ("type" 필드 기준) | require() 또는 import | (JS 원본) |
| .cjs | CommonJS | 무시함 (무조건 CJS) | require(), module.exports | (JS 원본) |
| .mjs | ES Modules | 무시함 (무조건 ESM) | import, export | (JS 원본) |
| .ts | CJS 또는 ESM | 영향 받음 ("type" 및 TS 설정) | import/export (TS 구문) | .js + .d.ts |
| .cts | CommonJS | 무시함 (무조건 CJS) | import/export -> require 변환 | .cjs + .d.cts |
| .mts | ES Modules | 무시함 (무조건 ESM) | import/export 유지 | .mjs + .d.mts |
5. CJS와 ESM 상호 운용 시 주의사항 (Interoperability)
두 모듈 체계가 혼용될 때 런타임 에러가 발생할 수 있으므로 구문 호출 규칙을 준수해야 합니다.
(1) ESM 파일에서 CJS 모듈 불러오기
ESM (.mjs 또는 "type": "module" 적용 .js) 내부에서 CJS 모듈을 가져올 때는 import 구문을 사용할 수 있습니다.
// ESM 환경에서 CJS 모듈 로딩
import pkg from './legacy-module.cjs'; // default import 방식 권장
- 주의: CJS 모듈은 동기적으로 평가되므로 Named Export(import { foo } from ...) 호출 시 CJS 내부의 module.exports 개체 분석 형태에 따라 undefined 에러가 발생할 수 있습니다. Default Import 사용이 권장됩니다.
(2) CJS 파일에서 ESM 모듈 불러오기
CJS (.cjs 또는 기본 .js) 내부에서는 동기식 구문인 require()로 ESM 모듈을 직접 로딩할 수 없습니다. (ERR_REQUIRE_ESM 에러 발생)
// CJS 환경에서 ESM 모듈을 로딩하려면 동적 임포트(Dynamic Import) 필수
async function loadESM() {
const esmModule = await import('./modern-module.mjs');
esmModule.doSomething();
}
6. 결론
프로젝트 환경이 ESM으로 전환되는 추세에 따라 기본 모듈은 ESM을 사용하되, CJS 패키지 호환성 및 빌드 출력 형태를 명확히 고정해야 할 때는 .cjs/.mjs 및 .cts/.mts 확장자를 명시하여 모듈 혼선을 방지할 수 있습니다.
'개발 > Web & Backend' 카테고리의 다른 글
| Electron과 Tauri 비교 : 성능, 메모리, 사용법 및 도입 가이드 (0) | 2026.08.07 |
|---|---|
| Vercel 및 Supabase 유사 서비스(대안 플랫폼) 비교 가이드 (0) | 2026.07.31 |
| Vercel과 Supabase로 0원 풀스택 웹사이트 무료 배포 및 서버 구축 방법 (0) | 2026.07.31 |

