Files
2nd/10_Wiki/Topics/Coding/JS_Module_System_ESM_CJS.md
T
2026-05-09 21:08:02 +09:00

3.6 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
js-module-system-esm-cjs JS Module System — ESM vs CJS 그리고 dual package Coding draft B conceptual 2026-05-09 2026-05-09
javascript
esm
cjs
package
vibe-coding
language applicable_to
Node.js / TypeScript / bundler
Backend
Library
import/export
require
exports field
package.json

JS 모듈 시스템 — ESM vs CJS

2026 현재도 라이브러리 작성자에게 가장 골치 아픈 영역. 신규 코드는 ESM 디폴트, but Node 일부 도구 / 레거시는 CJS 만 — package.json exports 의 dual export 가 표준 답.

📖 핵심 개념

  • ESM: import/export. 정적 분석 가능. tree-shake 친화. 비동기 가능.
  • CJS: require()/module.exports. 동적 (조건부 require). 동기.
  • Node.js: package.json "type": "module" 이면 .js = ESM. 아니면 CJS.
  • .mjs = 항상 ESM, .cjs = 항상 CJS.

💻 코드 패턴

라이브러리 dual package (ESM + CJS)

{
  "name": "my-lib",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.mjs"
    },
    "./package.json": "./package.json"
  },
  "files": ["dist"]
}

TypeScript 빌드 — tsup 또는 unbuild

// package.json scripts
{ "build": "tsup src/index.ts --format esm,cjs --dts" }

Subpath exports

{
  "exports": {
    ".": "./dist/index.mjs",
    "./parser": "./dist/parser.mjs",
    "./internal": null
  }
}

null → 임포트 차단.

CJS 안에서 ESM 동적 import

// CJS 환경 (.cjs)
async function loadESM() {
  const mod = await import('esm-only-package'); // 동적 import 만 가능
  return mod.default;
}

ESM 안에서 CJS import

// ESM (.mjs / "type":"module")
import lodash from 'lodash'; // CJS default — Node 가 자동 wrap
import { merge } from 'lodash'; // named export 동작 (Node 가 분석)

🤔 의사결정 기준

상황 형식
신규 라이브러리 ESM-first + dual export
신규 앱 (Next/Vite) ESM (bundler 가 처리)
Node 서버 (TypeScript) ESM with "type": "module"
옛 도구 의존성 다수 (postcss plugin, eslint plugin) CJS 또는 dual
동적 require 필요 CJS (또는 ESM 의 await import)
__dirname / __filename ESM 에는 없음 — import.meta.url + fileURLToPath

안티패턴

  • ESM 라이브러리에 main: CJS 사용자 import 시 default 잘못 판정. exports 필드 명시.
  • CJS + ESM 양쪽에서 같은 인스턴스 가정: 두 번 evaluate 됨 → 두 인스턴스 (singleton 깨짐).
  • require("package") 가 ESM-only 패키지: 런타임 에러. 동적 import 로.
  • exports 필드 빠뜨리고 internal path 노출: 사용자가 import 'pkg/dist/foo' 가능. 명시 차단.
  • TypeScript paths 만 의존: 빌드 시 alias 안 풀림. tsc-alias 또는 bundler.
  • 순환 import: ESM 도 깨짐. CJS 와 다른 방식으로 깨짐. 의존성 그래프 정리.
  • default export 와 named 혼용: tree shake / IDE 자동 import 헷갈림. named 권장.

🤖 LLM 활용 힌트

  • 신규 = ESM. 라이브러리는 dual export.
  • exports 필드 + types + import + require 4종 세트.
  • Node ESM 환경: import.meta.url, top-level await OK.

🔗 관련 문서