
이 글은 모던 리액트 Deep Dive를 기반으로 작성되었습니다.
조만간 19버전이 나온다는 소식을 들었는데 19버전이 나오기전에 17버전과 18버전의 주요 변화를 보면서, 미리 변화를 감지해보자 :)!

자동 배치란 React 18에서 도입된 기능으로 React 18 이전에는 이벤트 핸들러 내부의 상태 업데이트만 배치 처리되었고, 다른 비동기 작업(예: setTimeout, fetch)에서는 배치가 자동으로 이루어지지 않았다. React 18에서는 모든 상태 업데이트를 자동 배치하여, 모든 상황에서 성능 최적화를 지원한다.
여러 상태 업데이트를 한 번의 렌더링으로 처리하는 것을 의미한다.
그렇다면, React 18 이전에는 어땠고 무슨 문제가 있었길래 React 18에서 새롭게 추가가 되었을까?
React 17까지는 동기적 상태 업데이트를 기본으로 했다. 이벤트 핸들러 내부에서는 상태 업데이트가 자동으로 배치 처리되었지만, 비동기 작업에서는 각 상태 업데이트가 개별적으로 처리되었다.
이벤트 핸들러 내부에는 자동 배치 처리, 그러나 비동기 작업에서는 개별적 처리
비동기 작업에서는 자동 배치 처리가 불가능하기에 개별적 처리가 일어났고, 그에 따라 상태 업데이트마다 렌더링이 발생
상태 업데이트마다 렌더링이 발생된다는 건 성능상에 문제점
버튼 클릭 시 로딩 UI가 뜨며 데이터 로드 코드
import React, { useState } from "react";
function FetchDataExample() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const fetchData = async () => {
setIsLoading(true); // 로딩 시작
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const result = await response.json();
setData(result); // 데이터 업데이트
setIsLoading(false); // 로딩 종료
};
return (
<div>
<button onClick={fetchData}>Fetch Data</button>
{isLoading && <p>Loading...</p>}
{data && <p>{data.title}</p>}
</div>
);
}
export default FetchDataExample;
이 코드의 렌더링을 분석해보면
🌸첫 번째 상태 업데이트 (setIsLoading(true)):
isLoading 상태가 true로 업데이트되면서 첫 번째 렌더링 발생.
화면에 "Loading..." 메시지가 표시
🌸두 번째 상태 업데이트 (setData(result)):
data 상태가 업데이트되면서 두 번째 렌더링 발생.
로드된 데이터가 화면에 표시
🌸세 번째 상태 업데이트 (setIsLoading(false)):
isLoading 상태가 false로 업데이트되면서 세 번째 렌더링 발생.
"Loading..." 메시지가 사라짐
총 3번의 렌더링
위의 코드 예제와 같이 비동기함수에도 자동배치가 가능하여 단 1번의 렌더링만 일어난다.
React 18에서는 상태 업데이트를 React 내부 큐에 저장하고, 이것들을 한 번의 렌더링으로 묶어서 처리한다. 그러나 17에서는 이 동작이 제한적으로 특정 조건에서만 이루어졌음. 즉, React에서 제어하는 이벤트 핸들러에서만 배치
setTimeout이나 Fetch, Axios 등 브라우저에서 동작하거나 React 내부에서 발생하는 비동기 작업에는 제한적이었음.

과연 비동기 함수도 자동 배치되어 하나의 렌더링으로 처리하는 것이 항상 좋지 않을 수도 있다. 결국 즉각적인 렌더링이 필요할 수도 있다. 이럴 땐 flushSync를 사용하여 배치를 강제 중단하고 즉시 렌더링을 수행할 수 있다.
import React, { useState } from "react";
import { flushSync } from "react-dom";
function ScrollSyncExample() {
const [items, setItems] = useState([1, 2, 3]);
const addItem = () => {
flushSync(() => {
setItems((prevItems) => [...prevItems, prevItems.length + 1]);
});
const container = document.getElementById("container");
container.scrollTop = container.scrollHeight;
};
return (
<div>
<button onClick={addItem}>Add Item</button>
<div
id="container"
style={{
maxHeight: "100px",
overflowY: "auto",
border: "1px solid black",
}}
>
<ul>
{items.map((item) => (
<li key={item}>{Item ${item}}</li>
))}
</ul>
</div>
</div>
);
}
export default ScrollSyncExample;
즉각적인 아이템 추가 렌더링으로 스크롤 위치의 조정이 정확해진다.
const addItem = () => {
setItems((prevItems) => [...prevItems, prevItems.length + 1]);
const container = document.getElementById("container");
container.scrollTop = container.scrollHeight;
};
스크롤 위치가 새로 추가된 항목을 고려하지 않은 상태로 계산될 가능성이 있다.
만약 그냥 자동배치에 대해 공부했다면 React 18에 대해서만 공부했을텐데, 17에서 18의 변화과정을 보니 양쪽 버전을 공부할 수 있어 좋은 것 같다.
벌써 React19라니 너무 빨라잉~
무럭무럭자라라 리액트야 ~