jest에서 undefined (reading 'spawn') 에러가 발생하는 이유
문제 상황
@kubernetes/client-node와 같은 최신 라이브러리를 사용하여 통합 테스트를 수행하던 중, 갑자기 다음과 같은 에러가 발생하며 테스트가 실패
TypeError: Cannot read properties of undefined (reading 'spawn')원인 분석
이 에러는 "ESM(ECMAScript Modules)과 CJS(CommonJS) 사이의 번역 과정에서 생긴 오해" 때문에 발생
- 라이브러리의 코드 (ESM)
@kubernetes/client-node는 최신 ESM 문법으로 작성
node:child_process를 불러올 때 Default Import 방식을 사용
// @kubernetes/client-node/dist/exec_auth.js (원본)
import child_process from 'node:child_process'; // ESM 문법
// child_process.spawn(...) 을 호출하려고 함- ts-jest의 변환 (Transpilation)
Jest 설정(jest.config.cjs)에서 transformIgnorePatterns를 통해 이 라이브러리를 CJS로 변환하도록 허용했음
그리고 ts-jest는 useESM: false(기본값)이므로 코드를 CommonJS(require)로 바꿈
이때, TypeScript 컴파일러의 기본 동작(Interop 옵션이 꺼져 있을 때)은
Default Import(import x from 'y')를 require('y').default로 변환한다
ESM(import/export) 문법에서는 모듈을 내보낼 때 두 가지 방식이 있는데
- Named Export (조연들): 이름이 있는 여러 개의 수출품 (
export const a = 1;) - Default Export (주연/주인공): 이 모듈을 대표하는 단 하나의 수출품 (
export default class ...)
export const a = 1;
export default class ...위와 같은 ESM 모듈을 객체로 표현하면 대충 아래와 같다
const module = {
a: 1, // Named Export
b: 2, // Named Export
default: MainClass // Default Export (여기에 'default'라는 키가 생깁니다!)
};그래서 우리가 import Main from './module'이라고 쓰면
컴퓨터는 **"저 객체에서 default라는 이름의 속성을 꺼내와서 Main이라는 변수에 담아라"**라고 해석한다
// 변환된 코드 (esModuleInterop: false 일 때)
const child_process = require("node:child_process");
// [문제 발생 지점]
// TypeScript는 child_process 안에 'default'라는 속성이 있을 것이라 가정하고 접근
child_process.default.spawn(...);- CJS : "우린 그런 거 없다"
반면 Node.js의 내장 모듈인 child_process는 commonjs 방식으로 구현되어 있다
default라는 ESM 전용 속성이라는건 있는지도 모르는 상태다
즉 child_process.default.spawn(...); 는 child_process.undefined.spawn(...); 코드와 마찬가지다
그래서 undefined (reading 'spawn') 에러가 발생한다
해결책
tsconfig.json(jest에서는 일반적으로 따로 두니까 tsconfig.jest.json) 에서 "esModuleInterop": true 코드를 추가한다
"compilerOptions": {
"esModuleInterop": true,
}-
es: ECMAScript (자바스크립트 표준)
-
Module: 모듈 시스템 (import/export)
-
Interop: Interoperability (상호 운용성)의 줄임말
즉, 합치면 **"ES 모듈 상호 운용성"**이라는 뜻입니다.
CJS 와 ESM 상호 운용이 가능하도록 하는 옵션을 뜻한다
** "esModuleInterop": true 필자의 생각에, 그냥 node.js개발할거면 필수다 걍 항상 키고 살아라**