📦

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로 사용