develop

useState의 비동기 문제 - 함수형 업데이트

crab. 2023. 3. 29. 08:42
반응형

해결방법 1. 함수형 업데이트

  • 해결방법 중 하나는 useState의 인자로 값이 아닌 업데이트된 최신의 state와 함께 함수를 전달하는 방법이다.
  • react의 setState코드를 보면 다음과 같이 값을 받거나, 이전 state와 함께 함수를 전달받을 수 있도록 되어있다.
// React Hooks
// ----------------------------------------------------------------------

// based on the code in <https://github.com/facebook/react/pull/13968>

// Unlike the class component setState, the updates are not allowed to be partial
type SetStateAction = S | ((prevState: S) => S);
  • 이렇게 되면, 여러번 전달받는 함수들은 큐에 저장되어 순서대로 실행된다.
  • 따라서 큐에서 먼저 수행된 함수의 결과로 반영된 state값이 다음 수행할 함수의 인자로 들어가게 되므로, 항상 최신의 state를 유지하게 된다.
import React, { useState } from "react"

function App() {

  const [num, setNum] = useState(1)

  async function plus() {
    setNum(num => num + 1)
    setNum(num => num + 1)
    setNum(num => num + 1)
  }

  async function minus() {
    setNum(num - 1)
  }

  return (
    <div className="App">
      <h1>{num}</h1>
      <button onClick={plus}>PLUS</button>
      <button onClick={minus}>MINUS</button>
    </div>
  );
}

export default App;
// 첫번째 h1
>> 1

// 두번째 h1
>> 4

// 두번째 h1
>> 7
  • 다만 문제는 아직 존재한다.

함수형 업데이트로 해결 못하는 경우

import { useState } from "react";

export default function StateTestT() {
  const [memberId, setMemberId] = useState(1);

  const updateMemberId = () => {
    // (1)
    setMemberId(memberId => memberId + 1);
    setMemberId(memberId => memberId + 1);
    setMemberId(memberId => memberId + 1);

    // (2)
    validateMemberId();
  };

  const validateMemberId = () => {
    setMemberId(memberId => memberId + 1);
    setMemberId(memberId => memberId + 1);

    if (memberId > 3) {
      console.log("5 이상");
      return 0;
    }
  };

  return (
    <>
      <div onClick={updateMemberId}>test</div>
    </>
  );
}
  • 위와 같은 컴포넌트가 있다고 해보자
  • 얼핏 봤을 때는 5번은 더했으니 콘솔에 “5이상” 이라는 문장이 찍혀야 할 것 으로 예상된다.
  • 하지만 결과는 아무 것도 찍히지 않는다
  • 왜 그럴까?
  • 그 이유는 useState가 하나의 이벤트 헨들러에서는 여전히 모든 변화값들을 가지고만 있고 반영하지않다가 이벤트 헨들러가 끝나는 시점에서 반영하기 때문이다.
  • 전에 얘기했듯이 useState는 여러차례 setState가 있으면 여러 state의 변경을 통합해서 한꺼번에 리렌더링한다.
  • 그리고 리렌더링하는 그 기준은 우리가 컨트롤하려는 그 이벤트헨들러(함수)가 끝나는 시점이다.
  • 즉, 함수형 업데이트는 여러 state들이 온전히 그 값을 차례로 수정은 해주지만
  • 그 차례대로 수정된 값을 하나의 이벤트 헨들러 내에서는 반영할 수 없다.

다시 해결 방법 1, 2, 3

  1. 모든 함수를 하나로 합친다.
    1. 만약 모든 함수가 하나로 합쳐진다면, 그 안에서 하나의 임시변수를 통해 새로운 값을 저장하고 사용하면 된다. 하지만, 이 방법은 함수의 크기를 과하게 크게 만드는 부작용이 있다.
  2. 명시적으로 값을 다음 함수에 넘긴다.
    1. 위의 경우에서는 updateMemberId 함수 내부에서 validateMemberId를 호출할 때 value를 인자로 넘겨주자는 의미이다. 하지만, 이러한 방식은 코드를 이해하기 더욱 어렵게 만든다. 만약 로직이 조금 더 복잡해진다면 계속해서 인자값이 넘어가야하는 상황이 발생하고, 이는 유지보수의 비용을 증가시킨다.
  3. reducer를 사용하는 방법
    1. useState() 대신 useReducer()를 사용하는 것이다. useReducer()는 동기적으로 동작하기 때문에 useState()가 비동기적으로 동작함으로 인해 발생됐던 문제들을 해결할 수 있다. 하지만, 하나의 컴포넌트에서만 사용될 상태값들을 이렇게 전역으로 관리하는 것 자체가 너무 별로이다.
반응형