본문 바로가기
일상이 개발

Next.js × React Query 최적화 전략 – SSR, Hydration, 캐싱, 에러 처리까지 완벽 가이드

by 아빠고미 2025. 5. 9.
반응형

Next.js와 React Query 조합으로 데이터 패칭 최적화하기 – 실시간, 캐싱, 에러처리까지 완벽 가이드

Next.js는 SSR/SSG/ISR 등 다양한 렌더링 전략을 제공하고, React Query는 데이터 요청/캐싱/로딩/에러 관리를 자동화해주는 도구입니다.

이 둘을 함께 사용하면 성능, 사용자 경험, 개발 효율성을 동시에 끌어올릴 수 있습니다.

Next.js × React Query 최적화 전략 – SSR, Hydration, 캐싱, 에러 처리까지 완벽 가이드

이번 글에서는 Next.js와 React Query를 실무에서 어떻게 조합해 완성도 높은 데이터 패칭 시스템을 만들 수 있는지 렌더링 전략, prefetch, hydrate, 에러 핸들링, 캐싱 관리까지 총정리합니다.


1. 🔍 왜 React Query인가?

✅ React Query의 핵심 기능

  • 자동 캐싱 / 자동 리패칭
  • 로딩/에러/성공 상태 관리 내장
  • 배경 refetch, 쿼리 무효화, prefetch
  • devtools로 시각화 가능

📦 설치

npm install @tanstack/react-query

✅ 기본 구조

// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

<QueryClientProvider client={queryClient}>
  <Component {...pageProps} />
</QueryClientProvider>

✅ 데이터 패칭

import { useQuery } from '@tanstack/react-query';

const { data, isLoading, error } = useQuery({
  queryKey: ['posts'],
  queryFn: () => fetch('/api/posts').then(res => res.json()),
});

2. 🔄 Next.js와 조합할 때 주의할 점

📌 CSR 기본 – React Query 자체는 CSR 기반

  • Next.js에서 기본으로 사용하면 CSR 렌더링
  • 서버 측에서 데이터를 가져오려면 hydration 필요

✅ SSR과 함께 쓰는 방법 (Next.js 13 이하)

  • getServerSideProps or getStaticProps에서 미리 데이터 패칭
  • hydration으로 React Query에 넘겨줌
// pages/posts.js
export async function getServerSideProps() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
      posts,
    },
  };
}

➡️ Next.js에서 React Query를 제대로 쓰려면 서버 측 prefetch → 클라이언트 측 hydration 흐름을 이해해야 합니다.

3. 💧 서버 prefetch + 클라이언트 Hydration 구조

✅ dehydrate & hydrate를 이용한 데이터 초기화

Next.js에서 SSR or SSG 중 데이터를 미리 받아서 React Query에 전달하고, 클라이언트에서 해당 데이터로 초기화(Hydrate)하면 “깜빡임 없이” 렌더링할 수 있습니다.

// getServerSideProps 예시
import { dehydrate, QueryClient } from '@tanstack/react-query';

export async function getServerSideProps() {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('https://api.example.com/posts').then(res => res.json()),
  });

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
}
// _app.tsx 또는 페이지 내부
import { Hydrate } from '@tanstack/react-query';

<Hydrate state={pageProps.dehydratedState}>
  <Component {...pageProps} />
</Hydrate>

🎯 장점

  • 초기 로딩 없음 → 사용자 경험 향상
  • SEO 대응 → 서버에서 데이터 포함

4. 🗂️ 캐싱 전략 – staleTime과 cacheTime

✅ staleTime

데이터를 신선한 상태로 간주하는 시간 (ms 단위)

  • 기본값: 0 (페이지 포커스되면 바로 refetch)
  • 자주 바뀌지 않는 데이터 → 5분 이상 권장
useQuery({
  queryKey: ['settings'],
  queryFn: fetchSettings,
  staleTime: 1000 * 60 * 5, // 5분
});

✅ cacheTime

사용하지 않는 데이터가 메모리에서 유지되는 시간

useQuery({
  queryKey: ['settings'],
  queryFn: fetchSettings,
  cacheTime: 1000 * 60 * 10, // 10분
});

📌 전략 팁

  • staleTime은 사용자 기준 ‘데이터 최신성’
  • cacheTime은 개발자 기준 ‘메모리 보존’

5. ♻️ 쿼리 무효화와 재요청 전략

📌 mutate 후 자동 refetch – queryClient.invalidateQueries()

const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: updateUser,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['user'] });
  },
});

➡️ 데이터가 바뀌면 관련 쿼리를 무효화하고 자동으로 다시 불러오게 설정합니다.

✅ 예시: 게시글 수정 → 목록 리패치

onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['posts'] });
}

무효화 없이 수동으로 다시 불러오면 버그와 누락의 위험이 있습니다. React Query의 자동성능을 최대한 활용하세요.

6. ❗ 에러 처리 – 사용자 친화적인 피드백 제공

✅ useQuery의 에러 상태

const { data, isLoading, error } = useQuery({
  queryKey: ['profile'],
  queryFn: fetchUserProfile,
});

📌 에러 메시지 표시 예시

if (error) {
  return <ErrorMessage text="데이터를 불러오는 중 문제가 발생했습니다." />;
}

✅ 공통 에러 처리 전략

  • Axios 인터셉터 + toast 연동
  • React Query devtools로 에러 추적
  • HTTP 401/403/500 별 에러 분기

7. 🚀 Prefetch – 사용자가 도달하기 전에 미리 준비하자

📦 페이지 이동 전 데이터 미리 불러오기

queryClient.prefetchQuery({
  queryKey: ['products'],
  queryFn: fetchProducts,
});

✅ 링크 hover 기반 prefetch

<Link
  href="/products"
  onMouseEnter={() => {
    queryClient.prefetchQuery({
      queryKey: ['products'],
      queryFn: fetchProducts,
    });
  }}
>
  제품 보기
</Link>

➡️ 사용자가 이동하기 전에 데이터를 미리 준비해 “즉시 로딩되는 듯한 경험”을 제공합니다.


8. ⛓️ 병렬 요청 & 조건부 쿼리

✅ useQueries로 병렬 호출

const results = useQueries({
  queries: [
    { queryKey: ['user'], queryFn: fetchUser },
    { queryKey: ['settings'], queryFn: fetchSettings },
  ],
});

✅ 조건부 호출 (enabled)

useQuery({
  queryKey: ['userDetail', id],
  queryFn: () => fetchUserDetail(id),
  enabled: !!id, // id가 있을 때만 실행
});

➡️ 페이지에 따라 데이터 요청 시점을 제어하면 불필요한 네트워크 낭비를 줄이고 UX가 부드러워집니다.


9. 🧠 마무리 – Next.js × React Query는 최강의 데이터 조합

✨ 전략 요약

  • SSR/SSG에서 dehydrate → Hydrate 패턴으로 초기 렌더링 최적화
  • staleTime / cacheTime 조정으로 불필요한 요청 방지
  • invalidateQueries로 상태 변경 시 자동 refetch
  • 에러 핸들링과 토스트/뷰 처리 연계
  • prefetch로 UX 속도 향상
  • useQueries + enabled로 병렬 및 조건부 요청 설계

React Query는 단순히 데이터 가져오기 도구가 아닙니다. 데이터 흐름을 UX와 함께 최적화할 수 있는 설계의 핵심 도구입니다.


긴 글 읽어주셔서 감사합니다! 공감, 댓글, 공유로 응원해주시면 다음 글 제작에 큰 힘이 됩니다 🙌

반응형