[Next.js] 검색 요청 최적화: 이전 요청 취소하고 최신 데이터 유지하기 (feat. AbortController)

hyejinJo·2025년 7월 4일
0

React

목록 보기
17/18
post-thumbnail

검색 페이지에서 탭을 누를때마다 그에 해당하는 뉴스 목록이 나오도록 구현중이었다. 근데 여기서 하나의 문제가 발생했는데…
“정책뉴스” 라는 탭의 목록 api 요청 시간이 다른 탭보다 오래걸려 요청중 다른 탭을 누르면, 다른 탭의 목록 데이터가 뿌려진 그 이후에 한 번 더 이전에 요청된 목록 데이터값이 덮어씌어지는 이슈가 있었다.


...
const useSearchList = () => {
  const router = useRouter();

  const [searchType, setSearchType] = useState<string>();
  const [searchList, setSearchList] = useState<any[]>([]);
  const [isFetching, setIsFetching] = useState(false);
  
  const currentAbortController = useRef<AbortController | null>(null);

  ...
  const getSearchList = useCallback(
    async (newSearchType: string) => {
      setIsFetching(true);
      const { word } = router.query;

      try {
        const params: SearchListParameter = new SearchListParameter({
          size: LIST_SIZE,
          word: word as string,
        });
        const {
          data: { content, last },
        } = await axios.get<SearchListResponse>(
          `/api/search/${newSearchType}${params.stringify()}`);
        setSearchType(newSearchType);

        // 요청값을 content 목록에 담음
        setSearchList(content);
      } catch (error: any) {
        console.error(error.response || error);
      } finally {
        setIsFetching(false);
      }
    },
    [router],
  );

...

setSearchList 에 데이터의 content 배열이 들어가는 구조다. 현재 이전의 요청이 한 번 더 setSearchList 에 들어가고 있다.

문제점

정책뉴스 탭 클릭

정책뉴스 목록 api 가 요청중인 pending 상태일 때, 중간에 “소플 Today” 라는 탭을 누르면 소플 Today의 목록 api 가 요청되는데, 이전에 보냈던 정책뉴스 목록 api 가 그대로 남아 요청중임을 알 수 있다.

몇 초 뒤 정책뉴스 목록 요청값이 뒤 늦게 들어오면서, 원래 “소플 Today” 데이터 값이 “정책뉴스” 데이터로 바뀌어버린 것을 확인할 수 있다.

그래서 이전에 요청된 api 를 중간에 취소할 수 있을까 하며 구글을 찾아봤는데 그 해결책은 바로 AbortController 였다.

AbortController

AbortController 는 자바스크립트에서 비동기 작업을 중단 할 수 있도록 도와주는 API 로, 특히 fetch 요청을 취소하거나 setTimeout 등의 작업을 중단할 때 유용하게 쓰인다고 한다. → 공식문서

Axios 요청 취소

AbortController 의 기본적인 형태로 아래와 같이 사용할 수 있다.

const controller = new AbortController(); // 새로운 요청 취소 컨트롤러 생성

axios.get("https://api.example.com/data", { signal: controller.signal })
  .then((response) => console.log(response.data))
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("요청이 취소됨");
    } else {
      console.error(error);
    }
  });

// 요청 취소
controller.abort();
  • 새로운 AbortController 인스턴스를 생성
  • get 요청의 두번째 인수로 { signal: controller.signal } 를 전달 (해당 요청을 AbortController로 제어하기 위해서)
  • controller.abort() 로 연결된 요청을 취소

해결

수정한 코드는 아래와 같다.

 	const currentAbortController = useRef<AbortController | null>(null);

  ...
  const getSearchList = useCallback(
    async (newSearchType: string) => {
      setIsFetching(true);
      const { word } = router.query;

      // ✅ 이전 요청 취소
      currentAbortController.current?.abort();

      // ✅ 새로운 AbortController 생성
      const controller = new AbortController();
      currentAbortController.current = controller;

      try {
        const params: SearchListParameter = new SearchListParameter({
          size: LIST_SIZE,
          word: word as string,
        });
        const {
          data: { content, last },
        } = await axios.get<SearchListResponse>(
          `/api/search/${newSearchType}${params.stringify()}`,
          { signal: controller.signal }, // ✅ 요청 취소 가능하도록 signal 전달
        );
        setSearchType(newSearchType);

        // ✅ 요청이 취소되었으면 상태 변경 x
        if (controller.signal.aborted) return;
        setSearchList(content);
      } catch (error: any) {
        console.error(error.response || error);
      } finally {
        setIsFetching(false);
      }
    },
    [router],
  );

...
  • useRef를 사용해서 컴포넌트가 리렌더링돼도 기존 AbortController를 유지
  • api 요청이 가기전에 이전 요청을 취소한 후, 다시 정상적으로 요청이 될 수 있도록 AbortController 를 새롭게 생성
  • 요청이 취소된 상태일 땐 setSearchList 에 데이터가 담기지 않도록 함

결과

이전의 요청이 pending 중인 상태에서 다른 탭을 눌러도, 그 이전의 요청이 취소되어 정상적으로 리스트가 로드된 것을 확인할 수 있었다!

참고:
https://gobae.tistory.com/145
https://velog.io/@jhjung3/AbortController로-fetch-요청-취소하기

profile
Frontend Developer 💡

0개의 댓글