nodejs
jest에서 undefined (reading 'spawn') 에러가 발생하는 이유

jest에서 undefined (reading 'spawn') 에러가 발생하는 이유

문제 상황

@kubernetes/client-node와 같은 최신 라이브러리를 사용하여 통합 테스트를 수행하던 중, 갑자기 다음과 같은 에러가 발생하며 테스트가 실패

TypeError: Cannot read properties of undefined (reading 'spawn')

원인 분석

이 에러는 "ESM(ECMAScript Modules)과 CJS(CommonJS) 사이의 번역 과정에서 생긴 오해" 때문에 발생

  1. 라이브러리의 코드 (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(...) 을 호출하려고 함
  1. ts-jest의 변환 (Transpilation)

Jest 설정(jest.config.cjs)에서 transformIgnorePatterns를 통해 이 라이브러리를 CJS로 변환하도록 허용했음

그리고 ts-jestuseESM: false(기본값)이므로 코드를 CommonJS(require)로 바꿈

이때, TypeScript 컴파일러의 기본 동작(Interop 옵션이 꺼져 있을 때)은

Default Import(import x from 'y')require('y').default로 변환한다

Default Import란?

ESM(import/export) 문법에서는 모듈을 내보낼 때 두 가지 방식이 있는데

  1. Named Export (조연들): 이름이 있는 여러 개의 수출품 (export const a = 1;)
  2. 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(...);
  1. CJS : "우린 그런 거 없다"

반면 Node.js의 내장 모듈인 child_processcommonjs 방식으로 구현되어 있다

default라는 ESM 전용 속성이라는건 있는지도 모르는 상태다

child_process.default.spawn(...);child_process.undefined.spawn(...); 코드와 마찬가지다

그래서 undefined (reading 'spawn') 에러가 발생한다

해결책

tsconfig.json(jest에서는 일반적으로 따로 두니까 tsconfig.jest.json) 에서 "esModuleInterop": true 코드를 추가한다

"compilerOptions": {
    "esModuleInterop": true,
}
esModuleInterop란?
  • es: ECMAScript (자바스크립트 표준)

  • Module: 모듈 시스템 (import/export)

  • Interop: Interoperability (상호 운용성)의 줄임말

즉, 합치면 **"ES 모듈 상호 운용성"**이라는 뜻입니다.

CJS 와 ESM 상호 운용이 가능하도록 하는 옵션을 뜻한다

** "esModuleInterop": true 필자의 생각에, 그냥 node.js개발할거면 필수다 걍 항상 키고 살아라**