주로 Node.js에서 사용되는 전통적인 모듈 시스템이다.
require()
로 모듈을 불러오고, module.exports
로 내보낸다.
// add.js
module.exports.add = (x, y) => x + y;
// index.js
const { add } = require("./add");
add(1, 2);
require
를 코드 어디서든(조건문, 함수 내부 등) 동적으로 사용할 수 있다.💡 Tree-shaking이란?
번들링 과정에서 사용되지 않는 코드(Dead Code)를 자동으로 제거하여 최종 번들 파일의 크기를 줄이는 최적화 기법이다.
이름 그대로 “나무를 흔들어서(Shaking the tree) 불필요한 가지(Dead Code)를 떨어뜨린다”는 의미이다.
정적 분석: 번들러(Webpack, Rollup 등)는 소스 코드를 정적으로 분석하여, 실제로 사용되지 않는 함수, 변수, 모듈 등을 식별한다.
코드 제거: 사용되지 않는 부분은 최종 번들에서 제외되어 배포 파일이 더 가볍고 빠르게 로드된다.
ECMAScript Modules(ESM) 구조: ESM은 import / export
가 정적으로 선언되어 있어, 번들러가 코드 의존성을 미리 파악하고 최적화하기 쉽다.
반면 CJS는 동적으로 모듈을 불러오기 때문에 Tree-shaking이 어렵다.
최신 JavaScript 표준에서 도입된 모듈 시스템이다.
import
, export
키워드를 사용한다.
// add.js
export const add = (x, y) => x + y;
// index.js
import { add } from "./add.js";
add(1, 2);
import / export
는 반드시 파일의 최상위에 위치해야 하며, 동적으로 변경할 수 없다.import()
함수를 통해 별도로 처리해야 한다.(비동기적으로 동작)💡 Top-level await이란?
ES2022(ES13)부터 도입된 기능으로, JavaScript 모듈의 최상위 레벨(함수 내부가 아닌 파일의 맨 위)에서 바로await
키워드를 사용할 수 있게 해준다.
기존에는 반드시 async
함수 내부에서만 await
를 사용할 수 있었지만, 이제는 모듈 스코프에서도 비동기 작업을 간단하게 처리할 수 있다.
const fn = async() => {
// async 함수 내부에서만 await 사용 가능
const data = await fetchData();
}
// 모듈 스코프에서 바로 await 사용
const data = await fetchData();
특징
구분 | CommonJS(CJS) | ECMASCript Modules(ESM) |
---|---|---|
불러오기 | require | import |
내보내기 | module.exports | export |
로딩 방식 | 동기 | 비동기 |
동적 로딩 | 가능 | 별도의 동적 import() 함수 사용 |
최적화(Tree-shaking) | 어려움 | 쉬움 |
Top-level await | X | O |
호환성 | Node.js 코드 | 최신 JS/브라우저 표준 |
import
문법으로 CJS 모듈을 문제없이 불러올 수 있다.module.exports
가 ESM의 default
로 매핑되어 동작한다.require
로 ESM을 불러올 수 없었고 반드시 동적 import()
를 사용해야 했다.require
하기 때문에 구조적으로 호환이 어렵다.💡 Node.js v22부터
--experimental-require-module
플래그를 사용하면 CJS에서require
로 ESM을 불러오는 것이 일부 조건에서 가능해졌다.(최신 버전에서는 플래그가 필요 없을 수 있다)
require
로 불러올 수 있다..mjs
확장자 사용 / package.json의 type
필드 module
설정)ESM ↔ CJS 상호 호환성 정리하기
require
불가, 동적 import()
만 가능)--experimental-require-module
플래그(또는 최신 버전에서는 플래그 없이)require
로 ESM import 가능package.json의 type
필드로 기본 모듈 시스템을 지정한다.
"type": "commonjs"
: .js
확장자의 파일을 CJS로 해석한다."type": "module"
: .js
확장자의 파일을 ESM으로 해석한다.type
필드가 생략된 경우: .js
확장자의 파일을 CJS로 해석한다.확장자 규칙
.cjs
: 항상 CJS로 해석한다..mjs
: 항상 ESM으로 해석한다.TypeScript도 .cts
(CJS), .mts
(ESM) 확장자를 사용하며, tsconfig의 moduleResolution
옵션에 따라 동작한다.
💡 tsconfig의
moduleResolution
옵션 역할
moduleResolution
옵션은 TypeScript가import/export
구문에서 모듈을 어떻게 찾고 해석할지 결정한다.
node16
/ nodenext
: Node.js의 최신 모듈 해석 방식을 따른다..cts
/ .mts
확장자가 항상 각각 CJS/ESM으로 해석된다..ts
/ .js
파일은 package.json의 type
필드에 따라 CJS/ESM이 결정된다.package.json의 exports
필드를 활용해 환경에 따라 다른 엔트리 파일을 지정할 수 있다.
"exports": {
".": {
// conditional exports
"require": "./dist/index.cjs",
"import": "./esm/index.mjs"
}
}
require
경로, ESM 환경에서는 import
경로를 사용한다.🚨 주의 사항
exports
필드는 모두 .
으로 시작하는 상대 경로로 작성되어야 한다.type
필드)에 따라 올바른 JS 확장자를 사용해야 한다.(타입 정의도 동일하다.)type
필드가 module
인 경우(ESM): CJS 파일/ 타입 정의는 각각 .cjs
/ .d.cts
확장자를 제공해야 정상 동작한다.type
필드가 commonjs
인 경우(CJS): ESM 파일/ 타입 정의는 각각 .mjs
/ .d.mts
를 사용해야 한다.CommonJS(CJS)
require / module.exports
사용ECMAScript Modules(ESM)
import / export
사용ESM ↔ CJS 상호 호환성
default
로 매핑)확장자 / exports 규칙
.js
는 package.json의 type
필드에 따라 CJS/ESM 결정.cjs
(CJS), .mjs
(ESM)는 항상 명확.d.cts
(CJS), .d.mts
(ESM)로 구분exports
필드는 반드시 .
으로 시작하는 상대 경로만 사용
우혁님 너무 오랜만에 글 쓰시는거 아닌가요???ㅋㅋㅋㅋㅋ