Node.js require(esm) 공식 안정화: CJS 프로젝트에서 ESM 패키지를 동기식으로 불러오는

Maru

@maru

Node.js require(esm) 공식 안정화: CJS 프로젝트에서 ESM 패키지를 동기식으로 불러오는 원리와 제약

Node.js require(esm) 공식 안정화: CJS 프로젝트에서 ESM 패키지를 동기식으로 불러오는 원리와 제약

자바스크립트 백엔드 생태계에서 커먼JS(CJS)와 ES 모듈(ESM) 간의 파편화는 라이브러리 마이그레이션을 가로막는 가장 큰 걸림돌이었습니다. 특히 CJS 프로젝트에서 ESM 패키지를 불러오려 할 때 발생하는 ERR_REQUIRE_ESM 에러는 많은 개발자에게 깊은 피로감을 주었습니다. 하지만 Node.js 코어 팀이 v20.19.0 이상, v22.12.0 이상, 그리고 v24 버전 등 모든 활성 장기 지원(LTS) 버전에서 동기식 require(esm)을 공식 안정화하면서 이 고질적인 문제가 마침내 해결되었습니다. 이번 변화가 실무 환경과 주요 프레임워크 생태계에 가져올 직접적인 영향을 살펴보겠습니다.

ERR_REQUIRE_ESM 에러와의 작별: 라이브러리 이중 패키징의 종말

그동안 라이브러리 개발자들은 CJS와 ESM 사용자를 모두 지원하기 위해 두 버전을 함께 배포하는 이중 패키징의 불편함을 감수해야 했습니다. 빌드 도구 설정을 복잡하게 유지하거나 두 모듈 시스템에 맞춘 별도의 코드를 관리하는 과정은 패키지 배포의 난이도를 높이는 주된 원인이었습니다.

동기식 require(esm)의 공식 안정화는 이 고질적인 이중 패키징의 수고를 덜어줍니다. Node.js v20.19.0 이상, v22.12.0 이상, v24 버전부터 이 기능이 공식 지원되면서 기존 CJS 프로젝트에서도 동적 import() 비동기 호출 없이 일반 require() 문법으로 ESM 패키지를 직접 불러올 수 있게 되었습니다.

이 덕분에 패키지 유지보수자는 기존 CJS 환경을 사용하는 개발자들의 호환성이 깨질 걱정 없이 패키지를 점진적으로 ESM 전용으로 전환할 수 있습니다. 이는 모듈 파편화 문제를 극복하고 전체 자바스크립트 생태계가 더욱 견고한 ESM 표준으로 통합되는 발판이 됩니다.

동작 원리와 핵심 제약 조건: 탑레벨 어웨이트의 절대적 한계

require(esm)의 동작 원리는 단순합니다. 불러오려는 ES 모듈의 의존성 그래프가 처음부터 끝까지 완전히 동기적으로 평가될 수 있어야 합니다. CommonJS의 require()는 파일 시스템에서 코드를 읽고 실행하는 과정 전체가 중간에 멈춤 없이 동기식으로 진행된다는 전제로 동작하기 때문입니다.

이러한 설계 방식 때문에 탑레벨 어웨이트가 적용된 모듈은 require()로 불러올 수 없습니다. 가져오려는 모듈이나 그 모듈이 의존하는 하위 패키지 중 단 하나라도 최상위 수준에서 비동기 작업을 기다리는 코드가 포함되어 있다면, Node.js는 즉시 에러를 발생시킵니다. 동기적인 흐름 속에서는 비동기적인 대기 상태를 제어할 수 없습니다.

따라서 라이브러리 배포자라면 폭넓은 하위 호환성을 보장하기 위해 최상위 수준에서의 await 사용을 피해야 합니다. 비동기 초기화 작업이 꼭 필요하다면 별도의 초기화 메서드를 노출하거나, 사용자가 동적 import()를 호출하도록 유도하는 설계가 권장됩니다.

대규모 프레임워크의 변화: NestJS 12가 보여주는 ESM 마이그레이션 전략

NestJS 12는 Node.js의 require(esm) 지원 덕분에 큰 마찰 없이 프레임워크 전체를 ESM 전용으로 안전하게 전환하고 있습니다. 공식 프리릴리즈 단계에 있는 NestJS 12는 핵심 패키지인 core와 common을 포함한 공식 패키지 전체를 CommonJS에서 ESM으로 전면 이전합니다. 기존이라면 CJS 기반 생태계가 ESM 전용 패키지를 require()로 불러올 수 없어 대규모 마찰이 발생했겠지만, require(esm)이 안정화되면서 이런 호환성 문제가 대부분 해소되었습니다.

다만 모든 프로젝트를 강제로 ESM으로 바꾸는 것은 아닙니다. NestJS CLI는 새 프로젝트를 생성할 때 CJS와 ESM 중 하나를 선택하도록 안내하며, ESM을 선택하면 Vitest·oxlint·Rspack이 기본 툴체인으로 함께 적용됩니다. 기존 CJS 프로젝트는 그대로 Jest·ESLint·Webpack 조합을 유지할 수 있어, 마이그레이션 시점을 팀 상황에 맞게 조율할 수 있습니다. 프레임워크 창시자 카밀 미실리에비치가 "
require(esm)이 없었다면 이 마이그레이션은 애초에 의미가 없었을 것"이라고 언급했듯, NestJS의 이번 결정은 Node.js 런타임 자체의 성숙도가 뒷받침되었기에 가능했던 전환입니다.

우리가 지금 준비해야 할 일

Node.js v20.19.0, v22.12.0, 그리고 v24 이상을 사용하고 있다면 별도의 플래그 설정 없이 동기식 require(esm)을 바로 사용할 수 있습니다. 백엔드 프로젝트의 런타임을 최신 LTS 버전으로 업데이트하는 것만으로도 CJS와 ESM 간의 호환성 고민을 대부분 덜어내게 됩니다.

더 이상 오래된 CJS 전용 대안 라이브러리를 찾거나 복잡한 이중 패키징 설정을 유지하며 시간을 낭비할 필요가 없습니다. 이제 운영 중인 프로젝트의 Node.js 버전을 점검하고, 적극적으로 최신 ESM 패키지 생태계를 도입해 개발 생산성을 높여 보시길 바랍니다.

(수정됨)

아직 댓글이 없습니다.