React Query 알아보기
React Query의 기본적인 내용에 대해 알아보고 정리한 내용입니다.
1. React Query
React Query는 리액트 애플리케이션에서 서버의 데이터를 조회하거나 캐싱, 업데이트, 에러 처리와 같은 비동기 로직을 지원하는 fetching 라이브러리입니다. Server-side와 Client-side 사이에서 비동기 로직을 보다 쉽게 다루게 해주며 Server State를 관리해줍니다.
React Query에서는 Server State를 다음과 같이 정의합니다.
- Client가 제어하거나 소유하지 않는 위치에서 원격으로 유지 됨.
- fetching 및 updating을 위한 비동기 API를 필요로 함.
- 상태가 공유되며 사용자 모르게 변경될 수 있음.
- 주의하지 않으면 애플리케이션이 잠재적으로 "out of date" 상태가 될 수 있음.
React Query는 앞서 언급한 Server State의 관리 기능을 포함하여 아래와 같은 특징도 가지고 있습니다.
- Caching 지원.
- 동일한 데이터에 대한 중복 요청을 제거하고 한 번만 요청하도록 함.
- "out of date" 상태의 데이터를 파악하고 updating 지원.
- Pagination 및 Lazy Loading 성능 최적화.
- Server State의 메모리 관리 및 garbage collection 지원.
- React Hooks와 유사한 인터페이스 제공.
React Query에서 Server State를 모델링하는 과정은 다음과 같습니다.
- Fetching
초기 상태이며 백엔드와 같은 외부 소스로부터 데이터를 가져오기 위해 동작. - Fresh
Fetching 이후에 Server-side와 Client-side의 데이터가 동일하게 유지되는 상태. - Stale
데이터가 오래된 상태이며 Fetching을 통해 Fresh 상태로 유지해줘야 함. - Inactive
애플리케이션에서 사용되지 않는 데이터에 대한 상태.
React Query에서 브라우저 캐시를 관리하는 가비지 컬렉터에 의해 삭제 됨. - Deleted
Inactive 상태의 데이터가 캐시에서 삭제된 상태.
2. React Query 사용하기
React Query 사용을 위한 설정 방법에 대해 알아보겠습니다.
2.1. React Query 설치
다음 명령어를 실행하여 React Query를 설치해줍니다.
$ yarn add react-query
2.2. QueryClientProvider
QueryClientProvider는 리액트 애플리케이션에서 비동기 요청을 처리하기위한 Context Provider로 동작하며 하위 컴포넌트에서 QueryClient를 사용할 수 있게 해줍니다.
index.tsx 파일에 React Query를 다음과 같이 설정해줍니다.
/* index.tsx */
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
2.3. QueryCache
QueryCache는 React Query를 이용하여 사용된 쿼리의 메타 정보와 상태 등의 데이터를 저장하는 용도로 사용합니다. 또한 onError, onSuccess 콜백을 사용하여 애플리케이션 전역에서 이벤트를 핸들링 할 수 있습니다.
QueryCache 사용을 위해 index.tsx 파일에서 QueryClient 부분을 아래와 같이 설정해줍니다.
/* index.tsx */
import { QueryClient, QueryClientProvider, QueryCache } from 'react-query';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
console.log('onError', error);
},
onSuccess: (data) => {
console.log('onSuccess', data);
}
})
});
3. React Query API
React Query를 이용한 API 사용 방법에 대해 알아보겠습니다.
3.1. useQuery
useQuery는 서버에서 데이터를 가져오기(get) 위해 사용하는 hook 입니다. unique key, promise 기반의 함수, 옵션 값을 파라미터로 받아서 동작합니다. unique key는 애플리케이션 전역에서 해당 쿼리를 refetching, caching, sharing 하는 용도로 사용되며, 쿼리의 리턴 값으로는 status, data, error와 같은 템플릿을 포함하여 데이터 사용에 필요한 정보가 제공됩니다.
다음과 같이 기본적인 쿼리의 상태(status)와 프로퍼티를 사용할 수 있습니다.
- isLoading
쿼리에 데이터가 없고 fetching 하는 상태. - isError
쿼리에 에러가 발생한 상태. - isSuccess
쿼리가 성공적으로 실행되었고 데이터를 사용가능한 상태. - isIdle
쿼리를 사용할 수 없는 상태. (disabled) - error
쿼리가 isError 상태인 경우 에러 정보 확인을 위해 사용하는 프로퍼티. - data
쿼리가 isSucess 상태인 경우 데이터 사용을 위해 사용하는 프로퍼티. - isFetching
쿼리의 fetching/refetching 여부에 대한 boolean 값.
다음 예제를 통해 useQuery의 사용 방법에 대해 알아보겠습니다. useQuery에 사용한 파라미터는 순서대로 다음과 같습니다.
- queryKey
쿼리에 사용할 unique key 값. - queryFn
쿼리에 사용할 promise 기반의 비동기 API 함수. - options
쿼리에 사용할 옵션 값.
const {
isSuccess,
isError,
isLoading,
isFetching,
data,
error
} = useQuery(
'getUsers',
() => axios.get(`https://jsonplaceholder.typicode.com/users`),
{
onSuccess: (data) => {
console.log('onSuccess', data);
},
onError: (error) => {
console.log('onError', error);
}
}
);
if (isFetching) {
console.log('fetching...');
}
if (isLoading) {
console.log('loading...');
}
if (isError) {
console.log('error', error);
}
if (isSuccess) {
console.log('success', data);
}
예제를 실행한 결과는 다음과 같습니다.
쿼리의 프로퍼티 중에서 isLoading, isError, isSuccess는 다음과 같이 status 프로퍼티로 대체하여 사용할 수 있습니다.
const {
status,
isFetching,
data,
error
} = useQuery(
'getUsers',
() => axios.get(`https://jsonplaceholder.typicode.com/users`)
);
if (isFetching) {
console.log('fetching...');
}
if (status === 'loading') {
console.log('loading...');
}
if (status === 'error') {
console.log('error', error);
}
if (status === 'success') {
console.log('success', data);
}
3.1.1. useQuery의 Query Key
useQuery에서 파라미터로 사용되는 Query Key는 React Query에서 쿼리 캐싱을 관리하기 위한 unique key로 사용됩니다. 문자열이나 배열로 설정이 가능합니다.
Query Key를 문자열로 사용하면 내부적으로 배열로 변환됩니다. 일반적인 목록이나 인덱싱 된 리소스, 비계층적(Non-hierarchical) 리소스를 대상으로 유용하게 사용할 수 있습니다.
const {
status,
isFetching,
data,
error
} = useQuery(
'getUsers',
() => axios.get(`https://jsonplaceholder.typicode.com/users`)
);
Query Key를 배열로 사용하는 경우엔 unique key로 사용할 문자열과 serializable 객체를 사용해줍니다. 계층적(Hierarchical) 리소스나 중첩된(nested) 리소스를 대상으로 ID 또는 추가적인 파라미터를 전달하여 사용합니다.
다음과 같이 쿼리의 unique key와 함께 쿼리 내부에 전달할 파라미터를 작성하여 사용할 수 있습니다.
const user = {
id: 1
};
const {
status,
isFetching,
data,
error
} = useQuery(
['getUser', user.id],
() => axios.get(`https://jsonplaceholder.typicode.com/users/${user.id}`)
);
배열을 사용한 Query Key는 해싱에서도 차이가 있는데 배열 요소의 순서가 다르면 다르게 해싱됩니다.
// 배열의 순서가 같으면 같은 쿼리로 해싱
useQuery(['todos', { status, page }], ...)
useQuery(['todos', { page, status }], ...)
useQuery(['todos', { page, status, other: undefined }], ...)
// 배열의 순서가 다르면 다른 쿼리로 해싱
useQuery(['todos', status, page], ...)
useQuery(['todos', page, status], ...)
useQuery(['todos', undefined, page, status], ...)
Query Key에 대한 좀 더 자세한 내용은 아래 링크에서 확인할 수 있습니다.
3.1.2. useQuery 동기적으로 사용하기
useQuery는 기본적으로 비동기로 동작합니다. useQuery에 다음과 같이 enabled 옵션을 false로 사용하면 동기적으로 사용할 수 있습니다.
enabled 옵션을 false로 사용하게 되면 컴포넌트가 mount 되거나 window focus 되어도 쿼리가 자동으로 실행되지 않습니다. 또한 queryClient에서 invalidateQueries 또는 refetchQueries 함수를 호출해도 refetching 되지 않습니다. 쿼리는 캐싱되지 않은 idle 상태이며 fetching을 위해서는 refetch 함수를 트리거로 사용해야 합니다.
다음 예제를 통해 useQuery의 동기적인 사용 방법에 대해 알아보겠습니다.
const {
status,
refetch,
isFetching,
data,
error
} = useQuery(
'getUsers',
() => axios.get(`https://jsonplaceholder.typicode.com/users`),
{
enabled: false,
onSuccess: (data) => {
console.log('onSuccess', data);
},
onError: (error) => {
console.log('onError', error);
}
}
);
useEffect(() => {
console.log(status);
}, [status]);
return (
<>
<button onClick={() => refetch()}>[ GET USERS ]</button>
</>
);
예제를 실행한 결과는 다음과 같습니다.
이와 관련된 좀 더 자세한 내용은 아래 링크에서 확인할 수 있습니다.
3.1.3. useQueries를 이용하여 useQuery 여러개 사용하기
useQuery는 기본적으로 비동기로 동작하기 때문에 컴포넌트 내에 useQuery가 여러 개 있다면 순서대로 실행되지 않고 동시에 실행됩니다.
이러한 경우에 다음과 같이 useQueries를 이용하면 여러개의 쿼리를 하나로 묶어서 사용할 수 있습니다. 아래 예제를 실행하면 UseQueryResult가 배열로 반환됩니다.
const users = [1, 2, 3, 4, 5];
const results = useQueries(
users.map(user => {
return {
queryKey: ['getUser', user],
queryFn: () => axios.get(`https://jsonplaceholder.typicode.com/users/${user}`)
}
})
);
3.1.4. useQuery 순차적으로 실행하기
앞서 알아본 useQuery의 enabled 옵션을 다음과 같이 사용하면 여러개의 쿼리를 순차적으로 실행할 수 있습니다.
const {
status: firstQueryStatus,
refetch: firstQueryRefetch,
isFetching: firstQueryIsFetching,
data: firstQueryData,
error: firstQueryError
} = useQuery(
['getUser', 1],
() => axios.get(`https://jsonplaceholder.typicode.com/users/1`),
{
onSuccess: (data) => {
console.log('firstQueryData', data);
},
}
);
const {
status: secondQueryStatus,
refetch: secondQueryRefetch,
isFetching: secondQueryIsFetching,
data: secondQueryData,
error: secondQueryError
} = useQuery(
['getUser', 2],
() => axios.get(`https://jsonplaceholder.typicode.com/users/2`),
{
enabled: !firstQueryData,
onSuccess: (data) => {
console.log('secondQueryData', data);
},
}
);
3.1.5. useQuery의 유용한 옵션
useQuery에는 다양한 옵션을 사용할 수 있는데 이 중에서 몇가지 유용한 옵션에 대해 정리하면 다음과 같습니다.
- enabled
false로 설정하면 쿼리가 자동으로 실행되지 않음. - retry
쿼리가 실패한 경우에 대한 재시도 횟수 - staleTime
데이터가 stale state로 변경되는 시간 (Infinity로 설정하면 stale state로 변경되지 않음) - cacheTime
inactive state의 캐시 데이터가 메모리에 남아있는 시간 - refetchInterval
설정한 시간(밀리초)에 따라 주기적으로 fetching 실행. - refetchOnWindowFocus
창에 포커스가 된 경우에 대한 refetch 여부. - initialData
쿼리의 초기값 설정
이 외의 다른 다양한 옵션들에 대한 내용은 아래 링크에서 확인할 수 있습니다.
3.2. useMutation
useMutation은 서버를 대상으로 데이터를 create, update, delete 하기 위해 사용하는 hook 입니다.
useMutation에서는 다음과 같은 상태와 프로퍼티를 제공합니다. 앞서 알아본 useQuery와 거의 동일합니다.
- isIdle
mutation이 실행 되지 않아 아직 캐싱되지 않은 상태. - isLoading
mutation이 실행중인 상태. - isError
mutation에 에러가 발생한 상태. - isSuccess
mutation이 성공적으로 실행되었고 데이터를 사용 가능한 상태. - error
mutation이 isError 상태인 경우 에러 정보 확인을 위해 사용하는 프로퍼티. - data
mutation이 isSucess 상태인 경우 데이터 사용을 위해 사용하는 프로퍼티.
다음 예제를 통해 useMutation의 사용 방법에 대해 알아보겠습니다. useMutation에 사용한 파라미터는 순서대로 다음과 같은데 파라미터의 구성도 useQuery와 동일합니다.
- mutationKey
mutation에 사용할 unique key 값. - mutationFn
mutation에 사용할 promise 기반의 비동기 API 함수. - options
mutation에 사용할 옵션 값.
const mutation = useMutation(
'addUser',
(param: any) => axios.post(
`https://jsonplaceholder.typicode.com/users`,
param
),
{
onMutate: (variables) => {
console.log('onMutate', variables);
},
onError: (error, variables, context) => {
console.log('onError', context);
},
onSuccess: (data, variables, context) => {
console.log('onSuccess', data);
},
onSettled: (data, error, variables, context) => {
console.log('onSettled', data);
}
}
);
return (
<>
<button onClick={() => mutation.mutate({title: `title`, body: `body`, userId: 1})}>
[ ADD USER ]
</button>
</>
);
mutate 함수를 호출하여 mutation을 실행해줍니다. 예제를 실행한 결과는 다음과 같습니다.
useMutation의 옵션에 사용된 함수를 정리하면 다음과 같습니다.
- onMutate
mutation이 실행되기 전에 실행되는 함수.
mutationFn에 전달되는 파라미터를 동일하게 받음.
mutation 실패시 onError, onSettled 함수에 return 값을 전달함.
rollback 처리가 필요한 경우에 사용되며 return 값은 context 파라미터로 사용 가능. - onError
mutation 실패시 에러를 전달. - onSuccess
mutation 성공시 데이터를 전달. - onSettled
mutation 성공 또는 실패시 데이터나 에러를 전달.
3.2.1. useMutation에서 쿼리 Invalidation 처리하기
일반적으로 mutation이 성공적으로 동작한 이후에는 다른 관련된 쿼리의 refetch를 필요로 할 가능성이 높습니다. 이러한 경우엔 다음과 같이 QueryClient의 invalidQueries 함수를 사용해줍니다. 이렇게 하면 mutation 성공 이후에 해당 쿼리가 stale 상태로 변경 되어 캐시에서 삭제되고 refetch가 실행되게 됩니다.
const queryClient = new QueryClient();
const mutation = useMutation(
'addUser',
(param: any) => axios.post(
`https://jsonplaceholder.typicode.com/users`,
param
),
{
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries('getUsers');
}
}
);
이와 관련된 좀 더 자세한 내용은 아래 링크에서 확인할 수 있습니다.
3.2.2. useMutation에서 쿼리 데이터 업데이트하기
mutation의 성공 이후에는 response에 새로운 데이터가 반환되는 경우도 많은데 이러한 경우엔 다음과 같이 QueryClient의 setQueryData 함수를 사용하여 해당 쿼리의 데이터를 새로운 데이터로 업데이트해줍니다.
const queryClient = new QueryClient();
const mutation = useMutation(
'updateUser',
(param: any) => axios.put(
`https://jsonplaceholder.typicode.com/users/1`,
param
),
{
onSuccess: (data, variables, context) => {
queryClient.setQueryData('getUser', data)
}
}
);
이와 관련된 좀 더 자세한 내용은 아래 링크에서 확인할 수 있습니다.
이상으로 React Query의 기본적인 내용에 대해 알아봤습니다.
※ References
- react-query.tanstack.com, React Query - Hooks for fetching, caching and updating asynchronous data in React, https://react-query.tanstack.com/
- www.rootstrap.com, React Query and Management of Server State, https://www.rootstrap.com/blog/react-query-and-management-of-server-state/
- maxkim-j.github.io, React-Query 살펴보기, https://maxkim-j.github.io/posts/react-query-preview
- kyounghwan01.github.io, react-query, https://kyounghwan01.github.io/blog/React/react-query/basic/
- techblog.woowahan.com, Store에서 비동기 통신 분리하기 (feat. React Query), https://techblog.woowahan.com/6339/
- velog.io/@woodong, 2. useQuery의 활용, https://velog.io/@woodong/2.-useQuery%EC%9D%98-%ED%99%9C%EC%9A%A9