
프로젝트 폴더를 열었는데 .js 파일도 있고 .mjs 파일도 있습니다. 분명 둘 다 자바스크립트인데, 왜 확장자가 다른 걸까요? 처음 보시는 분은 오타인가 싶을 수도 있습니다.
결론부터 말씀드리면, .mjs는 ES 모듈(ESM) 방식의 자바스크립트 파일이라는 뜻입니다. 반면 .js는 상황에 따라 CommonJS일 수도, ES 모듈일 수도 있습니다. 이 차이를 제대로 이해하면 Node.js 프로젝트에서 겪는 모듈 관련 오류 대부분을 해결할 수 있습니다.
자바스크립트에 모듈 시스템이 두 개인 이유
자바스크립트는 원래 모듈이라는 개념 자체가 없었습니다. 브라우저에서 <script> 태그로 불러오면 전부 전역 스코프에 섞이던 시절이 있었습니다.
Node.js가 2009년에 등장하면서 서버 환경에서 파일을 나눠 관리할 필요가 생겼고, CommonJS(CJS) 방식이 만들어졌습니다. require()와 module.exports를 쓰는 그 방식입니다.
// CommonJS 방식 (.js)
const fs = require('fs');
module.exports = { myFunction };
그 후 2015년, ECMAScript 표준에 ES Modules(ESM) 이 공식 포함되었습니다. import와 export를 쓰는 방식입니다.
// ES Module 방식 (.mjs 또는 ESM 설정된 .js)
import fs from 'fs';
export function myFunction() {}
문제는 두 방식이 호환되지 않는다는 점입니다. Node.js 입장에서는 .js 파일을 읽을 때 이게 CommonJS인지 ESM인지 판단해야 합니다. 이 판단 기준이 바로 확장자와 package.json 설정입니다.
.js와 .mjs, 핵심 차이 정리
확장자가 결정하는 것
구분 .js .mjs
| 기본 해석 방식 | package.json의 "type" 필드에 따라 달라짐 | 항상 ES Module로 해석 |
| require() 사용 | CJS 모드일 때 가능 | 불가능 |
| import/export 사용 | ESM 모드일 때만 가능 | 항상 가능 |
| 브라우저 호환 | 별도 설정 필요 | 명시적 |
핵심은 이겁니다. .mjs는 어떤 환경에서든 "나는 ES Module이야"라고 명확하게 선언하는 확장자입니다. 반면 .js는 주변 설정에 따라 해석이 달라지는, 일종의 중립적인 확장자입니다.
package.json의 "type" 필드
.js 파일이 어떤 모듈로 해석될지는 가장 가까운 package.json의 "type" 필드가 결정합니다.
{
"type": "module"
}
- "type": "module" → .js 파일을 ESM으로 해석
- "type": "commonjs" 또는 생략 → .js 파일을 CJS로 해석
그런데 .mjs는 이 설정과 무관하게 항상 ESM입니다. 마찬가지로 .cjs는 항상 CommonJS입니다.
실무에서 자주 만나는 상황
상황 1: "Cannot use import statement" 오류
SyntaxError: Cannot use import statement outside a module
이 에러가 뜬다면 .js 파일에서 import를 썼는데, Node.js가 해당 파일을 CommonJS로 해석하고 있다는 뜻입니다.
해결 방법은 두 가지입니다.
- 파일 확장자를 .mjs로 변경
- package.json에 "type": "module" 추가
상황 2: "require is not defined" 오류
ReferenceError: require is not defined in ES module scope
반대 케이스입니다. ESM 환경에서 require()를 쓰면 발생합니다.
해결 방법:
- require() 대신 import로 변경
- 해당 파일만 .cjs로 확장자 변경
상황 3: 하나의 프로젝트에서 두 방식을 섞어야 할 때
레거시 코드가 CommonJS이고 새 코드는 ESM을 쓰고 싶은 경우가 있습니다. 이때 확장자로 구분하는 방법이 가장 깔끔합니다.
- 기존 CommonJS 파일 → .cjs 유지
- 새 ES Module 파일 → .mjs 사용
- package.json의 "type"은 프로젝트 방향에 맞춰 설정
그래서 뭘 써야 하나요?
요즘 흐름은 확실히 ESM 쪽으로 기울고 있습니다. Node.js 공식 문서에서도 ESM을 표준으로 안내하고 있고, Deno나 Bun 같은 최신 런타임은 처음부터 ESM을 기본으로 채택했습니다.
새 프로젝트를 시작한다면 package.json에 "type": "module"을 설정하고 .js 확장자를 쓰는 방식이 가장 깔끔합니다. 이렇게 하면 별도의 .mjs 확장자 없이도 import/export를 자연스럽게 사용할 수 있습니다.
기존 프로젝트를 유지보수 중이라면 무리하게 전환하기보다, 새로 추가하는 파일부터 .mjs를 쓰면서 점진적으로 마이그레이션하는 방법이 현실적입니다.
한눈에 보는 정리
항목 CommonJS (CJS) ES Modules (ESM)
| 확장자 | .js (기본), .cjs | .mjs, 또는 "type":"module" 설정 시 .js |
| 문법 | require() / module.exports | import / export |
| 로딩 방식 | 동기(synchronous) | 비동기(asynchronous) |
| Top-level await | 불가능 | 가능 |
| this 스코프 | module.exports를 가리킴 | undefined |
| 트리 셰이킹 | 어려움 | 지원 |
| 방향성 | 레거시 유지 | 표준, 미래 지향 |
마무리
.js와 .mjs의 차이는 결국 자바스크립트 모듈 시스템의 역사에서 비롯된 것입니다. CommonJS가 먼저 자리를 잡았고, ES Modules가 표준으로 들어오면서 두 체계를 구분할 방법이 필요해졌습니다. 확장자는 그 구분을 가장 명확하게 해주는 수단입니다.
복잡해 보이지만 핵심은 간단합니다. .mjs는 항상 ESM이고, .js는 설정에 따라 달라진다. 이것만 기억하시면 대부분의 모듈 관련 문제를 해결하실 수 있습니다.
다음에 프로젝트에서 모듈 오류를 만나셨을 때, 이 글이 떠오르신다면 그것만으로도 충분합니다.
'IT일반' 카테고리의 다른 글
| 오픈클로 vs 헤르메스 에이전트 — 내 컴퓨터를 대신 움직이는 AI, 어느 쪽을 써야 할까 (0) | 2026.04.06 |
|---|---|
| SQLD 대비 - 엔터티(Entity) — "테이블이랑 뭐가 다른데?" 에 대한 정확한 답 (0) | 2026.03.25 |
| SQLD 대비 - 데이터 모델링, 설계 안 하면 나중에 울게 되는 이유 (0) | 2026.03.24 |
| SQLD, 개발자 굳이 따야 할까? — 시험 구조부터 합격 전략까지 (0) | 2026.03.23 |
| GTQ 자격증 독학으로 따는 법 — 비전공자도 2주면 충분합니다 (0) | 2026.03.23 |