이 포스트에서는 JavaScript를 사용하며 발생할 수 있는 RangeError: Maximum call stack size exceeded 에러에 대해 다루며, 그 원인과 해결 방법을 자세히 설명합니다.
문제상황
에러가 발생한 코드는 다음과 같습니다.
function deepClone(obj) {
if (typeof obj !== "object") return obj;
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
clone[key] = deepClone(obj[key]);
}
return clone;
}
const sampleObject = {
a: 1,
b: {
c: 2,
d: {
e: 3
}
}
};
const clonedObject = deepClone(sampleObject);
위의 코드는 주어진 객체를 깊게 복사하는 deepClone 함수를 정의하고 사용하는 예시입니다. 이 함수는 객체의 모든 중첩된 속성을 재귀적으로 복사하여 새로운 객체를 생성하며, 원본 객체와 복사본 객체가 서로 영향을 주지 않습니다.
에러로그 내용:
RangeError: Maximum call stack size exceeded
원인분석
이 에러는 재귀 호출이 너무 깊어져서 JavaScript 엔진이 처리할 수 있는 최대 호출 스택 크기를 초과할 때 발생합니다. JavaScript 엔진은 함수 호출에 대해 내부적으로 스택 자료구조를 사용하여 호출 정보를 저장하며, 각 호출마다 스택에 정보를 저장합니다. 따라서 스택 크기가 제한되어 있기 때문에 너무 많은 함수 호출이 일어날 경우 스택 오버플로가 발생하게 됩니다.
deepClone 함수에서 에러가 발생한 원인은 다음과 같습니다.
- 함수는 재귀적으로 정의되어 있어, 객체의 중첩 수준이 매우 깊을 경우 호출 스택이 급격히 증가합니다.
- 함수는 배열과 일반 객체 모두에 대해 동일한 방식으로 처리하려고 하기 때문에, 중첩된 배열이나 객체의 구조에 따라 호출 스택이 추가로 증가할 수 있습니다.
해결방법-1 (최적화된 재귀)
에러가 수정된 코드와 수정된 부분에 대한 주석:
function optimizedDeepClone(obj, cache = new WeakMap()) {
if (typeof obj !== "object") return obj;
if (cache.has(obj)) return cache.get(obj); // 캐시된 결과 반환
const clone = Array.isArray(obj) ? [] : {};
cache.set(obj, clone); // 캐시에 저장
for (const key in obj) {
clone[key] = optimizedDeepClone(obj[key], cache); // 캐시 전달
}
return clone;
}
const clonedObject = optimizedDeepClone(sampleObject);
위의 수정된 코드는 최적화된 재귀를 사용하여 호출 스택 크기를 줄이는 방법을 도입하였습니다. 이 방법은 이미 처리된 객체를 WeakMap
을 사용하여 캐시에 저장하고, 동일한 객체가 다시 처리되어야 할 때 캐시에서 결과를 반환하여 중복 처리를 방지합니다. 이렇게 하면 호출 스택의 크기를 줄일 수 있습니다.
해결된 코드의 작동원리를 각각 단계별로 설명:
- 객체가 아닌 경우, 즉 기본 데이터 타입인 경우 그대로 반환합니다.
- 이미 처리된 객체인 경우, 캐시에서 결과를 반환합니다.
- 객체를 복사하기 위해 새로운 객체 또는 배열을 생성합니다.
- 캐시에 원본 객체와 생성된 복사본을 저장합니다.
- 원본 객체의 모든 속성을 순회하며, 각 속성을 재귀적으로 복사하여 복사본에 할당합니다.
해결방법-2 (반복을 사용한 깊은 복사)
에러가 수정된 코드와 수정된 부분에 대한 주석:
function iterativeDeepClone(obj) {
const stack = [{ src: obj, dest: Array.isArray(obj) ? [] : {} }];
const cache = new WeakMap([[obj, stack[0].dest]]);
while (stack.length > 0) {
const { src, dest } = stack.pop();
for (const key in src) {
if (typeof src[key] === "object") {
if (!cache.has(src[key])) {
cache.set(src[key], Array.isArray(src[key]) ? [] : {});
stack.push({ src: src[key], dest: cache.get(src[key]) });
}
dest[key] = cache.get(src[key]);
} else {
dest[key] = src[key];
}
}
}
return cache.get(obj);
}
const clonedObject = iterativeDeepClone(sampleObject);
위의 수정된 코드는 재귀 호출 대신 반복문을 사용하여 객체를 깊게 복사하는 방법을 구현합니다. 이렇게 하면 호출 스택의 크기를 줄이고 에러를 방지할 수 있습니다.
해결된 코드의 작동원리를 각각 단계별로 설명:
- 원본 객체와 복사본 객체를 스택에 저장합니다.
- 캐시에 원본 객체와 생성된 복사본을 저장합니다.
- 스택에 객체가 남아있는 동안 반복합니다.
- 스택에서 객체를 꺼냅니다.
- 원본 객체의 모든 속성을 순회하며, 각 속성을 복사합니다.
- 속성이 객체인 경우, 캐시에서 결과를 가져오거나 새로 생성하고 스택에 저장합니다.
- 속성이 기본 데이터 타입인 경우, 복사본에 값을 할당합니다.
- 캐시에서 최종 복사본을 반환합니다.
이 방법은 재귀 호출을 사용하지 않기 때문에 호출 스택 크기를 줄일 수 있으며, RangeError: Maximum call stack size exceeded 에러를 방지할 수 있습니다.