질문과 피드백은 언제나 환영!

댓글로 남겨주세요:)

React

[React] state가 동작하는 방식 / batching / 업데이트 큐

깅강이 2024. 2. 15. 14:20

글의 목적 

state를 공부하다 보니 비동기처리 시 state가 어떻게 동작하는지가 궁금해졌다. 

따라서 React가 state를 처리하는 과정을 알아보게 되었다. 

이 글은 모두 공식 문서에 명시된 내용만 참고해 적었다.  

 


State는 스냅샷

리액트를 공부 중이라면 state를 바꾸면 컴포넌트가 리렌더링이 된다는 것을 알고 있을 것이다. 

이때 렌더링의 개념을 이해하는 것이 중요하다. 

 

렌더링이란, react가 컴포넌트를 호출한다는 뜻이다. 이 컴포넌트가 반환하는 것은 당시의 화면 스냅숏이다.

쉽게 생각해 당시의 상태를 사진으로 찍어둔다고 이해하면 된다. 렌더링시의 변수 등의 값이다. 

prop, 이벤트 핸들러, 로컬 변수는 모두 렌더링 당시의 state를 이용해서 계산된다. 

 

1. react가 함수를 다시 호출 ( 리렌더링 )

2. 함수가 새로운 JSX 스냅숏( 변수등의 값 ) 반환

3. React가 함수가 반환한 스냅샷과 일치하도록 화면 업데이트 

 

https://react.dev/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time

 

 

같은 맥락에서 state의 변경은 위의 과정으로 이루어진다. 이벤트 핸들러로 state를 변경하는 경우로 이해해 보자. 

 

1. 우리가 react에게 state를 업데이트하도록 명령. 보통은 이벤트를 발생시킴

2. 이벤트 핸들러 안의 내용 모두 동작 완료. react가 state 업데이트

3. react가 state값의 스냅샷을 컴포넌트에 보내줌

 

 

이때 중요한 것은 state는 이벤트 발생 시점의 스냅샷을 기준으로 계산된다는 점이다. 

즉 state에 대한 각 문장은 서로를 인식하지 못하고

이벤트 발생 당시의 state의 스냅샷 값

다음 렌더링 때 state의 스냅샷이 어떻게 될 지만 고려한다. 

 

지금까지의 내용이 이해가 어려워도 괜찮다. 다음의 예시를 보며 이해해보자.

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

//현재 number가 0이라고 가정

 

 

따라서 이 코드에서 핸들러가 하는 일은 다음과 같다. 

 

setNumber (number + 1); : 현재 number는 0이군. 다음 스냅샷 보니 number가 1이니까 다음 렌더링에서 number를 0 + 1로 변경해야지! 

setNumber (number + 1); : 현재 number는 0이군. 다음 스냅샷 보니 number가 1이니까 다음 렌더링에서 number를 0 +1로 변경해야지! 

setNumber (number + 1); : 현재 number는 0이군. 다음 스냅샷 보니 number가 1이니까  다음 렌더링에서 number를 0 +1로 변경해야지! 

 

 

다음 렌더링에서도 마찬가지로 다음과 같이 일한다. 

 

setNumber (number + 1); : 현재 number는 1이군. 다음 렌더링에서 number를 1 + 1 로 변경해야지! 

setNumber (number + 1); : 현재 number는 1이군. 다음 렌더링에서 number를 1 +1 로 변경해야지! 

setNumber (number + 1); : 현재 number는 1이군. 다음 렌더링에서 number를 1 +1 로 변경해야지! 

 

즉 onClick 이벤트가 실행되는 중간에는 스냅샷이 업데이트 되지 않는다.


 

이제 나의 궁금증을 해결해 보자. 다음의 코드는 어떻게 동작할까?

 

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

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>
    </>
  )
}

 

 

setNumber에서 number를 5로 변경하고

3초 후(컴포넌트 렌더링 후)에 number를 aleart 하라고 했으니  5가 출력되지 않을까? 

 

 이때 중요한 것은 state는 이벤트 발생 시점의 스냅샷을 기준으로
계산된다는 점이다. 


 

위의 이 문장을 잘 이해했다면 당연히 0이 출력된다는 답을 할 수 있다. 

 

위 코드의 동작 과정은 다음과 같다.  

클릭 시 스냅샷: state가 0

 

setNumber ( number + 5 ); : 현재 number는 0이군. 다음 렌더링에서 number를 0 + 5로 바꿔야지!

setTimeout() ~ : 현재 number는 0이군. 다음 렌더링에서 3초 후에 number (0)를 alert 해야지! 

 

 


 

 

여기까지 이해했다면 다음의 내용들은 당연하게 받아들여진다.

state batching 

 

batching은 한 마디로 '모았다가 한 번에 처리'이다.

위의 코드들에서 봤지만 React는 state를 업데이트하기 전에 이벤트 핸들러의 모든 코드들이 실행될 때까지 기다린다. 

setNumber 하나가 호출됐을 때 바로 state를 변경하는 것이 아니라

setNumber 3개가 모두 실행되고 나서 state를 변경했다. 

 

https://react-ko.dev/learn/queueing-a-series-of-state-updates

 

이 그림을 보면 이해가 쉽다.

결국 react는 식당의 웨이터처럼 주문이 끝날 때까지 기다렸다가

손님이 최종으로 원하는 메뉴를 갖다 준다. 

 

 

batching을 하는 이유는 리렌더링을 주문을 받을 때마다,

즉 이벤트 핸들러 내부의 코드 한 줄 실행할 때마다 하는 것에 비해 효율적이며

여러 컴포넌트에서 나온 여러 개의 state를 한 번에 업데이트할 수 있기 때문이다. 

(한 state가 업데이트된 상황에 어떤 state는 중간정도 업데이트 된 상태에서 리렌더링 되는 불상사를 막을 수 있다. )

 

 


 

 

그렇다면 다음 렌더링 전에 state를 여러 번 업데이트하고 싶다면 어떻게 할까? 

결론부터 말하자면 

setNumber ( number + 1 ) 대신 setNumber ( number  =>  number+1 )로 사용하면 된다. 

setNumber(number => number +1)의 의미는 단순히 state를 대체하라는 것이 아니라

state값을 가지고 무언가 동작을 해! 라는 뜻이다. 

이때 number => number +1과 같은 setter의 콜백 함수를 업데이터 함수라고 한다. 

어렵다면 코드를 보고 이해해 보자.

 

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

 

 

이 코드의 동작 과정은 다음과 같다. 

 

이벤트가 발생하기 전 react는 이벤트 핸들러 내부의 다른 코드가 모두 실행된 후

이 함수들이 처리되도록

아래의 사진과 같이 queue에 넣는다. 

 

1. setNumber( n => n+1 ) : setter의 인자로 함수가 들어왔네? queue에 n => n + 1 넣자!

2. setNumber( n => n+1 ) : setter의 인자로 함수가 들어왔네? queue에 n => n + 1 넣자!

3. setNumber( n => n+1 ) : setter의 인자로 함수가 들어왔네? queue에 n => n + 1 넣자!

 

 

https://react-ko.dev/learn/queueing-a-series-of-state-updates

 

 

이벤트 발생! 

이벤트 발생 당시 state 스냅샷: state는 0

 

1. React는 queue를 순회

2. 첫 번째 행에서 n에 0+1을 넣어야지! 다음 행에 n = 1 전달.

3. 두 번째 행에서 n에 1+1을 넣어야지! 다음 행에 n = 2 전달.

4. 세 번째 행에서 n 에 2+1을 넣어야지! 

 


 

 

이전에 보았던 setNumber ( n + 1) 과의 차이는 다음 행의 n으로 어떤 값을 넘겨주는지이다.

setNumber(n+1)코드의 큐

 

 

setNumber (n + 1) : 다음 행의 n으로 이벤트 발생 당시 state의 스냅샷을 넘겨줌 (0)

setNumber (n => n+1 ) : 다음 행의 n으로 현재 내가 update한 state의 스냅샷을 넘겨줌 (1) 

 


 

 

마지막으로 이 예시를 보고 답을 예상해 보자

<button onClick={() => {
  setNumber(number + 5);
  setNumber(n => n + 1);
}}>

 

실행 순서는 다음과 같다. 

 

1. queue에 number + 5와 n=> n+1을 push

2. 첫 번째 행 실행. 클릭 당시 number => 0. 따라서 0 + 5 반환.

3. 두 번째 행 실행. 반환받은 5에 1을 더한 6 반환. 

 

 

 

 


 

결론

 

React 18 버전부터는 이벤트 핸들러뿐만 아니라 비동기 처리 등 다양한 항목에 자동으로 batching을 해준다.

batching은 한 마디로 '한 번에 몰아서 처리하기'이며

batching이 일어나는 게 싫고 렌더링 전 state가 여러 번 바뀌는 기능을 원한다면

setter의 argument로 콜백 함수를 넘겨주면 된다.