치춘짱베리굿나이스
react-query 1. useQuery 본문
react-query
설치
$> npm i react-query
$> yarn add react-query
npm 링크
yarn 링크
용례
비동기 로직을 다루는 데에 도움을 주는 라이브러리
대개 서버에서 가져오는 상태값들을 효율적으로 관리하거나, 서버의 상태값을 변경하는 데 큰 도움을 준다
리액트 쿼리로 편-안하게 개발할 수 있는 요소들은 다음과 같다
- 데이터 캐싱하기
- get해서 가져온 데이터 업데이트할 때 get 재수행
- 예시: 게시판에 게시글 업로드했을 때, 게시글 목록 다시 불러오기 등
- 오래된 데이터 get 재수행하여 새로운 데이터로 만들어주기
- 무한스크롤
isLoading
,isError
등Suspense
와ErrorBoundary
로 다뤄주던 요소들- 중복 호출 허용 시간 조절 (같은 데이터 짧은 시간 내에 여러번 호출하지 않도록 방지)
대충 직접 짜려고 하면 아주 머리터지는 요소들이 많이 보인다... 이것들을 쉽게 해결해 준다
대신 설정해야할 값이 다양하고 캐시의 라이프사이클, 비동기에 대해 사전지식을 어느정도 갖추어야 설정값들을 자유자재로 다룰 수 있어 입문 난이도가 어려운 편이다
워낙에 다뤄야 할 내용이 많고 방대하니 이번에는 useQuery
위주로 정리해보도록 하겠다
1. 사전 세팅
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevTools } from 'react-query/devtools';
...
// 최상단 요소
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ReactQueryDevTools /> {/* 개발 중에 리액트 쿼리 관련 요소들 쉽게 체크할 수 있도록 도와주는 툴 */}
<Router>
</QueryClientProvider>
</React.StrictMode>
라이브러리를 설치했다면, 리액트 프로젝트의 최상단에 (또는 리액트 쿼리를 적용할 컴포넌트에) QueryClientProvider
로 감싸주자
만약 리액트 쿼리 개발에 도움을 주는 툴을 쓰고 싶다면 ReactQueryDevTools
컴포넌트도 추가한다 (단, 배포 단계에서 지워주지 않으면 앱 최상단에 리액트 쿼리 아이콘이 둥둥 떠다니는 ㅡ,,ㅡ; 흉물스러운 앱이 되어버리며, 사용자들이 API 데이터에 접근할 수 있게 되므로 위험하다)
1-1. React-Query Devtools
왼쪽 구석 (기본값) 에 있는 이 꽃이 데브툴이다
컴포넌트의 position
속성으로 위치를 변경할 수 있다
꽃 아이콘을 클릭하면 지금까지 요청된 쿼리들의 상태와 데이터, 설정값, 해당 쿼리를 요청하는 observer의 수, 마지막 업데이트 시점 등을 볼 수 있다
props로 받아오는 client
는 react-query
라이브러리에서 불러온 QueryClient
를 전달해 주자
1-2. 쿼리의 상태값 (Data fetching State)
const { data } = useQuery(
['todos'], // Query가 의존성을 갖는 값들
fetchTodos,
{
// 옵션들
// staleTime, cacheTime이 지정되어 있지 않다고 가정하자
// staleTime의 기본값은 0, cacheTime의 기본값은 5분이다
},
);
... // TodoList 컴포넌트
const TodoList = () => {
const { data } = useQuery(['todos'], fetchTodos, ...);
...
}
... // SettingPage 컴포넌트
const SettingPage = () => {
const { data } = useQuery(['todos'], fetchTodos, ...);
...
}
위의 아주 간단한 useQuery
훅으로 라이프사이클을 알아보자
TodoList
컴포넌트가 마운트되면서, 컴포넌트 내부에서 지정한 쿼리 인스턴스가 마운트된다- 첫 마운트 시에는 ‘todos’ 키를 갖는 (또는 배열 안의 모든 값들에 의존성을 갖는) 쿼리가 존재하지 않으므로,
fetchTodos
함수를 사용해서 데이터를 가져온다 useQuery
훅에서는 받아온 데이터를 ‘todos’ (또는 배열 안의 값들) 을 고유한 식별자로 갖는 캐시로 저장한다- 이 캐시는 처음에는
fresh
하지만,staleTime
만큼의 시간이 흐르면stale
상태로 변경된다
- 첫 마운트 시에는 ‘todos’ 키를 갖는 (또는 배열 안의 모든 값들에 의존성을 갖는) 쿼리가 존재하지 않으므로,
- 같은 쿼리를 사용하는 다른 컴포넌트
SettingPage
가 마운트되면서, 새로운 인스턴스가 마운트된다- 위에서 이미 ‘todo’ 키를 갖는 쿼리가 생성되었으므로,
useQuery
는 데이터를 서버에서 가져오지 않고 고유 식별자를 이용하여 캐시에서 쿼리를 찾아 바로 데이터를 반환한다
- 위에서 이미 ‘todo’ 키를 갖는 쿼리가 생성되었으므로,
- 화면에 새로운 인스턴스가 렌더링되면서
useQuery
는 백그라운드에서 refetch를 진행한다- 한 쿼리를 두 컴포넌트가 사용하기 때문에, fetch는 단 한번만 이뤄지며 데이터는 양쪽에서 모두 업데이트된다
useQuery
를 쓰는 모든 컴포넌트가 언마운트되면, 해당 쿼리를 사용하는 컴포넌트가 없으므로 캐시는inactive
상태로 변경된다inactive
상태에서cacheTime
만큼의 시간이 지나면 (기본값은 5분이다) 캐시된 쿼리와 데이터가 가비지 콜렉터에 의해 삭제된다
가능한 상태값의 종류는 다음과 같다
fetching
: 현재 요청 중인 쿼리이며, 요청에 성공했을 경우fresh
로 바뀐다fresh
: 만료되지 않은 신선한 쿼리, 설정한staleTime
이 지나면 해당 쿼리는stale
상태로 변경된다fresh
한 쿼리는 컴포넌트의 마운트와 업데이트가 발생해도 데이터를 재요청하지 않는다- 예를 들어
input
태그 내의 값이 변경되면onChange
함수에 의해 컴포넌트가 업데이트되지만, 해당 값으로 불러오는 쿼리가fresh
한 쿼리일 경우 재요청 없이 값을 바로 가져온다
stale
: 안 신선한 쿼리,fresh
상태의 쿼리에서 일정 시간이 지날 경우stale
로 변경된다- 컴포넌트가 마운트되거나 업데이트되었을 때
- 브라우저 윈도우가 다시 포커스되었을 때 (
refetchOnWindowFocus
설정값에 의존한다) - 네트워크가 다시 연결되었을 때 (
refectchOnReconnect
설정값에 의존한다) refetchInterval
옵션에 의해 설정된 refetch 주기가 지났을 때- 위의 조건을 만족하면 쿼리가 재요청되면서
fresh
로 돌아온다
inactive
: 사용되지 않는 쿼리, 캐시에만 남아있다- 설정한
cacheTime
이 지나면 가비지 컬렉터가 해당 쿼리를 제거한다 - 현재 화면에 보이는 컴포넌트 중 하나라도 쿼리를 사용하고 있을 경우 해당 쿼리는
stale
상태가 유지되며, 모든 컴포넌트가 쿼리를 사용하지 않을 때inactive
로 변경된다 cacheTime
값이infinity
라면inactive
한 쿼리는 영원히 삭제되지 않는다stale
은 리액트 쿼리가 데이터를 최신으로 유지할 지 말 지 결정하도록 돕는 상태값이고,inactive
는 해당 쿼리 데이터가 실제로 사용되고 있는지 여부를 판단하는 상태값이라고 생각하면 된다
- 설정한
fresh
데이터가 staleTime
이 지난 후 stale
로 변경되는 모습이다 (Devtool)
위의 검색결과 컴포넌트에서 해당 데이터를 사용하고 있기 때문에, inactive
로 변하진 않는다
2. 데이터 받아오는 api 함수 작성하기
import axios from "axios";
export const getDiseaseData = (searchText: string) =>
axios.get("/getDissNameCodeList", {
params: {
serviceKey: process.env.REACT_APP_API_KEY,
searchText,
},
});
아주 간단하게 axios
를 사용해서 작성해보자
예시로는 axios.get
메서드만 사용해서 바로 반환하도록 해주었지만, then
/ catch
나 try
/ catch
, finally
등을 이용하여 데이터를 가공해도 무방하다
Promise
형태만 띄고 있으면 된다
3. useQuery 이용하여 컴포넌트 내에서 데이터 받아오기
import { useQuery } from 'react-query';
import { getDiseaseData } from 'services';
export const SearchResultList = () => {
const searchText = // redux, recoil, useState 등으로 상태값 선언
const { data } = useQuery(
['diseaseData', searchText],
() => getDiseaseData(searchText),
{
// 옵션
},
});
return (
{ data.map((item) => { /* 받아온 결과값으로 렌더링하는 부분 */} }
);
};
useQuery
의 첫 번째 인자로 배열이 들어온다
- 보통 배열의 0번째 인덱스 값은 고유 키값이 들어오며, 리액트 쿼리에서 이 값을 보고 어떤 쿼리인지 판단한다
- 배열에 추가적으로 해당 쿼리가 의존성을 가지는 값을 넣어줄 수 있으며, 여기에 들어온 상태값 중 하나라도 바뀔 때마다 리액트 쿼리는 데이터를 새로 요청하거나, 캐시에서 값을 가져온다
- 예시에서는
[’diseaseData’, searchText]
가 첫 번째 인자로 들어왔으므로, 쿼리는 고유한 식별자로‘diseaseData’
와searchText
상태값을 가진다- 예를 들어
searchText
가‘간’
이라는 문자열일 경우, 이 때 요청된 쿼리는‘diseaseData’
와‘간’
이라는 두 개의 식별자를 가지며, 이후에searchText
가 다시 한번‘간’
이라는 값을 가질 때 재요청을 할 지, 같은 식별자를 가진 쿼리 캐시를 가져올지 판단한다
- 예를 들어
- 의존성을 갖는 값이 바뀔 때마다 리액트 쿼리는 데이터를 재요청한다
- 예를 들어,
searchText
가 ‘간' 에서 ‘뇌' 로 바뀔 경우, 식별자가‘diseaseData’
,‘뇌'
인 쿼리를 서버에 요청하거나, 캐시에서 찾는다
- 예를 들어,
두 번째 인자는 데이터를 서버에 요청할 때 사용하는 함수이다
- 굳이
axios
나fetch
를 사용할 필요는 없고, 비동기Promise
형태이기만 하면 된다 then
이나try
/catch
체인을 이용하여 값을 반환하는 함수여도 상관없다- 이 함수에서 반환되는 값이 각 쿼리별 데이터로 캐싱된다
- 캐시의 키 값 (식별자) 는 첫 번째 인자로 넣은 배열의 값들, 데이터 값은 두 번째 인자로 넣은 함수로 받아온 데이터가 들어간다고 생각하면 된다
세 번째 인자는 옵션으로, 객체가 들어온다
suspense
옵션은Suspense
(데이터 요청 시 Loading 컴포넌트 fallback 설정) 여부를 결정한다useErrorBoundary
옵션은 에러 발생 시 별도의fallback
컴포넌트 렌더링 여부를 결정한다staleTime
은fresh
에서stale
상태로 바뀔 때까지 걸리는 시간을 결정한다 (기본: 0)cacheTime
은inactive
상태에서 캐시가 제거되기까지 걸리는 시간을 결정한다 (기본: 5분)enabled
옵션은false
일 때 쿼리 요청을 중단하며, 조건식을 넣어 특정 조건에서만 쿼리가 요청되도록 조정할 수 있다refetchOnWindowFocus
,refetchOnReconnect
등은 특정 상황에서 refetch (데이터 재요청) 를 진행할 지 여부를 설정할 수 있다onSuccess
,onError
은 데이터 받아오기 성공, 실패 시 후속 동작 함수를 정의할 수 있다
4. useQuery 반환값
useQuery로부터 다양한 반환값을 받아올 수 있으며, 이를 이용하여 로딩 중 여부, 재요청, 캐시의 상태, 에러 값 등을 받아올 수 있다
data
: 데이터 요청에 대한 응답을 성공적으로 받았을 때 응답이 반환된다error
: 데이터 요청이 실패했을 때 에러 값이 반환된다refetch
: 이 함수를 호출하면 현재 지정된 쿼리가 재요청된다isLoading
,isError
,isSuccess
, … : 데이터 요청 상황을 반환한다
5. 커스텀 훅으로 만들기
const useFilteredQuery = () => {
const searchText = // redux, recoil, useState 등으로 상태값 선언
const { data, refetch, } = useQuery(
['diseaseData', searchText],
() => axios.get('test',
{
params: {
apiKey: process.env.REACT_APP_API_KEY,
searchText,
pages: 1,
},
}).then((res) => {
return data.filter((item) => {
// 필터링 조건
});
}),
{
staleTime: 60000, // 1분
suspense: true,
useErrorBoundary: true,
enabled: searchText !== "",
onSuccess: () => {
console.log("fetched successfully");
},
}
);
return { data, refetch, searchText };
};
같은 쿼리 설정으로 다른 컴포넌트에서도 데이터를 요청하거나, useQuery
인자값들이 너무 길어질 경우 커스텀 훅으로 만들어 분리하여 사용할 수 있다
커스텀 훅 특성상 컴포넌트의 리렌더나 내부에서 의존성을 갖는 값이 있을 경우 재실행되어 값을 최신 상태로 유지하므로 어느 컴포넌트던 커스텀 훅 호출 코드 한 줄이면 리액트 쿼리로 데이터를 관리할 수 있다
번외: 쿼리 무효화하기
import { useQueryClient } from "react-query";
...
const queryClient = useQueryClient();
queryClient.invalidateQueries();
커뮤니티 게시판 등, 데이터를 가급적 최신 상태로 유지해야 하는 경우 임의로 쿼리를 무효화한 후 새 데이터를 요청하도록 만들 수 있다
useQueryClient
훅으로 쿼리 클라이언트를 받아오고, invalidateQueries
메서드로 특정 쿼리를 무효화하자
- 인자로 아무 것도 안 넣을 경우, 현재 클라이언트 내의 모든 쿼리를 무효화한다
- 인자로 배열을 넣을 경우, 해당 배열의 값들을 의존성으로 갖는 쿼리를 무효화한다
- 인자로 키 하나만을 넣을 경우, 해당 키를 의존성으로 갖는 모든 쿼리를 무효화한다
무효화란, fresh
상태 (재요청을 하지 않는 상태) 를 무시하고 즉각 서버에 데이터 재요청을 보낸다는 것이다
원래대로라면 쿼리가 stale
상태여야 재요청을 시도하지만, 쿼리를 무효화하면 stale
여부와 관계없이 바로 데이터를 새로 가져오도록 할 수 있다
inactive
쿼리와는 관계 없다
번외: Suspense, ErrorBoundary 사용하기
const { data } = useQuery(
['diseaseData', searchText],
() => getDiseaseData(searchText),
{
suspense: true, // Suspense 사용
useErrorBoundary: true, // Error Boundary 사용
},
});
위와 같이 설정하면 각각 suspense, errorBoundary 를 사용할 수 있다
const Loading = () => { // suspense fallback 컴포넌트
return (
<div classame={styles.loading}>
Loading...
</div>
);
};
const Error = ({ error }: Props) => { // errorBoundary fallback 컴포넌트
return (
<div className={styles.error}>
{error.message}
</div>
);
};
로딩 컴포넌트와 에러 컴포넌트를 만들어준다
각각 데이터 응답을 기다리는 중일 때와, 에러가 발생하였을 때 폴백으로 렌더링되는 컴포넌트이다
<Suspense fallback={<Loading />}>
<ErrorBoundary fallbackRender={({ error }) => <Error error={error} />}>
<Weather />
</ErrorBoundary>
</Suspense>
위에서 지정한 useQuery
로 데이터를 받아오는 컴포넌트를 ErrorBoundary
와 Suspense
컴포넌트로 감싸주자
ErrorBoundary
는 react-error-boundary
라이브러리를 사용하고, Suspense
는 기본으로 탑재되어 있다
Weather
컴포넌트 내의 useQuery
에서 에러 발생 시, 가장 가까운 ErrorBoundary
로 에러 처리를 위임한다
또한 데이터를 받아오는 중일 때 Loading 컴포넌트가 렌더링된다
번외: 그외 정보
- 같은 키 (식별자) 를 갖는 쿼리가 컴포넌트별로 여러 개 존재하더라도, 리액트 쿼리에 의한 요청은 1번만 수행된다 = 모든 쿼리는 키 값 (배열 내의 값) 에 의해 구분되므로, 서로 다른 데이터를 요청하고 싶다면 키 값을 다르게 주어야 한다
- 같은 키에 대한 쿼리 요청을 넣었을 때, 응답이 이전 응답 데이터와 같다면, 메모리에 다시 할당하지 않고 기존의 참조를 반환한다
queryClient
의 메서드를 이용하여 캐싱된 데이터를 가져오거나 조작하는 방법이 있다
참고자료
What Is The Difference Between Stale State and Inactive State in React Query
'ClientSide > 라이브러리' 카테고리의 다른 글
구글 애널리틱스 (0) | 2022.05.29 |
---|---|
redux + toolkit (0) | 2022.05.21 |
gh-pages로 리액트 프로젝트 배포 + 404 에러처리 (1) | 2022.05.15 |
React-Beautiful-Dnd (0) | 2022.05.15 |
lodash (0) | 2022.05.14 |