Maximum call stack size exceeded — 무한 재귀 원인·해결
자바스크립트에서 "Uncaught RangeError: Maximum call stack size exceeded"가 뜨면, 함수 호출이 너무 깊이 쌓여 호출 스택이 한도를 넘쳤다는 뜻이다. 거의 모든 경우 범인은 무한 재귀다. 함수가 자기 자신을 부르고 또 부르는데 멈출 조건이 없어, 호출이 끝없이 쌓이다 스택이 가득 차 터진다. 종료 조건(base case)을 빠뜨린 재귀, 서로를 부르는 두 함수, 자기 자신을 건드리는 getter처럼 의도치 않은 자기 호출이 단골이다. 이 글은 이 에러의 의미, 원인, 그리고 base case 점검과 반복문 전환을 비롯한 해결을 정리한다.

이 글의 구성
🔎 에러 메시지의 의미
자바스크립트는 함수를 호출할 때마다 그 호출 정보를 "호출 스택(call stack)"이라는 공간에 차곡차곡 쌓는다. 함수가 끝나면 그 칸을 비우고, 다음 함수로 넘어간다. 그런데 함수가 끝나지 않고 계속 다른 함수(보통 자기 자신)를 부르면 스택이 비워지지 않고 계속 쌓이기만 한다. 엔진이 정한 스택 한도를 넘는 순간, "더는 못 쌓는다"며 RangeError를 던진다.
RangeError는 "값이 허용 범위를 벗어났다"는 종류의 에러인데, 여기서는 호출 깊이가 허용 범위를 넘은 것이다. (파이어폭스는 같은 상황을 "InternalError: too much recursion"이라는 다른 문구로 표시하지만, 의미는 동일하다.) 메시지는 엔진마다 조금 달라도, 콘솔의 스택 추적을 보면 같은 함수 이름이 수없이 반복돼 나타난다. 그 반복되는 함수가 바로 멈추지 못하고 자신을 계속 부른 범인이다.
핵심 — 멈추지 못하고 계속 쌓였다
이 에러의 거의 전부는 "끝나는 지점이 없는 재귀"다. 함수가 자신을 부를 때는 언젠가 멈출 조건(base case)이 반드시 있어야 하는데, 그게 없거나 도달하지 못하면 호출이 무한히 쌓인다. 그래서 디버깅은 스택 추적에서 반복되는 함수를 찾아, "이 함수는 어떤 조건에서 멈추는가?"를 묻는 데서 시작한다.
🧩 자주 나오는 발생 원인 4가지
원인 1 — 종료 조건(base case)이 없거나 못 미침
가장 흔하다. 재귀 함수에 멈추는 조건을 안 넣었거나, 넣었어도 그 조건에 영영 도달하지 못하는 경우다. 예를 들어 숫자를 1씩 줄여 0에서 멈춰야 하는데 줄이는 걸 빠뜨리면 끝없이 호출된다.
원인 2 — 서로를 부르는 상호 재귀
A가 B를 부르고 B가 다시 A를 부르는데, 둘 다 멈출 조건이 없는 경우다. 한 함수만 봐서는 재귀처럼 안 보여 놓치기 쉽다. 스택 추적에 두 함수가 번갈아 반복되면 상호 재귀를 의심한다.
원인 3 — getter·setter의 자기 참조
get name() 안에서 this.name을 읽으면, 그 접근이 다시 같은 getter를 호출해 무한 반복된다. setter도 마찬가지다. 보통은 내부 저장용 다른 이름(예: this._name)을 써야 하는데 같은 이름을 쓴 실수다.
원인 4 — 의도치 않은 자기 호출·과도하게 깊은 재귀
함수를 감싸 덮어쓰면서 원본 대신 자신을 부르거나, 이벤트 핸들러가 자신을 다시 트리거하는 경우다. 또 종료 조건은 맞지만 재귀 깊이가 수만 단계로 과도해 한도를 넘는 경우도 있다.
💻 재현과 진단
전형적인 재현이다. 종료 조건을 빠뜨린 재귀는 즉시 스택을 넘긴다.
// 멈출 조건이 없는 재귀
function countdown(n) {
console.log(n);
return countdown(n - 1); // n이 0이어도 안 멈춤
}
countdown(10);
// ✗ RangeError: Maximum call stack size exceeded
진단의 핵심은 스택 추적이다. 콘솔에서 에러를 펼치면 같은 함수가 끝없이 반복돼 나타나는데, 그 함수가 멈추지 못한 범인이다. 상호 재귀라면 두 함수가 번갈아 보인다. getter 자기 참조라면 그 접근자 이름이 반복된다.
// getter 자기 참조 — 무한 호출
const user = {
get name() { return this.name; } // name이 또 getter 호출
};
user.name;
// ✗ Maximum call stack size exceeded
🛠 해결 — base case·반복문 전환
해결의 1순위는 멈출 조건을 제대로 다는 것이다. 재귀에는 반드시 "더 이상 자신을 부르지 않고 끝나는" 분기가 있어야 한다.
// ✓ base case 추가 — 0 이하면 멈춤
function countdown(n) {
if (n < 0) return; // 종료 조건
console.log(n);
return countdown(n - 1);
}
// ✓ getter 자기 참조 → 내부 저장용 다른 이름 사용
const user = {
_name: "김개발",
get name() { return this._name; }
};
종료 조건이 맞는데도 깊이가 과도하다면, 재귀를 반복문으로 바꾸는 게 답이다. 반복문은 호출 스택을 쌓지 않으므로 깊이 제한에서 자유롭다.
상황별 해결 방법
한 가지 주의할 점이 있다. "꼬리 재귀(tail call)로 최적화하면 된다"는 조언을 보기도 하는데, 현실에서는 신뢰하기 어렵다. 명세에는 꼬리 호출 최적화가 있지만, 주요 엔진 중 이를 실제로 적용하는 곳은 사실상 사파리(JavaScriptCore) 정도다. 크롬(V8)은 한때 구현했다가 비활성화했고, 파이어폭스는 구현한 적이 없다. 그래서 깊은 재귀의 안전한 해법은 꼬리 재귀에 기대는 것이 아니라 반복문으로 바꾸는 것이다.
⚖️ 무한 루프(freeze)와의 차이
"끝없이 반복된다"는 점에서 무한 while 루프와 헷갈리기 쉬운데, 증상과 원인이 다르다.
| 구분 | 무한 재귀 (스택 초과) | 무한 루프 (while) |
|---|---|---|
| 쌓이는 것 | 함수 호출이 스택에 계속 쌓임 | 한 함수 안에서 돌기만 함 |
| 결과 | RangeError로 곧 중단 | 멈추지 않고 화면이 멈춤(freeze) |
| 단서 | 스택 추적에 같은 함수 반복 | 에러 없이 탭이 응답 없음 |
핵심 차이는 "스택을 쌓느냐"다. 무한 재귀는 호출이 계속 쌓이다 한도를 넘어 RangeError로 비교적 빨리 멈춘다. 반면 무한 while 루프는 한 함수 안에서 돌기만 해 스택이 안 쌓이고, 에러도 없이 페이지가 응답하지 않는 freeze 상태가 된다. 그래서 RangeError가 떴다면 "재귀·자기 호출"을, 탭이 그냥 멈췄다면 "반복문 조건"을 의심하는 게 빠르다.
스택 추적에 반복되는 그 함수가 범인이다. "이건 어떤 조건에서 멈추는가?"를 물어라. 깊은 재귀는 반복문으로.
— 거의 항상 종료 조건의 문제
💬 자주 묻는 질문 5가지
Q1재귀를 안 썼는데도 이 에러가 나요.
직접적인 재귀가 아니어도 간접 자기 호출일 수 있습니다. 서로를 부르는 두 함수(상호 재귀), getter·setter의 자기 참조, 함수를 덮어쓰며 자신을 부르는 경우가 대표적입니다. 스택 추적에서 반복되는 함수·접근자 이름을 보면 숨은 자기 호출이 드러납니다.
Q2종료 조건은 있는데도 터져요.
조건은 있지만 매 호출이 그 조건으로 다가가지 못하는 경우입니다. 예를 들어 n이 0에서 멈춰야 하는데 n을 줄이지 않거나 엉뚱하게 바꾸면 영영 0에 못 닿죠. 또는 조건은 맞지만 재귀 깊이가 수만 단계로 과도해 한도를 넘는 경우라면 반복문으로 바꿔야 합니다.
Q3스택 한도는 정확히 몇인가요?
고정값이 아니라 엔진·브라우저·환경에 따라 다릅니다. 보통 수천에서 수만 단계 수준이고, 각 호출이 쓰는 메모리에 따라서도 달라집니다. 그래서 "몇까지 되나"에 의존하기보다, 깊이가 커질 수 있는 로직은 처음부터 반복문으로 설계하는 게 안전합니다.
Q4꼬리 재귀로 만들면 해결되나요?
신뢰하기 어렵습니다. 명세상 꼬리 호출 최적화가 있지만 실제 적용하는 곳은 사실상 사파리뿐입니다. 크롬(V8)은 구현했다가 비활성화했고, 파이어폭스는 구현하지 않았습니다. 그래서 꼬리 재귀로 바꿔도 대부분 환경에서는 여전히 스택이 쌓입니다. 깊은 재귀의 확실한 해법은 반복문 전환입니다.
Q5화면이 그냥 멈추는 거랑은 다른 건가요?
다릅니다. 무한 재귀는 호출이 스택에 쌓이다 한도를 넘어 RangeError로 비교적 빨리 멈춥니다. 반면 무한 while 루프는 한 함수 안에서 돌기만 해 스택이 안 쌓이고, 에러 없이 탭이 응답하지 않는 freeze가 됩니다. RangeError가 떴으면 재귀를, 탭이 멈췄으면 반복문 조건을 의심하세요.
📌 결론
"Maximum call stack size exceeded"는 함수 호출이 너무 깊이 쌓여 호출 스택 한도를 넘은 RangeError다. 거의 항상 멈출 조건이 없는 무한 재귀가 원인이다. 그래서 스택 추적에서 반복되는 함수를 찾아 "어떤 조건에서 멈추는가"를 따지는 게 출발점이다.
실무 원칙은 다음과 같다.
🔁 종료 조건을 확인한다. 재귀에는 자신을 더 부르지 않고 끝나는 base case가 있어야 하고, 매 호출이 그쪽으로 다가가야 한다.
🪜 스택 한도에 의존하지 않는다. 한도는 엔진마다 다르고 고정값이 아니다. 깊이가 커질 로직은 처음부터 안전하게 설계한다.
🔄 깊은 재귀는 반복문으로 바꾼다. 반복문이나 명시적 스택은 호출 스택을 안 쌓는다. 꼬리 재귀 최적화는 대부분 환경에서 적용되지 않으니 의존하지 않는다.
스택 초과 에러 체크리스트
본 글은 자바스크립트 RangeError(call stack 초과)의 일반적 원인과 해결 방법을 정리한 자료다. 코드 수정은 영향 범위를 확인한 뒤 신중히 적용한다.
#JavaScript #RangeError #callstack #maximumcallstack #무한재귀 #재귀함수 #basecase #반복문 #스택오버플로 #꼬리재귀 #프론트엔드 #JS디버깅 #런타임에러 #웹개발 #코딩초보
'IT' 카테고리의 다른 글
| Cannot set properties of undefined (setting 'x') — 원인·해결·예방 (0) | 2026.07.01 |
|---|---|
| Cannot read properties of null (reading 'x') — 원인·해결·예방 (0) | 2026.07.01 |
| Uncaught (in promise) — 처리 안 된 Promise 거부 원인·해결 (0) | 2026.06.30 |
| x is not a function — 원인·해결·예방 (0) | 2026.06.30 |
| SyntaxError: Unexpected token — 원인·해결·예방 (0) | 2026.06.30 |
댓글