fetch의 요청을 강제로 중단하기 (1)

eeensu·2025년 7월 6일
0

Javascript

목록 보기
37/41

개요

실무에서, text editor에 이미지를 업로드하는 기능을 고도화 하던 중, 이미지를 업로드하면서 로딩중일 때, 취소 버튼을 누르면 업로드하는 중이었던 api 요청도 같이 취소하는 기능을 구현해보기로 했다. 이때, "진행중인 api 요청 취소" 라는 기능은 js에서 특별하게 지원해주는 객체가 있었다.


AbortController 는 js에서 비동기 작업(특히 fetch 요청)을 중단할 수 있는 기능을 제공하는 객체이다. 예를 들어, 네트워크 요청이 너무 오래 걸리거나 사용자가 다른 페이지로 이동할 때, 더 이상 필요하지 않은 요청을 중단할 수 있다. 이를 통해 불필요한 리소스 낭비를 줄이고, 성능을 최적화할 수 있다.

AbortController 객체는 signalabort라는 두 가지 주요 속성을 가지고 있다.

  • signal : AbortSignal 객체로, 비동기 작업을 제어하는 신호 역할을 한다. 이 signalfetch 같은 비동기 작업에 연결해 중단 요청을 보낼 수 있다.
  • abort : 호출하면 연결된 모든 비동기 작업을 중단시키는 메서드이다.



React에서의 사용

아래는 fetch 요청을 AbortController로 취소하는 예제이다.
이곳에선 useEffect 훅을 사용해 컴포넌트가 마운트될 때 요청을 보내고, 언마운트될 때 controller.abort()를 호출하여 요청을 취소한다.

const DataFetcher: FC = () => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController(); // AbortController 생성
    const signal = controller.signal;

    // fetch 요청
    fetch('https://api.example.com/data', { signal })
      .then((response) => response.json())
      .then((data) => setData(data))
      .catch((error) => {
        if (error.name === 'AbortError') {
          console.log('요청이 중단되었습니다.');
        } else {
          setError(error);
        }
      });

    // 컴포넌트가 언마운트될 때 요청 중단
    return () => {
      controller.abort();
    };
  }, []); // 빈 배열 의존성으로 첫 렌더링 때만 실행

  return (
    <div>
      {error && <p>에러: {error.message}</p>}
      {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>데이터 로딩 중...</p>}
    </div>
  );
}

이 기능은 `axios` 에서 timeout 속성으로 간단하게 지원해주긴 한다..


타임아웃을 설정하여 자동으로 요청 중단하기

React에서 setTimeout을 사용해 일정 시간 후 요청을 자동으로 중단할 수 있다. 아래 예제에서는 5초 후에 요청을 취소하는 타임아웃을 설정한다.

const TimeoutDataFetcher = () => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    fetch('https://api.example.com/data', { signal })
      .then((response) => response.json())
      .then((data) => setData(data))
      .catch((error) => {
        if (error.name === 'AbortError') {
          console.log('요청이 타임아웃되어 중단되었습니다.');
        } else {
          setError(error);
        }
      });

 	// 5초후 자동으로 요청 중단
   	const timerId = setTimeout(() => controller.abort(), 5000)
    
    return () => {
      clearTimeout(timeoutId); // 타임아웃 해제
      controller.abort(); // 컴포넌트 언마운트 시 요청 중단
    };
  }, []);

  return (
    <div>
      {error && <p>에러: {error.message}</p>}
      {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>데이터 로딩 중...</p>}
    </div>
  );
}


검색기능과 같이 사용

React에서 검색 기능을 구현할 때, 사용자가 입력할 때마다 이전 요청을 취소하고 새로운 요청을 보낼 수 있다. 이렇게 하면 사용자가 빠르게 입력할 때 불필요한 요청을 방지할 수 있다. (사실 이렇게 구현하기 보단 debounce 를 활용해서 최적화된 이벤트에 요청을 보내는 방법이 더 좋긴하다.)

export const SearchComponent = () => {
    const [searchKeyword, setSearchKeyword] = useState<string>('')
    const [isLoading, setIsLoading] = useState<boolean>(false)
    const [result, setResult] = useState<any | null>(null)
    const [error, setError] = useState<Error | null>(null)

    useEffect(() => {
        if (!searchKeyword || searchKeyword.trim().length < 0) return

        setIsLoading(true)
        const controller = new AbortController()
        const signal = controller.signal

        fetch(`https://openlibrary.org/search.json?title=${searchKeyword}`, {
            signal,
        })
            .then((response) => response.json())
            .then((data) => setResult(data))
            .catch((err) => {
                if (err.name === 'AbortError') {
                    console.log('Request was aborted')
                } else {
                    setError(err)
                }
            })
            .finally(() => setIsLoading(false))

        return () => {
            controller.abort()
        }
    }, [searchKeyword])

    return (
        <div>
            <input
                type='text'
                placeholder='검색어 입력...'
                value={searchKeyword}
                onChange={(e) => setSearchKeyword(e.target.value)}
            />
            {error && <p>에러: {error.message}</p>}
            {isLoading ? (
                <p>검색 결과 로딩 중...</p>
            ) : (
                !error && <pre>{result?.numFound}</pre>
            )}
        </div>
    )
}


여러개의 fetch 요청에서의 중단

여러 개의 fetch 요청을 동시에 중단하기위해 AbortController 하나를 여러 요청에 공유할 수 있다.

const MultiRequestComponent = () => {
  const [data1, setData1] = useState(null);
  const [data2, setData2] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    Promise.all([
      fetch('https://api.example.com/data1', { signal }).then((res) => res.json()),
      fetch('https://api.example.com/data2', { signal }).then((res) => res.json()),
    ])
      .then(([result1, result2]) => {
        setData1(result1);
        setData2(result2);
      })
      .catch((error) => {
        if (error.name === 'AbortError') {
          console.log('모든 요청이 중단되었습니다.');
        } else {
          setError(error);
        }
      });

    return () => {
      controller.abort(); // 컴포넌트 언마운트 시 모든 요청 중단
    };
  }, []);

  return (
    <div>
      {error && <p>에러: {error.message}</p>}
      {data1 && <pre>데이터 1: {JSON.stringify(data1, null, 2)}</pre>}
      {data2 && <pre>데이터 2: {JSON.stringify(data2, null, 2)}</pre>}
    </div>
  );
}
profile
안녕하세요! 프론트엔드 개발자입니다! (2024/03 ~)

0개의 댓글