일상이 개발

Next.js + React Query로 서버 상태 완벽 최적화하기: 실전 적용 가이드

디어노미 2025. 4. 9. 05:55
반응형

⚙️ Next.js + React Query로 서버 상태 최적화하기

클라이언트 중심의 React 앱이 점점 커지고, 페이지 단위로 분리된 Next.js 프로젝트가 대세가 되면서 이제는 “데이터 패칭” 그 자체보다도 서버 상태를 어떻게 효율적으로 관리할 것인가가 더 중요한 이슈가 되었습니다.

그 중심에 있는 조합이 바로 Next.js + React Query입니다.

단순한 useEffect + fetch가 아니라, SSR/SSG와 클라이언트 사이드의 상태까지 아우르는 통합적 서버 상태 관리 전략을 구성할 수 있게 되죠.

이번 글에서는 실제로 활용 가능한 서버 상태 최적화 전략을 Next.js와 React Query 기반으로 차근차근 설명드리겠습니다.


1. 왜 서버 상태 최적화가 필요할까?

  • 비동기적으로 외부(서버)에서 가져오는 데이터
  • 여러 컴포넌트에서 공유되고 사용됨
  • 변경 가능성이 있으며, 동기화 필요

보통은 useEffect + fetch로 처리하지만, 다음과 같은 문제가 생깁니다:

  • 중복 fetch → 비효율적인 네트워크 요청
  • 전역 상태와 서버 상태가 섞임
  • 초기화/로딩 처리 분산
  • 캐싱/갱신 전략 부재

React Query의 장점

  • 요청 캐싱 / 자동 갱신
  • 요청 중복 제거 / 쿼리 키 기반 상태 분리
  • 데이터 prefetch / SSR/SSG 대응

2. 기본 세팅

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

const queryClient = new QueryClient();

function MyApp({ Component, pageProps }) {
  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
    </QueryClientProvider>
  );
}

3. 기본적인 서버 상태 패칭 구조

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

function fetchUser() {
  return fetch('/api/me').then(res => res.json());
}

function Profile() {
  const { data, isLoading, error } = useQuery(['user'], fetchUser);

  if (isLoading) return <p>로딩 중...</p>;
  if (error) return <p>에러 발생</p>;

  return <div>{data.name}님 환영합니다</div>;
}

4. SSR/SSG와 React Query 조합하기

// pages/posts.tsx
import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query';

function fetchPosts() {
  return fetch('https://api.example.com/posts').then(res => res.json());
}

function PostsPage() {
  const { data } = useQuery(['posts'], fetchPosts);
  return <div>{data.length}개의 글</div>;
}

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

  await queryClient.prefetchQuery(['posts'], fetchPosts);

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

5. 서버 상태 무효화 및 갱신 전략

const queryClient = useQueryClient();

const mutation = useMutation(
  (newPost) => fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify(newPost),
  }),
  {
    onSuccess: () => {
      queryClient.invalidateQueries(['posts']);
    },
  }
);

6. 조건부 패칭 및 staleTime 활용

const { data } = useQuery(['user'], fetchUser, {
  enabled: !!session,
});

const { data } = useQuery(['products'], fetchProducts, {
  staleTime: 300000, // 5분
  cacheTime: 600000  // 10분
});

7. 데이터 미리 불러오기

const queryClient = useQueryClient();

const handleHover = () => {
  queryClient.prefetchQuery(['user'], fetchUser);
};

8. 실전 패턴 예시

구성 방법
목록 초기 데이터 getStaticProps + prefetchQuery
상세 페이지 useQuery 클라이언트 요청
댓글 등록 useMutation + invalidateQueries
로그인 후 접근 SSR + 쿠키 인증

9. React Query DevTools

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

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

✅ 마무리 요약

  • useQuery: fetch + 캐싱 + 상태 관리
  • SSR 대응: getServerSideProps + dehydrate
  • 갱신: useMutation + invalidateQueries
  • UX 최적화: staleTime, enabled, prefetchQuery

다음 글에서는 App Router 기반에서 React Query + Suspense 조합을 다뤄보겠습니다.

반응형