본문 바로가기
JavaScript/node

ESM(ECMAScript Module)과 CJS(CommonJS)의 차이점

by curious week 2025. 9. 25.

 

1) 요약

  • ESM: 표준 모듈 시스템. import/export, 정적 분석 가능, 트리 셰이킹 유리, Top-Level await 가능, 브라우저/Node 공통 표준.
  • CJS: Node 전통 모듈. require/module.exports, 런타임 로딩(동적), 간편하지만 정적 분석·트리 셰이킹에 불리.

실무적으로:

  • 새 프로젝트·프론트엔드·라이브러리는 ESM 권장.
  • 레거시 Node 코드나 간단한 스크립트는 CJS가 편할 수 있음.

2) 문법 비교

ESM

// math.js (또는 .mjs / ESM 프로젝트면 .js도 가능)
export const add = (a, b) => a + b;
export default function mul(a, b) { return a * b; }

// 사용처
import mul, { add } from './math.js';   // 확장자 주의(ESM+Node 규칙)
console.log(add(2,3), mul(2,3));

특징:

  • 정적 import: 컴파일 시점에 의존성 그래프가 고정 → 트리 셰이킹 가능.
  • Top-Level await 가능:
  • const data = await fetch('https://...').then(r=>r.json());
  • 모듈 스코프에서 this === undefined (브라우저 ESM과 일치).

CJS

// math.cjs (또는 .js, CJS 프로젝트면 기본 .js)
exports.add = (a, b) => a + b;
module.exports.mul = (a, b) => a * b;

// 사용처
const { add, mul } = require('./math.cjs');
console.log(add(2,3), mul(2,3));

특징:

  • 동적 require: 런타임에 조건부 로딩 가능.
  • 모듈 스코프에서 this === module.exports.
  • Top-Level await 불가(별도 트랜스파일/러너 요건 제외).

3) 런타임 동작/해석 차이

해석 시점

  • ESM: 정적 의존성 파악 → 번들러 최적화, 사이드이펙트 최소화.
  • CJS: require가 실행될 때 로딩 → 조건부/동적 로딩 쉬움.

바인딩 특성

  • ESM: 라이브 바인딩. 원본이 바뀌면 import한 쪽도 갱신을 “볼 수” 있음(재할당은 안 됨).
  • CJS: 값이 복사(스냅샷) 되어 넘어오는 경우가 흔함(객체 참조 제외).

순환 의존성

  • ESM: 순환에도 대비되지만 초기화 순서를 주의.
  • CJS: 순환 시 exports가 부분적으로 채워진 상태로 노출될 수 있어 더 주의.

4) Node.js에서의 설정/파일 확장자

ESM로 프로젝트 운영

  • package.json
  • { "type": "module" }
  • 파일 확장자:
    • 프로젝트가 ESM이면 일반 .js가 ESM으로 인식.
    • 개별 파일을 강제 ESM/CJS로 나누고 싶다면:
      • ESM: *.mjs
      • CJS: *.cjs
  • 상대 경로 import는 확장자 필수:
  • import { foo } from './utils.js'; // 소스가 utils.ts여도 .js로 적기(빌드 산출 기준)

CJS로 프로젝트 운영

  • package.json에 type을 넣지 않거나 "type": "commonjs" 명시.
  • 기본 .js는 CJS로 인식, ESM은 *.mjs로 구분 가능.

5) TypeScript에서의 차이/권장 설정

ESM(+Node) 권장 tsconfig

{
  "compilerOptions": {
    "module": "NodeNext",          // Node ESM 규칙
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "lib": ["ES2022"],
    "verbatimModuleSyntax": true,  // import/export를 TS가 임의 변환하지 않음
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "baseUrl": "src",
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "ignoreDeprecations": "6.0"
}
  • 중요: ESM 규칙에서는 import 경로에 .js 확장자를 적는다.
  • 경로 별칭은 번들러/런타임 해석기(tsconfig-paths)로 보완.

CJS 권장 tsconfig

{
  "compilerOptions": {
    "module": "CommonJS",
    "moduleResolution": "node",
    "target": "ES2020",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "ignoreDeprecations": "6.0"
}

6) 상호 운용(ESM ↔︎ CJS) 패턴

ESM에서 CJS 패키지 사용

  • 보통 default import로 들어옴:
  • import express from 'express'; // 내부는 CJS지만 default로 매핑되는 경우 많음
  • 예외 케이스가 있으면 createRequire 사용:
  • import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const cjsOnly = require('some-cjs-only');

CJS에서 ESM 패키지 사용

  • 정적 require 불가 → 동적 import() 사용:
  • (async () => { const { default: dayjs } = await import('dayjs'); // ESM console.log(dayjs().format()); })();

ESM에서 Node 내장 CJS 유틸 대체

import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname  = dirname(__filename);

7) 번들링·최적화 관점

  • ESM: 정적 의존성 → 트리 셰이킹미니파이에 유리. 라이브러리나 프론트엔드에서는 사실상 표준.
  • CJS: 정적 분석 어려움 → 트리 셰이킹 불리. Node 전용 도구나 스크립트형 유틸에는 여전히 실용적.

8) 테스트 러너/빌드 도구

  • Vitest: ESM 친화적. ESM 프로젝트면 거의 “그냥 동작”.
  • Jest: ESM 제약이 큼. ts-jest의 useESM: true, extensionsToTreatAsEsm 설정, NODE_OPTIONS=--experimental-vm-modules 등 필요.
  • 빌드: 라이브러리는 tsup/esbuild/rollupESM + CJS 이중 타깃 산출을 자주 함.
    • 예: "exports"에 import(ESM)와 require(CJS) 경로를 모두 제공.

9) 마이그레이션 체크리스트(CJS → ESM)

  1. package.json에 "type": "module" 추가(또는 파일별 .mjs로 점진적 전환).
  2. require/module.exports → import/export로 변환.
  3. 상대 import 경로에 .js 확장자 추가.
  4. __dirname/__filename 치환 코드 적용(fileURLToPath).
  5. CJS 전용 패키지는 default import 또는 createRequire로 처리.
  6. 테스트 러너(Vitest/Jest) ESM 설정 맞추기.
  7. TS 사용 시 module: "NodeNext", moduleResolution: "NodeNext", verbatimModuleSyntax: true.
  8. 실행 환경(Node 18+)과 target/lib 정합 확인.
  9. CI/배포 스크립트 업데이트(예: node dist/index.js).

10) 어느 쪽을 선택할까?

  • 새 프로젝트/라이브러리/프론트엔드/장기 유지보수: ESM 권장.
  • 레거시 코드/간단 스크립트/도입 비용 최소: CJS 유지 가능.
  • 패키지 배포: 사용자 호환을 위해 ESM + CJS 동시 제공이 베스트.