본문 바로가기

React

useState의 렌더링 방식 이해하기(feat.batching)

useState란?

사용자와의 상호작용을 통해 값이 변경되는 데이터들이 있다.

예를 들어, 버튼을 누르면 화면의 카운트 숫자가 1씩 증가한다거나, 특정 필드의 숨김이 on/off 된다거나…

 

그러나 만약 x=1, a='apple' 처럼 변수에 값을 직접 할당하는 방식으로 값을 업데이트한다면,

화면은 어떠한 변화도 없을 것이다. 

내부적으로 데이터의 값은 변경됐더라도 그뿐, 컴포넌트가 재렌더링이 되진 않기 때문이다.

 

만일 재렌더링이 된다 치더라도 기존의 값이 초기화되면서 제대로 값의 변경을 반영할 수 없다.

버튼을 누를때마다 카운트 숫자가 1씩 증가하려해도 이전 값을 기억못하면 올바르게 반영되지 않을 것이다. 

이벤트 함수가 실행되더라도 컴포넌트 렌더링을 호출하지도 못할 뿐더러,

렌더링되더라도 다시 초반과 똑같이 변수가 정의되고, 초기값을 할당하고, 함수를 정의하고… 다시 원상태로 돌아오게 된다.

따라서 이러한 사용자와의 상호작용을 통해 값이 변경되는 데이터들은

useState를 통해 값을 정의한다.

 

useState 훅은 두 가지를 제공한다.

컴포넌트가 렌더링될때 데이터의 직전 값을 기억할 수 있는 state variable과,

컴포넌트 렌더링을 호출하는 state setter function

const [index, setIndex] = useState(0);

 

useState
⇒ 데이터의 직전 값을 기억(state)하며 컴포넌트를 렌더링(state setter function)해
데이터의 업데이트를 반영하는 React Hook

 


useState의 렌더링 이해하기

 

useState의 렌더링은

Trigger, Render, Commit의 세 가지 단계로 나뉠 수 있다.

React 공식문서에선 다음과 같이 설명한다.

  1. Triggering a render → 부엌에 주문을 전달한다.
  2. Rendering the component → 부엌에선 주문에 맞춰 요리를 한다.
  3. Committing to the DOM → 테이블 위에 완성된 요리를 세팅한다.

예시를 보며 생각해보자.

export default function Form() {
  const [isSent, setIsSent] = useState(false);
  const [message, setMessage] = useState('Hi!');
  if (isSent) {
    return <h1>Your message is on its way!</h1>
  }
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      setIsSent(true);
      sendMessage(message);
    }}>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

function sendMessage(message) {
  // ...
}

 

Send 버튼을 클릭하며 onSubmit이 이벤트를 실행하면

setIsSent가 isSent를 true로 set하고,

“Your message is on its way!”를 렌더링할 것이다.

이 과정을 단계별로 세분화해서 살펴보면,

  1. onSubmit이 이벤트 실행
  2. setIsSent가 isSent를 true로 set & 렌더링 호출
  3. ‘isSent는 true’라는 state를 기억하며, 리렌더링
  4. DOM에 commit

여기서 중요한 것은,

렌더링 시, 그 시점에서의 state를 사용한다는 것이다.

 

When React calls your component, it gives you a snapshot of the state for that particular render. Your component returns a snapshot of the UI with a fresh set of props and event handlers in its JSX, all calculated using the state values from that render!




Batching

 

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

 

위 코드에서

‘+3’ 버튼을 누르면 숫자가 얼마씩 증가할까?

답은 ‘1씩 증가’이다.

 

상태업데이트는 큐(queue)를 통해 이뤄지는데,

React는 상태업데이트를 일괄 처리(batching)한다는 특성때문에

바로 상태 업데이트를 처리하지 않고, 우선 number + 1을 큐에 쌓아놓는다.

그리고 렌더링 시점에, 상태 업데이트를 한번에 처리한다.

 

따라서

  • 첫 번째 렌더링

setNumber(0 + 1) → setNumber(1)

setNumber(0 + 1) → setNumber(1)

setNumber(0 + 1) → setNumber(1)

 

setNumber를 3번 호출하지만, 렌더링 시점에서 number는 0이기 때문에, number를 1로 3번 set하는 것과 같다.

  • 두 번째 렌더링

setNumber(1 + 1) → setNumber(2)

setNumber(1 + 1) → setNumber(2)

setNumber(1 + 1) → setNumber(2)

 

렌더링 도중에 사용하고 있는 state 값은 절대 변하지 않는다. 

batching:
 React는 상태 업데이트를 바로 처리하지 않고, 이벤트 핸들러 코드가 모두 실행된 후에 한 번에 처리한다. 
상태 업데이트 일괄 처리

 


업데이트 함수를 통해 해결하기

 

그렇다면 어떻게 3씩 증가하도록 처리할 수 있을까?

다음과 같은 업데이트 “함수”를 이용해 가능하다.

<button onClick={() => {
  setNumber(n => n+1);
  setNumber(n => n+1);
  setNumber(n => n+1);
}}>+3</button>

 

일괄처리한다는 특성 때문에

n ⇒ n+1을 큐에 쌓아놓을 것이다.

그리고 렌더링 시점에서

  • 첫 번째 렌더링

setNumber(n => n + 1) → 0+1=1 반환

setNumber(n => n + 1) → 1+1=2 반환

setNumber( n => n + 1 ) → 2+1=3 반환

'React' 카테고리의 다른 글

(번역) {상태 전이} = f(상태)  (0) 2025.04.15
리액트 서버 컴포넌트 톺아보기 (번역)  (0) 2025.04.03
Tanstack-Query(React-Query)  (0) 2025.02.05
Redux 정리  (0) 2025.01.21
state를 reducer로 작성하기  (0) 2025.01.20