React: infinite loop when using setState inside useEffect

Jene Hojin Choi·2021년 7월 16일
0
post-thumbnail

솝트에서 앱잼을 하면서 useEffect안에 setState를 쓰게 되면 side-effect가 있음을 알게되었다. 이 글은 How to Solve the Infinite Loop of React.useEffect()의 짧은 번역본이라고 보면 되겠다.

무한 루프

아래의 코드를 보자.

import { useEffect, useState } from 'react';

function CountInputChanges() {
  const [value, setValue] = useState('');
  const [count, setCount] = useState(-1);

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

  const onChange = ({ target }) => setValue(target.value);

  return (
    <div>
      <input type="text" value={value} onChange={onChange} />
      <div>Number of changes: {count}</div>
    </div>
  )
}

언뜻 보기엔 전혀 문제가 없는 것 같으나 useEffect가 무한정으로 불리면서 count가 무한대까지 오르게 된다... 그렇다면 이를 막기 위해서는 어떻게 해야할까?

Solution

1. dependency array 수정

useEffect의 dependency를 바꾸어주면 된다. value가 바뀔 때마다 count가 increment되기를 원하면 아래와 같이 하면 된다.
혹은 처음 한번만 렌더링이 되길 원한다면 dependency array를 빈 array로 만들어주면 된다.

import { useEffect, useState } from 'react';

function CountInputChanges() {
  const [value, setValue] = useState('');
  const [count, setCount] = useState(-1);

  useEffect(() => setCount(count + 1), [value]);

  const onChange = ({ target }) => setValue(target.value);

  return (
    <div>
      <input type="text" value={value} onChange={onChange} />
      <div>Number of changes: {count}</div>
    </div>
  );
}

2. useRef

혹은 useRef를 통해 Reference를 사용할 수 있다.
위의 코드처럼 count를 state 대신에 reference로 사용한다. reference가 업데이트 되는 것은 리렌더가 되도록 하지 않기 때문에 더 안전한 방법이라고 볼 수 있다.

import { useEffect, useState, useRef } from "react";

function CountInputChanges() {
  const [value, setValue] = useState("");
  const countRef = useRef(0);

  useEffect(() => countRef.current++);

  const onChange = ({ target }) => setValue(target.value);

  return (
    <div>
      <input type="text" value={value} onChange={onChange} />
      <div>Number of changes: {countRef.current}</div>
    </div>
  );
}

객체를 state로 사용하는 경우

위의 예시들처럼 단순하게 count가 아닌, 객체를 state로 사용하는 경우도 있을 수 있다.

import { useEffect, useState } from "react";

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

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

  const onChange = ({ target }) => {
    setSecret(s => ({ ...s, value: target.value }));
  };

  return (
    <div>
      <input type="text" value={secret.value} onChange={onChange} />
      <div>Number of secrets: {secret.countSecrets}</div>
    </div>
  );
}

위의 count 예시와 마찬가지로 secret이 업데이트 되면서 리렌더가 되고, 또 secret이 업데이트가 되는 무한 루프가 생성된다.

Solution

object 전체를 dependency로 하지 않기

아래와 같이 object 전체를 쓰지않고, object의 property를 dependency로 사용하면 무한 루프에서 벗어날 수 있다.

let myObject = {
  prop: 'Value'
};

useEffect(() => {
  // some logic
}, [myObject]); // Not good!

useEffect(() => {
  // some logic
}, [myObject.prop]); // Good!

아래는 원래 코드를 고친 예시이다.

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

원본

0개의 댓글