react-query 활용
React Query는 React 애플리케이션에서 데이터를 관리하고 처리하기 위한 라이브러리입니다. 주로 서버에서 가져온 데이터를 캐시하고 관리하며, 서버로부터 데이터를 가져오고 업데이트하는 데 사용됩니다.
react hook 이랑 굉장히 비슷한 인터페이스를 제공하기 때문에 접근성이 좋고 친숙하다.
우선 Overview 를 살펴보자면
React 애플리케이션의 서버 상태를 불러오기, 캐싱, 동기화 및 데이터를 업데이트하는 것을 쉽게 해줍니다.
즉 "음..서버 스테이트를 관리하기 아주 좋다" 라고 생각할 수가 있다.
react query 홈페이지 소개를 보면 one of the best libraries for managing server state 라고 자신만만하게 소개 하고 있다.
React Query는 zero-config로 즉시 사용가능, But 원하면 언제든 config도 커스텀 가능!
* config는 React Query 라이브러리의 동작을 조정하고 사용자 정의하는 데 사용되는 설정(configuration) 객체를 가리킵니다. 이 설정은 기본적으로 미리 정의된 값을 사용하여 React Query가 데이터를 가져오고 관리하는 방식을 제어하는 데 도움을 줍니다.
주요한 몇 가지 구성 옵션은 다음과 같습니다:
queries: 쿼리들에 대한 설정을 지정하는 곳입니다. 각 쿼리별로 캐시, 재시도 로직, 데이터 갱신 주기 등을 설정할 수 있습니다.(String 형태와, Array 형태가 있다.)
Query Function - Data Fetching 할때 Promise 함수를 만들죠?
promise 를 반환하는 함수! - 데이터 resolve 하거나 error 를 throw
useQuery('fetchOrder', () => fetOrder(orderNo), options)
mutations: 뮤테이션에 대한 설정을 담당하며, 요청의 성공 또는 실패에 따른 캐시 갱신, 재시도 로직 등을 지정할 수 있습니다.
mutationsThrottleTime: 뮤테이션 요청 사이의 간격을 제어하는 옵션입니다.
shared: 여러 쿼리 또는 뮤테이션 간에 공유되는 설정을 지정할 수 있는 곳입니다.
본격적으로 알아보기 전에
React에서 쓰려면 QueryClientProvider 필수!
import {QueryClient, QueryClientProvider} from 'react-query'
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
고급활용 예제
React-Query는 useInfiniteQuery hook을 제공합니다.
import axios from "axios";
import { useInfiniteQuery } from "react-query";
interface PostData {
body: string;
id: number;
title: string;
userId: number;
}
import axios from "axios";
import { useInfiniteQuery } from "react-query";
interface PostData {
body: string;
id: number;
title: string;
userId: number;
}
function Test1() {
const API_URL = "https://jsonplaceholder.typicode.com";
const fetchPosts = async ({ pageParam = 0 }) => {
const start = pageParam || 0;
const limit = 5;
const { data } = await axios.get<PostData[]>(`${API_URL}/posts?_start=${start}&_limit=${limit}`);
return data;
};
const {
data,
fetchNextPage,
hasNextPage,
isFetching,
} = useInfiniteQuery("getPosts", fetchPosts, {
getNextPageParam: (lastPage, allPages) => {
const lastPost = lastPage[lastPage.length - 1];
return lastPost.id < 100 ? lastPost.id + 1 : undefined;
},
});
return (
<div>
{data.pages.map((page, pageIndex) => (
<React.Fragment key={pageIndex}>
{page.map((post) => (
<div key={`post-${post.id}`} style={{ marginBottom: "50px" }}>
<p>{post.id}</p>
<p>{post.title}</p>
</div>
))}
</React.Fragment>
))}
<div>
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetching}
>
{isFetching ? 'Loading...' : 'Load More...'}
</button>
)}
</div>
</div>
);
}
export default Test1;
// useInfiniteQuery
const { data, fetchNextPage } = useInfiniteQuery({
queryKey: "getPosts",
queryFn: fetchPost,
getNextPageParam: (lastPage) => {
const lastIdx = lastPage.length - 1;
const lastPost = lastPage[lastIdx];
return lastPost.id < 100
? {
start: lastPost.id + 1,
limit: 5,
}
: false;
},
Interface 정의
- PostData: 포스트 데이터의 형태를 정의한 TypeScript interface입니다. body, id, title, userId를 포함합니다.
Test1 컴포넌트
- API_URL: 데이터를 가져올 API의 기본 URL을 정의합니다.
- fetchPost 함수: 페이지별 데이터를 가져오는 함수입니다. param 객체를 받아 시작 위치(start)와 한 페이지에 보여줄 아이템 개수(limit)를 설정합니다. Axios를 사용하여 API를 호출하고, start와 limit에 따라 적절한 데이터를 가져옵니다.
useInfiniteQuery 훅 사용
- useInfiniteQuery 훅을 사용하여 무한 스크롤이나 페이지네이션을 구현합니다.
- queryKey: 쿼리를 식별하는 유일한 키로서, 여기서는 "getPosts"로 설정됩니다.
- queryFn: 실제 데이터를 가져오는 함수인 fetchPost를 지정합니다.
- getNextPageParam: 다음 페이지를 가져오기 위해 호출되는 함수입니다. lastPage 파라미터는 이전 페이지의 데이터를 나타냅니다. 마지막 포스트의 ID를 확인하여 다음 페이지의 시작 위치와 데이터 개수를 결정합니다. 마지막 포스트의 ID가 100 미만인 경우 다음 페이지의 시작 위치와 한 페이지에 보여줄 아이템 개수를 반환하고, 그렇지 않으면 false를 반환하여 페이지네이션을 중단합니다.
- 렌더링
- data?.pages.map: 가져온 데이터를 표시하기 위해 data 객체의 pages 배열을 map을 사용해서 화면에 렌더링하며 각 페이지의 포스트들을 표시합니다.
- 각 포스트의 id와 title을 보여주는 간단한 UI를 생성합니다.
- "Load More..." 버튼을 클릭하면 fetchNextPage 함수가 호출되어 다음 페이지의 데이터를 가져옵니다.
이 코드는 JSONPlaceholder API를 사용하여 페이지네이션을 구현하고, React Query의 useInfiniteQuery를 활용하여 무한 스크롤 또는 페이지 단위의 데이터 로딩을 처리하는 방법을 보여줍니다.
캐시 커스터마이징
React Query에서의 캐시 커스터마이징은 데이터를 캐싱하고 관리하는 방식을 사용자가 원하는 대로 조정하는 것을 말합니다. 이를 통해 기본 캐시 동작을 커스텀하거나, 데이터 캐싱 및 재사용 전략을 조정하여 애플리케이션의 성능을 최적화할 수 있습니다.
Custom Query Key
- queryKey를 사용하여 캐시 키를 커스텀할 수 있습니다. 이를 활용하여 동일한 쿼리에 대해 여러 캐시를 유지하거나, 특정 매개변수에 기반하여 캐시를 구분할 수 있습니다.
- 캐시 키를 커스텀하여 브라우저에서 어떻게 활용되는지 예시를 들어보면. 가령, 브라우저에서 두 가지 다른 사용자에 대한 프로필 정보를 가져와야 하는 경우를 가정해봅시다. 사용자 프로필은 사용자 ID에 따라 가져오며, 동일한 사용자의 프로필 정보를 여러 번 불러올 가능성이 있습니다. React Query의 queryKey를 사용하여 이를 처리할 수 있습니다.
{ data } = useQuery(['userProfile', { userId: 1 }], fetchUserProfile);
여기서 'userProfile'은 캐시 키의 일부이며, { userId: 1 }은 쿼리의 매개변수입니다.
- 첫 번째 요청: 이 쿼리를 처음 호출하면, 사용자 ID가 1인 프로필을 가져와서 캐시에 저장됩니다. 캐시 키는 'userProfile'와 { userId: 1 }의 조합으로 생성됩니다.
- 두 번째 요청: 같은 사용자 ID(1)에 대한 정보를 다시 요청하면 React Query는 캐시 키를 확인하여 동일한 쿼리를 실행합니다. 이 때, 동일한 캐시 키가 이미 존재하므로 네트워크 요청을 보내지 않고 이전에 캐시된 데이터를 사용합니다.
- 다른 사용자 ID에 대한 요청: 이제 사용자 ID가 2인 프로필을 가져와야 한다면, 다른 ID에 대한 쿼리를 실행할 때 다른 userId를 사용하여 queryKey를 변경합니다. 이 경우, 다른 사용자의 프로필을 가져오기 위한 별도의 네트워크 요청이 발생하며, 그 결과가 다른 캐시 키에 저장됩니다.
이런 식으로 queryKey를 활용하여 동일한 쿼리이지만 매개변수가 다른 경우에도 React Query는 각각의 캐시 키를 구분하고 적절한 캐시를 활용하여 중복된 요청을 최소화합니다. 이는 브라우저에서 중복된 데이터를 효율적으로 관리하고, 동일한 요청에 대한 네트워크 부하를 줄여주는 데 도움을 줍니다.
Stale Time 및 Cache Time
- staleTime과 cacheTime을 설정하여 데이터가 캐시되는 기간을 제어할 수 있습니다. staleTime은 데이터가 갱신되기 전까지 캐시된 데이터를 사용할 수 있는 시간을 나타내며, cacheTime은 데이터가 캐시되는 시간을 나타냅니다.
- cacheTime은 데이터가 캐시되는 시간을 나타냅니다. 이 시간이 지나면 React Query는 데이터를 캐시에서 삭제하고 다시 가져와야 합니다. 따라서 cacheTime을 사용하면 캐시된 데이터를 일정 시간 동안 유지하고, 그 이후에는 새로운 데이터를 가져와 캐시를 업데이트할 수 있습니다.
- staleTime은 캐시된 데이터가 만료되기 전까지 허용되는 시간을 나타냅니다. 데이터가 만료되면 React Query는 백그라운드에서 새로운 데이터를 가져오고 캐시를 갱신합니다. 이를 통해 빠른 응답성을 유지하면서도 데이터의 신선도를 유지할 수 있습니다.
const { data } = useQuery('todos', fetchTodos, {
staleTime: 10000, // 10 seconds
cacheTime: 60000, // 1 minute
});
위 코드에서는 staleTime을 10초로 설정했습니다. 이는 데이터가 10초 동안 캐시되어 있으면 만료되기 전까지 캐시된 데이터를 사용할 수 있다는 것을 의미합니다. 따라서 10초가 지나면 React Query는 백그라운드에서 새로운 데이터를 가져와 캐시를 갱신합니다.
또한 cacheTime을 1분(60초)으로 설정했습니다. 이는 데이터가 최대 1분 동안 캐시되어 있음을 의미합니다. 1분이 지나면 React Query는 데이터를 삭제하고 새로운 데이터를 가져와 캐시를 업데이트합니다.
이렇게 staleTime과 cacheTime을 조절하여 데이터의 캐싱 및 갱신 시점을 조절할 수 있으며, 이를 통해 애플리케이션의 성능과 사용자 경험을 최적화할 수 있습니다.
Invalidate Queries
- invalidateQueries 함수를 사용하여 특정 쿼리를 강제로 재로딩하거나 캐시를 무효화할 수 있습니다. 이를 통해 특정 이벤트에 대한 데이터 갱신을 트리거하거나 새로운 정보를 강제로 가져올 수 있습니다.
- 예를 들어, 사용자가 블로그 게시물을 작성하고 게시하는 경우를 가정해봅시다. 새로운 게시물을 작성하면 블로그 리스트에 새로운 게시물이 반영되어야 합니다. 이때 invalidateQueries를 사용하여 게시물 목록 쿼리를 강제로 다시 불러와 업데이트할 수 있습니다.
const { mutate } = useMutation(createBlogPost, {
onSuccess: () => {
// 새로운 게시물이 작성되면 게시물 목록 쿼리를 갱신한다.
queryClient.invalidateQueries('blogPosts');
},
});
위의 예시에서, useMutation 훅을 사용하여 새로운 게시물을 작성하는데 성공했을 때 onSuccess 콜백을 실행합니다. 이 콜백에서 queryClient.invalidateQueries('blogPosts')를 호출하여 'blogPosts'라는 키를 가진 쿼리의 캐시를 무효화합니다.
이렇게 하면 React Query는 'blogPosts' 쿼리의 캐시를 지우고, 다음에 해당 쿼리를 요청할 때 새로운 데이터를 가져와서 캐시를 업데이트합니다. 이러한 방식으로 invalidateQueries를 사용하여 특정 이벤트에 대한 데이터 갱신을 트리거하거나 새로운 정보를 가져올 수 있습니다.
Custom Query Resolvers
- queryKey에 대한 커스텀 resolver 함수( React Query에서 캐시된 데이터를 가져오거나 업데이트할 때 사용되는 함수 )를 정의하여 데이터를 가져오는 방법을 완전히 커스텀할 수 있습니다. 이를 사용하여 캐시에 저장되는 데이터의 형태나 내용을 조정하거나, 특정 조건에 따라 다른 데이터를 반환할 수 있습니다.
- 일반적으로 React Query는 queryKey를 기반으로 데이터를 가져옵니다. 하지만 때때로 이 queryKey만으로는 원하는 데이터를 가져오기에 충분하지 않을 수 있습니다. 이때 커스텀 쿼리 리졸버를 사용하여 원하는 데이터를 가져오는 방법을 정의할 수 있습니다.
- 예를 들어, 쿼리의 키로는 사용자 ID만 받지만 실제로는 사용자 정보와 관련된 다른 데이터도 필요한 경우가 있습니다. 이때 커스텀 쿼리 resolver 를 사용하여 해당 사용자 정보와 관련된 다른 데이터도 함께 가져올 수 있습니다.
const fetchUserData = async (userId) => {
// 사용자 데이터를 가져오는 API 호출
const userData = await axios.get(`/users/${userId}`);
// 사용자의 포스트 목록을 가져오는 API 호출
const userPosts = await axios.get(`/posts?userId=${userId}`);
// 사용자 데이터와 포스트 목록을 함께 반환
return {
userData: userData.data,
userPosts: userPosts.data,
};
};
const { data } = useQuery(['userProfile', userId], async () => {
const userData = await fetchUserData(userId);
return userData;
});
위의 예제에서 fetchUserData 함수는 주어진 사용자 ID를 기반으로 사용자 데이터와 포스트 목록을 가져오는 함수입니다. 그리고 useQuery 훅을 사용하여 커스텀 쿼리 리졸버를 구현합니다. 이때 쿼리 키로는 userId를 포함하는 배열 ['userProfile', userId]를 사용합니다.
커스텀 쿼리 resolver 함수인 useQuery의 두 번째 파라미터로 전달된 함수는 데이터를 가져오는 로직을 담고 있습니다. 이 함수 안에서 fetchUserData를 호출하여 필요한 데이터를 한꺼번에 가져옵니다. 이를 통해 React Query는 이 데이터를 캐싱하고 필요할 때마다 재사용할 수 있게 됩니다.
이렇게 커스텀 쿼리 resolver 를 사용하면 단일 쿼리로 여러 종류의 데이터를 가져올 수 있으며, 필요에 따라 데이터 소스와의 상호작용 방법을 완전히 제어할 수 있습니다.
Query Filters
- queryKey에 대한 필터링을 통해 특정 쿼리만 캐시에 저장하거나, 특정 쿼리의 결과를 변경할 수 있습니다.
const { data } = useQuery(['todos', { userId: 1 }], fetchTodos, {
enabled: shouldFetchData(), // 특정 조건에 따라 데이터 가져오기 활성화 여부 설정
select: (data) => data?.filter(todo => todo.completed), // 가져온 데이터 필터링
keepPreviousData: true, // 이전 데이터 유지 설정
});
위의 예제에서 useQuery 훅을 사용하여 'todos' 쿼리를 호출하고 있습니다. 이 쿼리는 특정 사용자(ID가 1인 사용자)의 할 일 목록을 가져오는 것으로 가정합니다.
여기서 사용된 Query Filters는 다음과 같습니다:
- enabled
- enabled 옵션을 사용하여 특정 조건에 따라 데이터를 가져오는 활성화 여부를 설정할 수 있습니다. shouldFetchData() 함수가 참을 반환하면 데이터를 가져오도록 활성화됩니다.
- select
- select 옵션은 가져온 데이터를 필터링하는데 사용됩니다. 여기서는 data 배열에서 completed가 true인 할 일들만 필터링하여 반환합니다.
- keepPreviousData
- keepPreviousData 옵션은 이전 데이터를 유지하는 옵션입니다. 이전에 쿼리된 데이터가 존재할 경우 이전 데이터를 유지하고, 새로운 데이터를 가져오면 새로운 데이터로 대체합니다.
Query Filters를 사용하여 특정 조건에 맞게 데이터를 가져오거나 결과를 변경할 수 있습니다. 이는 쿼리 결과를 조작하고 필요에 따라 특정 데이터만 캐시에 저장하는 등의 유연한 데이터 관리를 가능케 한다.
결론,
이러한 캐시 커스터마이징 옵션들을 조합하여 데이터를 보다 효율적으로 관리하고, 필요에 따라 캐시 동작을 조정하여 React Query를 활용할 수 있습니다. 또한 이러한 기능들은 애플리케이션의 성능을 향상시키고, 데이터 관리를 더욱 유연하게 만들어줍니다.