리액트의 useEffect는 데이터 가져오기, 구독 관리, DOM 업데이트, 사이드 이펙트 처리 등 다양한 작업에 사용된다. 그러나 useEffect를 과도하게 사용하면 성능 저하, 불필요한 렌더링, 디버깅의 복잡성 같은 문제가 발생할 수 있다. "Leave useEffect Alone!" 라는 가이드 글을 참고하여 올바른 useEffect 사용법에 대한 추가 설명을 덧붙여서 정리해봤다.

경쟁 상태(Race Condition) ⭐


경쟁 상태는 여러 비동기 작업이 동시에 실행될 때, 실행 순서나 결과가 예측하지 않은 방식으로 작동하는 현상을 가리킨다.

아래 코드에서 버튼을 여러 번 클릭하면 counter 값이 증가하고, 각 요청은 랜덤한 시간만큼 대기한 후 response 상태를 업데이트한다. 하지만 이 과정에서 경쟁 상태가 발생할 수 있다.

function RaceConditionExample() {
  const [counter, setCounter] = useState(0);
  const [response, setResponse] = useState(0);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const request = async (requestId) => {
      setIsLoading(true);
      await sleep(Math.random() * 3000);
      setResponse(requestId);
      setIsLoading(false);
    };
    request(counter);
  }, [counter]);

  const handleClick = () => {
    setCounter((prev) => ++prev);
  };

  return (
    <>
      <h3>Current Value: {counter}</h3>
      <h3>Settled Response: {response}</h3>
      <button onClick={handleClick}>Increment</button>
      {/* ... */}
    </>
  );
}
  1. 첫 번째 클릭: counter + 1, requestId 1 요청 시작
  2. 두 번째 클릭: counter + 1, requestId 2 요청 시작
  3. 만약 두 번째 요청이 더 빨리 완료되면 response 상태는 2로 업데이트
  4. 이후 첫 번째 요청이 완료되면서 response 상태를 1로 덮어씀

20241215_175141.gif

이처럼 response 상태는 가장 최신 요청의 결과를 반영해야 하지만, 비동기 요청의 완료 순서가 랜덤하기 때문에 의도치 않은 결과가 발생하고 있다.

이러한 경쟁 상태는 클린업 함수를 사용해서 처리할 수 있다. 클린업 함수는 다음 이펙트 함수가 실행되기 전에 호출되며, 이전 이펙트 함수의 스코프에서 동작한다.

useEffect(() => {
  let ignore = false; // 현재 useEffect 함수의 스코프
  const request = async (requestId) => {
    setIsLoading(true);
    await sleep(Math.random() * 3000);
    if (!ignore) {
      setResponse(requestId);
      setIsLoading(false);
    }
  };
  request(counter);

  return () => {
    ignore = true; // 이전 useEffect 함수의 스코프
  };
}, [counter]);
  1. 첫 번째 클릭: counter + 1, requestId 1 요청 시작, ignore = false로 초기화
  2. 두 번째 클릭: counter + 1, requestId 2 요청 시작
    1. 첫 번째 클릭에서 실행된 useEffect 클린업 함수 호출 → ignore = true로 변경 (이전 요청 무효화)
    2. 두 번째 클릭에서 실행된 useEffect 함수 호출 → ignore = false로 초기화
  3. 두 번째 요청 완료: 두 번째 useEffect의 ignore 변수가 false 이므로 response 상태 2로 업데이트
  4. 첫 번째 요청 완료: 첫 번째 useEffect의 ignore 변수가 true 이므로 response 상태 변경 안함

불필요한 렌더링