npm ESM vs CJS 모듈 시스템 혼동 해결
Node.js의 ESM과 CJS 모듈 시스템 간의 차이점과 호환성 문제를 해결하는 방법을 다룹니다.
Introduction
Node.js 생태계에서 ESM(ECMAScript Modules)과 CJS(CommonJS) 모듈 시스템이 공존하면서 많은 개발자들이 혼란을 겪고 있습니다. 이번 포스트에서는 두 시스템의 차이점과 혼동을 해결하는 방법을 살펴보겠습니다.
Environment
node --version
# v20.11.0
npm --version
# 10.2.4
# 프로젝트 모듈 타입 확인
cat package.json | grep type
# "type": "module"Problem
다음과 같은 에러들이 빈번하게 발생했습니다:
# ESM 파일에서 require 사용 시
ReferenceError: require is not defined in ES module scope
# CJS 파일에서 import 사용 시
SyntaxError: Cannot use import statement outside a module
# package.json 설정 오류
ERR_REQUIRE_ESM: require() of ES Module혼동 사례
// utils.js (ESM으로 작성)
export const formatDate = (date) => {
return date.toISOString();
};
// app.js (CJS에서 사용 시도)
const utils = require('./utils'); // 에러 발생!Analysis
CJS vs ESM 비교
| 항목 | CJS | ESM |
|---|---|---|
| 문법 | require() | import/export |
| 로딩 | 동기식 | 비동기식 |
| 바인딩 | 값 복사 | 라이브 바인딩 |
| 확장자 | .js | .mjs 또는 "type": "module" |
| Top-level await | 불가 | 가능 |
모듈 해석 과정
CJS: require('./file')
→ file.js → file.json → file.node → index.js
ESM: import './file'
→ file.js (확장자 필수)
→ file.mjs
→ package.json의 exports 필드 확인Solution
1. package.json 모듈 타입 설정
{
"name": "my-project",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "rollup -c",
"dev": "node --watch src/index.js"
}
}2. CJS에서 ESM 사용하기
// dynamic import() 사용
async function loadESMModule() {
const { formatDate } = await import('./utils.mjs');
return formatDate(new Date());
}
// 또는 require() 래퍼 사용
function createRequire shim() {
const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
return require;
}
// 사용 예시
const require = await createRequireShim();
const lodash = require('lodash');3. ESM에서 CJS 사용하기
// ESM 파일에서 CJS 패키지 사용
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// CJS 패키지 가져오기
const express = require('express');
// 또는 동적 import 사용
const expressModule = await import('express');
const express = expressModule.default;4. 듀얼 모듈 패키지 작성
// src/index.mjs (ESM)
export function formatDate(date) {
return date.toISOString();
}
export default function app() {
return 'Hello from ESM';
}
// src/index.cjs (CJS)
function formatDate(date) {
return date.toISOString();
}
function app() {
return 'Hello from CJS';
}
module.exports = { formatDate, default: app };5. 빌드 설정 (Rollup)
// rollup.config.mjs
export default {
input: 'src/index.mjs',
output: [
{
file: 'dist/index.cjs',
format: 'cjs',
exports: 'named',
},
{
file: 'dist/index.mjs',
format: 'es',
},
],
};6. TypeScript에서 모듈 시스템 처리
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"outDir": "./dist",
"rootDir": "./src"
}
}// src/index.ts
import { formatDate } from './utils.js'; // .js 확장자 명시
export { formatDate };7. 테스트 환경 설정
// jest.config.js (CJS)
module.exports = {
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
};
// jest.config.mjs (ESM)
export default {
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
};8. 스크립트에서 모듈 처리
{
"scripts": {
"dev": "node --experimental-specifier-resolution=node src/index.js",
"build": "tsc",
"start": "node dist/index.js"
}
}Lessons Learned
- package.json "type" 필드: 프로젝트의 모듈 타입을 명확히 설정하세요
- 확장자 명시: ESM에서는 항상 파일 확장자를 명시하세요
- createRequire 활용: ESM에서 CJS 패키지를 사용할 때 createRequire를 활용하세요
- 빌드 도구 활용: Rollup이나 TypeScript로 듀얼 모듈을 빌드하세요
- 점진적 마이그레이션: 기존 CJS 프로젝트를 ESM으로 마이그레이션할 때 점진적으로 진행하세요
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.