티스토리 뷰

 

React 개발을 하다 보면 한 번쯤은 마주치는 붉은색 에러 화면, 바로 **"Too many re-renders. React limits the number of renders to prevent an infinite loop."**입니다. 이 에러는 단순한 버그를 넘어 React의 렌더링 사이클에 대한 이해가 부족할 때 발생하는 '경고장'과 같습니다.

현대 웹 애플리케이션은 사용자 인터랙션이 복잡해짐에 따라 상태 변화가 빈번하게 일어납니다. 특히 실시간 데이터가 중요한 이커머스나 대시보드 환경에서 잘못 설계된 상태 업데이트 로직은 순식간에 브라우저 자원을 고갈시키는 무한 루프를 만들어내죠. 오늘은 이 무한 루프가 발생하는 근본 원인과 이를 우아하게 해결하는 실전 전략을 심도 있게 살펴보겠습니다.


1. Deep Dive: 왜 무한 루프에 빠지는가?

React의 렌더링 메커니즘은 매우 직관적입니다. **"상태(State)가 변하면 컴포넌트를 다시 그린다"**는 대전제죠. 하지만 이 단순함 속에 함정이 있습니다.

작동 원리와 비유

렌더링 프로세스 중에 상태를 변경하는 코드가 포함되어 있다면 어떤 일이 벌어질까요?

  1. React가 컴포넌트 함수를 실행합니다 (Render).
  2. 실행 도중 setState를 만납니다.
  3. React는 "상태가 변했네? 다시 그려야겠다"라고 예약합니다.
  4. 다시 1번으로 돌아가 함수를 실행합니다.
  5. 또 setState를 만납니다.
  6. 이 과정이 무한히 반복됩니다.

이를 **"거울 방(Mirror Room)"**에 비유할 수 있습니다. 내가 거울 앞에 서서(렌더링), 거울 속의 나에게 "손을 들어(상태 변경)"라고 명령하면, 거울 속의 나도 똑같이 손을 들고, 그 모습을 본 나는 다시 손을 드는 행위가 끝없이 반복되어 결국 에너지가 소진되는 것과 같습니다.


2. Hands-on: 실전 이커머스 장바구니 로직으로 보는 사례

가장 흔하게 실수하는 사례 중 하나는 렌더링 본문(Body) 내에서의 직접적인 상태 변경입니다. 사용자가 상품 수량을 변경할 때마다 할인 혜택을 계산하여 상태를 업데이트하는 로직을 가정해 봅시다.

❌ 위험한 코드: 무한 루프 유발

JavaScript
 
import React, { useState } from 'react';

const CartSummary = ({ items }) => {
  const [totalPrice, setTotalPrice] = useState(0);

  // ⚠️ 위험: 렌더링 도중에 상태를 변경하고 있습니다.
  const calculatedTotal = items.reduce((acc, item) => acc + item.price, 0);
  
  // 상태 업데이트가 다시 렌더링을 유발하고, 다시 이 줄이 실행됩니다.
  setTotalPrice(calculatedTotal); 

  return (
    <div>
      <h3>총 합계: {totalPrice}원</h3>
    </div>
  );
};

✅ 개선된 코드: 파생된 상태(Derived State)와 useEffect 활용

위와 같은 상황에서는 별도의 state를 만들지 않는 것이 최선이지만, 만약 복잡한 부수 효과(Side Effect)가 필요하다면 useEffect나 메모이제이션을 활용해야 합니다.

JavaScript
 
import React, { useState, useEffect, useMemo } from 'react';

const CartSummary = ({ items }) => {
  // 1. 단순 계산은 변수로 처리 (Derived State)
  // 매 렌더링마다 계산되지만, 상태를 직접 바꾸지 않으므로 안전합니다.
  const totalPrice = useMemo(() => {
    return items.reduce((acc, item) => acc + item.price, 0);
  }, [items]); // items가 변경될 때만 재계산

  // 2. 만약 API 호출 등 부수 효과가 필요하다면 useEffect 사용
  useEffect(() => {
    // 특정 조건(items 변경) 하에서만 실행되도록 의존성 배열을 관리합니다.
    console.log("장바구니가 업데이트되었습니다.");
    // 필요 시 외부 API로 데이터 전송 등의 로직 수행
  }, [items]);

  return (
    <div className="p-4 border rounded shadow">
      <h3 className="font-bold">주문 요약</h3>
      <p>총 {items.length}개의 상품</p>
      <p className="text-blue-600">결제 예정 금액: {totalPrice.toLocaleString()}원</p>
    </div>
  );
};

3. Troubleshooting: 흔히 놓치는 '범인'들

무한 루프 에러를 만났을 때 가장 먼저 점검해야 할 3가지 체크포인트입니다.

  • 이벤트 핸들러의 즉시 실행:
  • onClick={handleDelete()} 처럼 작성하면 함수가 렌더링 시점에 즉시 호출됩니다. 반드시 onClick={() => handleDelete()}와 같이 콜백 형태로 전달하세요.
  • 의존성 배열(Dependency Array)의 실수:$A \rightarrow B \rightarrow A \dots$ 형태의 순환 참조가 발생하지 않는지 확인해야 합니다.
  • useEffect 안에서 수정하는 상태값을 해당 useEffect의 의존성 배열에 넣지는 않았나요?
  • 객체/배열의 참조 무결성:
  • useEffect의 의존성에 객체나 배열을 넣을 경우, 렌더링 때마다 새로운 참조값이 생성되어 내용이 같더라도 React는 "변경되었다"고 판단할 수 있습니다. useMemo나 useRef로 참조를 유지하세요.

4. Trade-offs: 성능과 가독성 사이의 선택

모든 값을 useMemo나 useCallback으로 감싸는 것이 정답은 아닙니다.

전략 장점 단점
파생된 상태 사용 구조가 단순하고 무한 루프 위험이 거의 없음. 렌더링 시마다 복잡한 연산이 반복될 경우 성능 저하.
useEffect + State 비동기 작업이나 복잡한 로직 처리에 용이. 의존성 관리 부주의 시 무한 루프 발생 가능성 높음.
useMemo 활용 불필요한 재계산을 방지하여 최적화 가능. 메모리 사용량이 늘어나며 코드 복잡도가 증가함.

결국 핵심은 **"상태는 최소화하고, 계산할 수 있는 값은 렌더링 흐름 속에서 계산하라"**는 원칙을 지키는 것입니다.


결론 및 요약

React에서 "Too many re-renders" 에러는 우리에게 데이터 흐름의 단방향성을 다시금 상기시켜 줍니다. 렌더링은 순수하게 화면을 그리는 행위여야 하며, 상태 변경은 명확한 이벤트나 부수 효과 처리 단계에서 이루어져야 합니다.

지금 작성 중인 코드에서 useState로 선언된 변수 중, 사실은 다른 props나 state로부터 단순 계산될 수 있는 값(Derived State)이 있지는 않나요? 그 연결 고리를 끊는 것만으로도 대부분의 무한 루프 문제는 마법처럼 사라질 것입니다.

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함