Next.js + React Query로 서버 상태 최적화 – 동기화, 데이터 일관성, 성능 개선 전략 총정리
Next.js와 React Query는 단순히 데이터를 불러오는 도구를 넘어 서버 상태(server state)를 클라이언트에 안전하고 효율적으로 유지할 수 있는 강력한 조합입니다.
이번 글에서는 이 두 도구를 활용해 서버-클라이언트 상태의 동기화, 데이터 일관성 유지, 중복 요청 최소화, 성능 최적화까지 달성하는 실전 전략을 전방위적으로 안내합니다.
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. UI 먼저 변경 (setQueryData)
- 2. 서버 요청 보냄
- 3. 성공 시 → 쿼리 무효화 or 유지
- 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
- 📌 낙관적 업데이트는 되돌릴 준비도 함께
긴 글 읽어주셔서 감사합니다! 공감, 댓글, 공유로 응원해주시면 다음 글 제작에 큰 힘이 됩니다 🙌
'일상이 개발' 카테고리의 다른 글
Next.js 로그인/회원가입 UX 최적화 전략 – 인증 흐름부터 에러 처리, 리디렉션까지 완벽 가이드 (0) | 2025.05.12 |
---|---|
Next.js 로딩 UX 완전 정복 – Skeleton, Suspense, 버튼 피드백까지 실전 적용 전략 (0) | 2025.05.11 |
Next.js × React Query 최적화 전략 – SSR, Hydration, 캐싱, 에러 처리까지 완벽 가이드 (0) | 2025.05.09 |
Next.js UX 전략 완전 정복 – 로딩, 인터랙션, 접근성까지 실전 중심 가이드 (0) | 2025.05.08 |
Next.js 최적화 전략 – SSR, SSG, ISR 실전 활용법 총정리 (0) | 2025.05.07 |