공부기록/웹 개발

setState에 대해 고민했던 하루

_우지 2023. 2. 10. 14:59

사건의 발단

채팅방의 한분께서 위와 같은 질문을 해주셨다.

여러가지 답변이 오갔고 내가 똥답변을 해버렸다.

이때 까지만해도 setState 안의 함수를 넣는 형태인 함수형 업데이트를 사용하면 batching 이 발생하지 않고   setState 마다 리렌더가 발생할 것이라고 생각했었다.

 

하지만 아래의 코드를 돌려본 결과. 

내 생각이 잘못되었다는 것을 알게되었다. 

import { useState } from "react";
import "./styles.css";

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

  console.log("render");

  return (
    <div className="App">
      <button
        onClick={() => {
          setCount((c) => c + 1);
          setCount((c) => c + 1);
        }}
      >
        {count}
      </button>
    </div>
  );
}

count가 2씩 증가하고 render 콘솔이 한번만 찍히는 것으로 보아. Batching이 아주 잘 동작하고 있음을 나타낸다.

 

오케이! 답변을 함으로써 내 지식이 잘못되었다는걸 깨달았어! 오히려 좋아!

 

설명을 할 수 없다면 아는 것이 아니다.

그런데 두가지에 대한 의문점이 생겼다.

1. 일반적인 setState

import React, { useState } from 'react'

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

  const plus = () => {
    setCount(count + 1) // 0 + 1
    setCount(count + 1) // 0 + 1
    setCount(count + 1) // 0 + 1   <- 이 코드만 적용됨
    console.log(count) // 0
  }

  return (
    <>
      <h2>{count}</h2>
      <button onClick={plus}>+3</button>
    </>
  )
}

2. setState 함수 업데이트

import React, { useState } from 'react'

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

  const plus = () => {
    setCount(prev => prev + 1) // 0 + 1
    setCount(prev => prev + 1) // 1 + 1
    setCount(prev => prev + 1) // 2 + 1
    console.log(count) // 0
  }

  return (
    <div className="App">
      <h2>{count}</h2>
      <button onClick={plus}>+3</button>
    </div>
  )
}

위의 두가지에 대해 어렴풋이 알고있어 제대로 설명을 할 수 없다는 걸 깨달았다.

 

파헤쳐 보자.

그럼 이제 저런 동작이 발생하는 근거를 정리해보자.

1. 일반적인 setState

import React, { useState } from 'react'

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

  const plus = () => {
    setCount(count + 1) // 0 + 1
    setCount(count + 1) // 0 + 1
    setCount(count + 1) // 0 + 1   <- 이 코드만 적용됨
    console.log(count) // 0
  }

  return (
    <>
      <h2>{count}</h2>
      <button onClick={plus}>+3</button>
    </>
  )
}
  1. closure에 의해 컴포넌트가 렌더링 될때의 state 와 props 는 고정된다.
  2. plus 함수가 호출될때의 count 는 모두 0 이다.
  3. setState 는 각각 업데이트가 되지 않고 batching 을 통해 한번에 업데이트가 이루어진다.
  4. 오브젝트 컴포지션으로 인해 state 객체가 merge 된다.
    React의 state 는 객체로 구현되어있다.
  const currentState = {
    number: 1
  };

  const newState = Object.assign(currentState, { number: 1 }, { number: 1 });
  setCount(newState)

   Object.assign은 같은 키값은 덮어씌우기 때문에 마지막 setState 만 동작하게 되는 것이다.

 

   5. setState의 내부 동작 (다음 내용은 해당 블로그에서 발췌한 내용입니다.)

   5-1. update가 일어나기 전 hook의 상태 (setCount 호출 전)

{
  memoizedState: 0, 
  baseState: 0,
  queue: {
  	last: null,
    dispatch: dispatchAction.bind(bull, currenctlyRenderingFiber$1, queue),
    lastRenderedReducer: basicStateReducer(state, action),
    lastRenderedState: 0,
  },
  baseUpdate: null,
  next: null
}

baseState가 0 이라는 것을 알 수 있다.

 

5-2. update가 일어나기 전 hook의 상태 (setCount 호출 전)

last에는 setCount를 통해 넘어온 액션과, React의 Batching Process를 통해 최종적으로 업데이트될 상태를 담고 있는 eagerState  변수, 그리고 action으로부터 eagerState를 계산하는 eagerReducer의 값이 세팅된다. 또한 basicStateReducer 는 후에 함수 업데이트 과정에서 한번 더 언급되기 때문에 한번 더 살펴주면 좋다.

{
  memoizedState: 0, 
  baseState: 0,
  queue: {
   last: {
      expirationTime: 1073741823,
      suspenseConfig: null,
      action: 1, // setCount를 통해 설정한 값.
      eagerReducer: basicStateReducer(state, action),
      eagerState: 1, // 실제로 상태 업데이트를 마치고 렌더링되는 값.
      next: { /* ... */},
      priority: 98
    },
    dispatch: dispatchAction.bind(bull, currenctlyRenderingFiber$1, queue),
    lastRenderedReducer: basicStateReducer(state, action),
    lastRenderedState: 0,
  },
  baseUpdate: null,
  next: null
}

위와 같은 과정을 통하여 결국 +3 이 아닌 +1이 된다는 것을 알 수 있다.

 

2. setState 함수 업데이트

그렇다면 함수 업데이트에서는 어떻게 동작하길래 변경 state 값을 이용할 수 있었던 걸까?

import React, { useState } from 'react'

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

  const plus = () => {
    setCount(prev => prev + 1) // 0 + 1
    setCount(prev => prev + 1) // 1 + 1
    setCount(prev => prev + 1) // 2 + 1
    console.log(count) // 0
  }

  return (
    <div className="App">
      <h2>{count}</h2>
      <button onClick={plus}>+3</button>
    </div>
  )
}
  1. closure에 의해 컴포넌트가 렌더링 될때의 state 와 props 는 고정된다.
  2. plus 함수가 호출될때의 count 는 모두 0 이다.
  3. setState 는 각각 업데이트가 되지 않고 batching 을 통해 한번에 업데이트가 이루어진다.
  4. 하지만 이번에는 함수를 콜백으로 넣어주었기 때문에 객체의 composition 이 발생하지 않는다.
    따라서 호출된 순서대로 setState 를 큐에 넣는다.
  5. 큐에 넣어진 setState가 실행되는 과정 (자세한 내용은 해당 블로그를 참고해 주세요.)
    아까 언급드렸던 basicStateReducer 의 코드를 보도록하자.
    action 이 함수일때와 그렇지 않은 경우가 분기처리 된 것을 알 수 있다.
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

따라서 일반적인 setState에서는 다음과 같이 동작한다. action 의 type 이 함수가 아니기 때문에 기존 state 에 +1 을 하게 되는 것이다.

last: {
	  ...other options // 필요한 부분만 남겨놓고 생략하였음.
      action: count + 1,
      eagerReducer: basicStateReducer(state, action),
      eagerState: count + 1, 
      next: {
      	last: {
        	... otherOptions,
            action: count + 1,
            eagerReducer: basicStateReducer(state, action),
            eagerState: count + 1, 
            next: null
        }
      }
 },

그렇다면 함수 업데이트에서는 어떻게 동작할까?

다음처럼 큐에 들어있던 함수들이 실행되게 된다. 

last: {
	  ...other options // 필요한 부분만 남겨놓고 생략하였음.
      action: count => count + 1,
      eagerReducer: basicStateReducer(state, action),
      eagerState: count + 1, 
      next: {
      	last: {
        	... otherOptions,
            action: count => count + 1,
            eagerReducer: basicStateReducer(state, action),
            eagerState: (count + 1) + 1, 
            next: null
        }
      }
 },

위 예시대로 생각해본다면 eagerState 는 ((count + 1) + 1) +1 이 될 것 이다. 

이러한 이유로 인해 state 는 +3이 된다.

 

참고자료

https://overreacted.io/ko/react-as-a-ui-runtime/

https://overreacted.io/ko/a-complete-guide-to-useeffect/

https://overreacted.io/how-are-function-components-different-from-classes/

https://velog.io/@ja960508/setState%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%84%B1

https://velog.io/@gml9812/useState%EC%99%80-useEffect-%EC%A2%80-%EB%8D%94-%EA%B9%8A%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

https://yeoulcoding.tistory.com/169?category=806488

https://velog.io/@tastestar/state-batch-update

https://www.youtube.com/watch?v=hSdVDBPTT0U&t=333s

https://usecode.pw/functional-set-state-is-the-future-of-react/