본문 바로가기

개발 공부/React

React useMutation + invalidateQueries

서버 데이터 변경(mutation) → 기존 캐시 무효화 → 최신 데이터 재요청(query 재실행)

 

서버 상태와 React Query의 역할

React Query가 관리하는 건 UI 상태가 아니라 서버 상태입니다.

서버 상태의 특징

  • 서버가 진짜 진실(Single Source of Truth)
  • 클라이언트가 마음대로 바꾸면 안 됨
  • 언제든 다른 사용자/요인으로 바뀔 수 있음

그래서 React Query는 이렇게 동작합니다.

  • 내가 아는 데이터는 캐시일 뿐이고, 틀릴 수 있다

 


updateAppointmentStatus: 서버 변경

export async function updateAppointmentStatus(input: {
  id: string;
  status: AppointmentStatus;
}) {
  try {
    const appointment = await prisma.appointment.update({
      where: { id: input.id },
      data: { status: input.status },
    });

    return appointment;
  } catch (error) {
    console.error("Error updating appointment:", error);
    throw new Error("Failed to update appointment");
  }
}
  • 여기서 실제로 일어나는 일은 단 하나입니다.
  • DB의 appointment 상태가 변경됨

중요한 점은  이 시점에서 프론트 캐시는 아무 변화가 없습니다

 


useMutation은 "실행 트리거"다

export function useUpdateAppointmentStatus() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateAppointmentStatus,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["getAppointments"] });
    },
    onError: (error) => console.error("Failed to update appointment:", error),
  });
}

useMutation의 역할

  • 서버에 변경 요청을 보내는 버튼
  • 자동으로 캐시를 바꾸지 않음
  • mutation은 캐시를 갱신하지 않는다
  •  mutation은 "서버가 바뀌었다"는 사실만 만든다

 


 

왜 invalidateQueries를 써야 할까?

invalidateQueries가 하는 일

queryClient.invalidateQueries({ queryKey: ["getAppointments"] });
  • 이 코드는 이렇게 번역할 수 있습니다.
  • "getAppointments 캐시는 이제 믿을 수 없으니 무효 처리해라"

invalidate = 삭제 

invalidate = "다음에 필요하면 다시 가져와" 

 

invalidate 이후 실제로 일어나는 흐름

  1. appointment 상태 변경 (mutation 성공)
  2. getAppointments 캐시 → stale 상태
  3. 화면 어딘가에서 useQuery(['getAppointments']) 사용 중
  4. React Query 판단 "이 쿼리는 stale이네? 다시 fetch 해야겠다"
  5. 서버에서 최신 데이터 재요청
  6. UI 자동 갱신

 invalidateQueries를 안 쓰면 생기는 문제

상황

  • 서버에서는 상태가 바뀜
  • 캐시는 이전 데이터 유지

결과

  • 화면은 안 바뀐 것처럼 보임
  • 새로고침해야만 반영됨

왜 그냥 setQueryData 안 쓰나요?

queryClient.setQueryData(['getAppointments'], ...)

 

setQueryData 단점

  • 서버 응답 구조를 정확히 알아야 함
  • 로직이 복잡해짐
  • 실수하면 서버 상태와 불일치

invalidateQueries 장점

  • 서버를 다시 신뢰
  • 로직 단순
  • 유지보수 안정적

그래서 기본 전략은 invalidate입니다.

 


 

 

invalidateQueries 할 때 쿼리 인풋은 안 넣어도 되나요?

결론부터 말하면 "항상 안 넣어도 되는 건 아니고, 쿼리 키를 어떻게 설계했느냐에 따라 다릅니다."

쿼리 키에 인풋이 없는 경우 (고정 키)

useQuery({
  queryKey: ['getAppointments'],
  queryFn: fetchAppointments,
});
  • 이런 구조라면 invalidate도 동일하게 인풋 없이 써도 됩니다.
queryClient.invalidateQueries({ queryKey: ['getAppointments'] });
  • 이 경우 캐시 대상이 하나뿐이라 모호함이 없습니다.

쿼리 키에 인풋이 포함된 경우 (필터, 페이지, 조건)

useQuery({
  queryKey: ['getAppointments', { status, page }],
  queryFn: fetchAppointments,
});
  • 이 경우 invalidate 전략이 두 가지로 나뉩니다.

 특정 조건만 무효화

queryClient.invalidateQueries({
  queryKey: ['getAppointments', { status, page }],
});
  • 정확하지만
  • 조건이 많아질수록 관리가 어려움

prefix 기준으로 전체 무효화 (실무에서 가장 많이 사용)

queryClient.invalidateQueries({ queryKey: ['getAppointments'] });

React Query는 queryKey를 prefix 매칭으로 처리하기 때문에,
['getAppointments']는 아래 쿼리들을 전부 stale 처리합니다.

  • ['getAppointments', { status: 'CONFIRMED' }]
  • ['getAppointments', { status: 'COMPLETED', page: 2 }]

 


 

mutation 결과로 캐시를 직접 업데이트하면 더 효율적이지 않나요?


쿼리 비용만 보면 mutation 결과를 활용해서 캐시를 직접 수정하는 편이 더 효율적일 수 있습니다.

 

 mutation 결과로 캐시를 직접 갱신하는 방식

return useMutation({
  mutationFn: updateAppointmentStatus,
  onSuccess: (updated) => {
    queryClient.setQueryData(['getAppointments'], (old: any[]) => {
      if (!old) return old;
      return old.map((apt) =>
        apt.id === updated.id
          ? { ...apt, status: updated.status }
          : apt
      );
    });
  },
});

장점

  • 네트워크 재요청 없음 → 성능/비용 절약
  • UI 즉시 반영 → 체감 UX 좋음

단점 

  • 필터/페이지/정렬 캐시가 여러 개면 전부 수동 업데이트 필요
  • 서버에서 같이 바뀌는 값(updatedAt, 통계, 연관 데이터 등)을 놓치기 쉬움
  • 캐시와 서버 상태 불일치 위험

'개발 공부 > React' 카테고리의 다른 글

Next.js API Route  (0) 2026.01.11
Clerk PricingTable  (0) 2025.12.25
탭 간 상태 동기화 - React  (0) 2025.12.21
Clerk 로그인/회원가입  (0) 2025.11.29
UploadThing을 통한 이미지 업로드 구현하기  (0) 2025.11.27