Node.js, require(esm) 공식 지원

CJS와 ESM, 15년 생태계 갈등의 끝 – Joyee Cheung의 ‘Bridging CommonJS and ESM in Node.js’ 정리

Web Engines Hackfest 2025에서 있었던 Joyee Cheung의 발표 영상을 보고 정리한 글입니다. 원본 발표는 글 맨 아래에 붙여 뒀습니다.


1. 도입: Node.js, 모듈의 시대를 바꾼다

Node.js 개발자라면 누구나 한 번쯤 겪어본 그 문제.

“import랑 require, 둘이 왜 서로 안 맞냐?”

실제로 2008년 Node.js 등장 이후 CommonJS(CJS)가 대세였지만, 2015년 ES6(ES2015)에서 ESM(ECMAScript Modules)이 표준화되면서 생태계가 둘로 갈라져 버림.

Joyee Cheung(Igalia, Bloomberg 후원, Node.js/V8 핵심 커미터)은 2025년 Web Engines Hackfest에서 이 긴 여정을 “기술을 넘어, 생태계 전체를 점진적·비파괴적으로 바꾸는 전략”이라고 설명했음.


2. CJS와 ESM의 차이와 그로 인한 상호 운용성 문제

CJS란?

  • require(), module.exports
  • 동기(synchronous) 로딩
  • Node.js 초기부터 대세

ESM이란?

  • import/export
  • 비동기(문서상), 정적 분석 가능
  • 표준(ECMAScript) 기반

상호 운용성 문제

  • ESM에서 CJS 불러오기: import로 가능. require 쓰려면 createRequire 필요, 파일 확장자도 꼭 명시해야 함.
  • CJS에서 ESM 불러오기: require로 불가! (ERR_REQUIRE_ESM), import()만 가능(비동기 코드에서만)
  • 이로 인해 CJS 진영이 ESM-only 패키지 쓰기 힘들고, ESM 쪽도 CJS와 혼용이 불편함

3. 생태계가 고안한 임시방편: Faux-ESM, Dual Package

Faux-ESM

  • 코드 ESM으로 작성 → 빌드 때 CJS로 변환
  • 실제 실행은 CJS, “가짜” ESM
  • 진짜 ESM과 미묘하게 다르고, ERR_REQUIRE_ESM 같은 에러도 흔함

Dual Package

  • 같은 기능을 CJS+ESM 두 버전으로 동시 배포
  • package.json의 exports 필드로 분기
  • 문제: node_modules 용량 두 배, 같은 패키지 두 버전이 동시에 참조될 수도 있음(dual package hazard)
  • NPM 메인 패키지들 대부분(2025년 기준 11%만 pure ESM) 아직도 이런 방식

4. require(esm) 부활 – 왜 이제서야 가능해졌나?

Cheung은 V8 엔진 코드를 분석하다가, “top-level await만 없으면 ESM도 동기적으로 평가 가능”하다는 점을 발견.

사실 스펙에도 명시돼 있었지만, 모두가 “ESM=비동기, CJS=동기”라는 고정관념에 갇혀 있었던 셈.

그래서 Node.js에

  • top-level await 없는 ESM은 동기적으로 require 가능
  • 있으면 ERR_REQUIRE_ASYNC_MODULE로 실패

하는 새 require(esm) 경로를 추가.

이게 바로 Node.js 20/22/24 이상에서 require()로 ESM 모듈을 바로 불러올 수 있게 된 핵심 변화!


5. 이 변화가 실제로 가능했던 구체적 기술적/생태계적 해법

Faux-ESM과의 호환성

  • 번들러들이 만든 __esModule 마커 패턴을 Node.js가 공식 도입
  • require(esm)로 불러온 네이티브 ESM에도 이 마커를 자동 추가해, 변환된 코드/트랜스파일러와의 호환성 보장

CJS → ESM 전환 지원

  • ESM 코드 내에 export { Logger as 'module.exports' }처럼 CJS에서 require할 때 기존 사용 방식 그대로 동작하도록 지원

Dual Package 해소

  • 새로운 "module-sync" 조건 추가: 최신 Node에선 require/import 모두 ESM 버전 우선 사용
  • 점진적으로 CJS 코드/배포를 생태계에서 걷어낼 수 있는 로드맵 제공

6. 실제 적용과 백포팅, 그리고 현재 현황

  • Node.js 23에서 최초 플래그 없이 정식 지원
  • Node.js 22/20에도 핵심 커밋 33개만 골라 백포팅(구버전 지원까지 신경 씀)
  • Node.js 24에선 아예 기본 탑재
  • 릴리스 후보 상태로, 안정화 후 npm 주요 패키지(vite, babel, graphql 등)들이 ESM-only로 대거 전환 중

7. 이제 dual package는 안녕? 실제 적용법

  • 이제는 package.json에 "type": "module"만 있으면, CJS(require), ESM(import) 코드 모두에서 ESM 패키지 바로 사용 가능!
  • 여러 엔트리(서브모듈)가 필요하면 exports 필드만 추가
  • 구버전 Node(v18 이하)나 특이한 상황 아니면 dual package 필요 없음

예시

text
{
  "type": "module",
  "exports": {
    ".": "./index.js"
  }
}
text
// ESM에서
import foo, { bar } from './index.js';
// CJS에서
const mod = require('./index.js');
mod.default();
mod.bar();

8. 향후 과제와 생태계의 미래

  • CJS/ESM 로더 내부 완전 통합 (동기/비동기 race 문제 없애기)
  • Loader hook 공식화 및 안정화(확장/트레이싱 등 도구 더 쉽게)
  • ESM 로딩/실행 성능 계속 개선(CJS만큼 빠르게)
  • 구버전/레거시 환경 점진적 정리

Cheung은 “생태계를 깨지 않으면서 혁신하는 법”을 기술/문화 두 관점 모두에서 보여줬다고 할 수 있음.


9. 결론 & 인사이트

Node.js require(esm) 공식 지원은, 마침내 CJS/ESM의 오랜 갈등을 풀고 진짜 “모듈 표준 시대”를 여는 결정적 변화다.

  • 앞으로 npm 패키지, 사내 라이브러리, 오픈소스 모두 ESM-only가 표준이 될 것
  • “이제는 import만 쓰고, require도 걱정 없이 호환된다”는 게 현실

이 흐름 위에 우리 개발자들도 더 간단하고, 빠르고, 표준적인 모듈 시스템을 누릴 수 있게 된 셈.


참고

발표: Web Engines Hackfest 2025 – Joyee Cheung, Bridging CommonJS and ESM in Node.js

그 외 Node.js 공식 문서, 발표 요약 등을 참고했음.