티스토리 뷰

 

 

useEffect에 대해 공부하던 중 공식문서에서 useEffect 내에 fetch를 사용하면 race condition이 발생할 수 있다는 것을 알게됐다. 공식문서에서는 race condition을 해결하기 위해 boolean flag를 사용했는데, 처음 보는 방법이라 정리해두려 한다.

 

https://react.dev/reference/react/useEffect#fetching-data-with-effects

 

 

1. Race condition


Race condition(경쟁 상태)이란 여러 개의 프로세스가 공유 자원에 동시 접근 할 때 실행 순서에 따라 결과값이 달라질 수 있는 현상이다.

 

Race condition은 컴퓨터의 입장에서 큰 문제이다. 이유는 똑같은 코드를 실행할 경우 실행 결과가 항상 같아야 하는데, Race condition이 발생하면 결과가 달라질 수 있기 때문이다. 이러한 문제를 방지하기 위해서 공유 메모리를 쓰는 프로세스끼리 '동기화'를 해주어야 한다. 

 

정리하자면 공유 메모리를 쓰는 프로세스끼리는 Race condition이 발생할 수 있는데, 이에 대한 해결책은 '동기화'이다.

 

 

2. useEffect에서의 Race condition


아래는 props로 id를 받아와서 useEffect를 사용해서 데이터를 가져오는 코드이다. 

 

export default function DataDisplayer(props) {
  const [data, setData] = useState(null);
  const [fetchedId, setFetchedId] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setTimeout(async () => {
        const response = await fetch(
          `https://swapi.dev/api/people/${props.id}/`
        );
        const newData = await response.json();

        setFetchedId(props.id);
        setData(newData);
      }, Math.round(Math.random() * 12000));
    };

    fetchData();
  }, [props.id]);

  if (data) {
    return (
      <div>
        <p style={{ color: fetchedId === props.id ? 'green' : 'red' }}>
          Displaying Data for: {fetchedId}
        </p>
        <p>{data.name}</p>
      </div>
    );
  } else {
    return null;
  }
}

 

이 코드가 의도하는 바는 가장 마지막에 실행한 fetchData를 보여주는 것인데, fetchData가 랜덤한 timeout 딜레이로 실행되기 때문에 의도치 않게 가장 마지막에 실행하지 않은 fetchData를 보여주게 된다. 

 

따라서 여러개의 fetchData가 실행되며 원치 않는 결과값을 얻는 것이 멀티 프로세스가 공유 자원을 경쟁하는 모습과 같아서 race condition이 일어난다고 할 수 있다. 

 

 

3. useEffect Clean-up 함수와 Boolean flag 이용하기


위 문제를 해결하려면 Clean-up 함수와 boolen flag를 이용하면 된다.

useEffect(() => {
  let active = true;

  const fetchData = async () => {
    setTimeout(async () => {
      const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
      const newData = await response.json();
      if (active) {
        setFetchedId(props.id);
        setData(newData);
      }
    }, Math.round(Math.random() * 12000));
  };

  fetchData();
  return () => {
    active = false;
  };
}, [props.id]);

만약 fetchData를 여러번 클릭 할 경우, fetchData가 딜레이 된 후 실행될 때 active는 Clean-up 함수에 의해 false가 되었기 때문에 처음에 실행하던 fetchData에서는 setFetchId와 setData가 실행되지 않는다.

 

Clean-up 함수

컴포넌트는 리렌더링 될 때 재평가-언마운트-업데이트 순으로 진행된다.
'Clean-up 함수는 'unmount' 될 때 실행된다.' 라는 말이 모호하다면, update 전에 실행된다고 생각하면 된다. 
따라서 위 코드에서 setFetchId와 setData가 실행되지 않는다.

 

따라서 setFetchId와 setData는 여러번 실행되더라도(race condtition 이지만) 마지막 요청의 결과만 출력된다.

 

하지만 위의 방법은 fetch 요청 자체는 막지 못하기 때문에 fetchData를 10번 클릭하면 서버에게 10번 요청하게 된다. 만약 요청의 개수가 많아진다면, 브라우저에서 실행되는 새로운 요청을 차단시켜 앱 속도를 느리게 만들 수 있다.

 

만약 서버에 요청하는 것도 중단하기 위해서는 AbortController를 사용하면 된다. (IE11은 지원이 안된다.)

useEffect(() => {
  const abortController = new AbortController();

  const fetchData = async () => {
    setTimeout(async () => {
      try {
        const response = await fetch(`https://swapi.dev/api/people/${id}/`, {
          signal: abortController.signal,
        });
        const newData = await response.json();

        setFetchedId(id);
        setData(newData);
      } catch (error) {
        if (error.name === 'AbortError') {
          // Aborting a fetch throws an error
          // So we can't update state afterwards
        }
        // Handle other request errors here
      }
    }, Math.round(Math.random() * 12000));
  };

  fetchData();
  return () => {
    abortController.abort();
  };
}, [id]);

 

 

 

AbortController가 동작하는 방법은 아래와 같다.

const abortController = new AbortController(); // 1
const abortSignal = abortController.signal; // 2

fetch( 'http://example.com', {
  signal: abortSignal // 3
} ).catch( ( { message } ) => { // 5
  console.log( message );
} );

abortController.abort(); // 4

 

1. 우선 AbortController DOM 인터페이스의 새로운 인스턴스를 만든 후

2. 인스턴스의 signal 프로퍼티를

3. fetch  signal 옵션에 할당하는 것을 볼 수 있다.

4. 패칭을 중단하기 위해서는 단순히 abortController.abort() 를 호출하기만 하면 된다.

5. abort 를 호출하게 되면 fetch 의 Promise 는 자동으로 reject 되게 되고 제어는 catch() 블럭으로 진입하게 된다.

 

 

참고

https://react.dev/reference/react/useEffect#fetching-data-with-effects

https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect

https://genie-youn.github.io/journal/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%97%90%EC%84%9C_AbortController%EB%A5%BC_%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC_%EB%B9%84%EB%8F%99%EA%B8%B0_%EC%9E%91%EC%97%85_%EC%A4%91%EB%8B%A8%ED%95%98%EA%B8%B0.html