본문으로 건너뛰기
개발 머꼬
개발 노트JavaScript
hohyeon.dev24

Maximum call stack size exceeded가 재귀를 안 쓴 코드에서 난 이유

  • #Debugging
  • #Engineering Note
  • #JavaScript

문제 발생

재귀 함수를 쓴 적이 없는데 이 오류가 났습니다.

RangeError: Maximum call stack size exceeded

엔진마다 문구가 다릅니다 — Chrome과 Safari는 위 메시지, Firefox는 InternalError: too much recursion입니다.

문제의 코드는 이랬습니다.

class Person {
  set name(name) {
    this.name = name;   // setter 안에서 다시 setter를 호출한다
  }
}

new Person().name = "지수";   // 스택 오버플로

원인 분석

이 오류는 호출 스택이 한계를 넘었다는 뜻입니다. 함수 호출이 너무 깊어지면 발생하고, 원인은 명시적인 재귀만이 아닙니다.

setter 안에서 같은 속성에 대입. this.name = name이 다시 name setter를 호출합니다. 코드에 재귀가 보이지 않지만 실제로는 무한 재귀입니다.

종료 조건이 없거나 닿지 않는 재귀.

function loop(x) { loop(x + 1); }        // 종료 조건 없음
function loop2(x) { if (x >= 1e12) return; loop2(x + 1); }   // 닿기 전에 스택이 넘침

서로를 호출하는 함수 쌍. a()b()를, b()a()를 부르면 한 함수만 봐서는 재귀가 보이지 않습니다.

깊은 자료구조의 순회. 순환 참조가 있는 객체를 재귀로 훑으면 끝나지 않습니다.

해결 방안

  1. 스택 트레이스에서 반복되는 패턴을 찾습니다. 같은 함수 이름이 계속 나오면 그 함수가 범인이고, 두 이름이 번갈아 나오면 상호 재귀입니다.
  2. setter는 다른 속성에 저장합니다.
class Person {
  set name(name) { this._name = name; }
  get name() { return this._name; }
}
  1. 재귀에는 종료 조건과 진행 보장을 함께 둡니다. 종료 조건이 있어도 매 호출이 그쪽으로 가까워지지 않으면 소용없습니다.
  2. 깊이가 큰 순회는 반복문과 명시적 스택으로 바꿉니다. 호출 스택 대신 배열을 쓰면 한계가 메모리까지 늘어납니다.
function walk(root) {
  const stack = [root];
  while (stack.length > 0) {
    const node = stack.pop();
    visit(node);
    stack.push(...node.children);
  }
}
  1. 순환 참조를 방문 집합으로 막습니다. WeakSet에 방문한 객체를 기록하면 같은 노드로 되돌아가지 않습니다 — 깊은 복사나 직렬화를 직접 구현할 때 필요합니다.
  2. 꼬리 호출 최적화에 기대지 않습니다. 명세에는 있지만 실제로 구현한 엔진이 제한적이라, "꼬리 재귀로 썼으니 괜찮다"는 가정은 서지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.