ERR_REQUIRE_ESM, require, ES module | 패키지를 불러오다 막히면?
ERR_REQUIRE_ESM은 파일이 없어서가 아니라 CommonJS require가 ES 모듈만 있는 패키지를 열려고 해서 납니다. type 필드, import 전환, 로컬과 CI 노드 버전. Node.js, npm, 한국 1인 넥스트 기준. 2026년 9월 Node.js ERR_REQUIRE_ESM 문서.
파일이 없는 게 아니라 모듈 형식이 다른 줄입니다. 노드는 파일을 열 때 CommonJS와 ES 모듈을 다른 로더로 다룹니다. require()는 기본적으로 CJS 로더이고, import는 ESM 로더입니다. 대상 패키지가 "type": "module"이거나 진입점이 .mjs면, 구버전 노드의 require는 그 파일을 열지 못하고 ERR_REQUIRE_ESM을 던집니다.
Cannot find module과 자리가 다릅니다. 그쪽은 이름을 node_modules 사다리에서 못 찾은 줄이고, 이 줄은 이름은 찾았는데 로더가 거부한 줄입니다. 그래서 rm -rf node_modules를 반복해도 같은 문장이 남습니다.
에러 메시지에는 보통 어떤 파일이 어떤 패키지를 불러다 막혔는지 경로가 같이 찍힙니다. 맨 아래 스택만 보지 말고, Error [ERR_REQUIRE_ESM]: require() of ES Module 바로 아래 패키지 이름부터 복사하세요.
먼저 기억할 것 | 설치가 실패한 게 아닙니다. 디스크에 패키지는 있고, 불러오는 문법과 패키지가 선언한 형식이 어긋난 겁니다. 지워 깔기 전에 그 두 칸을 나란히 보세요.
이름은 찾았는데 CJS 로더가 ESM 파일을 거부하면 이 줄이 난다
범인을 찾는 세 칸
노드 버전, 패키지 type, 불러오기 문법 세 칸이면 원인이 갈립니다. 표로 먼저 나눕니다.
칸
증상
먼저 할 일
노드 버전
로컬은 되고 CI만 실패
node -v, .nvmrc, engines
패키지 type
특정 라이브러리만 죽음
node_modules/그이름/package.json
불러오기 문법
앱은 되고 스크립트만 실패
require인지 import인지
확장자
.mjs만 실패
진입점 main vs exports
트랜스파일
ts-node, tsx만 실패
실행기가 CJS로 강제하는지
노드 22와 20.19 근처부터는 require()로 ESM을 여는 길이 문서에 나와 있습니다. 다만 대상 모듈이 최상위 await를 쓰거나, CI가 아직 18을 쓰면 같은 줄이 그대로 납니다. 로컬이 22인데 액션이 18이면 설치가 아니라 런타임 버전이 원인입니다.
패키지 칸은 node_modules/패키지/package.json을 직접 엽니다. "type": "module"이 있고 exports에 CJS 조건이 없으면, 그 패키지는 import 전용입니다. 락파일 충돌과는 다른 자리입니다. 트리를 못 만드는 줄은 ERESOLVE를 보세요.
고치는 원칙은 불러오기와 대상 형식을 같게 만드는 것입니다. 내 파일이 이미 ESM이면 require를 import로 바꿉니다. 스크립트가 아직 CJS인데 새 패키지가 ESM만 주면, 스크립트를 .mjs로 바꾸거나 package.json에 "type": "module"을 넣고 import로 통일합니다.
당장 스크립트 전체를 못 바꾸면 동적 import()를 씁니다. import()는 Promise를 돌려서, 최상위 await가 되는 맥락이거나 async 함수 안에서 받아야 합니다. CJS 파일 한가운데에 그냥 await import를 넣으면 문법 에러가 나니, 진입점만 먼저 ESM으로 올리는 편이 덜 꼬입니다.
패키지 쪽을 내릴 수도 있습니다. 같은 라이브러리의 이전 메이저가 CJS 진입점을 남겨 둔 경우가 있습니다. 다만 보안 패치가 끊긴 버전으로 고정하는 건 임시입니다. 가능하면 내 코드를 import로 맞추는 쪽이 오래갑니다.
앱은 되는데 node scripts/만 죽는 경우가 많습니다. 넥스트 번들러는 import를 알아서 묶고, 순수 노드 스크립트는 파일 확장자와 type을 그대로 따릅니다. 화면이 켜진다고 발행 스크립트까지 ESM을 소화하는 건 아닙니다.
tsx나 ts-node로 타입스크립트 스크립트를 돌릴 때도 갈립니다. 실행기가 내부에서 require를 쓰면, 의존성 하나가 ESM 전용인 순간 같은 줄이 납니다. 그럴 때는 실행기를 node --import tsx처럼 ESM 경로로 바꾸거나, 스크립트를 빌드한 뒤 돌리는 편이 안전합니다.
모듈 이름을 못 찾는 줄과 섞이면 진단이 늦어집니다. 이름이 디스크에 없는 줄은 Cannot find module입니다. 노드 버전 자체가 프로젝트와 다르면 버전 매니저 글을 먼저 보세요.
NODE_OPTIONS=--experimental-require-module 는 마지막 | 플래그로 열면 버전마다 동작이 갈리고, CI에 같은 플래그가 없으면 다시 죽습니다. 공식 경로는 불러오기 문법을 맞추는 쪽입니다.
실전에서 고치는 순서
순서를 정해 두면 설치를 반복하지 않습니다. 메시지에서 패키지 이름을 읽고, 그 폴더의 type을 확인하고, 내 파일이 require인지 봅니다. ESM 전용이면 import로 바꾸거나 스크립트 진입점을 ESM으로 올립니다.
그다음에야 노드 버전을 맞춥니다. 로컬 22, 액션 18처럼 어긋난 채 패키지만 올리면 같은 줄이 액션에서만 재현됩니다. engines와 버전 파일을 커밋해 두면 다음 클론에서 덜 헤맵니다.
그래도 안 열리면 그 패키지의 CJS 진입점 여부를 exports에서 확인합니다. require 조건이 있으면 버전을 그 조건이 있는 줄로 맞출 수 있습니다. 포트가 막혀 서버가 안 뜨는 줄은 EADDRINUSE라 이 에러와 동시에 보여도 원인 칸이 다릅니다.