---
id: js-module-system-esm-cjs
title: JS Module System — ESM vs CJS 그리고 dual package
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [javascript, esm, cjs, package, vibe-coding]
tech_stack: { language: "Node.js / TypeScript / bundler", applicable_to: ["Backend", "Library"] }
applied_in: []
aliases: [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)
```json
{
  "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
```json
// package.json scripts
{ "build": "tsup src/index.ts --format esm,cjs --dts" }
```

### Subpath exports
```json
{
  "exports": {
    ".": "./dist/index.mjs",
    "./parser": "./dist/parser.mjs",
    "./internal": null
  }
}
```
`null` → 임포트 차단.

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

### ESM 안에서 CJS import
```ts
// 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.

## 🔗 관련 문서
- [[TypeScript_Const_Assertions]]
- [[DevOps_Build_Performance]]
