
React 19와 새로운 렌더링 패러다임
import { useEffect, useState } from "react";
const Test = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setCount((count) => count + 1);
}, []);
return <div></div>;
};
export default Test;
위의 테스트코드는 기존에 사용하던 익숙한 코드이다.
리액트 19에서는 useEffect 내부에서 setState를
동기적으로 호출하는 것을 지양하는 규칙이 강화되었다.
이는 새로운 규칙이 추가된 것을 넘어
리액트가 지향하는 코드 패러다임이
근본적으로 변화하고 있음을 의미한다.
setState in Effect 규칙의 등장과 연쇄 렌더링의 함정

새로운 ESLint 규칙이 경고하는 핵심은
연쇄적인 렌더링(Cascading Rendering)이다.
실제 린트 에러 메시지를 살펴보면 다음과 같은 문구가 등장한다.
"이펙트 내부에서 setState를 동기적으로 호출하면 연쇄적인 렌더링을 요할 수 있다"

의존성 배열을 비워두면([]) 마운트 시점에
단 한 번만 실행되므로 성능에 큰 영향이 없을 것이라 오해한다.
하지만 리액트 19에서는 의존성 배열의 유무와 상관없이
이펙트 내에서의 동기적 setState 호출 자체를 지양한다.
렌더링 흐름 5단계 vs 2단계

왜 리액트가 이 패턴을 그토록 싫어하는지
렌더링 흐름을 단계별로 쪼개보면 명확해진다.
useEffect 내에서 setState를 사용할 경우,
브라우저는 다음과 같은 5단계를 거쳐야 한다.
* 1단계: 컴포넌트 렌더링
* 2단계: DOM 업데이트 (첫 번째 페인트)
* 3단계: 이펙트(useEffect) 실행
* 4단계: setState 호출로 인한 상태 변경
* 5단계: 컴포넌트 재렌더링 및 다시 DOM 업데이트
여기서 3번부터 5번까지의 과정은 사실상 불필요한 비용이다.
만약 처음부터 상태의 초기값을 적절히 설정하거나
렌더링 로직 내에서 값을 계산한다면 ?
2단계(렌더링 -> 업데이트)만으로
동일한 화면을 보여줄 수 있다.
리액트 컴파일러와 'Rules of React'의 강제화

이 규칙이 지금 이 시점에 강조되는 결정적인 이유는
리액트 컴파일러(React Compiler)의 등장 때문이다.
컴파일러는 모든 개발자가 Rules of React(리액트 규칙, 룰리트)를
정확히 준수한다고 가정하고 자동 최적화를 수행한다.
- 자동 메모이제이션: 렌더링 과정이 예측 가능할 때, 컴파일러는 불필요한 재렌더링을 제거하고 안전하게 메모이제이션을 적용함.
- 렌더링의 철학: 리액트는 렌더(Render)는 계산이고, 이펙트(Effect)는 외부 시스템과의 동기화를 위해서만 사용하라는 철학을 강조함.
리액트의 핵심 철학에 따르면 렌더링은 순수한 '계산'이어야 한다.
이 과정이 예측 가능해야만 컴파일러가 불필요한 재렌더링을 안전하게 제거할 수 있다.
setState in Effect 규칙을 지키는 것은 최신 리액트의
최적화 엔진을 온전히 가동하기 위한 전제 조건이다.
그럼 마운트 시점의 분기 처리가 반드시 필요한 상황에서는 어떻게 해야 할까?
분기처리에 따른 렌더링
useSyncExternalStore 활용하기

useEffect 대신 useSyncExternalStore훅을 활용하여
커스텀 훅(예: useIsMounted)을 만들어 사용하는 것이 권장된다.
이 방식을 사용하면 setState in Effect 에러를 회피하면서도
동일한 조건부 렌더링 기능을 구현할 수 있다.
import { useSyncExternalStore } from "react";
/**
* 서버 사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR)의 일치를 돕는 훅
* 리액트 19의 'setState in Effect' 지양 원칙을 준수합니다.
*/
const useMounted = () => {
return useSyncExternalStore(
// 1. subscribe: 클라이언트에서 마운트된 후 변경 사항이 없으므로 빈 함수 반환
() => () => {},
// 2. getSnapshot: 브라우저(클라이언트) 환경일 때 반환할 값
() => true,
// 3. getServerSnapshot: 서버 환경(SSR)일 때 반환할 값
() => false
);
};
export default useMounted;
- 연쇄 렌더링(Cascading) 방지: useEffect 안에서 setCount(1)을 하면 0으로 렌더링 -> 이펙트 실행 -> 1로 다시 렌더링이라는 2번의 과정이 생기지만, 위 방식은 리액트가 렌더링 단계에서 즉시 마운트 여부를 판단하게 도와준다.
- Hydration 오류 해결: 서버에서는 false를, 클라이언트에서는 true를 반환하게 하여 서버와 클라이언트의 HTML 구조가 달라 발생하는 에러를 해결한다.
- 컴파일러 최적화: 상태 변경(setState)이 이펙트 안에서 일어나지 않으므로, 리액트 컴파일러가 이 컴포넌트는 "순수하다"고 판단하여 훨씬 강력한 최적화(메모이제이션)를 적용할 수 있다.
결론: 리액트 철학의 이해와 미래를 위한 준비

결국 렌더링은 순수한 계산이고,
이펙트는 오직 외부 시스템과의 동기화에만
사용한다는 정석적인 철학을 강제함으로
더 높은 품질의 애플리케이션을 만들도록 가이드하는 것이다.
'🍎 Dev Log > Article' 카테고리의 다른 글
| AI로 완벽한 웹사이트를 만드는 5가지 전략 (0) | 2026.04.01 |
|---|---|
| 자바스크립트 Date의 시대가 끝났다: 우리가 Temporal을 기다려온 이유 (0) | 2026.03.15 |
| 크롬 Performance 패널로 웹 성능 테스트하는 법 (0) | 2025.11.14 |
| DO TOO MUCH, 과하게 하는 것이 리더십이다 (0) | 2024.12.01 |
| 사람을 뽑을 때 중요한 단 하나: 진짜 ‘신경 쓰는 사람’을 뽑아라 - 성공을 위한 단순한 공식 (0) | 2024.11.13 |