useState 의 setState는 비동적으로 동작

jhplus13·2022년 6월 14일
2

React

목록 보기
16/16

참조자료
1. https://stackoverflow.com/questions/42038590/when-to-use-react-setstate-callback
2. https://bamtory29.tistory.com/entry/React-useState%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%A0%81-%EB%8F%99%EC%9E%91
3. https://velog.io/@kym123123/%EB%B9%84%EB%8F%99%EA%B8%B0%EB%A1%9C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94-react%EC%9D%98-setState%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC


🔶동일한 setState 함수를  ”useState(value) 형태로” 여러번 호출한다면?

  • 맨 마지막 업데이트된 state로만 반영됨.
  • value = 6 , 12 , 18… 이 아닌, 3, 6, 9가 됨.
const InputPrac = () => {
  const [value, setValue] = useState(0); // value = 3 6 9 ...
  const addNum = (e) => {
    setValue(value + 1);
    setValue(value + 2);
    setValue(value + 3);
  };

  return (
    <>
      <p>{value}</p>
      <button onClick={addNum}>클릭</button>
    </>
  );
};

export default InputPrac;

🔆 설명

리액트는 setState가 호출되면 state가 변경되는 것으로 인지하는데,
한번의 이벤트 발생시 연속으로 같은 setstate가 실행되면,
한 state가 업데이트되어 연속으로 리렌더링 될 것이다.

이는 비효율적이고 비용도 많이 들 것이다.

때문에 이벤트 발생시 실행되는 콜백함수 내에서의 state의 가장 마지막 갱신 내용으로 state를 업데이트 시키게 된다.


🔶 동일한 setstate함수를 “콜백함수 형식으로” 여러번 호출한다면?

  • setstate함수를 호출할 때 마다 state가 새로 갱신되고, 그 값을 인자로 받아서 사용할 수 있다.
  • 이 방식을 통해 갱신된 state값을 사용할 수 있다.
const InputPrac = () => {
  const [value, setValue] = useState(0);  // value = 6 12 18 ...
  const addNum = (e) => {
    setValue((prev) => prev + 1); // 1
    setValue((prev) => prev + 2); // 3
    setValue((prev) => prev + 3); // 6
  };

  return (
    <>
      <p>{value}</p>
      <button onClick={addNum}>클릭</button>
    </>
  );
};

🔆 설명

  • setstate를 콜백함수 형식으로 쓴다면, setstate의 인자로 업데이트 된 state값을 받아올 수 있다.
  • 하지만, setstate함수를 통해 갱신된 state가 DOM에 리렌더 되는 때는, addNum 콜백함수 본문이 다 끝나고 나서 반영되기 때문에, 최종 업데이트된 값인 6, 12 , 18로 보여지게 되는 것이다.

🔶 useState 의 setState는 비동적으로 동작한다

const InputPrac = () => {
  const [value, setValue] = useState(0); 
// value : 0 , 6 , 12 , 18

  const [valueMul, setValueMul] = useState(100); 
// valueMul : 100(value = 0) , 106(value = 6) , 118(value = 12)

  const addNum = (e) => {
    setValue((prev) => prev + 1); // 1
    setValue((prev) => prev + 2); // 3
    setValue((prev) => prev + 3); // 6, 12, 18
    setValueMul((prev) => value + prev); // 100(value = 0) 106(value = 6) 118(value = 12)
  };

  return (
    <>
      <p>{value}</p>
      <p>value가 바로 반영이 안됨 => {valueMul}</p>
      <button onClick={addNum}>클릭</button>
    </>
  );
};

🔆 문제

  • setValue가 최종적으로 6으로 업데이트 되니까 그 값을 활용하여 setValueMul함수를 실행하려고 했음.
  • 그러나 valueMul(100) + value(6) = 106을 기대했는데, 100이 나오고, 다음번 addNum실행시 value(6)이 참조되어 106이 나옴. 이런식으로 하나씩 밀려서 나옴.

🔆 이유 - 클로져의 원리 때문에

  1. setValueMul 함수가 실행될 땐, InputPrac함수의 스코프가 이미 끝나있다.
  2. 때문에 setValueMul함수 안에서 받는 prev인자는 InputPrac함수의 스코프가 있을당시의 값인 0을 참조하게 되는것이다.



코드 사례

1️⃣ 문제 코드

const InputPrac = () => {
  const [inputValue, setInputValue] = useState("");
  const [nameList, setNameList] = useState(names);

  const onInput = (e) => {
    // 1. 첫번째 setState에서 업데이트 한 state를 이용해
    setInputValue(e.target.value);

    // 2. 두번째 setState의 콜백함수에서 사용하려 하였음.
    setNameList(() => {
      return names.filter((el) => el.name.includes(inputValue));
    });
  };

  return (
    <>
      <input type="text" onInput={onInput} />
      <div id="searchId">
        {nameList.map((el, idx) => {
          return (
            <div className="idBox" key={idx}>
              <div className="idBoxRight">
                <p>{el.name}</p>
                <p>{el.korean}</p>
              </div>
            </div>
          );
        })}
      </div>
    </>
  );
};

2️⃣ 문제 파악

  • input창에 해당 글자 입력 하자마자 filter된 값이 렌더되야 하는데, 글자를 입력후 지워야 렌더가 됨.

  • Input이벤트가 발행하면 두개의 setState함수가 실행됨.
    첫번째 setState는 state를 e.target.value값으로 업데이트 하고,
    두번째 setState 콜백에선 그 업데이트 된 값을 이용해 해당 값을 갖고있는 요소만 filter하여 state를 업데이트하려고 했음.
    그러나 e.target.value가 이전 값인 ""로 나오고, 다음 Input이벤트가 발생시 반영되는 식으로 밀려서 나옴.

3️⃣ 문제의 이유

  1. 하나의 이벤트에는 하나의 기능만 하는 함수를 걸어줘야 함

  2. setState는 비동적으로 동작해서

  • setInputValue의 리턴값이 있다고 하더라도, 바로 state가 업데이트되어 리렌더링 되는게 아니라, onInput 이벤트 본문이 다 끝나야지 state가 업데이트 되는거여서!

  • setState 실행 후 바뀐 state값을 리액트가 갖고 있다가, onInput의 본문이 다 끝난 후 업데이트 시켜주는 것임.
    따라서 안에서 setInputValue를 통해 inputValue를 수정한 후, 그 수정된 값을 사용하는 setNameList함수를 호출했다면, inputValue는 아직 바뀌지 않은 상태이기 때문에 이전 값인 ""를 사용하여 setNameList함수가 호출되고 있는것임.

4️⃣ 해결 코드

input창의 입력값을 state로 받아서 처리했는데, 굳이 state로 만들 필요 없이, 그냥 변수로 만들어 사용하여 해결함.

  const onInput = (e) => {
    // 1. 인풋창에 입력된 값을 변수로 바로 받음
    const searchValue = e.target.value;

    // 2. 두번째 setState의 콜백함수에서 사용하려 하였음.
    setNameList(() => {
      return names.filter((el) => el.name.includes(searchValue));
    });
profile
making-dev

0개의 댓글