본문 바로가기

프로그래밍 공부내용/리액트(React)

Batching에 관하여(리액트 setState 비동기 묶지 않고 따로따로 처리 하기)

리액트를 처음 배울때 당황했던 부분이 있다.

 

간단한 계산기를 만드는 미션이었는데, setCount를 하는데 잘 동작하지 않았다.

 

아래 예시코드를 보아라

 

 

## batching이란?

export default function App() {
  const [count, setCount] = useState(0);

  const onClick = () => {
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
  };

  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onClick}>올리기 버튼</button>
    </div>
  );
}

이런식으로 작성했더니, count 가 1씩 증가하지 않고 한번에 3씩 증가했다.

 

내가 보고 싶었던건 1,2,3 이 다르륵 하고 올라가는 거였는데 실제로 보니 바로 3이 찍혀있었다.

 

혹시나 너무 빠른속도로 rendering 된 것은 아닐까 하고 살펴봤지만 아니었다.

 

어째서 이런 일이 발생할까?

 

이것이 리액트의 batching이다.

 

batching이란 말을 찾아보면 '모임' 이라는 뜻이 있다.

 

말 그대로 state의 변화를 한번에 묶어서 처리하는 것을 batching이라고 한다.

 

 

## 근데 이렇게 불편한 batching이 왜 있을까?

 

대기업의 유수한 석박들과, 대단한 사람들(댄 아브라모와 같은..) 이 bathcing을 만든 이유가 있을 것이다.

 

우리를 불편하게 해서 엿먹이려고 만들었을리는 만무하다.

 

batching은 렌더링 최적화를 위해서 존재한다.

 

"Batching is when React groups multiple state updates into a single re-render for better performance."

 

위 예시처럼 단 기간에 일어나는 상태변화를 매번 렌더링 시키지 않고, 일괄 처리함으로써 성능을 최적화한다.

 

setState가 비동기로 동작하는 이유가 이 때문이다.

 

state를 모아서 처리하기 위한 내부적인 로직을 가지고 있고 스케쥴링을 해서 state를 한번에 처리한다.

 

 

## batching이 발생하는 조건

 

그렇다면 어떤 조건에서 batching이 동시에 일어나게 될까?

 

단순히 시간으로 쪼개는 것은 좀 이상할 것 같다.

 

리액트 공식문서에 힌트가 있다.

 

리액트 공식문서 링크

이 링크를 따라가 보면 알 수 있다.

 

"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:"

 

리액트 18전에는, react event handler에서만 batching했지만

아마 Component에 다는 event를 말하는 것 같다. 예를들면 <button onClick={} /> 에서의 onClick

 

리액트 18 이후부터는 react event handler, promises, setTimeout, native event handler 에서도 batching을 한다.

 

 

 

## batching이 안좋기만 한 것은 아니다?!

 

batching 자체가 이미 성능 최적화를 해 주기도 하지만, 사용자 입장에서도 개선할 수 있다.

 

export default function App() {
  const [count, setCount] = useState(0);
  const [isCountFive, setIsCountFive] = useState(false);

  const onClick = () => {
    setCount((prev) => prev + 1);
  };

  useEffect(() => {
    if (count === 5) {
      setIsCountFive(true);
    }
  }, [count]);

  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onClick}>올리기 버튼</button>
    </div>
  );
}

라고 할 것을

 

export default function App() {
  const [count, setCount] = useState(0);
  const [isCountFive, setIsCountFive] = useState(false);

  const onClick = () => {
    setCount((prev) => prev + 1);
    if (count === 5) {
      setIsCountFive(true);
    }
  };

  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onClick}>올리기 버튼</button>
    </div>
  );
}

이렇게 쓸 수 있다.

 

 

 

## 만약 batching을 해결하고 싶을 때

 

만약 이 글을 읽는다면 이런 의문이 들 수 있다.

 

그래서 batching이 안일어나게 하고 싶으면 어떻게 해야하는데?

 

물론 여러가지 잔기술로 극복할 수 있겠지만(react event 에서는 각각 따로 batching 됐던 것을 기억하자)

 

react 18 부터는 batching에 좀 더 진심이 됐는지 새 기능이 추가 됐다.

 

flush sync 라는 녀석을 사용하면 batching을 해결할 수 있다.

export default function App() {
  const [count, setCount] = useState(0);

  function onClick() {
    flushSync(() => {
      setCount((prev) => prev + 1);
    });
    // React has updated the DOM by now
    flushSync(() => {
      setCount((prev) => prev + 1);
    });
    // React has updated the DOM by now
  }

  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onClick}>올리기 버튼</button>
    </div>
  );
}

이렇게 하면 batching을 쪼개서 할 수 있다.

 

setCount는 한번에 모아서 처리되지 않고 2번 나눠서 처리 된다.

(다만 이 예시에서는 너무 빨라서 어차피 1번에 처리 되는 것처럼 보이겠지만...)

 

 

## bathcing deep dive

https://github.com/reactwg/react-18/discussions/21

https://ko.reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html