develop

useState의 비동기 문제 - 원인

crab. 2023. 3. 26. 22:09
  • 프론트 작업을 하면서 정말 나를 끈질기게 괴롭히고 또 이해하기 힘든 제일 큰 문제가 하나 있었다.
  • 바로 useState의 비동기 문제이다.
  • 구글링을 아무리 하고 구현을 아무리해도 안되고, 또 어쩔때는 됐었다.
  • 그나마 useEffect를 사용해서 매번 해결은 했었지만 불완전한 이해를 바탕으로 한 사상누각이었고 무엇보다 나 자신이 확신이 없었다.
  • 모든게 완전하게 이해를 못했어서 발생한 일이었다.
    • 심지어 어떨때 비동기문제가 발생하고 어떨때 안 발생하는지도 몰랐으니
    • 그야말로 슈뢰딩거의 비동기였다..
  • 그래도 이제는 useState의 비동기 문제를 조금은 이해하고 해결할 수 있게 되어 정리를 해본다.

문제 상황

import { useState } from "react";

export default function StateTest() {
  const [memberId, setMemberId] = useState("");

  const updateMemberId = e => {
    console.log(e.target.value);
    // (1)
    setMemberId(e.target.value);
    // (2)
    validateMemberId();
  };

  const validateMemberId = () => {
    console.log(memberId);
    if (memberId.length === 5) {
      console.log("5 이상");
      return 0;
    }
  };

  return (
    <>
      <input
        onChange={e => {
          updateMemberId(e);
        }}
      />
    </>
  );
}
  • 위와 같은 컴포넌트가 있을 때 우리는 자연스럽게 input에 ‘a’ 라는 값을 입력할 경우 그 값이 validateMemberId 를 통해 console로 찍히게 될 것을 예상한다.
  • 하지만 그 결과는 우리의 예상과 다르다.
// 첫번째 console.log
>> a

// 두번째 console.log
>> 
  • e.target.value값은 그대로 잘 나왔지만 그 값을 useState를 이용해 다른 함수에서 쓰려고 하니 값이 제대로 나오지 않았다..
  • 왜 이런 결과가 나온걸까?

useState가 비동기로 작동하는 이유

  • 하나의 페이지나 컴포넌트에는 수많은 state들이 존재한다.
  • 이때 이 state들이 하나하나 바뀔때마다 화면을 리렌더링 한다면 성능 저하가 발생할 것이다.
  • 리액트에서는 이 문제를 batch처리를 통해 해결한다.
  • setState가 연속으로 호출된다면 배치처리를 통해 한 번에 렌더링하도록 한다.
  • 즉, 아무리 많은 setState들이 연속적으로 사용되었어도 배치처리에 의해 한번의 렌더링으로 최신상태를 유지한다.
  • 배치란
    • 배치란 React가 너 나은 성능을 위해 여러개의 state 업데이트를 하나의 리렌더링으로 묶는 것을 의미한다.
    • React는 16ms 동안 변경된 상태 값들을 하나로 묶는다. (16ms 단위로 배치를 진행한다.)
  • 좀 더 자세히 들어가자면
    • setState는 이벤트 핸들러 내에서 비동기적으로 동작한다.
    • 하나의 이벤트 핸들러 내에서 setState가 여러 번 호출된다면, 이벤트가 끝날 시점에 state를 일괄적으로 업데이트하고 렌더링한다.
    • 즉, 리액트는 setState 호출 즉시 state를 변경하고 리렌더링하는 것이 아니라, 여러 차례 setState가 있으면 여러 state의 변경을 통합해서 한꺼번에 리렌더링한다.
  • 이 얘기를 내 방식으로 풀어 쓰자면
    • 하나의 함수에서(그 함수 내부에 선언된 다른 함수들까지도!, 이벤트 핸들러니까!)
      • setState가 한번 호출되든
      • 여러번 호출되든
      • 그 외의 다른 다양한 setState들이 호출되든
    • 그 state는 그 하나의 함수(함수 내부 다른 함수들에서도 포함)내에서 바뀌어 있지 않다!
    • 그 하나의 함수 밖으로 나왔을 때 비로소 그 state는 값이 바뀌어 있다.
  • 이게 핵심이다.

useState가 비동기인 이유 with React module

  • 이와 관련하여 react의 코드를 뜯어보자.
function useState(initialState) {
	var dispatcher = resolveDispatcher();
	return dispatcher.useState(initialState)
}
  • React의 useState 함수이다. 이 함수는 resolveDispatcher라는 함수가 반환하는 객체의 useState라는 메서드를 실행하여 반환되는 값을 리턴한다.
  • 그럼 이제 resolveDispatcher를 살펴보자.
function resolveDispatcher() {
	var dispatcher = ReactCurrentDispatcher.current;
	
	if (!(dispatcher !== null)) {
		{
			throw Error( "Invalid hook call. Hook can only
		}
	}

	return dispatcher;

}
  • resolveDispatcher함수는 다시 ReactCurrentDispathcer라는 객체의 current속성을 반환한다.
var ReactCurrentDispatcher = {
	/**
	 * @internal
	 * @type {ReactComponent}
	 */
	current: null
};
  • 즉 useState는 ReactCurrentDispatcher 객체의 useState 메서드를 실행시키는 것이다.
  • 이때 주목할 점은 ReactCurrentDispatcher가 객체라는 점이다.
  • 객체이기 때문에 동일한 key 값에 대하여 이전의 값을 계속해서 덮어쓴다.
  • 결국에는 마지막 명령어만 수행되는 셈이다.