React Query - 강력한 데이터 동기화 라이브러리
React Query: 서버 상태 관리를 혁신한 라이브러리
React Query(현재는 TanStack Query로 브랜딩 변경)는 Tanner Linsley가 개발한 오픈소스 라이브러리로, React 애플리케이션에서 서버 상태를 효율적으로 관리할 수 있게 해주는 혁신적인 도구입니다. GitHub에서 42,000개 이상의 스타를 받았으며, 현대적인 React 애플리케이션의 표준 데이터 페칭 솔루션이 되었습니다.
서버 상태 관리의 문제점
전통적으로 React 애플리케이션에서 서버 데이터를 가져오는 방법은 useState와 useEffect를 조합하는 것이었습니다. 하지만 이 접근 방식에는 여러 문제점이 있습니다:
1. 보일러플레이트 코드의 증가
// 전통적인 방식
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
이런 코드를 매번 작성해야 하며, 로딩 상태, 에러 처리, 취소 로직 등을 일일이 구현해야 합니다.
2. 캐싱의 부재
같은 데이터를 여러 컴포넌트에서 요청하면 불필요한 네트워크 요청이 발생합니다. 캐싱을 구현하려면 추가적인 상태 관리가 필요합니다.
3. 백그라운드 업데이트의 어려움
데이터를 주기적으로 갱신하거나, 사용자가 모르는 사이에 최신 데이터로 업데이트하는 것이 복잡합니다.
4. 낙관적 업데이트의 복잡성
사용자 액션에 대해 즉시 UI를 업데이트하고, 서버 응답에 따라 롤백하는 로직이 복잡합니다.
React Query는 이러한 모든 문제를 해결합니다.
React Query의 핵심 개념
Query: 데이터 가져오기
Query는 서버에서 데이터를 가져오는 비동기 작업을 관리합니다. useQuery 훅을 사용하여 간단하게 구현할 수 있습니다.
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
훨씬 간단해졌습니다! React Query가 로딩 상태, 에러 처리, 캐싱을 모두 자동으로 처리합니다.
Query Key: 캐싱의 핵심
Query Key는 쿼리의 고유 식별자입니다. 같은 Query Key를 가진 쿼리는 같은 캐시를 공유합니다.
// 이 두 쿼리는 같은 캐시를 공유합니다
useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
Query Key는 배열 형태로 계층 구조를 표현할 수 있습니다:
['users'] // 모든 사용자
['users', 1] // ID가 1인 사용자
['users', 1, 'posts'] // 사용자 1의 게시글
['users', 1, 'posts', 5] // 사용자 1의 게시글 5
Mutation: 데이터 변경하기
Mutation은 서버의 데이터를 생성, 수정, 삭제하는 작업을 관리합니다. useMutation 훅을 사용합니다.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newPost) =>
fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost),
}).then(res => res.json()),
onSuccess: () => {
// 성공 시 관련 쿼리 무효화하여 자동 리페치
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
return (
<button
onClick={() => mutation.mutate({ title: 'New Post' })}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
);
}
React Query의 강력한 기능들
1. 자동 캐싱
React Query는 자동으로 데이터를 캐싱합니다. 같은 Query Key를 가진 쿼리는 캐시된 데이터를 즉시 반환하고, 백그라운드에서 최신 데이터를 가져옵니다.
// 첫 번째 렌더링: 네트워크 요청
const { data } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
// 다른 컴포넌트에서 같은 쿼리 사용: 캐시에서 즉시 반환
const { data } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
2. 스마트한 리페칭
React Query는 다음 상황에서 자동으로 데이터를 다시 가져옵니다:
- 윈도우 포커스: 사용자가 다른 탭에서 돌아올 때
- 네트워크 재연결: 인터넷 연결이 복구될 때
- 컴포넌트 마운트: 컴포넌트가 다시 마운트될 때
- 쿼리 무효화:
invalidateQueries호출 시
이러한 동작은 모두 설정 가능합니다:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnWindowFocus: false, // 윈도우 포커스 시 리페치 비활성화
refetchOnReconnect: true, // 네트워크 재연결 시 리페치
staleTime: 5 * 60 * 1000, // 5분간 fresh 상태 유지
cacheTime: 10 * 60 * 1000, // 10분간 캐시 유지
});
3. 무한 스크롤
useInfiniteQuery를 사용하여 무한 스크롤을 쉽게 구현할 수 있습니다.
import { useInfiniteQuery } from '@tanstack/react-query';
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) =>
fetch(`/api/posts?page=${pageParam}`).then(res => res.json()),
getNextPageParam: (lastPage, pages) =>
lastPage.hasNextPage ? pages.length : undefined,
});
return (
<div>
{data.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<Post key={post.id} post={post} />
))}
</div>
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
4. 낙관적 업데이트
사용자 액션에 대해 즉시 UI를 업데이트하고, 서버 응답에 따라 롤백할 수 있습니다.
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
// 진행 중인 리페치 취소
await queryClient.cancelQueries({ queryKey: ['posts'] });
// 이전 값 백업
const previousPosts = queryClient.getQueryData(['posts']);
// 낙관적 업데이트
queryClient.setQueryData(['posts'], (old) => [...old, newPost]);
return { previousPosts };
},
onError: (err, newPost, context) => {
// 에러 시 롤백
queryClient.setQueryData(['posts'], context.previousPosts);
},
onSettled: () => {
// 성공/실패 관계없이 리페치
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
5. 쿼리 무효화 및 리페칭
데이터가 변경되었을 때 관련 쿼리를 무효화하여 자동으로 리페치할 수 있습니다.
// 특정 쿼리 무효화
queryClient.invalidateQueries({ queryKey: ['posts'] });
// 패턴으로 무효화
queryClient.invalidateQueries({ queryKey: ['posts'], exact: false });
// 특정 쿼리만 리페치
queryClient.refetchQueries({ queryKey: ['posts'] });
React Query 설정
QueryClient 설정
애플리케이션의 루트에서 QueryClient와 QueryClientProvider를 설정합니다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 기본 5분
cacheTime: 10 * 60 * 1000, // 기본 10분
retry: 3, // 실패 시 3번 재시도
refetchOnWindowFocus: true, // 윈도우 포커스 시 리페치
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
React Query DevTools
개발 환경에서 쿼리 상태를 시각적으로 확인할 수 있는 DevTools를 제공합니다.
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
실무에서의 활용 패턴
1. 커스텀 훅으로 추상화
비즈니스 로직을 커스텀 훅으로 추상화하여 재사용성을 높입니다.
// hooks/useUser.js
export function useUser(userId) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => userService.getUser(userId),
enabled: !!userId, // userId가 있을 때만 실행
});
}
// 컴포넌트에서 사용
function UserProfile({ userId }) {
const { data: user, isLoading } = useUser(userId);
// ...
}
2. 에러 처리 전략
전역 에러 핸들러를 설정하여 일관된 에러 처리를 구현합니다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
// 전역 에러 처리
if (error.status === 401) {
// 인증 에러 처리
redirectToLogin();
} else if (error.status >= 500) {
// 서버 에러 알림
showErrorNotification('서버 오류가 발생했습니다.');
}
},
},
},
});
3. 낙관적 업데이트 패턴
사용자 경험을 향상시키기 위한 낙관적 업데이트 패턴을 활용합니다.
function useUpdatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updatePost,
onMutate: async (updatedPost) => {
await queryClient.cancelQueries({ queryKey: ['post', updatedPost.id] });
const previousPost = queryClient.getQueryData(['post', updatedPost.id]);
queryClient.setQueryData(['post', updatedPost.id], updatedPost);
return { previousPost };
},
onError: (err, updatedPost, context) => {
queryClient.setQueryData(['post', updatedPost.id], context.previousPost);
},
});
}
React Query vs 다른 솔루션
vs SWR
SWR은 Vercel에서 개발한 유사한 라이브러리입니다. React Query와 비교하면:
- React Query: 더 많은 기능, 더 큰 커뮤니티, 더 활발한 개발
- SWR: 더 작은 번들 크기, 더 간단한 API
vs 전통적인 방식
useState + useEffect와 비교하면:
- 코드량: React Query가 훨씬 적은 코드로 같은 기능 구현
- 성능: 자동 캐싱과 리페칭으로 최적화된 성능
- 사용자 경험: 백그라운드 업데이트로 더 나은 UX
성능 최적화 팁
1. Query Key 설계
Query Key를 잘 설계하면 불필요한 리페치를 방지할 수 있습니다.
// 나쁜 예: 너무 일반적인 키
useQuery({ queryKey: ['data'], queryFn: fetchData });
// 좋은 예: 구체적인 키
useQuery({ queryKey: ['user', userId, 'posts'], queryFn: fetchUserPosts });
2. staleTime 활용
자주 변경되지 않는 데이터는 staleTime을 길게 설정합니다.
useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
staleTime: 10 * 60 * 1000, // 10분간 fresh 상태 유지
});
3. 선택적 필드 가져오기
필요한 필드만 가져와서 네트워크 트래픽을 줄입니다.
// 필요한 필드만 요청
useQuery({
queryKey: ['user', userId, 'summary'],
queryFn: () => fetchUserSummary(userId), // 이름, 이메일만
});
결론
React Query는 React 애플리케이션에서 서버 상태 관리를 혁신한 라이브러리입니다. 자동 캐싱, 스마트한 리페칭, 낙관적 업데이트 등의 기능을 통해 개발자 경험과 사용자 경험을 모두 향상시킵니다.
복잡한 서버 상태 관리가 필요한 프로젝트라면 React Query를 사용하는 것을 강력히 추천합니다. 특히 API 호출이 많은 애플리케이션, 실시간 데이터를 다루는 애플리케이션, 대시보드나 관리자 페이지 등에서 그 진가를 발휘합니다.
React Query를 배우는 것은 시간 투자 대비 매우 높은 수익을 가져다줍니다. 한번 익히면 서버 상태 관리에 대한 걱정 없이 비즈니스 로직에 집중할 수 있게 됩니다.
React Query의 진화와 미래
React Query는 2019년 Tanner Linsley에 의해 처음 공개된 이후 지속적으로 발전해왔습니다. 초기에는 React Query라는 이름이었지만, 현재는 TanStack Query로 브랜딩이 변경되었습니다. React Query v4에서는 더 나은 타입스크립트 지원과 성능 개선이 이루어졌고, v5에서는 더 강력한 기능들이 추가되었습니다.
특히 주목할 만한 것은 React Query의 생태계 확장입니다. TanStack Table, TanStack Router 등과 통합하여 더 강력한 개발 경험을 제공합니다. 또한 React Native, Vue, Svelte 등 다른 프레임워크에서도 사용할 수 있도록 확장되고 있습니다.
실무에서의 React Query 활용 전략
실무에서 React Query를 효과적으로 사용하기 위해서는 몇 가지 전략이 필요합니다. 첫째, 쿼리 키를 잘 설계하는 것입니다. 쿼리 키는 캐싱과 리페칭의 기준이 되므로, 일관되고 예측 가능한 구조로 설계해야 합니다.
둘째, 리페칭 전략을 수립하는 것입니다. staleTime과 cacheTime을 적절히 설정하여 불필요한 네트워크 요청을 방지하고, 필요한 경우에만 데이터를 리페칭하도록 해야 합니다. 셋째, 에러 처리를 잘하는 것입니다. React Query의 에러 핸들링 기능을 활용하여 사용자에게 적절한 피드백을 제공해야 합니다.
React Query와 다른 상태 관리 라이브러리와의 비교
React Query는 다른 상태 관리 라이브러리와 비교했을 때 독특한 위치에 있습니다. Redux와 비교하면, React Query는 서버 상태 관리에 특화되어 있으며, 클라이언트 상태 관리에는 Redux가 더 적합합니다. SWR과 비교하면, React Query는 더 많은 기능과 더 나은 타입스크립트 지원을 제공합니다.
Apollo Client와 비교하면, React Query는 GraphQL뿐만 아니라 REST API도 지원하며, 더 가볍고 유연합니다. 하지만 Apollo Client는 GraphQL에 특화된 강력한 기능들을 제공합니다.
React Query 학습 로드맵
React Query를 처음 배우는 개발자라면, 단계별로 학습하는 것이 좋습니다. 첫 번째 단계는 기본 개념을 이해하는 것입니다. useQuery를 사용하여 데이터를 가져오는 방법을 배워야 합니다. 두 번째 단계는 변이(mutation)를 학습하는 것입니다. useMutation을 사용하여 데이터를 수정하는 방법을 익혀야 합니다.
세 번째 단계는 쿼리 무효화를 이해하는 것입니다. queryClient.invalidateQueries를 사용하여 캐시를 무효화하고 리페칭하는 방법을 배워야 합니다. 네 번째 단계는 고급 기능을 학습하는 것입니다. 낙관적 업데이트, 무한 스크롤, 병렬 쿼리 등을 사용하여 더 강력한 애플리케이션을 만드는 방법을 익혀야 합니다.
React Query 생태계와 도구들
React Query 생태계는 다양한 도구들로 구성되어 있습니다. React Query DevTools는 쿼리 상태를 시각적으로 추적할 수 있게 해주는 도구입니다. TanStack Query는 React Query의 새로운 이름으로, 더 넓은 생태계를 제공합니다.
React Query는 다양한 백엔드와 통합할 수 있습니다. REST API, GraphQL, gRPC 등과 함께 사용할 수 있으며, Axios, Fetch, Apollo Client 등과 통합할 수 있습니다.
React Query의 성능과 최적화
React Query의 성능 최적화는 여러 측면에서 고려해야 합니다. 첫째, 쿼리 키를 최적화하는 것입니다. 쿼리 키가 자주 변경되면 불필요한 리페칭이 발생할 수 있으므로, 안정적인 쿼리 키를 사용해야 합니다. 둘째, staleTime과 cacheTime을 적절히 설정하는 것입니다. 자주 변경되지 않는 데이터는 긴 staleTime을 설정하여 불필요한 리페칭을 방지합니다.
셋째, 선택자를 사용하는 것입니다. useQuery의 select 옵션을 사용하여 필요한 데이터만 선택하면 불필요한 리렌더링을 방지할 수 있습니다. 넷째, 병렬 쿼리를 활용하는 것입니다. useQueries를 사용하여 여러 쿼리를 병렬로 실행하면 성능이 향상됩니다.
React Query의 실제 사용 사례
많은 기업들이 React Query를 프로덕션 환경에서 사용하고 있습니다. Netflix는 React Query를 사용하여 콘텐츠 데이터를 관리합니다. Airbnb는 React Query를 사용하여 예약 시스템을 구축했습니다. 이러한 사례들은 React Query가 대규모 프로젝트에서 얼마나 효과적인지를 보여줍니다.
결론: React Query의 가치와 미래
React Query는 React 애플리케이션에서 서버 상태 관리를 혁신한 라이브러리입니다. 자동 캐싱, 스마트한 리페칭, 낙관적 업데이트 등의 기능을 통해 개발자 경험과 사용자 경험을 모두 향상시킵니다. 복잡한 서버 상태 관리가 필요한 프로젝트라면 React Query를 사용하는 것을 강력히 추천합니다.
앞으로도 React Query는 계속 발전할 것입니다. 더 나은 성능, 더 강력한 기능, 더 나은 개발자 경험을 제공할 것입니다. React Query는 단순한 라이브러리를 넘어, 현대적인 서버 상태 관리 방법론의 핵심이 되었습니다.
개발자라면 React Query를 배워두는 것이 좋습니다. 한번 익히면 서버 상태 관리에 대한 걱정 없이 비즈니스 로직에 집중할 수 있게 됩니다. React Query는 React 개발자에게 필수적인 도구이며, 지금 배우는 것이 가장 좋은 시기입니다.
최종적으로, React Query는 서버 상태 관리에 혁명을 일으킨 라이브러리입니다. 자동 캐싱과 스마트한 리페칭이 제공하는 편의성과 성능은 어떤 프로젝트에서도 가치 있는 투자입니다. React Query를 배우고 활용하는 것은 개발자로서의 역량을 높이는 중요한 단계입니다.