useEffect와 setTimeout

Moon Ki, Kim·2023년 5월 16일
1
post-thumbnail

서론

5주차 세오스 과제를 진행하다가

useEffect(() => {
  setTimeout(() => {
    ~~~
  },5000)
},[])

와 같은 코드를 작성했는데, 지피티선생님께서 useEffect안의setTimeout를 선언한 경우 return에서 clearTimeout을 통해 지워주는것이 좋다고 하셨다.

그때는 그냥 막연히 메모리 정리와 최적화의 개념이구나~하고 넘어갔었는데,
프론트 팀메이트가 왜?라는 의문을 제기하였고, 다시 생각해보아도
정확히 어떤 이유에서 메모리 정리가 되고, 최적화가 되는지 모르겠어서
테스트와 리서치를 조금 해보았다.

테스트 코드

기본적인 코드를 아래와 같이 작성했다.

//App.js
import { useEffect, useState } from "react";

function App() {
  const [countVal, setCountVal] = useState(1);

  useEffect(() => {
    setTimeout(() => {
      setCountVal(countVal * 10);
    }, 3000);
  }, [countVal]);

  const Counter = () => {
    return <h2>count : {countVal}</h2>;
  };

  return (
    <div>
      <h1>Testing Timeout</h1>
      <Counter />
      <button onClick={() => setCountVal(countVal + 1)}>Increment</button>
    </div>
  );
}

export default App;


3초에 한번씩 timeout이 실행되어 count값이 10배로 증가하고,
Increment 버튼을 클릭하면 현재 count값에 1을 더하는 코드이다.

예상

count값이 100으로 변한 후에 increment를 1초에 1번씩 두번 누르면

100(0초) -> 101으로 증가(1초) -> 102로 증가(2초) -> timeout실행 : 1020으로 증가(3초) -> (4초) -> (5초) -> 10200으로 증가(6초) ...
와 같이 진행될 것으로 예상했다.

결과


예상과 같이 진행했을 때의 결과이다.
100(0초) -> 101으로 증가(1초) -> 102로 증가(2초) -> timeout실행: 1000으로 증가(3초) -> 1010으로 증가(4초) -> 1020으로 증가(5초) -> 10000으로 증가(6초)...

결과 분석

어찌보면 당연한 결과였지만, 놓친 부분이였다.
countValdependency에 있기 때문에 countVal의 변화가 될 때마다 새로운 setTimeout함수가 생성되고있었다.
다만, 예상하지 전혀 예상하지 못했던 부분은
102로 증가(2초) 후에 가장 처음의 의도한 timeout(10배 곱하는 함수)이 실행 될 때
1020으로의 증가가 아닌, 1000으로의 증가가 되는 점이였다.

closure

useEffectdependency에 있는 값들을 지켜보고 있다가, 변화가 생기면 useEffect내부의 코드를 실행시킨다.
이때, 변화를 감지하면 새로운 값(closure)을 capture하여 내부의 코드들에 전달하게 된다.
내부의 코드들이 실제로 실행되는 시간은 setTimeout으로 인하여 3초 후이여서, 전달된 100의 값이 3초 후 1000으로 변하는 것이다.

해결법

useEffect(() => {
  const timeout1 = setTimeout(() => {
	setCountVal(countVal * 10);
  }, 3000);
  return () => clearTimeout(timeout1);
},[countVal]);

제시할 수 있는 가장 간단하고, 에러가 적을 해결책은 setTimeout 함수를 변수에 저장하고, 해당 변수를 useEffectreturn문에 clearTimeout을 적어줌으로서 callStack에서 제거해주는 방법이다.

100(0초) -> 101(1초) -> 102(2초) -> ...-> 1020(5초) -> ...
의 단계로 진행되는것을 확인할 수 있다.

추가상황

직접 코드로 구현은 어려워서 상황만 정리하자면, useEffectreturn에서 clearTimeout을 통해 해제해주지 않는다면, 다음과 같은 오류가 발생할 수도 있다.

  1. 컴포넌트가 렌더링 되고 timeout함수가 실행됨
  2. 갑자기 컴포넌트가 unmount됨 (possibly 유저가 다른 페이지로 이동한 경우)
  3. unmount된 후, timeout이 종료되고 timeout으로 실행하는 함수를 호출함
  4. 이미 존재하지 않는 컴포넌트의 state를 변경하려 하기 때문에 react가 error을 throw.

결론

useEffect안에서 setTimeout으로 timeout함수를 생성한 경우, return에서 clearTimeout을 통해 timeout을 clear해주는것이
1. potential memory leak(잠점적인 메모리 누수)
2. executing state updates on an unmounted component(언마운트된 컴포넌트의 상태 업데이트 실행)
들을 방지할 수 있다고 할 수 있겠다.

0개의 댓글