본문 바로가기
일상이 개발

Next.js + React Query로 서버 상태 완전 정복 – 동기화, 캐싱, UX 최적화 전략 총정리

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

Next.js + React Query로 서버 상태 최적화 – 동기화, 데이터 일관성, 성능 개선 전략 총정리

Next.js와 React Query는 단순히 데이터를 불러오는 도구를 넘어 서버 상태(server state)를 클라이언트에 안전하고 효율적으로 유지할 수 있는 강력한 조합입니다.

 

Next.js + React Query로 서버 상태 완전 정복 – 동기화, 캐싱, UX 최적화 전략 총정리

이번 글에서는 이 두 도구를 활용해 서버-클라이언트 상태의 동기화, 데이터 일관성 유지, 중복 요청 최소화, 성능 최적화까지 달성하는 실전 전략을 전방위적으로 안내합니다.


1. 🔍 서버 상태(Server State)란 무엇인가?

📌 서버 상태의 정의

  • 클라이언트의 로컬 상태(useState, useReducer 등)와 구분
  • API 또는 DB에서 가져오는 외부 데이터
  • 네트워크 요청을 통해 fetch, mutate되는 정보

✅ 서버 상태의 특징

  • 항상 최신 정보를 보장하기 어려움
  • 네트워크 지연/에러 발생 가능
  • 데이터가 클라이언트 간 공유되지 않음

➡️ 이 상태를 ‘최신화’하고 ‘캐시’하며 ‘안정적으로 동기화’하는 것이 React Query의 주요 역할입니다.


2. ⚙️ Next.js에서 서버 상태 관리가 어려운 이유

❌ 일반적인 CSR 한계

  • 페이지 진입 시마다 동일한 API 요청 발생
  • 이전 페이지와 현재 페이지 간 캐시 공유 불가
  • 데이터 일관성이 깨지고, UX가 불안정해짐

📌 Next.js 구조에서 발생할 수 있는 예시

// A 페이지에서 product 리스트를 fetch
// B 페이지에서 product 상세 정보 요청
// → 같은 product인데 API 2번 요청됨

➡️ React Query를 함께 사용하면 캐싱, 공유, 리패칭 타이밍 제어가 가능해지고 UX와 성능을 동시에 향상시킬 수 있습니다.


3. 🔄 서버 상태 동기화 전략

✅ invalidateQueries를 통한 강력한 자동화

const mutation = useMutation({
  mutationFn: updateProduct,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['products'] });
  },
});
  • 사용자가 데이터를 수정하면 해당 쿼리를 무효화
  • 무효화된 쿼리는 자동으로 다시 fetch됨

✅ useQueryClient().setQueryData() – 직접 데이터 갱신

즉각적인 UI 업데이트를 원할 때 유용

queryClient.setQueryData(['product', id], updatedData);

➡️ 이렇게 하면 API를 다시 호출하지 않고도 사용자 입장에서는 데이터가 즉시 반영된 것처럼 느껴집니다.

4. 🧠 데이터 일관성 유지 전략

✅ 쿼리 키 관리가 핵심

  • 모든 쿼리는 queryKey를 기준으로 관리됨
  • 동일한 키 = 동일한 캐시 = 일관된 데이터
  • queryKey를 배열로 표현하면 구조화 용이
useQuery({
  queryKey: ['product', id],
  queryFn: () => fetchProduct(id),
});

✅ 데이터를 공유하려면 queryKey 통일

// A 페이지
useQuery(['product', 1]);

// B 페이지
useQuery(['product', 1]); // → A의 캐시를 그대로 사용함

➡️ queryKey가 다르면 같은 API를 요청해도 React Query는 “다른 요청”으로 인식하고 중복 호출됩니다.


5. ⏳ staleTime과 refetch 전략 – 데이터 신선도 유지

✅ staleTime

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

  • 짧게: 실시간성이 중요할 때 (예: 재고 수량)
  • 길게: 자주 바뀌지 않는 데이터 (예: 설정값, 정적 목록)
useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  staleTime: 1000 * 30, // 30초
});

✅ refetchOnWindowFocus

페이지 포커스 복귀 시 자동 리패칭

  • true (기본): 데이터가 stale하면 refetch
  • false: 수동 컨트롤 가능
useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  refetchOnWindowFocus: false,
});

6. 🧩 멀티탭 동기화 – 여러 창에서도 최신 상태 유지

React Query는 BroadcastChannel API를 통해 브라우저 탭 간 캐시 변경 사항을 공유할 수 있습니다.

✅ 동작 방식

  • 한 탭에서 쿼리가 invalidated 되면
  • 다른 탭에서도 해당 쿼리가 stale 처리됨

➡️ 사용자 입장에서는 “모든 탭이 항상 동기화된 상태”처럼 보입니다.

✅ 사용 조건

  • persistor 또는 BroadcastChannel 기반의 캐시 공유 설정
  • React Query Devtools도 탭 간 공유됨

7. ⚡ 서버 상태 최적화를 위한 성능 개선 전략

✅ select로 필요한 데이터만 추출

전체 API 응답이 커도 필요한 데이터만 가져올 수 있음

useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  select: (data) => data.profile.name,
});

✅ keepPreviousData로 전환 부드럽게

페이지네이션이나 필터링 시 이전 데이터를 유지

useQuery({
  queryKey: ['products', page],
  queryFn: () => fetchProducts(page),
  keepPreviousData: true,
});

➡️ 사용자 입장에서는 “데이터가 순간 사라지는 불편함”을 방지할 수 있어요.

8. 🔄 트랜잭션 기반 상태 처리 – 일괄적 쿼리 갱신

📌 여러 상태를 동시에 갱신해야 할 때

  • 상품 정보 수정 후 리스트, 상세 모두 새로고침 필요
  • 회원 탈퇴 시: 프로필, 알림, 대시보드 모두 동기화 필요

✅ invalidateQueries 병렬 호출

onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['products'] });
  queryClient.invalidateQueries({ queryKey: ['product', id] });
  queryClient.invalidateQueries({ queryKey: ['dashboard'] });
}

✅ 또는 group queryKey로 묶기

queryClient.invalidateQueries({ predicate: (query) =>
  query.queryKey[0] === 'product'
});

9. ✨ Optimistic Update – 먼저 보여주고 나중에 확인

✅ 낙관적 UI 업데이트 흐름

  1. 1. UI 먼저 변경 (setQueryData)
  2. 2. 서버 요청 보냄
  3. 3. 성공 시 → 쿼리 무효화 or 유지
  4. 4. 실패 시 → rollback

📦 실전 코드

const mutation = useMutation(updateTodo, {
  onMutate: async (updatedTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] });
    const previousTodos = queryClient.getQueryData(['todos']);
    queryClient.setQueryData(['todos'], (old) =>
      old.map((t) => t.id === updatedTodo.id ? updatedTodo : t)
    );
    return { previousTodos };
  },
  onError: (err, variables, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos);
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

➡️ 사용자에게 즉각적인 피드백을 제공하면서, 실패해도 안전하게 되돌릴 수 있는 설계입니다.


10. 🧠 마무리 – 서버 상태 최적화의 진짜 목표

✅ 핵심 정리

  • 서버 상태는 항상 ‘최신’이어야 하지만 ‘불필요하게 요청’되면 안 됨
  • React Query의 캐싱, 무효화, 동기화 전략을 활용하자
  • Hydration, prefetch, optimistic update로 UX까지 부드럽게
  • Next.js와 함께 사용하면 SSR/CSR 환경 모두에 대응 가능

✅ 실무 팁 요약

  • 📌 queryKey 구조를 일관성 있게 설계할 것
  • 📌 staleTime은 서비스 특성에 맞게 조정
  • 📌 mutate 이후 반드시 invalidate or setQueryData
  • 📌 낙관적 업데이트는 되돌릴 준비도 함께

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

반응형