TechFeedTechFeed
Programming Languages

동기 require, ESM, 노드 버전 | 패키지는 있는데 require만 막히면?

ERR_REQUIRE_ESM은 패키지는 있는데 require가 ES 모듈을 열지 못할 때 나는 코드입니다. node 버전과 process.features.require_module을 보고, 최상위 await면 ERR_REQUIRE_ASYNC_MODULE로 import를 씁니다. exports 서브패스 거절, 모듈 없음과는 다릅니다. Node.js, ESM, CI, 한국 1인 개발. 2026년 10월 문서.

by

이 오류 코드는 패키지 폴더가 없어서가 아니라, 진입 파일이 모듈인데 지금 프로세스의 동기 불러오기가 그 파일을 열지 못할 때 납니다. 버전과 기능 플래그를 먼저 보고, 맨 위 대기가 있으면 동적 불러오기로 바꿉니다.


노트북에서는 통과인데 배포 잡의 옛 런타임에서만 같은 줄이 납니다. 폴더를 지우고 다시 설치해도 반복됩니다. 이름은 맞는데 로더만 거절하는 자리입니다.


노드 문서는 이 코드를 오래전에 정한 세 버전 경계부터 폐기했다고 적습니다. 그 이상에서는 동기 모듈을 기본으로 읽습니다. 맨 위 대기가 있으면 코드 이름이 바뀝니다. 깊은 경로 거절, 파일 없음과는 문장이 다릅니다. 근거는 오류 코드 문서입니다.


패키지는 있는데 로더만 거절한다

코드 한 줄이 로더 거절입니다. 패키지 이름과 상대 경로는 디스크에 있습니다.


메시지는 보통 Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported입니다. package.json의 "type": "module"이거나, exports의 import 조건만 있는 진입점을 require로 연 경우입니다. 타입스크립트 컴파일이 초록이어도, 실행 파일에 require가 남으면 런타임에 이 줄이 납니다. esModuleInterop는 컴파일 옵션이라 노드의 로더를 바꾸지 않습니다.


2026년 문서에서 이 코드는 폐기(Stability 0)입니다. 기본이 켜진 노드에서는 동기 ESM을 require가 읽고, 반환값은 모듈 네임스페이스입니다. 기본 내보내기는 .default에 있습니다. 그래도 로그에 이 코드가 남아 있으면, 그 프로세스는 옛 노드이거나 기능을 꺼 둔 상태입니다.


chalk를 4로 고정하는 것은 노드 18을 유지할 때의 우회입니다. 로그가 이미 ERR_REQUIRE_ASYNC_MODULE이면 버전 고정은 그 최상위 await를 없애지 않습니다.


테이프가 붙은 상자와 감긴 파란 랜선이 놓인 책상
상자는 도착했는데 여는 방식만 거절된 자리와 비슷합니다

버전과 기능 플래그를 먼저 읽는다

설치를 반복하기 전에 그 프로세스가 ESM require를 여는지 확인합니다.


로컬 터미널과 CI의 노드가 다르면 재현이 갈립니다. engines에 20 이상을 적어 두고 액션은 18을 쓰면, 개발 노트북만 조용합니다. NODE_OPTIONS에 --no-require-module이 있으면 새 노드에서도 옛 거절이 돌아옵니다.


확인결과다음 동작
node -v20.19, 22.12, 23.0 미만동기 require로는 ESM이 안 열림. import() 또는 런타임 업그레이드
process.features.require_moduletrue동기 ESM은 require 가능. 반환은 네임스페이스
--no-require-module기능이 false로 보임플래그를 빼기 전에는 패키지 버전을 바꿔도 같은 코드

그 프로세스가 동기 ESM require를 여는지
node -v node -p "process.features.require_module" node -p "process.execArgv"

기능이 true일 때 require의 반환은 module.exports 객체가 아니라 네임스페이스입니다. 함수를 바로 부르면 타입이 함수가 아니라는 다음 오류로 바뀝니다. 기본 내보내기가 있으면 .default를 꺼냅니다. 패키지가 export 이름을 "module.exports"로 두면 require 반환이 그 값이 되지만, 이름 있는 내보내기는 그 값에 붙어 있지 않으면 require 쪽에서 사라집니다. import 문법으로 부를 때와 모양이 달라지니, 한쪽으로만 테스트하면 다른 로더에서 깨집니다.


최상위 await는 코드 이름이 다르다

동기 require는 파일 맨 위의 await를 기다리지 못합니다.


기능이 켜진 노드에서 그 파일을 require하면 ERR_REQUIRE_ASYNC_MODULE이 납니다. 노드 26.5.0부터 이 오류에 requireStack과 topLevelAwaitLocations가 붙습니다. --experimental-print-required-tla는 그 위치를 출력에 더하는 플래그입니다. 고치는 쪽은 그 파일을 await import()로 부르거나, 라이브러리에서 최상위 await를 빼는 것입니다.


같은 모듈을 import()로 읽는 도중에 require가 끼어들면 ERR_REQUIRE_ESM_RACE_CONDITION(26.1.0)입니다. 순환이면 ERR_REQUIRE_CYCLE_MODULE입니다. 세 이름은 서로 다른 다음 동작입니다. 하나를 보고 락 파일만 지우면 레이스가 안 사라집니다.


최상위 await가 있으면 import로 넘긴다
async function loadTool() { const ns = await import('some-esm-tool') const fn = ns.default || ns return fn }
나무 책상 위에 서로 떨어져 쌓인 서류철 두 더미
동기 require와 동적 import는 같은 더미가 아닙니다

경로 거절, 파일 없음과 문장이 다르다

패키지 이름이 같다고 같은 줄을 고치지 않습니다.


문장의미먼저 볼 곳
ERR_REQUIRE_ESM파일은 있고 require가 ESM을 거절노드 버전, require_module, NODE_OPTIONS
ERR_REQUIRE_ASYNC_MODULE동기 require가 최상위 await를 만남import() 또는 라이브러리의 최상위 await
ERR_PACKAGE_PATH_NOT_EXPORTEDexports에 없는 서브패스공개 입구. 로더 플래그와 무관
Cannot find module패키지 또는 파일 없음설치, 경로, node_modules

엣지 런타임이 fs 같은 노드 모듈을 거절하는 줄도 이 코드가 아닙니다. 그 로그는 런타임 이름과 함께 모듈이 지원되지 않는다고 말합니다. ESM require를 켜도 엣지 번들의 제한은 그대로입니다.


고치기 전에 코드 이름부터 맞춘다

이름 세 개를 한 처방으로 묶지 않습니다.


  • 로그의 코드가 ERR_REQUIRE_ESM인지, ERR_REQUIRE_ASYNC_MODULE인지, 레이스나 순환인지 한 줄로 적습니다.
  • 실패한 그 프로세스에서 node -v와 process.features.require_module을 찍습니다. 노트북 버전으로 CI를 대신하지 않습니다.
  • 기능이 false이고 코드가 ERR_REQUIRE_ESM이면 노드를 20.19, 22.12, 23 이상으로 올리거나, 그 호출만 await import()로 바꿉니다. --no-require-module이 NODE_OPTIONS에 있는지도 봅니다.
  • 기능이 true인데 ERR_REQUIRE_ASYNC_MODULE이면 최상위 await가 있는 파일을 require하지 않습니다.
  • require가 성공했는데 함수가 아니면 .default를 확인합니다. 타입 오류를 의존성 재설치로 넘기지 않습니다.
  • 깊은 경로가 exports에 없다거나 모듈을 못 찾는다는 문장이면 서브패스 거절과 모듈 없음으로 갑니다.

국내 저장소의 자동 빌드가 노드 18로 고정돼 있으면, 노트북의 최신 런타임에서는 같은 동기 불러오기가 통과합니다. 카카오 로그인 도우미나 토스 웹훅 파서처럼 순수 모듈로만 배포되는 도구가 늘면서, 설치는 되는데 실행만 거절되는 일이 잦아졌습니다. 패키지 폴더를 지우고 다시 받는 동안 런타임 버전은 그대로라 거절도 그대로입니다. 실패한 잡의 버전과 기능 플래그를 로그에 한 줄 남기면, 로컬과 배포 중 어느 쪽이 옛 로더인지 갈립니다.


맨 위에 대기가 있는 파일은 코드 이름부터 다릅니다. 의존성 버전을 낮춰 고정해도 그 대기는 사라지지 않습니다. 한글 주석을 달아 두어도 로더는 보지 않습니다. 실행 명령의 노드가 문서가 폐기했다고 적은 경계 이상인지, 기능을 끄는 옵션이 환경 변수에 숨어 있는지를 먼저 봅니다. 타입 검사만 초록인 스크립트는 실행 파일에 동기 불러오기가 남아 있는지 한 번 더 확인합니다. 엣지 함수가 노드 내장을 거절하는 문장과 이 코드를 한 설정으로 고치려 하면 둘 다 남습니다. 국내 팀 저장소에서 런타임만 올리고 잠금 파일은 그대로 두면, 설치는 성공해도 호출 방식이 옛 동기 불러오기면 거절이 반복됩니다. 실패한 명령의 버전을 화면으로 남기고, 기능을 끄는 환경 변수가 잡 설정에 있는지 검색합니다. 맨 위 대기가 있는 파일은 동적 불러오기로 옮긴 뒤 그 잡만 다시 돌립니다. 패키지 버전을 무작정 내리면 보안 수정이 빠질 수 있으니, 로더가 거절하는 이유와 라이브러리 업데이트는 따로 판단합니다. 호출이 이미 성공했는데 함수가 아니라는 다음 줄은 반환 모양 문제라, 설치를 다시 할 일이 아닙니다.


확인한 문서

버전 경계는 노드 문서의 그 페이지를 정본으로 둡니다. 2026-10-06에 확인했습니다.



자주 묻는 질문

node_modules를 지워도 같은 코드가 나오면요?

파일이 없어서가 아닙니다. 그 노드가 require로 ESM을 열 수 있는지부터 봅니다. 18에서는 설치를 반복해도 ERR_REQUIRE_ESM이 남습니다.


로컬은 되는데 액션만 실패하면요?

액션의 node-version과 로컬 node -v를 나란히 적습니다. 18과 22는 이 코드의 기본값이 다릅니다. 락 파일보다 런타임이 먼저입니다.


require가 성공했는데 함수가 아니라고 하면요?

반환이 네임스페이스입니다. 기본 내보내기는 .default에 있습니다. ERR_REQUIRE_ESM이 사라진 뒤의 다음 줄입니다.


타입체크는 통과하는데 실행만 죽으면요?

컴파일러는 require를 노드 대신 검사하지 않습니다. 실행 명령의 노드로 그 파일을 한 번 직접 부릅니다.


엣지 미들웨어의 모듈 거절과 같나요?

다릅니다. 엣지는 네이티브·노드 내장 일부를 런타임이 거절합니다. ERR_REQUIRE_ESM은 노드 프로세스 안의 require 로더 이야기입니다.


기능을 일부러 끌 일이 있나요?

네임스페이스 반환이 기존 CJS 호출과 깨질 때 --no-require-module로 잠시 되돌리는 경우가 있습니다. 끄면 ERR_REQUIRE_ESM이 다시 납니다. 끄기 전에 import()로 바꿀 호출을 정하세요.


ERR_REQUIRE_ESM은 그 노드의 require가 ES 모듈을 열지 않는다는 뜻입니다. 버전과 require_module을 읽고, 최상위 await면 코드 이름부터 다시 보세요. 서브패스 거절이나 파일 없음과는 다음 명령이 다릅니다. 관련 글: exports 서브패스, 모듈을 못 찾는 줄, 엣지 런타임의 노드 모듈.


ERR_REQUIRE_ESMrequireESMNode.jsimportCI모듈언어

함께 보면 좋은 문제 해결

EXPLORE / Programming Languages

이어서 읽어보기

전체 토픽 둘러보기 →