setState를 많이 실행시켜도 리렌더링이 한번만 일어나는 이유는 무엇인가?
React를 사용하다 보면 필연적으로 state를 다룰 수밖에 없게 되고, 이를 사용하다 보면 문득 드는 한 가지 의문이 존재한다. 분명 setState 함수는 리렌더링을 유발한다고 했는데 왜 여러번 실행을 해도 한 번만 리렌더링이 진행되는 걸까?
지금은 React 에서 이를 Batching 처리하여 일괄적으로 처리함을 알고 있었지만, React 18에서 소개하는 Automatic Batching은 기존의 Batching 작업을 개선했다고 말하길래 어떤 부분을 개선했는지가 굉장히 궁금하였다. 따라서 공식 문서와 React의 메인테이너 분께서 작성한 글을 토대로 React 에서는 state update 작업을 어떻게 처리하는지 파헤쳐보고자 한다.
state
값이 변경되었을 경우 React 에서는 해당 컴포넌트를 리렌더링 하며, 불필요한 리렌더링을 방지하기 위해 state를 변경하는 작업을 일괄적으로 처리한다.state
의 업데이트 작업을 모아 일괄 처리하는 방식을 Batching 이라고 하며, 이 덕에 React 에서는 불필요한 리렌더링을 방지할 수 있게 되었다.import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function increaseCountThree() {
// 아래의 작업은 모두 일괄적으로 묶여 처리된다. 한 번의 리렌더링만 발생한다.
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}
return (
<div>
<button onClick={increaseCountThree}>+1</button>
<p>Count : {count}</p>
</div>
);
}
export default Counter;
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 17 이전의 버전에서는 해당 작업을 Batching 처리하지 않는다.
// 왜냐하면 해당 작업은 이벤트가 종료된 이후 (100ms 뒤) 에 실행되기 때문이다.
setCount((c) => c + 1); // 리렌더링 유발
setFlag((f) => !f); // 리렌더링 유발
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
function fetchSomething() {
return new Promise((resolve) => setTimeout(resolve, 100));
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
ReactDOM.createRoot
메서드를 기반으로 렌더링을 진행할 경우 모든 state update 작업은 자동으로 Batching 처리된다. 이 기능을 Automatic Batching 이라고 한다.function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 18 이후에서는 해당 작업을 Batching 처리 한다.
setCount((c) => c + 1);
setFlag((f) => !f);
// React 는 해당 작업을 일괄 처리하여 한 번의 리렌더링만 진행한다.
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
function fetchSomething() {
return new Promise((resolve) => setTimeout(resolve, 100));
}
const rootElement = document.getElementById("root");
// React 18 에서 새롭게 제공하는 createRoot 메서드를 사용해야 한다!
ReactDOM.createRoot(rootElement).render(<App />);
import { useLayoutEffect, useRef, useState } from 'react';
function App() {
const [test, setTest] = useState(1);
const testRef = useRef<any>(0);
const trigger = () => setTest((prev) => {
console.log(prev + 1);
return prev + 1;
});
const changeState = async () => {
if (testRef.current) {
clearTimeout(testRef.current);
testRef.current = null;
}
// 요 안에서 한 개의 update function 은 하나로 batching 됨.
trigger();
trigger();
// 1.5초 후에는 4개의 update function 을 하나로 batching 시킴.
setTimeout(() => {
trigger();
trigger();
}, 1500);
setTimeout(() => {
trigger();
trigger();
}, 1500);
testRef.current = setTimeout(() => {
// 1초 후에는 두 개의 update function 을 하나로 batching 시킴
trigger();
trigger();
}, 1000);
return;
}
useLayoutEffect(() => {
console.log('render');
})
return (<div>
<button onClick={changeState}>change it</button>
<p> {test}</p>
</div>);
}
export default App;
ReactDOM.flushSync()
메서드는 Auto Batching 을 무시하고 즉시 DOM을 렌더링해준다.import { flushSync } from "react-dom";
function handleClick() {
// React 는 flushSync 메서드가 실행되는 즉시 DOM을 업데이트 한다.
flushSync(() => {
setCounter((c) => c + 1);
});
// React 는 flushSync 메서드가 실행되는 즉시 DOM을 업데이트 한다.
flushSync(() => {
setFlag((f) => !f);
});
// 따라서 해당 함수가 실행될 경우 React는 총 두 번의 리렌더링을 수행한다.
}