React에서 API 데이터를 가져오고 관리하는 것은 생각보다 복잡하다. useState와 useEffect를 사용해서 데이터 로딩, 에러 처리, 캐싱을 모두 직접 구현하면 보일러플레이트 코드가 엄청 많아진다. 이런 문제를 해결하기 위해 만들어진 것이 바로 React Query(현재는 TanStack Query라고 불린다)이다.
React Query는 서버 상태를 효율적으로 관리하기 위한 라이브러리다. API로부터 받아온 데이터를 자동으로 캐시하고, 필요할 때 자동으로 갱신하며, 중복 요청을 방지해준다.
일반적으로 데이터를 가져올 때 우리는 이런 코드를 작성한다.
// 😩 이렇게 일일이 처리해야 함
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/users')
.then(res => res.json())
.then(data => {
setData(data);
setError(null);
})
.catch(err => setError(err))
.finally(() => setLoading(false));
}, []);
React Query를 사용하면:
// 😊 간단해짐!
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json())
});
훨씬 깔끔하게 코드를 작성할 수 있다.
npm install @tanstack/react-query
앱의 최상단(main.jsx또는App.jsx)에서 QueryClientProvider로 감싸준다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
{/* 여기에 앱 컴포넌트들이 들어감 */}
</QueryClientProvider>
)
}
useQuery는 서버에서 데이터를 가져올 때 사용한다.
import { useQuery } from '@tanstack/react-query'
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
return res.json()
}
})
if (isLoading) return <div>로딩 중...</div>
if (error) return <div>에러 발생: {error.message}</div>
return (
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
📍 queryKey: 데이터의 고유한 식별자이다. 같은 queryKey를 가진 쿼리는 같은 캐시를 사용한다.
// 같은 유저 리스트는 같은 캐시 사용
const query1 = useQuery({ queryKey: ['users'], ... })
const query2 = useQuery({ queryKey: ['users'], ... })
// 다른 queryKey는 다른 캐시
const query3 = useQuery({ queryKey: ['posts'], ... })
// 동적 queryKey (유저 ID가 다르면 다른 캐시)
const userId = 1
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json())
})
🔄 queryFn: 실제로 데이터를 가져오는 함수이다.
⏱️ staleTime: 데이터가 "신선한" 상태로 유지되는 시간(밀리초)이다. 이 시간 내에는 자동으로 다시 가져오지 않는다. 기본값은 0이다.
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 1000 * 60 * 5 // 5분
})
🗑️ gcTime(garbage collection time): 캐시 데이터를 유지하는 시간이다. 이 시간 후에는 메모리에서 제거된다. 기본값은 5분이다.
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
gcTime: 1000 * 60 * 10 // 10분
})
useMutation은 POST, PUT, DELETE 같은 데이터 변경 작업에 사용한다.
import { useMutation, useQueryClient } from '@tanstack/react-query'
function CreateUser() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: async (newUser) => {
const res = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser)
})
return res.json()
},
onSuccess: () => {
// 유저 리스트 캐시를 다시 가져오기
queryClient.invalidateQueries({ queryKey: ['users'] })
}
})
const handleCreate = () => {
mutation.mutate({ name: '새로운 유저', email: 'user@example.com' })
}
return (
<div>
<button onClick={handleCreate} disabled={mutation.isPending}>
{mutation.isPending ? '저장 중...' : '유저 생성'}
</button>
{mutation.isError && <p>❌ 에러: {mutation.error.message}</p>}
{mutation.isSuccess && <p>✅ 생성 완료!</p>}
</div>
)
}
API 호출 로직을 따로 파일로 분리하면 관리가 쉬워진다.
// api.js
export const fetchUsers = () =>
fetch('/api/users').then(res => res.json())
export const createUser = (user) =>
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
}).then(res => res.json())
// 컴포넌트에서 사용
import { fetchUsers } from './api'
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
데이터가 변경되면 관련된 캐시를 무효화해야 한다.
// 특정 쿼리만 무효화
queryClient.invalidateQueries({ queryKey: ['users'] })
// 특정 쿼리와 관련된 모든 쿼리 무효화 (와일드카드)
queryClient.invalidateQueries({ queryKey: ['users'], exact: false })
// 모든 쿼리 무효화
queryClient.invalidateQueries()
전역 에러 처리를 설정할 수 있다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1, // 실패 시 1번 재시도
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000)
}
}
})
개발할 때 React Query DevTools를 사용하면 캐시 상태를 한눈에 볼 수 있다.
npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
React Query를 사용하면 서버 상태 관리가 훨씬 간단해진다. 핵심만 정리하면: