본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -Queueing a Series of State Updates(여러 state 업데이트를 큐에 담기)

state 변수를 설정하면 다음 렌더링이 큐(대기열, queue)에 들어가는데, 경우에 따라 다음 렌더링을 큐에 넣기 전에 값에 대한 여러 작업을 수행해야 하는 경우가 있다. 이를 위해서 React는 state 업데이트를 어떻게 배치하면 좋을지 이해해보자.


React batches state updates(state 업데이트 일괄처리)

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>
    </>
  )
}

 

위 예제에서 각 렌더링의 state 값은 고정되어 있으므로, 첫번째 렌더링의 이벤트 핸들러의 number 값은 setNumber(1)을 몇번 호출하든 항상 0이 된다. React는 state를 업데이트 하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다리기 때문에 리렌더링은 모든 setNumber() 호출이 완료된 이후에만 일어난다.

 

그렇기 때문에 너무 많은 리렌더링을 촉발하지 않고도 여러 컴포넌트에서 나온 다수의 state 변수를 업데이트할 수 있지만, 이벤트 핸들러와 그 안에 있는 코드가 완료될 때까지 UI가 업데이트되지 않는다는 의미이기도 하다.

 

따라서, 일괄처리(배칭, batching)라고도 하는 이 동작은 React 앱을 훨씬 바르게 실행할 수 있게 해주고 일부 변수만 업데이트된 "반쯤 완성된" 혼란스러운 렌더링을 처리하지 않아도 된다.

 

React는 클릭과 같은 여러 의도적인 이벤트에 대해 일괄 처리하지 않으며, 각 클릭은 개별적으로 처리된다. 일괄처리는 React가 안전한 경우에만 수행한다.

 

* 위 예제 코드에서 리렌더링은 실제로 1번만 일어난다(?)

setNumber 함수는 상태를 업데이트하고 컴포넌트를 다시 렌더링하는 비동기적인 함수입니다. 여러 번 호출되더라도 최종적으로는 하나의 렌더링만 발생하게 됩니다. React는 여러 번의 상태 업데이트를 최적화하여 하나의 렌더링으로 처리합니다. 따라서 버튼을 누르면 setNumber를 세 번 호출하게 되지만, 리액트는 배치 업데이트(batch update)라는 개념을 사용하여 연속적으로 호출된 setNumber 함수들을 한 번에 처리합니다. 따라서 실제로는 리렌더링이 한 번만 발생하게 됩니다.

 

React에서 setState를 사용하여 상태를 업데이트할 경우, 업데이트된 상태는 비동기적 특성 때문에 즉시 반영되지 않는다고 한다. 리랜더링된 후에야 업데이트된 state가 반영된다. 또한 리액트에서는 효율적으로 렌더링 하기위해서 여러 번의 상태값 변경 요청을 배치로 처리한다.
React는 상태 값을 업데이트 할 때 모든 요청에 따라 바로 리랜더링이 되는것이 아니라 변경 사항을 모아서 한번에 일괄 처리(batch update / 배치로 처리)를 한다. 이렇게 일괄적인 업데이트를 통해서 컴포넌트의 렌더링 횟수를 최소화하여 불필요한 렌더링을 방지한다고 한다.


Updating the same state multiple times before the next render(다음 렌더링 전에 동일한 state 변수 여러 번 업데이트 하기)

 

만약 위의 예제에서 버튼 한번의 클릭으로 +3 이라는 결과를 만들고 싶다면, 업데이터 함수(updater function)을 사용할 수 있다.

import { useState } from 'react';

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

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

한번의 클릭으로 +3 이라는 결과를 만들고 싶다는 의미는 다음 렌더링 전에 동일한 state 변수를 여러 번 업데이트 하고 싶다는 의미이다. 이때 업데이터 함수(n => n + 1)와 같이 큐의 이전 state를 기반으로 다음 state를 계산하는 방법을 사용할 수 있다.

 

다음 렌더링 중에 useState를 호출하면 React는 큐를 순회하고, 이전 number state가 0이었으므로 React는 이를 첫번째 업데이터 함수에 n 인수로 전달한다. 그리고 업데이터 함수의 반환값을 다음 업데이터 함수에 n으로 전달한다.

queued update n returns
n => n + 1 0 0 + 1 = 1
n => n + 1 1 1 + 1 = 2
n => n + 1 2 2 + 1 = 3

What happens if you update state after replacing it (state를 교체한 후 업데이트하면 어떻게 될까요?)

import { useState } from 'react';

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

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
      }}>Increase the number</button>
    </>
  )
}

 

위 예제코드의 이벤트 핸들러를 실행할 경우 결과는 어떻게 될까?

 

화면에 출력되는 number 값은 6이 된다. 이벤트 핸들러는 React에게 다음과 같은 작업을 지시한다.

 

1. setNumber(number + 5) : number에 +5를 한다. React는 큐에 "5"로 바꾸기를 추가한다.

2. setNumber(n => n + 1) : n => n + 1는 업데이터 함수이기 때문에, React는 해당 함수를 큐에 추가한다.

queued update n returns
"replace with 5" 0 (unused) 5
n => n + 1 5 5 + 1 = 6

What happens if you replace state after updating it(업데이트 후 state를 바꾸면 어떻게 될까요)

import { useState } from 'react';

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

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
        setNumber(42);
      }}>Increase the number</button>
    </>
  )
}

위 예제 코드에서 버튼을 클릭했을 때, number는 어떻게 될까?

 

화면에 출력되는 number 값은 42가 된다. 이벤트 핸들러는 React에게 다음과 같은 작업을 지시한다.

 

1. setNumber(number + 5) : number에 +5를 한다. React는 큐에 "5"로 바꾸기를 추가한다.

2. setNumber(n => n + 1) : n => n + 1는 업데이터 함수이기 때문에, React는 해당 함수를 큐에 추가한다.

3. setNumber(42) : React는 "42로 바꾸기"를 큐에 추가합니다.

queued update n returns
"replace with 5" 0 (unused) 5
n => n + 1 5 5 + 1 = 6
"replace with 42" 6 (unused) 42

 

* 이벤트 핸들러가 완료되면 React는 리렌더링을 실행하고 리렌더링 하는 동안 React는 큐를 처리한다. 업데이터 함수는 리렌더링 중에 실행되므로, 업데이터 함수는 순수해야 하며 결과만을 반환해야 한다.

 

- 관련 내용 chatGPT 답변

* 클릭 이벤트가 발생하면 setState 함수가 차례대로 큐에 들어가고 리액트는 이벤트 핸들러가 실행되는 동안 상태를 변경해도 바로 화면을 다시 그리지 않고, 상태 변경 요청을 큐에 저장한다. 그리고 이후 리렌더링이 발생할 때 큐에 있는 setState 호출을 비동기적으로 처리하면서 상태를 업데이트하고 화면을 다시 그린다. 이렇게 함으로써 여러 번의 상태 변경이 있더라도 한 번의 렌더링만 발생하여 성능을 최적화할 수 있다.

 

만약 동일한 렌더링 주기에서 여러 개의 setState 호출이 있더라도 리액트는 이들을 하나로 합쳐서 최종적으로 하나의 업데이트만 처리한다. 이러한 동작 방식으로 인해 성능 향상과 불필요한 렌더링을 줄일 수 있다. 단, 이런 특성을 이용하여 상태를 업데이트할 때 이전 상태를 기반으로 하는 경우에는 주의해야한다. 상태 업데이트가 비동기적으로 처리되기 때문에 여러 번의 setState 호출이 연속적으로 이루어진다면 예상치 못한 결과가 발생할 수 있다. 이러한 경우에는 함수형 업데이트나 useReducer를 사용하여 상태를 업데이트하는 것이 안전하다.

 

setState 함수의 실행은 실행 컨텍스트 스택을 통해 이루어집니다. 컴포넌트에서 setState 함수를 호출하면 해당 컴포넌트의 실행 컨텍스트가 스택에 쌓이고, setState 함수의 코드가 순차적으로 실행됩니다.

 

setState 함수는 상태를 변경하는 것이 아니라, 상태 변경을 요청하는 것입니다. 따라서 setState 함수의 실행이 끝나면, 실제 상태 변경은 큐에 들어가게 됩니다.

 

리액트는 상태 변경 요청을 큐에 쌓아두고, 이후에 리렌더링이 필요한 시점에서 큐에 있는 상태 변경들을 일괄적으로 처리합니다. 이때 상태 변경은 이벤트 루프와 함께 비동기적으로 처리됩니다. 즉, setState 함수가 호출된 직후에는 상태가 즉시 변경되는 것이 아니라, 해당 상태 변경은 이벤트 루프를 통해 이루어지고 큐에 대기하게 됩니다.

 

이후 리액트는 이벤트 루프를 통해 큐에 있는 상태 변경들을 순차적으로 처리하고, 최종적으로 한 번의 렌더링을 수행합니다. 이렇게 함으로써 여러번의 setState가 호출이 발생해도, 최종적으로는 한 번의 렌더링만이 발생하여 성능을 최적화할 수 있습니다.

 

따라서 setState 함수의 실행은 스택을 통해 이루어지고, 상태 변경은 큐를 통해 일어나며 이벤트 루프와 함께 비동기적으로 처리됩니다.


* 참고 : React 공식문서(https://react-ko.dev/learn)

          https://velog.io/@isabel_noh/React-useState-%EB%B9%84%EB%8F%99%EA%B8%B0-batch-update

          https://immigration9.github.io/react/2021/06/12/automatic-batching-react.html