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를 호출하면 어떤 일이 벌어질까?
- Render 1: 컴포넌트가 렌더링 된다. (Virtual DOM 생성)
- Browser Paint : 브라우저가 화면을 그린다.
- Effect 실행 :
useEffect가 실행된다. - setState 호출 : Effect 내부의
setState가 호출되어 상태가 변경된다. - Render 2 : 상태가 변했으니 React는 즉시 다시 렌더링을 시작한다.
- 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를 위한 훅이다.
때문에
- DOM 변경 (타이틀 변경 등)
- 구독 (이벤트 리스너)
- 네트워크 요청
이런 외부적인 요인이 아니라면, 단순한 데이터 흐름 제어를 위해 useEffect를 쓰는 것은 멈춰야 한다.
코드를 짤 때 useEffect 안에서 setState를 쓰고 싶다면 딱 한 번만 더 생각해보자.
"이거... 그냥 렌더링 중에 변수로 선언해도 되는 거 아니야?"
아마 대부분의 경우 답은 YES 일 것이다.
출처
https://react.dev/learn/you-might-not-need-an-effect How Upgrade to React 18