FrontEnd/React 이슈

[React] 콜백에서의 useState, useEffect 사용 이슈

기은P 2022. 9. 22. 17:46
반응형

1. 개요

위 아래로 스크롤 할 때, 섹션별로 스크롤이 이동하는 기능을 구현하는 과정에서 scroll event를 사용하게 되었는데, 이때 event가 발생할 때 콜백으로 scroll section index 값을 변경하려고 했습니다.

이때, 스크롤 인덱스가 무조건 변경되는 것이 아니라, 인덱스가 특정 범위를 넘지 않도록 필터링을 하는 조건을 추가하였는데요.

useEffect의 dependency로 등록한 scrollIndex의 console은 잘 찍히는 반면, 필터링하는 부분에서의 scrollIndex 값은 1로 고정이 되어 있었습니다.

 

사용된 코드

  const outerDivRef = useRef();
  const windowSize = useRef(getWindowDimensions());
  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
  const [scrollIndex, setScrollIndex] = useState(1);
  const scrollIndexRef = useRef(1);

  useEffect(() => {
    console.log("변경 후 index : " + scrollIndex)
  }, [scrollIndex])

  const moveDown = () => {
    console.log("move down")
    if (5 <= scrollIndex) return;
    setScrollIndex(scrollIndex + 1);
  }

  const moveUp = () => {
    console.log("move Up")
    if (scrollIndex <= 1) return;
    setScrollIndex(scrollIndex - 1);
  };

  function getWindowDimensions() {
    const { innerWidth: width, innerHeight: height } = window;
    return {
      width,
      height
    };
  }

  useEffect(() => {
    const wheelHandler = (e) => {
      e.preventDefault();
      windowSize.current = getWindowDimensions();
      const { deltaY } = e;
      if (deltaY < 0) {
        moveUp()
      }
      else if (deltaY > 0) {
        moveDown()
      }

    }

    const outerDivRefCurrent = outerDivRef.current;
    outerDivRefCurrent.addEventListener("wheel", wheelHandler);
    return () => {
      outerDivRefCurrent.removeEventListener("wheel", wheelHandler);
    }
  }, [])

 

 

2. 원인

왜 scrollIndex가 콜백 함수 호출시 값이 고정되어 있었냐면, 리스너를 통해 콜백 함수가 호출될 시, 호출될 때 state가 고정된다고 합니다. 

일단 콘솔을 찍어서 테스트 해봅니다!

 

2-1. setState 확인

 

moveDown은 callback 함수로 등록한 함수로, callback을 호출하면 스크롤 인덱스가 호출전과, 호출후에 변동이 있을 줄 알았는데, setState의 값은 변동이 없었습니다.

 

2-2. ref.current 확인

ref.current로 임시로 숫자를 등록해 증감하도록 useState와 같이 테스트를 해보았는데,
useState는 변동되지 않지만, ref.current값은 변동되는 것을 확인할 수 있었습니다!

 

2-3. let 확인

마지막으로 useState, ref.current, let 3개를 모두 테스트 해보았는데

콜백함수가 동작했을 때, 동기적으로 값이 바뀌는건 ref.current와 let 2개 인 것 같습니다.

 

결론: 콜백함수를 등록했을 때의 state 값은 고정이 되어 변동되지 않고, ref.current와 let이 변동됩니다.

 

 

 

3. 이유

조금 검색을 해보았는데, 이렇다고 합니다.

 

이 불변성이라는 이름이 아직도 거부감이 들 수도 있는데, 위에서 확인했듯이 한 번 랜더링된 컴포넌트가 가지고 있는 상태값은 그 중간에 변하지 않는다.

[count, setCount]  setCount 를 이용할 때, 이 setCount 는 메모리 어딘가에 있는 _val 을 변경한 것이지, 지금 옆에 가지고 있는 count 가 변경된 것이 아니다.

 count 값이 새로운 값이 되는건 리랜더링이 된 이후다. (이때 count 는 바로 직전의 count 와는 전혀 관계가 없는 새로운 녀석이다.) 리랜더링할 때(= Functional Component가 다시 호출될 때) 다시 useState 를 부르면 그때 변경된 _val 값을 가져온다고 생각할 수 있다.

그래서 아래와 같은 경우에 당황한 적이 많았다. 여러분들도 분명히 있을거라 생각한다.

const [state, setState] = useState(0);

useEffect(() => {
  setState(state + 1); // 분명 state에 1을 더했는데?
  console.log(state); // 호출: 0
}, []);

setState 를 이용한 직후에 state 값을 불렀는데, 업데이트가 되지 않는 상황이었다.

처음에는 뭔가 비동기적으로 발생해서 중간에 shadow 구간이 생기는건가… 라고 생각했는데 그게 아니었다.

state 값이 새로운 값이 되려면 리랜더링이 되어야 하는데, JS는 싱글 스레드로 돌아가기 때문에 useEffect 에 들어있는 콜백이 마무리된 이후에 리랜더링이 진행될 것이다. 그러면 아직 console.log(state) 를 실행하는 시점에는 리랜더링이 되기 전이라는거니까, state 값은 0인 것이 당연하다.

 

출처: https://medium.com/hcleedev/web-usestate%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC%EC%99%80-%ED%95%A8%EC%A0%95-7b4825c16b9

 

 

 

 

4. 결론

useState(): 리렌더링이 발생한 이후 state값이 변경됨(비동기적)

useEffect(): dependency로 등록한 값이 변경된 이후 등록한 effect 함수가 작동함

 

단순하게 useState를 사용하면 값이 바뀌고, useEffect내부에서 setState를 사용할 경우, 그 다음에 바로 값이 바뀌는 것도 아니라는 것을 알았습니다...

 

3번의 이유에서 작성된 코드처럼 setState를 한다고 해서 값이 바뀌는 것을 가정하고 코드를 작성하면 원하는 결과가 나오지 않는다는 점이죠.

 

 

4-1. useState + useEffect 개선안

const [state, setState] = useState(0);

setState(state + 1);
  
useEffect(() => {
  console.log(state); // 호출: 1
}, [state]);

위와 같이 useEffect에 dependency를 등록해서 값이 변경될 경우에 호출하거나,

 

4-2. useState 내부에서의 콜백

위처럼 useState 내부에서 콜백을 사용하도록 합니다.

 

 

4-3. setState 전 값 비교

  const [scrollIndex, setScrollIndex] = useState(0);
  let scrollIndexLet = 0;
  
  // not working
  const moveUp1 = () => {
    if (scrollIndex >= 5) return;
    setScrollIndex(scrollIndex + 1);
  };
  
  // working
  const moveUp2 = () => {
    if (scrollIndexLet >= 5) return;
    scrollIndexLet += 1;
    setScrollIndex(scrollIndexLet);
  };

 

setState전에 state 값에 대한 필터링이 필요하다면 ref.current나 let을 사용해서 state와 관계가 없는 값을 사용해야 합니다.

 

 

 

 

 

 

반응형