본문 바로가기
문제해결

useSearchParam 사용시 api 2번 호출되는 거 해결

by limew 2024. 12. 10.

 

 

처음에 훅을

export const useQueryParam = (defaultParams: I_QueryParam) => {
  const [searchParams, setSearchParams] = useSearchParams();

  // 디폴트 쿼리 파라미터 설정
  useEffect(() => {
    // 기본값을 기준으로 현재 searchParams에 없는 값을 설정
    let updated = false;
    for (const [key, value] of Object.entries(defaultParams)) {
      if (!searchParams.has(key)) {
        searchParams.set(key, value);
        updated = true;
      }
    }
    // 변경 사항이 있을 때만 URL 업데이트
    if (updated) {
      setSearchParams(searchParams);
    }
  }, [searchParams, setSearchParams]);

 

컴포넌트에서 이렇게 했는데 company api가 2번 호출 됐다.

const Company = () => {
    useEffect(() => {
        const initKeyword = getQueryParam("keyword");
        initKeyword && setKeyword(initKeyword);

        if (getQueryParam("status")) {
          if (getQueryParam("tab") === COMPANY_MENU.company) {
            deleteQueryParam("status"); // company탭은 status 없음
          } else {
            form.setFieldValue("status", getQueryParam("status"));
          }
        }

        if (getQueryParam("tab") === COMPANY_MENU.company) {
          fetchCompanies();
        } else if (getQueryParam("tab") === COMPANY_MENU.requestReview) {
          fetchRequestReview();
        }
      }, [searchParams]);

 

 

훅은 이렇게 고쳐야헸다 (dependency 빈 배열)

useEffect에서 searchParams와 setSearchParams를 의존성 배열에 포함하는 것은 일반적으로 비효율적일 수 있습니다. 특히, searchParams는 객체이므로 참조가 변경될 때마다 useEffect가 실행됩니다. 이를 방지하기 위해 의존성을 최적화해야 합니다.

문제점:

  1. searchParams 객체 참조 변경
    searchParams 객체가 변경될 때마다 useEffect가 실행됩니다. 이로 인해 불필요한 실행이 발생할 수 있습니다.
  2. setSearchParams는 stable 함수
    setSearchParams는 React Router에서 제공하는 함수로 보통 안정적입니다. 의존성 배열에서 제거해도 무방합니다.

 

컴포넌트에서 useeffect도 searchParams를 디펜던시로 사용하면 안 된다.

 

 

 

컴포넌트에 진입할 때 fetchCompanies가 두 번 호출되는 문제는 useEffect의 의존성 배열과 searchParams의 변경 방식에서 발생할 가능성이 높습니다.

문제 원인 분석

  1. setSearchParams가 useEffect를 트리거
    useQueryParam에서 setSearchParams를 호출하면 searchParams 객체가 갱신됩니다. 이로 인해 의존성 배열에 포함된 searchParams가 변경되고, 이를 감지한 두 번째 useEffect가 실행됩니다.
  2. searchParams의 참조 변경
    URLSearchParams 객체는 변경될 때마다 새 참조값을 가지므로, 불필요한 렌더링 및 useEffect 실행을 초래할 수 있습니다.

 

 

 

 

디펜던시에 [getQueryParam("tab")] 처럼 특정 쿼리가 변할때만 호출하게 하라는 방법도 추천해줬지만

나는 쿼리가 많고 queryparam이 바뀔때만 api를 호출하고 싶은데 어떻게 해결하냐

 

 

여기서 힌트를 얻었다

https://dev-thinking.tistory.com/23

 

[개발일지02/🏔️오름마켓 React] useSearchParams로 필터 Query String 구현하기

🙌 리팩토링을 통해 프론트엔드에서 처리되던 정렬 및 필터 기능을 API와 연결하는 작업을 진행했다. 렌더링 된 버튼의 값과 실제 API 요청을 위해 전달해 줘야 했던 Query String의 값이 달라 두 값

dev-thinking.tistory.com

 

 

useLocation을 활용하면 URL의 변경을 감지하여 useEffect가 실행되도록 제어할 수 있습니다. 이를 통해 searchParams의 변경으로 인해 발생하는 불필요한 재실행을 줄일 수 있습니다.

 

위 방법들로 초기 URL 상태가 빈 문자열일 때의 실행을 방지하거나, 중복 호출을 막을 수 있습니다:

  1. if (!location.search) 조건 추가로 빈 문자열 방지.
  2. hasFetched 상태로 중복 호출 방지.
  3. **useRef**를 사용해 초기 렌더링을 한 번만 실행하도록 제어.

 

 

최종 코드

 const location = useLocation();
 
useEffect(() => {
    if (!location.search) return;

    const initKeyword = getQueryParam("keyword");
    initKeyword && setKeyword(initKeyword);

    if (getQueryParam("status")) {
      if (getQueryParam("tab") === COMPANY_MENU.company) {
        deleteQueryParam("status"); // company탭은 status 없음
      } else {
        form.setFieldValue("status", getQueryParam("status"));
      }
    }

    if (getQueryParam("tab") === COMPANY_MENU.company) {
      fetchCompanies();
    } else if (getQueryParam("tab") === COMPANY_MENU.requestReview) {
      fetchRequestReview();
    }
  }, [location.search]); //  URL이 변경될 때만 호출

 

 

export const useQueryParam = (defaultParams: I_QueryParam) => {
  const [searchParams, setSearchParams] = useSearchParams();

  // 디폴트 쿼리 파라미터 설정
  useEffect(() => {
    // 기본값을 기준으로 현재 searchParams에 없는 값을 설정
    let updated = false;
    for (const [key, value] of Object.entries(defaultParams)) {
      if (!searchParams.has(key)) {
        searchParams.set(key, value);
        updated = true;
      }
    }
    // 변경 사항이 있을 때만 URL 업데이트
    if (updated) {
      setSearchParams(searchParams);
    }
  }, []);

 

 

2-1. API 호출 중복 방지 상태 추가

API 호출 중인지 상태를 관리하여 중복 호

const [hasFetched, setHasFetched] = useState(false);

useEffect(() => {
  if (!location.search || hasFetched) return;

  console.log(location.search);
  const initKeyword = getQueryParam("keyword");
  if (initKeyword) {
    setKeyword(initKeyword);
  }

  if (getQueryParam("status")) {
    if (getQueryParam("tab") === COMPANY_MENU.company) {
      deleteQueryParam("status"); // company탭은 status 없음
    } else {
      form.setFieldValue("status", getQueryParam("status"));
    }
  }

  if (getQueryParam("tab") === COMPANY_MENU.company) {
    fetchCompanies();
  } else if (getQueryParam("tab") === COMPANY_MENU.requestReview) {
    fetchRequestReview();
  }

  setHasFetched(true);
}, [location.search]);

 

3-1방법

초기 렌더링 시 딱 한 번만 실행

초기 렌더링에서 한 번 실행되도록 useRef를 사용할 수도 있습니다:

const isFirstRender = useRef(true);

useEffect(() => {
  if (isFirstRender.current && !location.search) {
    isFirstRender.current = false; // 첫 렌더링 시 실행 방지
    return;
  }

  console.log(location.search);
  const initKeyword = getQueryParam("keyword");
  if (initKeyword) {
    setKeyword(initKeyword);
  }

  if (getQueryParam("status")) {
    if (getQueryParam("tab") === COMPANY_MENU.company) {
      deleteQueryParam("status"); // company탭은 status 없음
    } else {
      form.setFieldValue("status", getQueryParam("status"));
    }
  }

  if (getQueryParam("tab") === COMPANY_MENU.company) {
    fetchCompanies();
  } else if (getQueryParam("tab") === COMPANY_MENU.requestReview) {
    fetchRequestReview();
  }
}, [location.search]);

 

 

 

추가사항

 

URL 전체 변경을 감지하고 싶다면?

location.pathname과 location.search를 함께 사용

useEffect(() => {
  fetchAPI(`${location.pathname}${location.search}`);
}, [location.pathname, location.search]);

 

 

Debounce를 통한 호출 최적화

URL 변경이 매우 빈번할 경우(예: 사용자가 검색어를 입력 중일 때), 호출을 최적화하기 위해 debounce를 사용

 

import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import { debounce } from "lodash";

const MyComponent = () => {
  const location = useLocation();

  useEffect(() => {
    const debouncedFetch = debounce(() => fetchAPI(location.search), 300);
    debouncedFetch();
    return debouncedFetch.cancel; // 컴포넌트 언마운트 시 debounce 해제
  }, [location.search]);

  const fetchAPI = async (queryString) => {
    // API 호출 로직
  };

  return <div>My Component</div>;
};