본문 바로가기

CS

왜 useEffect는 무한루프에 빠질까?

코드를 짤 때 불필요한 코드를 줄이기 위해서는 개념 이해가 정말 중요한 것 같다고 느끼는 요즘입니다,, 

오늘은 마구잡이로 넣다가 오류난 useEffect에 대해서 알아보고자 합니다. 

 

useEffect

useEffect 함수는 React component가 렌더링 될 때마다 특정 작업을 실행할 수 있도록 하는 Hook입니다.  

우리가 React에게 component가 렌더링 이후에 어떤 일을 수행해야하는지를 알려주는 것이죠 

그러면 useEffect를 왜 사용할까요? 

useEffect는 component의 렌더링이 완료된 후에 실행되기 때문에 렌더링 자체와 상관없는 side effect를 처리하려고 사용합니다. 

side effect : 함수 내 특정 동작이 함수 외부에 영향을 끼쳐 프로그램의 동작을 이해하기 어렵게 만드는 행위 

 

먼말임 ➡️ 함수의 내용이 함수 외부에 영향을 끼치면 해당 함수는 side effect가 있다고 하는데요, 

- 백엔드 서버에 API 데이터 요청 

- 브라우저 API와 상호 작용

- setTimeout, setInterval 등의 타이밍 함수 사용 

이렇게 외부와 상호 작용하면서 해당 컴포넌트의 렌더링이나 성능에 영향을 미치지 않도록 하려고 useEffect를 사용합니다. 

예를 들어서, 

localStorage에 데이터를 저장할 때 useEffect없이 렌더링 중에 실행한다면 

function App() {
  const [count, setCount] = useState(0);

  localStorage.setItem('count', count); 

  return <button onClick={() => setCount(c => c + 1)}>+</button>;
}

딱 보기에 button을 누르면 setCount하고 바뀌었으니 저장하는 것처럼 생각이 들지만 

사실 count와 상관없는 다른 state가 변경되어도 렌더링이 발생하기 때문에 

count가 바뀌지 않았는데도 저장이 계속 일어나는 문제가 발생합니다. 

 

useEffect 사용 방법

1. 
useEffect(() => {
  console.log("렌더링 될 때마다 실행되는 여기가 콜백함수");
});

2.
useEffect(() => {
  console.log("처음 렌더링 될 때만 실행됨");
}, []);

3.
useEffect(() => {
  console.log(name);
  console.log("name changed!");
}, [name]);

4.
useEffect(() => {
  const handleResize = () => console.log(window.innerWidth);

  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

useEffect안에 콜백함수를 넣어주고 

세번째 예시 코드에 []는 종속성 배열(deps)을 의미하는데요, side effect가 의존하는 모든 값을 포함해야 합니다. 

deps를 비워두면 처음 렌더링 될 때만 실행하고, deps에 name을 넣게 되면 name이 변경되었을 때 콜백 함수를 실행하게 됩니다. 

 

unmount 시점(컴포넌트 사라질 때), deps update 직전(특정 값 변경 직전)에 실행할 작업을 지정해주려면 cleanup 함수(return되는 함수)를 사용합니다. 4번 예시 코드에서 window의 resize 이벤트를 listen하다가 해당 컴포넌트가 unmount될 때 event listener가 계속 메모리에 남아있으면 낭비이기 때문에 window.removeEventListener로 정리해줍니다. 

 

useEffect에서의 무한루프

이제 사용법을 알아보았으니 useEffect에서의 무한루프에 대해 알아봅시다. 

제가 useEffect에 setter를 호출했을 때 이러한 문제가 발생했는데요 

useEffect(() => {
  setCount(count + 1);
});

deps를 쓰지 않는다면 처음 컴포넌트가 렌더링 될 때 count를 증가시키고 그럼 또 값이 변하면서 다시 렌더링, 그럼 또 count 증가, ... 이런 식으로 무한루프가 완성되는 것이죠 

useEffect(() => {
    setTimeLeft(LEVEL[level].time);
  }, [level]);

근데 저는 이렇게 작성했는데도 ESLint 에러가 발생했는데요..

Error: Calling setState synchronously within an effect can trigger cascading renders 경고라지만 빨간색이라 이거 뭐 고치지 않으면 안됐었기 때문에 

1) 그냥 handle 함수로 변경했습니다.

const handleLevelChange = (newLevel) => {
    setLevel(newLevel);
    setTimeLeft(LEVEL[newLevel].time);
  };

 

이 외에도 해결방법은 여러가지입니다.

2) 주석을 달아서 경고를 무시해주거나, 

useEffect(() => {
  // eslint-disable-next-line react-hooks/set-state-in-effect
  setTimeLeft(LEVEL[level].time);
}, [level]);

3) useRef로 이전 값을 추적하면 됩니다. 

function App() {
  // useRef는 "처음 한 번만 값 저장"함
  // 이후 렌더링돼도 이 값 유지됨 (새로 안 만들어짐)
  const levelRef = useRef({
    1: { time: 10 },
    2: { time: 20 }
  });

  const [level, setLevel] = useState(1);
  const [timeLeft, setTimeLeft] = useState(0);

  useEffect(() => {
    setTimeLeft(levelRef.current[level].time);
  }, [level]); // 

  return <div>{timeLeft}</div>;
}

useRef는 값을 한 번만 저장하게 되는데요 .current는 렌더링되어도 변하지 않기 때문에 무한루프를 해결할 수 있습니다. 

 

Object를 다룰 때 무한루프가 생길 수 있는데요,

const [secret, setSecret] = useState({ value: "", countSecrets: 0 });

useEffect(() => {
  if (secret.value === "secret") {
    setSecret((s) => ({ ...s, countSecrets: s.countSecrets + 1 }));
  }
}, [secret]);

1. secret.value가 변경되면 useEffect가 실행

2. setSecret 실행 (countSecrets 증가) 

3. secret 객체 바뀌었으니 또 useEffect 실행 

그래서 deps 배열에 object 통째로 넣지 않는 것이 좋습니다!!

useEffect(() => {
  if (secret.value === "secret") {
    setSecret((s) => ({ ...s, countSecrets: s.countSecrets + 1 }));
  }
}, [secret.value]);

secret.value가 바뀔 때만 실행하도록 변경합니다. 

 

여러 오류들을 예방하기 위해서는 state를 변경하는 effect는 그 state가 만들어진 컴포넌트에서 처리하는 게 안전합니다!!

값 넘길 때가 많다보니 state를 어디서 정의하냐.. 이것도 생각해야돼서.. 어렵다어려워

 

 

참고

https://velog.io/@sucream/%EA%B7%B8%EB%9E%98%EC%84%9C-useEffect%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%93%B0%EB%8A%94%EA%B1%B4%EB%8D%B0%EC%9A%94

 

그래서 useEffect는 언제 쓰는건데요?

Side effect? 리액트는 side effect라는 것을 처리하기 위해 useEffect를 사용하라고 한다. 그렇다면 side effect는 무엇이고, 이것을 왜 별도로 처리해야 하는 것일까? > 함수 내 특정 동작이 함수 외부에 영

velog.io

https://choar816.tistory.com/163

 

[React] Side effect(사이드 이펙트)란? | 부수 효과, useEffect

[React] Side effect(사이드 이펙트)란? | useEffect React 공식 문서를 읽다가 사이드 이펙트(side effect)라는 표현을 처음 봐서 무엇인지 찾아보았다. 찾아보다 좋은 영어 글을 발견해서 번역하며 공부해보

choar816.tistory.com

https://kasterra.github.io/preventing-useEffect-infinite-loop/

 

React의 useEffect에 대한 간단한 설명과 무한루프 예방하기 | Kasterra's Archive

열정 넘치는 프론트엔드 개발자의 블로그!

kasterra.github.io