fetching, caching, synchronizing, updating server state
가 가능한 라이브러리server state
client state
는 Redux, Recoil 등에서 관리- Client에서 제어하거나 소유되지 않은 원격의 공간에서 관리되고 유지됨
- Fetching이나 Updating에 비동기 API가 필요함
- 다른 사람들과 공유되는 것으로 사용자가 모르는 사이에 업데이트 될 수 있음
- 앱에서 사용하는 데이터가
유효기간이 지난
상태가 될 가능성을 가짐
기능의 특성에 의한 필요성
- 게시글 목록, 상품목록 등은 지속적인 업데이트를 통해 최신상태를 유지해야 하는 특성이 있고 그로인해 고려해야 할 부분들이 생긴다.
- 캐싱
- 서버 데이터 중복 호출 제거
- 만료된 데이터를 백그라운드에서 제거
- 데이터의 만료시점 인지
- 만료된 데이터의 업데이트
위에 작성한 것들을 client에서 관리하는 것이 적합하지 않다는 생각에서 React-Query의 필요성 느낌
우아한 형제들이 React Query를 선택한 이유
- Client Store(Redux, Recoil) 등이 비동기 데이터를 관리하려고 사용되는데 너무 비대해지는 것 같아 그 본질에 있어 의문
- 100개 이상의 데이터를 패칭하는 것과 받은 데이터를 조작과 가공까지 store에서 하는 문제점
- store는
상태 관리를
하는거냐API 데이터 관리를
하는 거냐?React Query가 server state를 관리함에 있어 유용하다고 해서 그냥 도입하는것이 아니라 기존에 사용되고 있는 기술에 있어 의문점을 품고 단점을 보완하기 위해 도입
- Unique Key를 통해 다른곳에서도 해당 query의 결과를 꺼내올 수 있음
$ npm i react-query
$ yarn add react-query
<script src="https://unpkg.com/react-query/dist/react-query.production.min.js"></script>
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from "react-query/devtools";
const client = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<QueryClientProvider client={client}>
<ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
<App />
</QueryClientProvider>
);
QueryClient
인스턴스 사용 및 client에 접근이 가능하도록 QueryClientProvider
설정React Query LifeCycle
- fetching: 현재 요청중인 쿼리
- fresh: stale 상태가 아닌 쿼리, 컴포넌트가 마운트 및 업데이트되도 데이터를 다시 요청하지 않음
- stale: fresh 상태가 아닌 상태로서 만료된 쿼리, 컴포넌트가 업데이트 되면 데이터를 다시 요청
- inactive: 사용하지 않는 쿼리로써 일정 시간이 지나면 가비지 컬렉터를 통해 캐시에서 제거
- delete: 캐시에서 제거된 쿼리
React Query 핵심 개념
- Queries: Unique Key를 가지며 GET, POST 메서드와 관련
- Mutations: C, U, D 등 서버 데이터의 수정과 관련된 기능
- Query Invalidation: 캐싱된 쿼리 데이터가 유효한지 여부를 판단
알아두면 도움이 되는 React Query의 기본 설정
- useQuery로 가져온 데이터는 기본적으로 stale한 상태(staleTime: 0)
- stale한 상태의 쿼리가 데이터를 다시 요청하는 경우
- 새로운 쿼리 인스턴스가 마운트되었을 때
- 브라우저 윈도우가 다시 포커스되었을 때
- 네트워크가 다시 연결되었을 때
- refetchInterval 옵션이 있을 때
- inactive한 상태의 쿼리는 300초 뒤에 메모리에서 해제(cacheTime: 5분)
- 백그라운드에서 3회 이상 실패한 쿼리는 에러처리 된다. (retry 옵션으로 재시도 횟수, retryDelay 옵션으로 재시도 대기시간 설정)
- 쿼리결과는 memoization 을 위해 structural sharing(구조상 데이터들을 공유, 원본 유지)을 사용. 데이터 레퍼런스는 불변.
useQuery의 필요성
import axios from 'axios';
import { useEffect, useState } from 'react';
interface IData {
userId: number;
id: number;
title: string;
body: string;
}
const ReactQuery = () => {
const [data, setData] = useState<IData>();
const fetchData = async () => {
axios.get('https://jsonplaceholder.typicode.com/posts').then((res: any) => {
setData(res.data);
});
};
useEffect(() => {
fetchData();
}, []);
return (
<>
<div>{data?.userId}</div>
<div>{data?.id}</div>
<div>{data?.title}</div>
<div>{data?.body}</div>
</>
);
};
export default ReactQuery;
// useQuery 사용
import axios from 'axios';
import { useQuery } from 'react-query';
interface IData {
userId: number;
id: number;
title: string;
body: string;
}
const ReactQuery = () => {
const fetchData = () => {
return axios.get('https://jsonplaceholder.typicode.com/posts');
};
const { isLoading: loading, data } = useQuery<IData>(['fetch-data'], fetchData);
if (loading) return;
return (
<>
<div>{data?.userId}</div>
<div>{data?.id}</div>
<div>{data?.title}</div>
<div>{data?.body}</div>
</>
);
};
export default ReactQuery;
useQuery의 주요 개념
- queryKey:
고유 키값
으로 문자열의 배열로 할당, 데이터 캐싱에 참조되며, key가 같은 쿼리들은 하나의 요청으로 값을 공유한다.- queryFn :
데이터 패치함수
로써 fetcher 즉 Promise 처리하는 함수를 할당. fetch나 axios를 통한 request 함수가 적용된다.- options : 필수가 아니며, 여러 옵션 지정 가능
반환값
- data: 요청 성공시 받는 데이터
- error: 요청 실패시 받는 에러 정보
- refetch: 수동으로 데이터를 refetch하는 함수(stale 및 cache 설정을 무시)
- status: idle(초기상태) / loading(fetch 중) / error(fetch 실패) / success(fetch 성공)
- status의 Boolean 값: isLoading(API 재호출 + 캐시저장), isFetching(API 재호출), isIdle, isSuccess, isError, isStale
stale Time 및 cache Time
const { isLoading: loading, data } =
useQuery<IData>(['fetch-data'],
fetchData,
{
staleTime: 3000,
cacheTime: 3000
});
데이터는 캐싱
되나, 항상 stale 상태를 가지기에 refetch가 계속해서 발생
하게 된다. 또한, cacheTime이 staleTime보다 짧다면 fresh 상태
동안 캐싱이 되지 않을 것
이므로 두 값 모두를 적절히 설정쿼리를 주기적으로 최신화: refetchInterval
const { isLoading: loading, data } =
useQuery<IData>(['fetch-data'],
fetchData,
{
refetchInterval: 2000, // refetch 주기
refetchIntervalInBackground: true, // 윈도우 focus 아니어도 refetch
});
쿼리를 필요할 때 최신화: refetch
const { isLoading: loading, data, refetch } =
useQuery<IData>(['fetch-data'],
fetchData,
{
// 데이터 자동패치 여부(default: true)
// flase로 설정하면 stale 상태에 자동 패칭 비 활성화
enable: false;
});
<button onClick={refetch}>refresh</button>
성공 및 에러 처리: onSuccess, onError
const { isLoading: loading, data } =
useQuery<IData>(['fetch-data'],
fetchData,
{
onSuccess: sucessData // 성공 시 호출 되는 callback 함수
onError: errorData // 에러 발생 시 호출 되는 callback 함수
});
필요한 데이터만 가져오고 싶다: select
const { isLoading: loading, data } =
useQuery<IData>(['fetch-data'],
fetchData,
{
// select() - Data를 변형시키는 옵션 메서드
select: (res) => {
return res.data.map((item) => item.title);
});
서버에 데이터 변경 작업을 가능하게하는 Hook
(Creat, Update, Delete)mutationFn
- axios 또는 fetch를 통해 서버에 API 요청을 하기위한 함수
// 1
const saveData = useMutation((data: any) => axios.post('https://jsonplaceholder.typicode.com/posts', data));
// 2
const saveData = useMutation({
mutationFn: (person: any) => axios.post('https://jsonplaceholder.typicode.com/posts', person)
})
mutate
- useMutaion의 전달인자로 넘겨준 콜백함수나 내용들이 실행될 수 있도록 도와주는 trigger 역할
import axios from 'axios';
import { useState } from 'react';
import { useMutation, useQuery } from 'react-query';
interface IData {
userId: number;
id: number;
title: string;
body: string;
}
const ReactQuery = () => {
const [data, setData] = useState(
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "hello"
},
)
const saveData = useMutation((data: IData) =>
axios.post('https://jsonplaceholder.typicode.com/posts',
data));
const saveData = useMutation(saveData); // useMutate 정의
const onSavePerson = () => {
savePerson.mutate(data); // 데이터 저장
}
return (
return (
<>
<div onClick={onSavePerson}>데이터 추가</div>
</>
);
};
export default ReactQuery;
onSuccess, onError, onSettled
const saveData = useMutation(saveData, {
onSuccess: () => { // 요청이 성공(try)
console.log('onSuccess');
},
onError: (error) => { // 요청시 에러 발생(cath)
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우(finally)
console.log('onSettled');
}
});
const onSavePerson = () => {
savePerson.mutate(data, {
onSuccess: () => { // 요청이 성공(try)
console.log('onSuccess');
},
onError: (error) => { // 요청시 에러 발생(cath)
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우(finally)
console.log('onSettled');
}); // 데이터 저장
}
// 쿼리와 쿼리 상태를 관리하는 메소드들을 포함한 객체 인스턴스
const queryClient = useQueryClient();
const saveData = useMutation(saveData, {
onSuccess: () => { // 요청이 성공(try)
console.log('onSuccess');
// query key를 전달인자로 넘겨줌으로써 쿼리 무효화
queryClient.invalidateQueries('fetch-data');
},
onError: (error) => { // 요청시 에러 발생(cath)
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우(finally)
console.log('onSettled');
}
});
setQueryData
- 기존 queryKey에 해당되는 데이터를 업데이트
onSuccess: (successData) => { // 요청이 성공(try)
console.log('onSuccess');
queryClient.setQueryData('fetch-data', (data) => {
const newData = data; // fetch-data의 현재 데이터
newData.data.push(successData); // 새로운 데이터 push
return newData; // 변경된 데이터로 set
})
},
우아한 형제들에서 React Query를 선택한 이유
https://techblog.woowahan.com/6339/
React-Query(기본 개념)
https://abangpa1ace.tistory.com/m/263
React-Query(useQuery)
https://abangpa1ace.tistory.com/m/264
React-Query(useMutation, invalidateQueries)
https://jforj.tistory.com/244