
주로 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 필드는 반드시 . 으로 시작하는 상대 경로만 사용
우혁님 너무 오랜만에 글 쓰시는거 아닌가요???ㅋㅋㅋㅋㅋ