탭을 선택하면 탭 내용에 애니메이션 효과를 주기위해 classname을 지웠다가 다시 보이도록 코드를 짜고 싶었다. 그런데 classname은 원하는대로 변경이 되지 않아서 살펴보니 Atomatic Batching 때문이었다.
let [fade, setFade] = useState('');
useEffect(() => {
setFade(''); // fade 값을 제거했다가
setFade('end'); // end 추가
}, [tabNum]) // tabNum 값이 변하면 useEffect 실행
return <div className={"start " + fade}>
간단하게 설명하면, state 변경 함수들이 근처에 모여 있으면 리렌더링을 딱 한번만 해준다는 것이다.
그러면 위의 코드에서 setFade 함수가 2번 쓰여도 각각 리렌더링이 이루어지지 않기때문에 setFade('end')만 실행되었다고 볼 수 있다.
그렇다면 React는 왜 Batching이 되도록 한걸까?
React Dev 문서를 살펴보니,
Automatic Batching
React 18 adds out-of-the-box performance improvements by doing more batching by default. Batching is when React groups multiple state updates into a single re-render for better performance.
어차피 다음 코드에서 또 리렌더링을 해야하는데 리렌더링 횟수를 줄이면 더 좋은 성능을 제공할 수 있다는 것이다.
문서를 더 살펴보면 이전 버전에도 Batching이 있었는데 이벤트 헨들러에만 적용되었고, 18버전부터는 Promises, setTimeout 등 다른 이벤트에도 적용되도록 업데이트를 했다고 한다.
Before React 18, we only batched updates inside React event handlers. Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default:
React에서는 원할 때, 리렌더링이 되도록 제공해주는 기능이 있는데 바로 flushSync 이다.
간단하게 flushSync 사용방법을 알아보자.
import { flushSync } from 'react-dom'; // 그냥 react 아님!
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React has updated the DOM by now
flushSync(() => {
setFlag(f => !f);
});
// React has updated the DOM by now
}
useEffect 안에 flushSync를 냅다 사용했는데 아래와 같은 에러가 발생했다..
생각해보면 useEffect hook은 렌더링이 끝난 이후에 실행되는 곳인데..
여기다가 flushSync를 사용하면, useEffect 실행 중에 '렌더링 한번 해줘'라고 생떼쓰는 것이다.

참고
https://react.dev/blog/2022/03/08/react-18-upgrade-guide#automatic-batching