[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user