본문 바로가기
카테고리 없음

Batching 배치

by 잘먹는 개발자 에단 2025. 3. 18.

리액트는 성능 최적화를 위해서 여러 상태 업데이트를 한번에 처리한다.

이벤트 핸들러나 라이프 사이클 내에서 발생하는 상태 업데이트들을 모아서, 한번의 리렌더링으로 처리하면 불필요한 렌더링 횟수를 줄여준다.

이를 통해서 앱이 더 부드럽게 동작할 수 있다. 

 

 

* 문제점

- 동일한 상태를 여러번 업데이트했을 때, 각 업데이트가 독립적으로 반영되지 않고 배치되어 버리면, 의도한 대로 상태가 변화하지 않을 수 있다. 예를 들어

const handleClick = () => {
  setCount(count + 1); // 현재 count 값 사용
  setCount(count + 1); // 여전히 이전 count 값 사용
};

이런 경우에 count는 +2가 아니라 +1 된다.

 

 

* 그러면 어떻게 이를 해결할 수 있을까? - 함수형 업데이트!

- 함수형 업데이트는 이전 상태값을 인자로 받아서 새 상태를 계산하기 때문에, 배치 되어도 각 업데이트가 순차적으로 반영된다.

const handleClick = () => {
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);
};

이렇게 하면 첫번째 업데이트 후의 새로운 count값이 두번째 업데이트에 반영되어, 원하는 대로 2가 증가하게 된다. 

 

--------------------- 이제 아래부터는 하단의 좋은 글을 참고해서 정리하였습니다. --------------------

https://yozm.wishket.com/magazine/detail/2493/

 

리액트 배칭(Batching)의 모든 것 | 요즘IT

아직까지 많은 리액트 개발자가 오해하고 있는 부분이 있는데요. 바로 자동 배칭의 정확한 개념을 모른다는 점입니다. 이번 글에서는 리액트 ‘배칭(Batching)’에 관한 전반적인 소개와 배칭과

yozm.wishket.com

 

* 대체 이런 배칭이 왜 생긴걸까?

- 개발자가 리액트에 대해서 정확히 어떻게 동작하는지 세세하게 모르더라도, 스스로 좋은 성능을 보장하도록 만들어진 것이다.

 

* 리액트에서 말하는 배칭이 정확히 무엇일까?

- 리액트의 상태 값을 일정한 주기로 처리하는 작업

 

 

 

* 상태 값이 변하는 그 즉시 컴포넌트가 호출되지는 않는다.

- 리액트는 렌더링 주기가 언제 컴포넌트를 다시 호출해야할 지 최적의 시간을 계산한다. 

- 언제 호출하는지는 우리가 정확히 알 수 없다. 

 

 

* 조정 Reconciliation

- 컴포넌트가 다시 호출되면, 변경된 상태 값을 바탕으로 화면에 표시할 돔을 다시 그리게 된다. 

- 이 때 가상돔을 만들면 리액트는 다음 단계로 진입한다.

- 이 단계를 조정단계라고 하는데, 렌더링 단계에서 만든 가상 돔을 현재 UI에 적용된 가장 최신의 가상돔과 비교한다.

- 상태값 변화를 렌더링하고 ( 리액트에 알려주고 ) 조정하는 일은 리액트에서 사용하고 있는 Fiber의 도움을 받아서 처리된다.

 

 

* 적용 Commit

- 가상 돔의 비교가 끝나고 최종 형태의 가상 돔이 만들어지면, 적용 단계로 진입한다.

- 이 때 리액트는 실제 돔에 가상돔을 적용해서, 화면이 변화를 만들어낸다. 

- 이게 완료되면 사용자가 화면상의 변화된 UI를 볼 수 있게 된다. 

 

 

** 그렇다면 이제 배칭을 알아보자. 

- 배칭은 버스에서 여러명이 내려야할 때를 생각하면 이해하기 편하다. 

- 여러명의 승객 ( 여러개의 상태 변화 ) 이 타고 내리는 과정에서 모든 승객이 전부 탑승하거나 내릴 때까지 기다리는 것과 비슷하다. 

- 하나의 승객(상태 변화) 이 내렸다고 그때마다 문을 열거나 멈추면 ( 조정, 적용, 렌더링 ) 언제 출발하나? 버스는 아마 계속 문을 열고 닫아서 결국 목적지에 늦게 도착할 것이다. 

 

* 리액트 17버전까지는 배칭에 문제점이 있었다. 

- 유저 이벤트에 연결된 함수 내에서 직접적으로 변경된 상태값만 배칭의 범위안에 들어왔었다.

- 타이머 setTimeout, 프로미스 Promise, Native Event 등은 배칭의 효과를 누릴 수 없었다. 때문에 성능 최적화와는 거리가 멀었다. 

 

 

예를 들어서 이런 코드가 있다고 쳐보자.

리액트 버전 17에서 돌아간다고 가정한다.

const sleep = t => new Promise(res => setTimeout(res, t);

const handleClick = async() => {
	await sleep(0);
    
    const randomA = getRandomNumber();
    
    setA(randomA);
    setB(MAX - randomA);
}

컴포넌트는 최종적으로 A가 변경될 때 1번, B가 변경될 때 또 1번 호출되어 총 2번에 걸쳐서 호출된다.

이는 성능의 저하를 야기할 수 있다. 

하나의 이벤트 함수에서 배칭 없이 이렇게 변경되는 상태 값이 많아진다면 성능이 저하되는 것이다. 

 

 

이것은 리액트 18버전에서 해결되었다. 

render() 대신에 createRoot()를 사용해서 리액트 루트 컴포넌트를 초기화해주면, 프로미스와 타이머도 같은 주기 내에서 처리될 수 있다.

 

import {StrictMode} from 'react';
import {render} from 'react-dom';

import App from './App';

const rootElement = document.getElementById('root');

render(
	<StrictMode>
    	<App/>
    </StrictMode>, 
    rootElement
);
import {StrictMode} from 'react';
import {render} from 'react-dom';

import App from './App';

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

root.render(
	<StrictMode>
    	<App/>
    </StrictMode>, 
);

 

 

** 마지막으로 정리 **

1. 리액트는 상태 값이 변경되면 상태값을 다루는 컴포넌트를 다시 호출하는 렌더링 과정을 거친다.

2. 이후 컴포넌트가 jsx엘리먼트를 반환하면 그것을 바탕으로 새로운 가상 돔을 생성한다.

3. 이렇게 만들어진 가상돔은 HTML에 적용된 가장 이전 버전의 가상 돔과 비교하게 된다. 이것을 조정 Reconciliation이라고 한다. 

4. 이렇게 최종적으로 적용할 가상 돔을 꾸리면, 적용 commit 단계로 넘어가고 UI가 변화하게 된다. 

 

5. 이러한 과정이 여러개라면 리액트는 여러 상태 값을 변경할 때 되도록 한번에 다 같이 적용될 수 있게 하는 기능을 갖고 있고 이를 배칭이라고 한다.