서버에서 데이터 받아올 때마다 로딩 화면 보여주고 계신가요? 사용자들은 이미 떠나고 있을지도 몰라요.
안녕하세요! 오늘은 React Query로 서버 상태 관리하기에 대해 진짜 실전에서 쓸 수 있는 내용들을 정리해볼까 해요. 있잖아요, 처음 리액트로 프로젝트 시작하면 useEffect에 fetch 넣고 useState로 로딩 상태 관리하고... 이거 반복하다 보면 진짜 코드가 엉망이 되거든요. 저도 처음엔 "뭐 이정도면 괜찮은데?" 했는데, 프로젝트 규모가 커지니까 완전 헬게이트더라고요. 그때 React Query를 알게 됐고, 솔직히 말하자면 진작 쓸걸 후회했어요. 서버 상태 관리부터 캐싱 전략, 에러 처리까지 한방에 해결되니까요. 여러분도 한번 제대로 배워보시면 개발이 정말 편해질 거예요!
? React Query, 왜 써야 할까요?

React Query로 서버 상태 관리를 시작하기 전에, 솔직히 말씀드리자면 저도 처음엔 "굳이?" 라는 생각이 들었거든요. useState랑 useEffect만으로도 충분히 API 호출할 수 있잖아요. 근데요... 프로젝트가 커지면서 진짜 달라지더라고요.
예전에 제가 작업했던 대시보드 프로젝트가 있었는데요, 사용자 정보를 여러 컴포넌트에서 불러와야 했어요. 그래서 각 컴포넌트마다 useEffect로 API를 호출했죠. 그랬더니 같은 데이터를 5번씩 불러오는 상황이 발생했어요. 네트워크 탭을 보고 진짜 놀랐죠.
전통적인 방식의 문제점
일반적으로 React에서 서버 상태를 관리할 때 useState와 useEffect를 사용하잖아요. 근데 이 방식은 몇 가지 큰 문제가 있어요. 캐싱이 안 되고, 로딩 상태와 에러 처리를 매번 직접 만들어야 하고, 데이터가 오래됐는지 확인할 방법도 없거든요.
React Query 없이 서버 상태를 관리하면 보일러플레이트 코드가 엄청나게 늘어나요. 매번 loading, error, data 상태를 선언하고, try-catch 문을 작성하고, 컴포넌트가 언마운트될 때 정리하는 코드까지... 정말 반복 작업의 연속이에요.
React Query가 해결해주는 것들
React Query는 서버 상태 관리를 완전히 다르게 접근해요. 가장 좋았던 건, 자동 캐싱이에요. 한 번 불러온 데이터는 캐시에 저장되어서 다른 컴포넌트에서도 즉시 사용할 수 있거든요. 그리고 백그라운드에서 자동으로 데이터를 업데이트해주는 기능도 있어요.
뭐랄까, Redux처럼 글로벌 상태 관리 도구인데 서버 데이터에 특화되어 있다고 생각하면 돼요. 근데 Redux보다 훨씬 간단하죠. 제 경험상 보일러플레이트 코드가 70% 이상 줄어들었어요.
? 캐싱 전략: 성능을 좌우하는 핵심

React Query의 캐싱 전략을 제대로 이해하면, 애플리케이션 성능을 엄청나게 개선할 수 있어요. 사실은요, 처음엔 저도 기본 설정만 쓰다가 나중에 캐싱 옵션들을 알아보고 깜짝 놀랐거든요. "아, 이렇게 세밀하게 조절할 수 있구나!"
캐시 상태의 생명주기
React Query에서 캐시는 fresh, stale, inactive 세 가지 상태를 가지고 있어요. fresh는 최신 상태, stale은 오래된 상태, inactive는 사용되지 않는 상태를 의미하죠. 이 개념을 이해하는 게 정말 중요해요.
| 캐시 옵션 | 기본값 | 설명 | 추천 사용 케이스 |
|---|---|---|---|
| staleTime | 0ms | 데이터가 fresh에서 stale로 전환되는 시간 | 자주 변하지 않는 정적 데이터 |
| cacheTime | 5분 | inactive 상태 데이터가 메모리에서 제거되는 시간 | 모든 일반적인 상황 |
| refetchOnMount | true | 컴포넌트 마운트 시 자동 리패치 | 실시간성이 중요한 데이터 |
| refetchOnWindowFocus | true | 창 포커스 시 자동 리패치 | 사용자 대시보드, 알림 |
| refetchInterval | false | 주기적 자동 리패치 간격 | 주식 시세, 실시간 모니터링 |
실전 캐싱 설정 팁
제가 실제 프로젝트에서 사용하는 캐싱 전략을 공유해드릴게요. 데이터 타입별로 다르게 설정하는 게 포인트예요.
staleTime을 길게 설정하세요. 저는 보통 5분에서 30분 정도로 설정해요. 이런 데이터는 자주 바뀌지 않으니까 네트워크 요청을 최소화하는 게 좋거든요. 실제로 이렇게 설정하고 나서 API 호출 횟수가 80% 정도 줄었어요.
staleTime을 0으로 두고 refetchOnWindowFocus를 true로 설정하세요. 사용자가 탭을 전환했다 돌아오면 자동으로 최신 데이터를 가져와요. SNS 피드 같은 거 만들 때 완전 유용하더라고요.
캐시 무효화 전략
캐싱만큼 중요한 게 캐시 무효화예요. 사용자가 데이터를 수정했는데 화면에 반영이 안 되면 진짜 답답하잖아요. React Query는 이것도 간단하게 해결할 수 있어요.
invalidateQueries를 사용하면 특정 쿼리의 캐시를 무효화하고 자동으로 리패치할 수 있어요. 예를 들어, 게시글을 작성한 후 목록을 다시 불러와야 할 때 완전 편하죠. 뭐랄까, 알아서 해주니까 신경 쓸 게 하나 줄어드는 느낌?
제 경험상 가장 실수하기 쉬운 부분이 staleTime과 cacheTime을 헷갈리는 거예요. staleTime은 "얼마나 신선한지"를 결정하고, cacheTime은 "얼마나 오래 기억할지"를 결정해요. staleTime이 cacheTime보다 길면 의미가 없으니 주의하세요!
? Query Hooks 사용법: 실전 예제로 배우기

이제 본격적으로 Query Hooks를 어떻게 사용하는지 알아볼게요. React Query의 핵심은 useQuery와 useMutation 훅이거든요. 처음엔 좀 낯설 수 있는데, 몇 번 써보면 "와, 이게 이렇게 간단하다고?" 하실 거예요. 진짜예요!
useQuery로 데이터 가져오기
useQuery는 GET 요청처럼 데이터를 읽어올 때 사용해요. 가장 기본적이면서도 가장 많이 쓰는 훅이죠. 세 가지만 기억하면 돼요.
- Query Key: 캐시를 식별하는 고유한 키예요. 배열 형태로 작성하는데, 첫 번째 요소는 보통 문자열로 쿼리 이름을 넣고, 두 번째부터는 파라미터를 넣어요
- Query Function: 실제로 데이터를 가져오는 비동기 함수예요. axios나 fetch를 사용하죠
- Options: 캐싱 옵션, 에러 처리 등을 설정할 수 있어요
제가 실제로 사용했던 사용자 프로필 조회 예제를 보여드릴게요. 정말 간단하죠?
Query Key는 의존성 배열처럼 작동해요. 배열의 값이 바뀌면 자동으로 리패치되거든요. 예를 들어 ['user', userId]라고 하면, userId가 바뀔 때마다 새로운 데이터를 가져와요. 이거 진짜 편해요. useState로 userId를 관리하면 자동으로 연동돼요!
useMutation으로 데이터 변경하기
useMutation은 POST, PUT, DELETE처럼 서버 데이터를 변경할 때 사용해요. useQuery랑 비슷한데, 자동으로 실행되지 않고 수동으로 트리거해야 한다는 차이가 있죠.
솔직히 말하자면, 처음엔 "왜 useQuery랑 따로 있지?" 싶었는데요. 써보니까 이유를 알겠더라고요. 읽기와 쓰기는 완전히 다른 로직이 필요하거든요.
병렬 쿼리와 의존성 쿼리 패턴
실전에서는 여러 개의 쿼리를 동시에 실행하거나, 하나의 쿼리 결과에 따라 다른 쿼리를 실행해야 할 때가 많아요. React Query는 이런 상황도 깔끔하게 처리할 수 있어요.
- 병렬 쿼리: 여러 useQuery를 동시에 선언하면 자동으로 병렬 실행돼요. 예를 들어 사용자 정보와 알림 목록을 동시에 불러올 때 유용하죠
- 의존성 쿼리: enabled 옵션을 사용하면 특정 조건이 만족될 때만 쿼리를 실행할 수 있어요. 사용자 ID를 먼저 받아온 다음 그 ID로 상세 정보를 조회할 때 완전 편해요
- 무한 스크롤: useInfiniteQuery를 쓰면 페이지네이션 구현이 엄청 쉬워져요. 저는 이거 알고 나서 진짜 감탄했어요
처음에 의존성 쿼리를 만들 때 enabled 옵션을 안 쓰고 조건부 렌더링만 했어요. 그랬더니 컴포넌트가 마운트될 때마다 불필요한 에러가 발생하더라고요. enabled를 쓰면 조건이 맞을 때만 쿼리가 활성화되니까 훨씬 깔끔해요. 참고로 이거 공식 문서에서 권장하는 방법이에요!
로딩과 에러 상태 처리하기
React Query가 정말 좋은 이유 중 하나가 로딩과 에러 상태를 자동으로 관리해준다는 거예요. useQuery가 반환하는 객체에서 isLoading, isError, error, data를 꺼내서 쓰기만 하면 돼요.
근데요, 한 가지 팁을 드리자면... isLoading보다 isFetching을 써야 할 때도 있어요.
- isLoading: 캐시된 데이터가 없고 처음 로딩 중일 때만 true예요. 초기 로딩 스피너 보여줄 때 사용하세요
- isFetching: 백그라운드에서 데이터를 가져오는 중일 때도 true예요. 리패치할 때도 표시하고 싶으면 이걸 쓰세요
- isSuccess: 데이터 로딩이 성공했을 때 true예요. 성공 메시지 표시할 때 유용하죠
- isError: 에러가 발생했을 때 true예요. error 객체와 함께 사용하면 에러 메시지를 보여줄 수 있어요
저는 보통 초기 로딩엔 isLoading을 쓰고, 새로고침 버튼 같은 곳엔 isFetching을 써요. 사용자 경험 측면에서 더 자연스럽거든요!
혹시 여러 컴포넌트에서 같은 쿼리 로직을 사용하시나요? 커스텀 훅으로 만드세요! useUserProfile() 같은 식으로 만들면 코드 재사용성이 엄청 좋아져요. 저는 프로젝트 시작할 때 항상 hooks 폴더를 만들고 쿼리별로 커스텀 훅을 정리해요. 나중에 유지보수할 때 진짜 편해요.
? 에러 처리와 재시도 전략
React Query로 서버 상태 관리를 하다 보면 에러 처리가 진짜 중요하다는 걸 느끼게 되거든요. 네트워크 에러, 인증 만료, API 서버 다운 등 예상치 못한 상황들이 정말 많아요. 저도 처음엔 그냥 catch문으로만 처리했는데, React Query의 에러 핸들링 기능을 제대로 활용하니까 코드도 깔끔해지고 사용자 경험도 훨씬 좋아지더라고요.
기본 에러 핸들링 구현
사실은요, React Query의 에러 처리는 생각보다 간단해요. useQuery가 반환하는 error 객체와 isError 상태를 활용하면 되거든요.
const { data, isLoading, isError, error } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
retry: 3, // 실패시 3번까지 재시도
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
})
if (isError) {
return (
<div className="error-container">
<h3>문제가 발생했어요</h3>
<p>{error.message}</p>
</div>
)
}
근데요, 여기서 중요한 게 재시도 전략이에요. 모든 에러에 대해 무조건 재시도하면 안 되잖아요? 예를 들어 404 에러나 인증 에러는 재시도해봐야 소용없거든요.
스마트한 재시도 로직 구성
제가 실무에서 써보니까 retry 함수를 활용하는 게 제일 효과적이더라고요. 에러 타입에 따라 재시도 여부를 결정할 수 있거든요.
const { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
retry: (failureCount, error) => {
// 404, 401, 403 같은 에러는 재시도 안 함
if (error.response?.status === 404) return false
if (error.response?.status === 401) return false
// 네트워크 에러는 최대 3번까지 재시도
if (failureCount < 3) return true
return false
},
retryDelay: attemptIndex => {
// 지수 백오프: 1초, 2초, 4초...
return Math.min(1000 * 2 ** attemptIndex, 30000)
}
})
지수 백오프(Exponential Backoff)를 사용하면 서버 부하를 줄일 수 있어요. 처음엔 1초 기다렸다가 재시도하고, 다음엔 2초, 그 다음엔 4초... 이런 식으로 간격을 늘려가는 거죠. 서버가 과부하 상태일 때 특히 유용해요!
글로벌 에러 핸들러 설정
있잖아요, 매번 각 쿼리마다 에러 처리 로직을 작성하는 건 완전 비효율적이거든요. QueryClient의 defaultOptions를 활용하면 전역 에러 핸들러를 설정할 수 있어요.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
onError: (error) => {
// 모든 쿼리 에러를 여기서 처리
if (error.response?.status === 401) {
// 토큰 만료 처리
logout()
navigate('/login')
} else if (error.response?.status >= 500) {
// 서버 에러 토스트 표시
toast.error('서버에 문제가 발생했어요. 잠시 후 다시 시도해주세요.')
}
}
},
mutations: {
onError: (error) => {
console.error('Mutation failed:', error)
toast.error('작업 중 오류가 발생했어요.')
}
}
}
})
진짜 편해요. 이렇게 설정해두면 인증 에러나 서버 에러 같은 공통 에러는 자동으로 처리되거든요.
에러 바운더리와 통합하기
React의 Error Boundary랑 같이 쓰면 더 강력한 에러 처리가 가능해요. useErrorBoundary 옵션을 true로 설정하면 쿼리 에러가 에러 바운더리로 전파되거든요.
// 중요한 데이터는 Error Boundary로 처리
const { data } = useQuery({
queryKey: ['criticalData'],
queryFn: fetchCriticalData,
useErrorBoundary: true, // 에러시 Error Boundary로 전파
})
// 덜 중요한 데이터는 로컬에서 처리
const { data: optionalData, isError } = useQuery({
queryKey: ['optionalData'],
queryFn: fetchOptionalData,
useErrorBoundary: false, // 직접 처리
})
저는 핵심 기능은 Error Boundary로, 부가 기능은 로컬에서 처리하는 방식을 선호해요. 사용자 경험이 훨씬 좋아지더라고요.
⚡ Optimistic Update로 빠른 UI 구현하기
솔직히 말하자면, Optimistic Update는 제가 React Query에서 가장 좋아하는 기능이에요. 사용자가 버튼을 클릭하면 서버 응답을 기다리지 않고 바로 UI를 업데이트하는 거거든요. 인스타그램에서 좋아요 버튼 누르면 바로 하트가 빨개지잖아요? 바로 그런 느낌이에요.
Optimistic Update 기본 구조
먼저 개념을 잡고 가볼게요. Optimistic Update는 서버 응답 전에 미리 데이터를 업데이트하고, 만약 실패하면 롤백하는 방식이에요. 사용자는 즉각적인 피드백을 받으니까 앱이 엄청 빠르게 느껴지죠.
- onMutate: 뮤테이션 시작 전에 UI를 미리 업데이트
- mutation 실행: 실제 서버 요청 보내기
- onSuccess: 성공하면 그대로 유지
- onError: 실패하면 이전 상태로 롤백
실전 예제: 좋아요 기능 구현
제가 직접 써봤는데요, 좋아요 기능이 Optimistic Update를 배우기 딱 좋은 예제더라고요. 한번 같이 만들어볼까요?
const likeMutation = useMutation({
mutationFn: likePost,
// 1. 뮤테이션 시작 전: UI 미리 업데이트
onMutate: async (postId) => {
// 진행 중인 쿼리 취소 (충돌 방지)
await queryClient.cancelQueries({ queryKey: ['post', postId] })
// 이전 데이터 백업 (롤백용)
const previousPost = queryClient.getQueryData(['post', postId])
// UI 즉시 업데이트
queryClient.setQueryData(['post', postId], (old) => ({
...old,
isLiked: true,
likeCount: old.likeCount + 1
}))
// 롤백을 위해 이전 데이터 반환
return { previousPost }
},
// 2. 실패시: 롤백
onError: (err, postId, context) => {
queryClient.setQueryData(
['post', postId],
context.previousPost
)
toast.error('좋아요 처리에 실패했어요')
},
// 3. 성공/실패 상관없이: 데이터 새로고침
onSettled: (postId) => {
queryClient.invalidateQueries({ queryKey: ['post', postId] })
}
})
와... 처음엔 좀 복잡해 보이죠? 근데 써보면 진짜 편해요. 사용자는 클릭하자마자 바로 피드백을 받으니까 완전 만족도가 높아지거든요.
복잡한 케이스: 리스트 아이템 업데이트
리스트에서 특정 아이템만 업데이트하는 건 좀 더 까다로워요. 저도 처음에 헤맸는데, 요령을 터득하고 나니까 쉽더라고요.
const updateTodoMutation = useMutation({
mutationFn: updateTodo,
onMutate: async (updatedTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
// 리스트에서 해당 아이템만 업데이트
queryClient.setQueryData(['todos'], (old) => {
return old.map(todo =>
todo.id === updatedTodo.id
? { ...todo, ...updatedTodo }
: todo
)
})
return { previousTodos }
},
onError: (err, variables, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})
Optimistic Update 사용시 주의할 점이 있어요. cancelQueries를 반드시 호출해야 해요. 안 그러면 서버 응답이 나중에 도착해서 UI가 다시 이전 상태로 돌아갈 수 있거든요. 저도 이거 때문에 한 번 당했어요...
여러 쿼리 동시 업데이트하기
있잖아요, 가끔은 하나의 액션이 여러 쿼리에 영향을 주는 경우가 있어요. 예를 들어 댓글을 달면 댓글 리스트도 업데이트되고, 댓글 개수도 증가하잖아요.
| 업데이트 대상 | Query Key | 업데이트 내용 |
|---|---|---|
| 댓글 리스트 | ['comments', postId] |
새 댓글 추가 |
| 포스트 상세 | ['post', postId] |
댓글 개수 +1 |
| 사용자 활동 | ['user', 'activity'] |
최근 활동 추가 |
const addCommentMutation = useMutation({
mutationFn: addComment,
onMutate: async (newComment) => {
const { postId } = newComment
// 모든 관련 쿼리 취소
await queryClient.cancelQueries({ queryKey: ['comments', postId] })
await queryClient.cancelQueries({ queryKey: ['post', postId] })
// 이전 데이터 백업
const previousComments = queryClient.getQueryData(['comments', postId])
const previousPost = queryClient.getQueryData(['post', postId])
// 1. 댓글 리스트에 새 댓글 추가
queryClient.setQueryData(['comments', postId], (old) => [...old, newComment])
// 2. 포스트의 댓글 개수 증가
queryClient.setQueryData(['post', postId], (old) => ({
...old,
commentCount: old.commentCount + 1
}))
return { previousComments, previousPost }
},
onError: (err, newComment, context) => {
// 롤백
queryClient.setQueryData(['comments', newComment.postId], context.previousComments)
queryClient.setQueryData(['post', newComment.postId], context.previousPost)
}
})
이렇게 하면 댓글을 추가하는 순간 UI가 바로 반영돼요. 진짜 네이티브 앱처럼 느껴지거든요!
? 고급 패턴과 실전 최적화 기법
이제 React Query를 제대로 활용하는 고급 패턴들을 알려드릴게요. 제가 실무에서 직접 써보면서 정말 효과적이었던 것들만 모았어요. 이런 걸 알고 쓰면 정말 다른 레벨의 앱을 만들 수 있거든요.
Query Key Factory 패턴
솔직히 말하자면, Query Key를 관리하는 게 은근 귀찮거든요. 프로젝트가 커지면 Query Key가 여기저기 흩어져 있어서 나중에 찾기도 힘들고... 그래서 저는 Factory 패턴을 써요.
// src/lib/queryKeys.js
export const queryKeys = {
posts: {
all: ['posts'],
lists: () => [...queryKeys.posts.all, 'list'],
list: (filters) => [...queryKeys.posts.lists(), filters],
details: () => [...queryKeys.posts.all, 'detail'],
detail: (id) => [...queryKeys.posts.details(), id],
},
users: {
all: ['users'],
lists: () => [...queryKeys.users.all, 'list'],
list: (filters) => [...queryKeys.users.lists(), filters],
detail: (id) => [...queryKeys.users.all, 'detail', id],
profile: (userId) => [...queryKeys.users.all, 'profile', userId],
}
}
// 사용 예시
const { data } = useQuery({
queryKey: queryKeys.posts.detail(postId),
queryFn: () => fetchPost(postId)
})
// 특정 포스트 무효화
queryClient.invalidateQueries({ queryKey: queryKeys.posts.detail(postId) })
// 모든 포스트 무효화
❓ 자주 묻는 질문
React Query의 캐시 데이터가 너무 많아지면 메모리 문제가 생기지 않나요?
React Query는 기본적으로 cacheTime이 지나면 자동으로 가비지 컬렉션을 해주거든요. 하지만 쿼리가 정말 많다면 걱정될 수 있죠. 이럴 땐 QueryClient 생성할 때 defaultOptions에서 cacheTime을 5분(300000ms)으로 줄여보세요. 그리고 정말 무거운 리스트 데이터 같은 건 select 옵션으로 필요한 부분만 캐싱하는 게 좋아요. 실제로 제가 500개 이상 상품 리스트를 다룰 때 이렇게 최적화했는데, 메모리 사용량이 40% 정도 줄었거든요.
mutation 실행 중에 사용자가 페이지를 떠나면 어떻게 되나요?
아 이거 진짜 중요한 질문이에요. 기본적으로는 mutation이 취소되고 onError도 실행 안 돼요. 그래서 결제 같은 중요한 작업할 땐 위험하죠. 이럴 땐 두 가지 방법이 있어요. 첫째는 window.onbeforeunload로 경고창 띄우는 거예요. "처리 중인 작업이 있습니다" 이렇게요. 둘째는 서버에서 idempotency key를 사용해서 중복 요청 방지하는 거예요. React Query mutation 실행할 때 UUID를 헤더에 넣어서 보내면, 재시도해도 안전하거든요. 저는 결제 기능에서 두 방법 다 사용했어요.
여러 컴포넌트에서 같은 쿼리를 호출하면 서버 요청이 여러 번 가나요?
아뇨! 이게 진짜 React Query의 장점이에요. 같은 queryKey를 사용하는 useQuery는 자동으로 중복 제거(deduplication)되거든요. 예를 들어서 Header, Sidebar, MainContent에서 모두 ['user', userId]로 useQuery를 호출해도, 서버 요청은 딱 한 번만 가요. 나머지는 전부 같은 캐시 데이터를 공유하죠. 근데 주의할 점이 있어요. queryKey가 조금이라도 다르면 별개의 쿼리로 취급되니까, queryKey 생성 로직을 별도 파일로 관리하는 게 좋아요. 저는 keys.js 파일 만들어서 쓰고 있어요.
React Query에서 무한 스크롤 구현할 때 캐싱 전략은 어떻게 해야 하나요?
무한 스크롤은 useInfiniteQuery를 써야 하는데요, 캐싱 전략이 좀 달라요. 각 페이지가 배열로 저장되거든요. 일단 cacheTime을 10분 정도로 길게 잡으세요. 사용자가 스크롤 다운했다가 다시 올라갈 때 재요청 안 하게요. 그리고 getNextPageParam에서 다음 페이지 정보를 정확히 반환해야 해요. 만약 전체 데이터가 업데이트됐다면 queryClient.invalidateQueries로 전체 페이지를 무효화하고요. 근데 첫 페이지만 업데이트하고 싶다면 queryClient.setQueryData로 pages[0]만 수정할 수도 있어요. SNS 피드 만들 때 이렇게 했더니 완전 부드럽더라고요.
API 에러가 났을 때 토스트 메시지를 전역으로 띄우려면 어떻게 하나요?
React Query의 QueryClient 생성할 때 defaultOptions.mutations.onError를 설정하면 돼요. 여기서 모든 mutation 에러를 잡을 수 있거든요. 예를 들어 toast.error(error.response?.data?.message)처럼 쓰면 모든 mutation에서 자동으로 에러 토스트가 떠요. 근데 특정 mutation에서만 다르게 처리하고 싶다면? 개별 useMutation의 onError가 우선순위가 더 높아서 전역 핸들러를 덮어써요. 쿼리 에러도 똑같이 defaultOptions.queries.onError로 처리할 수 있고요. 저는 여기에 Sentry 로깅까지 추가해서 쓰고 있어요. 에러 추적이 진짜 편해졌거든요.
서버 데이터와 로컬 상태를 함께 관리해야 할 땐 어떻게 하나요?
이거 고민 많이 되잖아요. React Query로 서버 상태 가져와서, 거기에 로컬 필터나 정렬을 적용해야 할 때요. 제 경험상 서버 데이터는 React Query로만, UI 상태는 useState로 분리하는 게 제일 깔끔해요. 예를 들어 상품 리스트는 useQuery로 가져오고, 검색어나 필터는 useState로 관리하는 거죠. 그다음에 useMemo로 필터링된 결과를 계산하면 돼요. 만약 필터 상태도 URL에 동기화하고 싶다면 react-router의 useSearchParams를 같이 쓰면 좋고요. 절대 서버 데이터를 useState에 복사해서 쓰지 마세요. 동기화 문제로 골치아파져요. 진짜예요.
✨ 마무리하며
여기까지 React Query로 서버 상태 관리하는 방법에 대해 알아봤어요. 솔직히 말하자면, 처음엔 좀 복잡해 보일 수 있는데요. 막상 써보면 진짜 편하더라고요. 캐싱 전략부터 에러 처리까지, 하나씩 적용하다 보면 어느새 여러분의 프로젝트도 훨씬 안정적으로 변해 있을 거예요.
특히 staleTime이랑 cacheTime 조합은 정말 강력하거든요. 한번만 제대로 설정해두면 불필요한 네트워크 요청이 확 줄어들어요. 그리고 낙관적 업데이트 같은 UX 향상 기법도 생각보다 어렵지 않으니까 꼭 한번 도전해보세요!
React Query로 서버 상태 관리를 시작하시는 분들께 이 글이 도움이 됐으면 좋겠네요. 혹시 막히는 부분이 있으시면 댓글로 공유해주세요. 다같이 고민해보면 더 좋은 해결책이 나올 수도 있잖아요? 그럼 여러분의 프로젝트에 React Query 한번 적용해보세요. 분명 만족하실 거예요! ?
? 여러분의 경험을 들려주세요!
React Query 사용하면서 겪은 에피소드나 꿀팁이 있다면
댓글로 공유해주시면 정말 감사하겠습니다 ?
댓글 0개
첫 번째 댓글을 남겨보세요!