React 18 릴리즈를 살펴보며 Automatic Batching에 대해 탐구해보고 실험한 것을 정리한 글입니다.
Batching이란 무엇일까?
React 공식문서 번역 ( https://reactjs.org/blog/2022/03/29/react-v18.html )
Batching이란 React가 더 나은 성능을 위해 여러개의 state 업데이트를 한 번의 리렌더링으로 묶어서 진행하는 것을 말한다.
Automatic batching이 없다면 React의 이벤트 핸들러 내부에서만 batched 업데이트가 가능했다.
기존에는 Promise의 내부나 setTimeout 혹은 native 이벤트 핸들러에서는 React에 의해 배칭 되지 않았다.
Batching은 어떤 이점을 주는가?
당연히 성능적인 이점이다.
여러개의 state 업데이트마다 발생하는 불필요한 리렌더링을 막아주기 때문이다.
레스토랑에서 종업원이 손님이 주문하는 첫번째 메뉴를 듣자마자 주방으로 달려가지 않듯이
Batching은 반드시 필요한 하나의 리렌더링을 수행한다.
그럼 Batching이 일어나지 않을 경우 어떤 문제가 생길 수 있을까?
여러개의 state 업데이트가 동시에 일어나지 않을 때 발생할 수 있는 일을 생각해보자.
이와 관련해 다음과 같은 예제를 한 번 가지고 와봤다.
줄다리기는 균형이 중요하다
한쪽 진영의 힘이 더 세지면 균형이 무너지기 마련이다.
이러한 점에서 teamA와 teamB 라는 state를 통해서 각 진영의 힘의 크기를 나타냈다.
const [teamA, setTeamA] = useState(10);
const [teamB, setTeamB] = useState(10);
또한 힘의 균형이 무너지면 게임이 종료됨을 표현하기 위해 다음과 같이 useEffect를 정의했다.
useEffect(() => {
if (teamA !== teamB) navigator("/end");
}, [teamA, teamB]);
한 명 줄이기라는 버튼을 누르면 teamA와 teamB 각각의 값을 1씩 줄일 것이다.
React의 이벤트 핸들러에서는 auto batching을 지원하기에 Promise의 콜백함수 안에 이 동작을 정의해서 batching이 발생하지 않도록 해보겠다.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const decrease = async () => {
await delay(1500);
setTeamA((a) => a - 1);
setTeamB((b) => b - 1);
};
<button onClick={decrease}>한명 줄이기</button>
우리가 기대한 실행결과는 1500ms 이후에
teamA와 teamB의 값이 1씩 감소한 다음과 같은 모습일 것이다.
하지만 실제 결과는 다음과 같이 게임이 종료되어버린다.
왜 이런 결과가 나왔을까?
리렌더링 흐름
- Auto Batching이 적용되지 않았기 때문에 setTeamA와 setTeamB 호출마다
리렌더링을 유발하게 된다. - 첫 번째 setTeamA 호출에 의해 teamA의 값이 먼저 변경된다. 10 -> 9
- useEffect 의존성 배열에 teamA가 포함되어있기 때문에 useEffect의 콜백함수가 실행된다.
- 아직 teamB의 값이 변경된 상태가 아니기 때문에 teamA !== teamB (9 !== 10) 로
게임이 종료된다.
Auto Batching 이 적용되지 않는다면 이러한 상황처럼 의도치 않은 버그를 마주할 수도 있다.
React에서는 이러한 상태를 컴포넌트의 업데이트가 "half-finished" 된 상태라고 정의한다.
우리가 의도한 로직 중 절반만 완성된 state에 의해 컴포넌트가 올바르게 동작하지 않는 상태를 말한다.
React의 Batching에 대해서 알지 못한 상태로 위와 같은 버그를 마주친다면 나는 아마 해결하지 못했을 것 같다.
그래서? React 18에서는...
위에서의 장황한 상황 설명과 이론들은 결국 React 18에서는 신경 쓰지 않아도 되는 부분이 되었다.
Automatic Batching 이 도입되었기 때문이다.
React 18에서는 timeout이던, promise던, native 이벤트 핸들러던
업데이트가 어느 곳에서 야기되었는지와 관계없이 Batching을 지원한다.
실제로 사용해보자.
Automatic Batching을 사용하기 위해서는 React 18에서 새롭게 등장한 ReactDOMClient의 createRoot 메서드를 사용해야 한다.
기존의 React의 entry 파일에서는 다음과 같이 정의했었다.
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
React 18부터는 createRoot를 사용해 다음과 같이 정의하면 된다.
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
결과는?
최종적으로 React 18에서는 Promise의 콜백함수 내부에서도 Batching이 잘 적용되는 모습을 확인할 수 있다!
예제가 조금 유치할 수도 있으나 재밌게 봐주셨으면 좋겠다 : )
틀린 부분이 있다면 언제든 지적해주세요.
'유연해지기 > React.js' 카테고리의 다른 글
간단하게 React 리렌더링 최적화하기 (3) | 2022.05.29 |
---|---|
React에서 Intersection Observer로 이미지 Lazyloading 구현하기 with 콜백 ref (4) | 2022.03.30 |
리액트에서 이미지 미리보기 만들어보기 (React Image Preview) (1) | 2021.11.14 |
간단한 리액트 커스텀 훅 만들어보기. (React custom hook) (0) | 2021.11.04 |
리액트 절대경로 설정 및 모듈/경로 별명 짓기 with CRA (absolute path, path alias) (0) | 2021.10.21 |