
react query 는 강력한 서버 상태 관리 툴 !
받아온 데이터를 key로 관리하고 캐싱처리를 통해 빠른 화면 구현 가넝한~!
아주 뛰어난 친구다.
근데 문제는 내가 잘 못 씀.
공식문서가 굉장히 잘 되어 있다구 합디다.
어쨌거나, 우리 프로젝트에서는 사용자가 뭔가 데이터 수정을 제출하고 난 뒤에 바로 get(Read) 요청 보내서 리스트를 업데이트 할 때 key를 무효화해서 데이터를 다시 받아오는 기술을 많이 쓰고있다.
그런데 이게 여기저기 중첩되면 무효화 무효화 무효화 라서 오히려 서버 리스폰스 기다리는 시간이 무진장 길어진다.
프론트 입장에서 왕로딩 = 왕지연 = 고객들 왕답답
따라서 중복 코드를 삭제하고 무효화시 선택할 수 있는 옵션들에 대해 기록하고 얼마나 최적화에 성공했는 지 기록해보고자 한다.

같은 파라미터로 api를 호출하는 게 4번이나 반복되었다.
사용자는 화면이 그려지기까지 무려 총 26초를 기다려야 했다
모달 컴포넌트에서 useMutation의 onSuccess 함수에서 키를 무효화하는데,
상위 컴포넌트에서 또 쿼리 키를 무효화하는 코드가 중복으로 들어가 있었다.
5.2배 속도 개선, 약 80.7%의 속도 향상 !
근데 코드 중복 사실을 파악하기 전에, 시도했던 방법은 useCallback을 이용한 최적화였다.
이전에 비슷한 코드도 이런 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 ...
};
뭔가 빨라지긴 했으나 그럼에도 좀 느렸고, 이때부터 네트워크 탭에서 키 무효화가 일어나는 부분을 찾았다. 그 와중에 또 시도해본 것
const onSuccess = useCallback(() => {
setAlertMessage("내역이 추가되었습니다.");
queryClient.invalidateQueries({
queryKey: ["depo"],
exact: false,
refetchType: "none",
});
}, [queryClient]);
exact 완전일치를 false를 줘서 depo와 depo, deta 키를 둘 다 무효화하는 코드다.
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번이 아니라, 팝업 여닫을 때도 다시 실행되는 게 문제 ㅡ,.ㅡ
그래서 일단 최적화 유지하면서 중복 코드를 제거했더니 엄청 빨라졌다..
구현한 김에 시간복잡도도 구해봤다.
두 번의 호출이 필요하지만, 각각은 상수 시간
메모리 상의 정확한 위치를 바로 찾아감
queryClient.invalidateQueries({ queryKey: ["depo", "deta"] });
queryClient.invalidateQueries({ queryKey: ["depo"] });
내부적으로 최적화된 트리 구조 사용
단일 호출로 처리
"deposit"으로 시작하는 모든 쿼리를 효율적으로 찾음
queryClient.invalidateQueries({
queryKey: ["depo"],
exact: false
});
모든 캐시된 쿼리를 순회 (O(n))
각 쿼리마다 includes() 검사 필요
가장 느린 방식
queryClient.invalidateQueries({
predicate: (query) => query.queryKey.includes("depo")
});

딱 필요한 api만 호출해서 엄청난 시간 단축 !!
수치적으로 표현하면 아래와 같다.
실행 시간 감소율
속도 향상 배수
성능 지표 (KPI)
사용자 경험 관점
웹 성능 기준(Core Web Vitals)
성능 개선 결과
졸려서 그럼 이만