📦
ESM vs CommonJS — require와 import가 다른 이유
동기 로딩 vs 비동기 로딩, 그리고 "type": "module"의 의미
// CommonJS (CJS)
const fs = require('fs'); // 동기 로딩 — 이 줄에서 fs가 완전히 로드됨
module.exports = { myFunc }; // 내보내기
// ESM
import fs from 'fs'; // 비동기 로딩 — 파일 파싱 단계에서 의존성 분석
export const myFunc = () => {}; // 내보내기
핵심 차이
CJS: 런타임 로딩 — require()는 함수 호출이다. 조건부로 쓸 수 있다 (if (x) require('y')). 모듈을 실행하면서 로딩한다.
ESM: 파싱 타임 로딩 — import는 문법이다. 파일 최상위에서만 쓸 수 있다. JavaScript 엔진이 코드를 실행하기 전에 의존성 그래프를 먼저 구축한다.
이 차이 때문에 ESM에서 CJS를 import할 수 있지만, CJS에서 ESM을 require할 수 없다. (ESM은 비동기인데 require는 동기라서)
package.json의 "type"
{ "type": "module" } // .js 파일을 ESM으로 해석
{ "type": "commonjs" } // .js 파일을 CJS로 해석 (기본값)
파일 확장자로도 강제할 수 있다: .mjs = ESM, .cjs = CJS.
실전에서의 고통
라이브러리가 ESM only로 전환하면 CJS 프로젝트에서 require()로 쓸 수 없다. node-fetch v3, chalk v5 같은 인기 패키지가 ESM only로 갔을 때 생태계에 대혼란이 왔다.
해결: dynamic import()를 쓰면 CJS에서도 ESM을 로드할 수 있다. 하지만 async가 된다.
// CJS에서 ESM 패키지 사용
const fetch = await import('node-fetch'); // dynamic import — async
핵심 포인트
1
CJS(require): 동기, 런타임 로딩, 조건부 가능
2
ESM(import): 비동기, 파싱 타임 로딩, 최상위만
3
ESM → CJS import 가능, CJS → ESM require 불가 (dynamic import로 우회)
4
package.json "type": "module"로 프로젝트 전체를 ESM으로 전환
사용 사례
새 프로젝트 — ESM으로 시작하는 게 미래 지향적
기존 CJS 프로젝트 — ESM only 라이브러리를 dynamic import로 사용