/개발/Cannot use import statement outside a module 30초 진단표+해결 런북
개발Node.jsESM

Cannot use import statement outside a module 30초 진단표+해결 런북

Cannot use import statement outside a module, ERR_REQUIRE_ESM, require is not defined 에러를 30초 진단표로 원인 특정하고 복붙 코드로 해결. package.json type부터 ts-node·Jest ESM 충돌까지 잡는 실전 런북.

Cannot use import statement outside a module 30초 진단표+해결 런북

어제까지 잘 돌던 코드가 왜 갑자기 깨졌을까

import/require를 혼용하다가, 혹은 package.json"type": "module" 한 줄을 추가한 직후 프로젝트 전체가 빨간 스택트레이스로 뒤덮인 경험은 실무에서 가장 흔하게 보고되는 상황입니다. 특히 chalk 5, node-fetch 3, execa, nanoid 같은 인기 패키지들이 ESM-only로 전환되면서, 기존 CommonJS 코드에서 그냥 require만 했을 뿐인데 ERR_REQUIRE_ESM이 터지는 사례가 급증했습니다.

이 글은 개념 강의가 아닙니다. ESM이 무엇인지, CommonJS가 무엇인지 설명하지 않습니다. 오직 다음 세 형제 에러를 만났을 때 원인을 30초 안에 특정하고, 복붙 코드로 즉시 복구하는 것만 다룹니다.

  • Cannot use import statement outside a module
  • ERR_REQUIRE_ESM (또는 Error [ERR_REQUIRE_ESM]: require() of ES Module ...)
  • require is not defined in ES module scope, you can use import instead

적용 범위는 Node.js 18/20/22, TypeScript 5.x, ts-node 10+, Jest 29+, Vite 5 / Webpack 5 입니다. 아래 진단표부터 보고 자기 상황에 해당하는 섹션으로 바로 점프하세요.

30초 진단표 — 에러 원문만 보고 원인 특정

먼저 자기가 만난 에러 원문 한 줄을 표에서 찾으세요. 원인과 이동할 섹션이 바로 매핑됩니다.

에러 원문가장 흔한 원인해결 섹션
Cannot use import statement outside a moduleimport를 쓰는데 파일이 CommonJS로 해석됨. package.jsontype 미설정이거나, .ts가 CJS로 컴파일되거나, 확장자가 .js인데 type이 없음①·② / TS는 도구 섹션
ERR_REQUIRE_ESM / require() of ES Module ...CJS 코드가 ESM-only 패키지를 require 함 (chalk 5, node-fetch 3 등)
require is not defined in ES module scope"type": "module" 파일에서 require/module.exports/__dirname 사용
Unknown file extension ".ts" (ts-node)ts-node가 ESM 모드로 .ts를 로드하는데 로더 설정 누락도구 섹션(ts-node)
SyntaxError: Cannot use import statement outside a module (Jest)Jest가 ESM/TS를 변환하지 못함도구 섹션(Jest)

한 문장 요약: "어느 쪽이 import를 쓰는데 상대가 CJS냐, 아니면 어느 쪽이 require를 쓰는데 상대가 ESM이냐" 이 두 축만 구분하면 끝입니다.

원인별 복붙 해결 런북 (Node 순수 실행)

type 설정 정리 — 프로젝트 전체 모드 결정

가장 먼저 확인할 것은 package.jsontype 필드입니다. 이 한 줄이 .js 파일을 ESM으로 볼지 CJS로 볼지 결정합니다.

JSON
// package.json — 프로젝트를 ESM으로 통일 (import/export 사용)
{
  "name": "my-app",
  "type": "module"
}
JSON
// package.json — 프로젝트를 CommonJS로 고정 (require/module.exports 사용)
{
  "name": "my-app",
  "type": "commonjs"   // 또는 type 필드 자체를 생략
}

동작 규칙 요약:

type.js 해석.mjs.cjs
"module"ESMESMCommonJS
"commonjs" 또는 생략CommonJSESMCommonJS

Cannot use import statement outside a module가 났다면 → import를 쓰는 .js 파일인데 type이 없거나 commonjs입니다. 프로젝트를 ESM으로 갈 거면 "type": "module"을 넣으세요.

.mjs / .cjs로 파일 단위 격리

프로젝트 전체를 건드리기 싫다면, 확장자로 파일 하나만 강제할 수 있습니다.

JavaScript
// script.mjs — type과 무관하게 항상 ESM
import fs from 'node:fs';
export const hello = () => 'esm';
JavaScript
// legacy.cjs — type과 무관하게 항상 CommonJS
const fs = require('node:fs');
module.exports = { hello: () => 'cjs' };

레거시 프로젝트에 ESM 스크립트 하나만 추가하고 싶을 때 .mjs가 가장 안전합니다. 반대로 "type": "module" 프로젝트에서 옛날 CJS 설정 파일만 남겨야 할 때 .cjs를 씁니다.

③ ESM에서 require / __dirname 대체

require is not defined in ES module scope는 ESM 파일 안에서 CJS 전용 문법을 쓴 것입니다. ESM에는 require, __dirname, __filename이 기본 제공되지 않습니다. 아래 스니펫을 그대로 넣으세요.

JavaScript
// ESM에서 require가 꼭 필요할 때 (CJS 패키지 로드 등)
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

const someCjsModule = require('some-legacy-cjs-pkg');
JavaScript
// ESM에서 __dirname / __filename 복원
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

require('./data.json') 처럼 JSON을 불러오던 코드는 ESM에서 import attributes로 바꿀 수 있습니다.

JavaScript
// Node 20.10+ / 22: JSON import (import attributes)
import data from './data.json' with { type: 'json' };

④ CommonJS에서 ESM-only 패키지 로드 — 동적 import()

ERR_REQUIRE_ESM의 전형적 원인은 CJS 코드에서 require('chalk')처럼 ESM-only 패키지를 불러온 것입니다. require를 동적 import()로 바꾸면 됩니다.

JavaScript
// ❌ CommonJS에서 ESM-only 패키지 require → ERR_REQUIRE_ESM
const chalk = require('chalk'); // chalk 5는 ESM-only

// ✅ 동적 import()로 우회 (CJS 파일에서도 동작)
async function main() {
  const { default: chalk } = await import('chalk');
  console.log(chalk.green('OK'));
}
main();

await를 최상위에서 쓸 수 없는 CJS라면 위처럼 async 함수로 감싸면 됩니다. 프로젝트를 통째로 ESM으로 옮기기 부담스러울 때 가장 현실적인 방법입니다. 근본 해결을 원하면 해당 패키지의 마지막 CJS 버전(예: chalk@4, node-fetch@2)으로 다운그레이드하는 것도 자주 쓰이는 우회입니다.

도구별 함정 해결 (ts-node · Jest · 번들러)

TypeScript / ts-node

TypeScript 5.x에서 Node 실행 대상이라면 NodeNext 조합이 표준 권장입니다. 이 설정은 package.jsontype과 파일 확장자를 그대로 존중합니다.

JSON
// tsconfig.json — 최신 Node ESM 대상 (권장)
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "esModuleInterop": true,
    "outDir": "dist"
  }
}
JSON
// tsconfig.json — 레거시 CommonJS 고정
{
  "compilerOptions": {
    "module": "CommonJS",
    "moduleResolution": "Node10",   // 또는 "Node"
    "target": "ES2020",
    "esModuleInterop": true
  }
}

module 설정별 요약:

설정출력 형태언제
NodeNexttype에 따라 ESM/CJS 자동신규 Node 프로젝트
CommonJS항상 require로 변환레거시 유지, Jest CJS
ESNext + Bundler moduleResolution번들러가 처리Vite/Webpack 앱

ts-node로 .ts를 직접 실행하다 Cannot use import statement outside a moduleUnknown file extension ".ts"가 나면 ESM 로더를 켜야 합니다.

JSON
// tsconfig.json 에 ts-node 블록 추가
{
  "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" },
  "ts-node": { "esm": true }
}
Bash
# 실행 (Node 20+). 정상 출력이면 스크립트 결과가 그대로 찍힘
node --loader ts-node/esm ./src/index.ts
# 또는
npx ts-node --esm ./src/index.ts

예상 정상 결과는 에러 없이 스크립트가 실행되는 것입니다. 여전히 Unknown file extension이 뜨면 → package.json"type": "module"이 있는지, .ts가 아닌 .cts/.mts를 섞어 쓰지 않았는지 확인하세요.

Jest

Jest의 ESM 지원은 여전히 실험적입니다. 두 갈래 중 하나를 고르세요.

갈래 A — 그냥 CommonJS로 되돌리기 (가장 안정적). ESM이 꼭 필요하지 않다면 ts-jest를 CJS로 두는 것이 트러블이 가장 적습니다.

JavaScript
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  // tsconfig의 module을 CommonJS로 컴파일하도록 두면 대부분 해결
};

갈래 B — ESM으로 실행. ESM-only 의존성을 반드시 그대로 써야 할 때입니다.

JSON
// package.json — Jest ESM 실행 스크립트
{
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/.bin/jest"
  }
}
JavaScript
// jest.config.js — ESM + ts-jest useESM
export default {
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'node',
  extensionsToTreatAsEsm: ['.ts'],
  transform: {
    '^.+\\.tsx?$': ['ts-jest', { useESM: true }],
  },
};

Cannot use import statement outside a module가 Jest 실행 중에만 난다면 대개 변환 설정(transform) 누락입니다. 실무에서는 갈래 A(CJS 회귀)로 빠르게 복구한 뒤, 여유가 있을 때 B로 마이그레이션하는 순서가 안전합니다.

Vite / Webpack 설정 파일 이슈

"type": "module" 프로젝트에서 postcss.config.js, .eslintrc.js 같은 설정 파일이 CJS 문법(module.exports)을 쓰면 깨집니다. 해당 설정 파일만 .cjs로 바꾸면 즉시 해결됩니다.

Bash
# ESM 프로젝트에서 CJS 문법 설정 파일만 격리
mv postcss.config.js postcss.config.cjs
mv .eslintrc.js .eslintrc.cjs   # 또는 flat config(eslint.config.js) ESM으로 이관

Vite 앱 코드 자체는 tsconfig에서 "moduleResolution": "Bundler", "module": "ESNext"를 쓰는 것이 TypeScript 5.x 권장 방향입니다.

되돌리기(rollback) & 결정 트리

type: module 추가로 깨졌을 때 최소 되돌리기

방금 "type": "module"을 넣고 프로젝트가 무너졌다면, 가장 빠른 복구는 그 한 줄을 지우는 것입니다.

JSON
// package.json — 원상복구
{
  "name": "my-app"
  // "type": "module"  ← 이 줄 삭제 (또는 "commonjs")
}

그럼에도 ESM을 유지해야 한다면(ESM-only 의존성 때문에) 다음 순서로 마이그레이션하세요.

  1. requireimport, module.exportsexport로 전면 교체
  2. __dirname/__filename → 섹션 ③의 fileURLToPath 스니펫으로 대체
  3. 로컬 상대경로 import에 확장자 명시: import x from './util.js' (ESM은 확장자 생략 불가)
  4. 설정 파일(*.config.js)은 .cjs로 격리
  5. tsconfig module/moduleResolutionNodeNext로 통일

상황별 가장 안전한 선택

TEXT
레거시 코드베이스 유지가 목표?
├─ 예 → CommonJS 고정 (type 생략/commonjs) + ESM-only 패키지는 동적 import()
│        + tsconfig module: CommonJS
└─ 아니오(신규/모던) → ESM 통일 (type: module)
         + tsconfig module/moduleResolution: NodeNext
         + Vite/Webpack 앱이면 moduleResolution: Bundler

재발 방지 체크리스트 5줄

  1. 새 패키지 설치 전 README에서 ESM-only 여부 확인 (chalk 5+, node-fetch 3+ 등)
  2. package.jsontype과 tsconfig module한 방향으로 통일
  3. CJS↔ESM 경계는 동적 import()로만 넘나들기
  4. ESM에서는 require/__dirname 대신 createRequire/import.meta.url 사용
  5. 설정 파일은 필요 시 .cjs로 격리해 앱 코드와 분리

자주 묻는 질문 (FAQ)

Q. chalk를 require하면 왜 ERR_REQUIRE_ESM이 나나요? A. chalk 5부터 ESM-only로 전환되어 CommonJS require로는 로드할 수 없습니다. 동적 const { default: chalk } = await import('chalk')로 우회하거나, CJS 프로젝트라면 chalk@4로 다운그레이드하는 방법이 자주 쓰입니다.

Q. tsconfig에서 NodeNextCommonJS 중 뭘 골라야 하나요? A. 신규 Node 프로젝트라면 NodeNext(module·moduleResolution 모두)가 표준입니다. 기존 CJS 코드와 Jest CommonJS 환경을 유지해야 하면 CommonJS + Node10 조합이 트러블이 적습니다. 번들러(Vite/Webpack) 앱은 moduleResolution: "Bundler"가 권장됩니다.

Q. ESM으로 바꿨더니 상대경로 import가 안 됩니다. A. ESM은 확장자 생략을 허용하지 않습니다. import x from './util'import x from './util.js'처럼 확장자를 명시해야 합니다(TS 소스라도 컴파일 출력 기준 .js). moduleResolution: "NodeNext"가 이 규칙을 강제합니다.

✦ ✦ ✦
편집 검토 · Editorial Review

이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

초안 · AI (Content Reviewer)·검토 · Nodelog 편집자·발행 ·
관련 공식 문서Node.js 공식 문서

댓글

첫 번째 댓글을 남겨보세요.