React Query 완벽 가이드: useQuery vs fetchQuery vs useMutation + 캐시 전략

1. useQuery

사용 시기

  • 컴포넌트에서 데이터를 실시간으로 표시할 때
  • 자동 리프레시나 폴링이 필요할 때
  • 캐시된 데이터를 재사용할 때
  • 데이터의 로딩/에러 상태를 UI에 표시해야 할 때

코드 예시

// 기본 사용  
const { data, isLoading, error } = useQuery({  
queryKey: \['posts'\],  
queryFn: fetchPosts  
});

// 자동 리프레시  
const { data } = useQuery({  
queryKey: \['notifications'\],  
queryFn: fetchNotifications,  
refetchInterval: 1000 \* 60 // 1분마다 갱신  
});

// 조건부 실행  
const { data } = useQuery({  
queryKey: \['post', postId\],  
queryFn: () => fetchPost(postId),  
enabled: !!postId,  
staleTime: 1000 \* 60, // 1분 동안 신선한 상태 유지  
cacheTime: 1000 \* 60 \* 5 // 5분 동안 캐시 유지  
});  

2. fetchQuery

사용 시기

  • 즉시 데이터가 필요한 경우
  • React Router의 loader/action 내부
  • 컴포넌트 외부에서 데이터 fetching이 필요할 때
  • Promise 기반의 동기적 처리가 필요할 때

코드 예시

// loader에서 기본 사용  
export const loader = (queryClient: QueryClient) => async () => {  
const data = await queryClient.fetchQuery({  
queryKey: \['posts'\],  
queryFn: fetchPosts  
});  
return { data };  
};

// 캐시 확인 후 fetch  
export const loader = (queryClient: QueryClient) => async () => {  
// 1. 캐시 확인  
const cached = queryClient.getQueryData(\['posts'\]);  
if (cached) return { data: cached };

// 2. 캐시 없으면 fetch  
const data = await queryClient.fetchQuery({  
queryKey: \['posts'\],  
queryFn: fetchPosts,  
staleTime: 1000 \* 60  
});  
return { data };  
};

// ensureQueryData 사용  
export const loader = (queryClient: QueryClient) => async () => {  
const data = await queryClient.ensureQueryData({  
queryKey: \['posts'\],  
queryFn: fetchPosts,  
staleTime: 1000 \* 60  
});  
return { data };  
};  

3. useMutation

사용 시기

  • 데이터 생성/수정/삭제 작업
  • 사용자 인터랙션에 의한 데이터 변경
  • 낙관적 업데이트가 필요한 경우
  • 변경 작업의 성공/실패에 따른 UI 처리가 필요할 때

코드 예시

// 기본 사용  
const mutation = useMutation({  
mutationFn: createPost,  
onSuccess: () => {  
queryClient.invalidateQueries({ queryKey: \['posts'\] });  
toast.success('포스트가 생성되었습니다');  
}  
});

// 낙관적 업데이트  
const mutation = useMutation({  
mutationFn: updateTodo,  
onMutate: async (newTodo) => {  
await queryClient.cancelQueries({ queryKey: \['todos'\] });  
const previousTodos = queryClient.getQueryData(\['todos'\]);  
queryClient.setQueryData(\['todos'\], old => \[...old, newTodo\]);  
return { previousTodos };  
},  
onError: (err, newTodo, context) => {  
queryClient.setQueryData(\['todos'\], context.previousTodos);  
}  
});  

4. 캐시 관리 전략

캐시 조작 메서드

// 1. 캐시 조회  
const cachedData = queryClient.getQueryData(\['posts'\]);  
const queryState = queryClient.getQueryState(\['posts'\]);

// 2. 캐시 수정  
queryClient.setQueryData(\['posts'\], newData);  
queryClient.setQueryData(\['posts'\], old => \[...old, newPost\]);

// 3. 캐시 무효화  
queryClient.invalidateQueries({ queryKey: \['posts'\] });  
queryClient.invalidateQueries({  
predicate: (query) => query.queryKey\[0\] === 'posts'  
});  

캐시 설정

  • staleTime: 데이터가 "신선"하다고 간주되는 시간
  • cacheTime: 데이터가 캐시에 유지되는 시간

최적화된 캐시 전략

// loader에서의 최적화된 캐시 사용  
export const loader = (queryClient: QueryClient) => async () => {  
try {  
// 1. 캐시 확인  
const cached = queryClient.getQueryData(\['posts'\]);  
if (cached) {  
const state = queryClient.getQueryState(\['posts'\]);  
// 캐시가 신선한 경우에만 사용  
if (!state?.isStale) return { data: cached };  
}

// 2. 캐시가 없거나 오래된 경우 새로 fetch
return queryClient.ensureQueryData({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60,
cacheTime: 1000 * 60 * 5
});


} catch (error) {  
// 에러 처리  
console.error('Data fetching failed:', error);  
throw error;  
}  
};  

5. 각 방법의 주요 차이점

1. 사용 환경

  • useQuery: React 컴포넌트 내부만
  • fetchQuery: 어디서든 사용 가능
  • useMutation: React 컴포넌트 내부만

2. 제공하는 기능

  • useQuery: 자동 캐싱, 자동 리프레시, 상태 관리, 데이터 구독
  • fetchQuery: Promise 기반 동작, 수동 캐시 제어
  • useMutation: 데이터 변경, 낙관적 업데이트, 롤백 기능

3. 상태 관리

// useQuery - 풍부한 상태 정보  
const {  
data,  
isLoading,  
error,  
isFetching,  
isStale,  
refetch,  
status  
} = useQuery(options);

// fetchQuery - Promise만 제공  
const data = await queryClient.fetchQuery(options);

// useMutation - 변경 관련 상태  
const {  
mutate,  
mutateAsync,  
isLoading,  
error,  
isSuccess,  
reset  
} = useMutation(options);  

+ Recent posts