[TIL]spread 연산자와 Maximum call stack size exceeded 에러

Choise.o·2025년 6월 30일
0

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


원인 분석

에러가 발생한 컴포넌트에서 재귀 함수를 사용 중이었다. 처음에는 그 부분이 문제일거라 생각했는데 막상 분석해보니 문제가 된건 아래의 코드 한 줄이었다.

itemList.push(...dataList);

dataList는 API 를 호출해서 응답으로 넘겨받은 데이터이다. 컴포넌트가 초기화될 때 한번만 실행된다.
흔히 사용되는 패턴이지만 dataList길이가 매우 크면 문제가 될 수 있다.


위의 코드를 해석해보면 아래와 같다.
  • Spread Operator(...)는 배열에 사용될 경우 배열의 각 요소를 분해하여 함수 인자로 전달한다.
  • 즉, push(...dataList)push(item1, item2, item3, ..., itemN)과 같은 형태로 변환된다.
  • push 함수에 여러 인자를 전달하면 이 모든 인자의 참조값은 브라우저의 스택 메모리에 적재된다.

인자 갯수가 브라우저 stack 메모리 한계를 초과할만큼 많으면 RangeError: Maximum call stack size exceeded 발생한다.


해결 방법

1. 배치 처리 (Chunking)

const chunkSize = 1000;
for (let i = 0; i < dataList.length; i += chunkSize) {
  const chunk = dataList.slice(i, i + chunkSize);
  itemList.push(...chunk);
}

데이터를 나눠서 처리해 stack 부담을 줄여준다.

2. concat() 사용

itemList = itemList.concat(dataList);

기존 배열과 새 배열을 병합해 새로운 배열 반환한다.
push(...dataList)처럼 개별 인자를 stack에 올리지 않으므로 안전하다.

3. forEach() 방식

dataList.forEach(item => {
  itemList.push(item);
});

각 요소를 개별적으로 처리하므로 stack 부담이 없다. 단, 대량일 경우 concat()보다 다소 느릴 수 있다.

4. 배열 재할당 방식

itemList = dataList;

기존 배열을 유지할 필요가 없다면 가장 단순하고 안전한 방식. 단, 반응형으로 연결된 컴포넌트가 있을 경우 바인딩이 유지되는지 확인은 필요하다.


💡참고1. - 브라우저 메모리 구조

브라우저는 JavaScript 코드를 실행할 때 내부적으로 아래의 메모리 영역을 사용한다.

메모리 영역설명예시
콜 스택 (Call Stack)실행 중인 함수들의 실행 컨텍스트를 저장하는 LIFO 스택함수 호출, 반환 추적
힙 (Heap)객체와 참조형 데이터가 저장되는 동적 메모리 공간객체, 배열, 함수 등
웹 API 영역setTimeout, DOM, fetch 등 브라우저가 제공하는 비동기 API 처리 공간타이머, 이벤트 등
태스크 큐 / 이벤트 루프비동기 작업 완료 후 실행할 콜백 저장콜백 대기 및 실행 순서 관리

함수의 매개변수들은 유형에 따라 저장 위치가 달라진다.

1. 기본형(Primitive) 매개변수

  • 숫자, 문자열, boolean 등
  • 함수가 호출될 때 **콜 스택(stack)**에 있는 실행 컨텍스트의 **Lexical Environment (환경 레코드)**에 저장
  • 저장 위치 : Stack 메모리
function sum(a, b) {
  return a + b;
}
sum(3, 4); // a, b는 스택에 저장

2. 참조형(Object, Array 등) 매개변수

  • 함수 인자로 객체나 배열이 전달되면, **참조값(reference)**만 **스택(stack)**에 저장되고, 실제 데이터는 **힙(heap)**에 저장
function update(obj) {
  obj.name = "Alice";
}
const user = { name: "Bob" };
update(user); 
  • obj는 stack에 저장되고, 실제 user 객체는 heap에 저장된다.

💡참고2. - 브라우저 메모리 확인

개발자 도구를 사용해서 브라우저 메모리에 어떤 데이터들이 올라가있는지 확인해보자.
아래처럼 debugger를 설정해두고

debugger;
state.parentCodeList.push(...dataList);

개발자 도구(F12) > Source 탭에서 기능을 실행시키면 메모리에 어떤 데이터들이 올라가 있는지 확인할 수 있다.


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

0개의 댓글