Cascading Renders
7 min read

Cascading Renders

React의 Cascading Renders에 대해 알아본다.


서론

최근 React 기반에서 개발을 하다보면 자주 보이는 문구가 있다.

Error: Calling setState synchronously within an effect can trigger cascading renders.

"아니, useEffect에서 state 좀 업데이트하겠다는데 왜 그래? 예전엔 잘만 됐잖아?"

주로 useEffect 내부에서 setState 를 호출하는 상황에서 출력되는 문구이다. 기존에 자주 사용되던 이 패턴에 무슨 문제가 있길래 React 18, 그리고 다가올 React 19에서 이 패턴은 '안티 패턴'으로 규정하게 된 걸까?

Cascading Renders

경고의 핵심은 비효율적인 렌더링 흐름 에 있다.

useEffect내부에서 setState를 호출하면 어떤 일이 벌어질까?

  1. Render 1: 컴포넌트가 렌더링 된다. (Virtual DOM 생성)
  2. Browser Paint : 브라우저가 화면을 그린다.
  3. Effect 실행 : useEffect가 실행된다.
  4. setState 호출 : Effect 내부의 setState가 호출되어 상태가 변경된다.
  5. Render 2 : 상태가 변했으니 React는 즉시 다시 렌더링을 시작한다.
  6. Browser Paint : 브라우저가 변경된 화면을 다시 그린다.

문제는 사용자가 Render 1의 결과를 볼 새도 없이 Render 2가 덮어씌워진다는 점이다. React는 렌더링을 두 번 계산했지만, 사용자는 결과적으로 한 번의 화면만 보게 된다. 이것이 'Cascading Renders'이며, 리소스 낭비라는 것이다.

근데 왜 지금 이슈가 된 거지?

React 18 에서 Concurrent Mode가 도입되면서 이 문제의 중요성이 높아졌다. React는 이제 렌더링을 중간에 멈추거나, 우선순위를 미루거나 하는 복잡한 스케줄링을 수행할 수 있게 된 것이다.

이런 상황에서 useEffect 내의 동기적인 setState는 React의 렌더링 파이프라인을 방해하고, 예측불가능한 UI 블로킹을 유발할 수 있다. 그래서 React 팀은 렌더링 중에 결정할 수 있는 것은 렌더링 중에 끝내라라고 가이드라인을 강화하고 있는것이다.

대표적인 안티 패턴과 해결책

가장 흔하게 발생하는 안티 패턴에 대해 알아보자면,

Case 1: Props에 따라 State를 변경

props로 받은 데이터가 바뀔 때마다 내부 state를 업데이트 하는 경우이다.

❌ 안티 패턴 (Don't)

function UserProfile({ user }) {
  const [fullName, setFullName] = useState('');
 
  // 😱 렌더링 -> Effect -> setState -> 재렌더링 발생!
  useEffect(() => {
    setFullName(`${user.firstName} ${user.lastName}`);
  }, [user]);
 
  return <div>{fullName}</div>;
}

✅ 해결책: 렌더링 중에 계산하기 (Derive State) 상태로 저장할 필요가 없다. 렌더링 중에 계산하고, 값이 비싸다면 useMemo를 쓰면 된다.

function UserProfile({ user }) {
  // 😎 렌더링 중에 즉시 계산됨. 추가 렌더링 없음.
  const fullName = `${user.firstName} ${user.lastName}`; 
 
  return <div>{fullName}</div>;
}

Case 2: Props가 변하면 State를 초기화해야 할 때

예를 들어, 선택된 userId가 바뀌면 작성 중이던 comment를 초기화해야 하는 경우이다.

❌ 안티 패턴 (Don't)

function CommentBox({ userId }) {
  const [comment, setComment] = useState('');
 
  // 😱 userId가 바뀔 때마다 재렌더링 유발
  useEffect(() => {
    setComment('');
  }, [userId]);
 
  return <input value={comment} onChange={e => setComment(e.target.value)} />;
}

✅ 해결책: Key를 이용해 컴포넌트 다시 만들기 이건 React의 강력한 기능을 이용하는 방법이다. 상위 컴포넌트에서 key 값을 변경하면 React는 해당 컴포넌트의 상태를 완전히 초기화하고 새로 그린다.

// 부모 컴포넌트
function PostPage({ userId }) {
  return (
    <>
      <Profile userId={userId} />
      {/* 😎 userId가 바뀌면 CommentBox는 자동으로 초기화됨 */}
      <CommentBox key={userId} userId={userId} />
    </>
  );
}

Case 3: 부모 컴포넌트에게 데이터 전달하기

자식 컴포넌트에서 데이터가 로드되거나 변경되었을 때, 부모의 상태를 업데이트하려고 useEffect를 사용하는 경우.

❌ 안티 패턴 (Don't)

function Child({ onLoaded }) {
  useEffect(() => {
    const data = performCalculations();
    onLoaded(data); // 😱 부모의 setState를 유발 -> 부모 재렌더링 -> 자식 재렌더링
  }, [onLoaded]);
  
  return <div>Child</div>;
}

✅ 해결책: 이벤트 핸들러에서 처리하거나 상태 끌어올리기 가능하면 렌더링 흐름이 아니라, 사용자 이벤트(클릭, 입력 등) 가 발생했을 때 데이터를 처리하도록 로직을 이동시켜야 합니다. 혹은 데이터를 가져오는 로직 자체를 부모로 옮겨야 합니다.

결론

Effect는 '동기화'를 위한 곳이다

useEffect의 이름이 왜 useChangeState가 아니라 useEffect인지 생각해 볼 때이다. useEffect는 어디까지나 Side Effect를 위한 훅이다.

때문에

이런 외부적인 요인이 아니라면, 단순한 데이터 흐름 제어를 위해 useEffect를 쓰는 것은 멈춰야 한다.

코드를 짤 때 useEffect 안에서 setState를 쓰고 싶다면 딱 한 번만 더 생각해보자.

"이거... 그냥 렌더링 중에 변수로 선언해도 되는 거 아니야?"

아마 대부분의 경우 답은 YES 일 것이다.

출처

https://react.dev/learn/you-might-not-need-an-effect How Upgrade to React 18