tanstack-query 구 react-query , query key vailidate 중첩을 피해 최적화하기

이라껠·2024년 11월 21일

react query 는 강력한 서버 상태 관리 툴 !

받아온 데이터를 key로 관리하고 캐싱처리를 통해 빠른 화면 구현 가넝한~!
아주 뛰어난 친구다.

근데 문제는 내가 잘 못 씀.

tanstack-query

공식문서가 굉장히 잘 되어 있다구 합디다.

어쨌거나, 우리 프로젝트에서는 사용자가 뭔가 데이터 수정을 제출하고 난 뒤에 바로 get(Read) 요청 보내서 리스트를 업데이트 할 때 key를 무효화해서 데이터를 다시 받아오는 기술을 많이 쓰고있다.

그런데 이게 여기저기 중첩되면 무효화 무효화 무효화 라서 오히려 서버 리스폰스 기다리는 시간이 무진장 길어진다.

프론트 입장에서 왕로딩 = 왕지연 = 고객들 왕답답

따라서 중복 코드를 삭제하고 무효화시 선택할 수 있는 옵션들에 대해 기록하고 얼마나 최적화에 성공했는 지 기록해보고자 한다.


상황 재현

일단 겪은 문제는 !

같은 파라미터로 api를 호출하는 게 4번이나 반복되었다.
사용자는 화면이 그려지기까지 무려 총 26초를 기다려야 했다

여러 과정을 거쳐 찾아낸 원인은 !

모달 컴포넌트에서 useMutation의 onSuccess 함수에서 키를 무효화하는데,
상위 컴포넌트에서 또 쿼리 키를 무효화하는 코드가 중복으로 들어가 있었다.

어쨌거나 해낸 결과는 !

5.2배 속도 개선, 약 80.7%의 속도 향상 !


근데 코드 중복 사실을 파악하기 전에, 시도했던 방법은 useCallback을 이용한 최적화였다.
이전에 비슷한 코드도 이런 useCallback으로 함수를 빼고 재사용하니까 렌더링까지의 속도가 빨라진 경험이 있기 때문.

또한 동작마다 무효화할 키가 동일하기 때문에 이 방법 먼저 사용했다.

방법 1. useCallback

// ... existing code ...

export const DepoPopup = ({
  // ...props
}: DepoPopupProps) => {
  const queryClient = useQueryClient();
  const [alertMessage, setAlertMessage] = useState("");

  const invalidateDepoQueries = useCallback(() => {
    queryClient.invalidateQueries({ queryKey: ["depo"] });
    queryClient.invalidateQueries({ queryKey: ["depo", "deta"] });
  }, [queryClient]);

  const onSuccess = useCallback(() => {
    setAlertMessage("내역이 추가되었습니다.");
    invalidateDepoQueries();
  }, [invalidateDeposQueries]);

  // ... existing code ...
};

뭔가 빨라지긴 했으나 그럼에도 좀 느렸고, 이때부터 네트워크 탭에서 키 무효화가 일어나는 부분을 찾았다. 그 와중에 또 시도해본 것

방법 2. refetchType: none , exact 설정 추가

const onSuccess = useCallback(() => {
    setAlertMessage("내역이 추가되었습니다.");
    queryClient.invalidateQueries({
      queryKey: ["depo"],
      exact: false,
      refetchType: "none",
    });
  }, [queryClient]);

exact 완전일치를 false를 줘서 depo와 depo, deta 키를 둘 다 무효화하는 코드다.

방법 3. predicate 속성 사용

queryClient.invalidateQueries({
  predicate: (query) => query.queryKey.includes("depo")
})

쿼리 키에 포함 여부도 찾아서 키를 무효화하는 방법도 있다.
predicate는 쿼리 키 필터링하는 함수이다.

이런 저런걸 해보고도 뭔가 어딘가 어그러진 것 같아서 하나하나 다 뜯어보기 시작했다.

코드 확인

// 팝업 내 호출

 const onSuccess = useCallback(() => {
    setAlertMessage("내역이 추가되었습니다.");
    queryClient.invalidateQueries({ queryKey: ["depo"] });
    queryClient.invalidateQueries({ queryKey: ["depo", "deta"] });
  }, [queryClient]);
  const onError = useCallback((e: AxiosError<DataResponse>) => {
    setAlertMessage(
      e.response?.data.message || "내역 추가에 실패했습니다."
    );
  }, []);
  const { mutateAsync, isPending } = useCreateDepo({ onSuccess, onError });
  const onSubmit = useCallback(
    (v: CreateDepoValues) => {
      mutateAsync(v);
    },
    [mutateAsync]
  );
// 팝업 밖의 상위 컴포넌트 호출 

  const onCloseUpdatePopup = useCallback(() => {
    setSelectedDepo(undefined);
    setOpenUpdatePopup(false);
    queryClient.invalidateQueries({ queryKey: ["depo", "deta"] });
    queryClient.invalidateQueries({ queryKey: ["depo"] });
  }, [queryClient]);

  const [openCreatePopup, setOpenCreatePopup] = useState(false);
  const onCloseCreatePopup = useCallback(() => {
    setSelectedDepo(undefined);
    setOpenCreatePopup(false);
     queryClient.invalidateQueries({ queryKey: ["depo", "deta"] });
    queryClient.invalidateQueries({ queryKey: ["depo"] });
  }, [queryClient]);

그냥 중첩이 많이 일어난 거 였다.
해당 동작마다 쿼리키 무효화가 1번이 아니라, 팝업 여닫을 때도 다시 실행되는 게 문제 ㅡ,.ㅡ

그래서 일단 최적화 유지하면서 중복 코드를 제거했더니 엄청 빨라졌다..

구현한 김에 시간복잡도도 구해봤다.

방법 1: 개별 쿼리키 무효화 O(1) + O(1)

두 번의 호출이 필요하지만, 각각은 상수 시간
메모리 상의 정확한 위치를 바로 찾아감

queryClient.invalidateQueries({ queryKey: ["depo", "deta"] });
queryClient.invalidateQueries({ queryKey: ["depo"] });

방법 2: exact: false 사용 O(1)

내부적으로 최적화된 트리 구조 사용
단일 호출로 처리
"deposit"으로 시작하는 모든 쿼리를 효율적으로 찾음

queryClient.invalidateQueries({
  queryKey: ["depo"],
  exact: false
});

방법 3: predicate 사용 O(n)

모든 캐시된 쿼리를 순회 (O(n))
각 쿼리마다 includes() 검사 필요
가장 느린 방식

queryClient.invalidateQueries({
  predicate: (query) => query.queryKey.includes("depo")
});

결과 26초 -> 5초

딱 필요한 api만 호출해서 엄청난 시간 단축 !!

수치적으로 표현하면 아래와 같다.


실행 시간 감소율

  • 감소율 = ((기존 시간 - 개선 시간) / 기존 시간) × 100
    = ((26초 - 5초) / 26초) × 100
    = 80.77%

속도 향상 배수

  • 속도 향상 = 기존 시간 / 개선 시간
    = 26초 / 5초
    = 5.2배 빨라짐

성능 지표 (KPI)

  • 응답 시간: 26초 → 5초
  • 절대적 시간 감소: 21초
  • 상대적 성능 개선: 520%

사용자 경험 관점
웹 성능 기준(Core Web Vitals)

  • 기존: 26초 (Poor)
  • 개선: 5초 (Needs Improvement)
  • 목표: 3초 이하 (Good)

성능 개선 결과

  • 🚀 실행 시간 80.77% 감소
  • ⚡️ 5.2배 성능 향상
  • 📊 응답 시간: 26초 → 5초
  • 💯 사용자 경험 2단계 향상 (Poor → Needs Improvement)

졸려서 그럼 이만

0개의 댓글