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

박준수·2023년 4월 17일
0

해결방법 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> = 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. 모든 함수를 하나로 합친다.

    만약 모든 함수가 하나로 합쳐진다면, 그 안에서 하나의 임시변수를 통해 새로운 값을 저장하고 사용하면 된다. 하지만, 이 방법은 함수의 크기를 과하게 크게 만드는 부작용이 있다.

  2. 명시적으로 값을 다음 함수에 넘긴다.

    위의 경우에서는 updateMemberId 함수 내부에서 validateMemberId를 호출할 때 value를 인자로 넘겨주자는 의미이다. 하지만, 이러한 방식은 코드를 이해하기 더욱 어렵게 만든다. 만약 로직이 조금 더 복잡해진다면 계속해서 인자값이 넘어가야하는 상황이 발생하고, 이는 유지보수의 비용을 증가시킨다.

  3. reducer를 사용하는 방법

    useState() 대신 useReducer()를 사용하는 것이다. useReducer()는 동기적으로 동작하기 때문에 useState()가 비동기적으로 동작함으로 인해 발생됐던 문제들을 해결할 수 있다. 하지만, 하나의 컴포넌트에서만 사용될 상태값들을 이렇게 전역으로 관리하는 것 자체가 너무 별로이다.

profile
심플한 개발자를 향해 한걸음 더 (과거 블로그 : https://crablab.tistory.com/)

0개의 댓글