잘 동작하던 기능이 대량 데이터 환경에서 RangeError: Maximum call stack size exceeded 오류를 발생시켰다.

에러가 발생한 컴포넌트에서 재귀 함수를 사용 중이었다. 처음에는 그 부분이 문제일거라 생각했는데 막상 분석해보니 문제가 된건 아래의 코드 한 줄이었다.
itemList.push(...dataList);
dataList는 API 를 호출해서 응답으로 넘겨받은 데이터이다. 컴포넌트가 초기화될 때 한번만 실행된다.
흔히 사용되는 패턴이지만 dataList길이가 매우 크면 문제가 될 수 있다.
...)는 배열에 사용될 경우 배열의 각 요소를 분해하여 함수 인자로 전달한다.push(...dataList)는 push(item1, item2, item3, ..., itemN)과 같은 형태로 변환된다.인자 갯수가 브라우저 stack 메모리 한계를 초과할만큼 많으면 RangeError: Maximum call stack size exceeded 발생한다.
const chunkSize = 1000;
for (let i = 0; i < dataList.length; i += chunkSize) {
const chunk = dataList.slice(i, i + chunkSize);
itemList.push(...chunk);
}
데이터를 나눠서 처리해 stack 부담을 줄여준다.
concat() 사용itemList = itemList.concat(dataList);
기존 배열과 새 배열을 병합해 새로운 배열 반환한다.
push(...dataList)처럼 개별 인자를 stack에 올리지 않으므로 안전하다.
forEach() 방식dataList.forEach(item => {
itemList.push(item);
});
각 요소를 개별적으로 처리하므로 stack 부담이 없다. 단, 대량일 경우 concat()보다 다소 느릴 수 있다.
itemList = dataList;
기존 배열을 유지할 필요가 없다면 가장 단순하고 안전한 방식. 단, 반응형으로 연결된 컴포넌트가 있을 경우 바인딩이 유지되는지 확인은 필요하다.
브라우저는 JavaScript 코드를 실행할 때 내부적으로 아래의 메모리 영역을 사용한다.
| 메모리 영역 | 설명 | 예시 |
|---|---|---|
| 콜 스택 (Call Stack) | 실행 중인 함수들의 실행 컨텍스트를 저장하는 LIFO 스택 | 함수 호출, 반환 추적 |
| 힙 (Heap) | 객체와 참조형 데이터가 저장되는 동적 메모리 공간 | 객체, 배열, 함수 등 |
| 웹 API 영역 | setTimeout, DOM, fetch 등 브라우저가 제공하는 비동기 API 처리 공간 | 타이머, 이벤트 등 |
| 태스크 큐 / 이벤트 루프 | 비동기 작업 완료 후 실행할 콜백 저장 | 콜백 대기 및 실행 순서 관리 |
1. 기본형(Primitive) 매개변수
function sum(a, b) {
return a + b;
}
sum(3, 4); // a, b는 스택에 저장
2. 참조형(Object, Array 등) 매개변수
function update(obj) {
obj.name = "Alice";
}
const user = { name: "Bob" };
update(user);
obj는 stack에 저장되고, 실제 user 객체는 heap에 저장된다.개발자 도구를 사용해서 브라우저 메모리에 어떤 데이터들이 올라가있는지 확인해보자.
아래처럼 debugger를 설정해두고
debugger;
state.parentCodeList.push(...dataList);
개발자 도구(F12) > Source 탭에서 기능을 실행시키면 메모리에 어떤 데이터들이 올라가 있는지 확인할 수 있다.

Spread Operator(...) 를 사용할 때는
데이터의 크기가 얼마나 될지,
메모리 부담은 얼마나 될지,
분할 처리(batch)가 더 적절한 방식일지 고려해보자.