- React-Query 설치
Client Props연결DevTools사용useQueryuseQueriesuseQueryClientuseMutation✨PagenationInfinite Query- Optimistic Update
- Error Handling
기본적인 사용 방식은 v4 버전을 기본값으로 코드를 작성
# v3
npm i react-query
# devtools는 자동으로 설치# or
yarn add react-query
# devtools는 자동으로 설치# v4
npm i @tanstack/react-query
npm i @tanstack/react-query-devtools
# or
yarn add @tanstack/react-query
yarn add @tanstack/react-query-devtoolsimportReactfrom"react";importReactDOMfrom"react-dom/client";import{BrowserRouter}from"react-router-dom";import{QueryClientProvider,QueryClient}from"@tanstack/react-query";import"./index.css";importAppfrom"./App";importreportWebVitalsfrom"./reportWebVitals";constqueryClient=newQueryClient();constroot=ReactDOM.createRoot(document.getElementById("root"));root.render(<QueryClientProviderclient={queryClient}>{/* client props 연결 */}<BrowserRouter><App/></BrowserRouter></QueryClientProvider>);reportWebVitals();importReactfrom"react";importReactDOMfrom"react-dom/client";import{BrowserRouter}from"react-router-dom";import{QueryClientProvider,QueryClient}from"@tanstack/react-query";import{ReactQueryDevtools}from"@tanstack/react-query-devtools";import"./index.css";importAppfrom"./App";importreportWebVitalsfrom"./reportWebVitals";constqueryClient=newQueryClient();constroot=ReactDOM.createRoot(document.getElementById("root"));root.render(<QueryClientProviderclient={queryClient}>{/* client props 연결 */}<BrowserRouter><App/></BrowserRouter><ReactQueryDevtoolsinitialIsOpen={false}position="bottom-right"/>{/* dev tool 사용 */}</QueryClientProvider>);reportWebVitals();importReactfrom"react";import{useQuery}from"@tanstack/react-query";importaxiosfrom"axios";constRQOdnBuoy=()=>{constgetBuoyData=async()=>{returnawaitaxios.get("https://api.odn-it.com/devices/");};const{// useQuery의 return 값 ex) status, data, error, ...}=useQuery(queryKey,// v4부터 배열의 형태로 작성 ex) ["buoy"]queryFn,// Promise를 반환하는 함수 ex) getBuoyData{// 기타 옵션 ex) cacheTime, staleTime, refetchOnWindowFocus, refetchOnMount, retry, ...});// ...};status: string- 쿼리 요청 함수의 상태를 표현하는 문자열 형태의 값 (에러 핸들링 가능)loading- 캐시된 데이터가 없고 로딩 중인 상태error- 요청 에러 발생 시의 상태success- 요청 성공 시의 상태
data: TData- Default는undefinedisLoading: boolean- 캐싱 된 데이터가 있다면 로딩 여부와 상관없이false를 반환isError: boolean- 에러가 발생할 시,true값 반환isFetched: boolean- 쿼리가 실행되며fetching진행 중(로딩)일 시,boolean을 반환error: null | TError- 쿼리 함수에 오류가 발생할 시, 쿼리에 대한 오튜 객체를 반환
cacheTime: number | Infinity- 데이터가
inactive상태일 시, 캐싱 된 상태로 남아있는 시간 staleTime과 관계없이, 무조건inactive된 시점을 기준으로 캐시 데이터 삭제를 결정- Default:
300000(5분)
- 데이터가
staleTime: number | Infinity- 데이터가
fresh에서stale상태로 변경되는 데 걸리는 시간 fresh상태일 때는 쿼리 인스턴스가 새롭게mount되어도 네트워크 요청(fetch)이 일어나지 않음- Default:
0
- 데이터가
refetchOnWindowFocus: boolean | "always"- 데이터가
stale상태일 경우 윈도우 포커싱 될 때마다refetch를 실행하는 옵션 "always"로 설정하면 항상 윈도우 포커싱 될 때마다refetch를 실행- Default:
true
- 데이터가
refetchOnMount: boolean | "always"- 데이터가
stale상태일 경우,mount마다refetch를 실행하는 옵션 "always"로 설정하면 마운트 시마다 매번refetch를 실행false로 설정하면 최초fetch이후에는refetch하지 않음- Default:
true
- 데이터가
Polling- 리얼타임 웹을 위한 기법으로 일정한 주기(특정한 시간)를 가지고 서버와 응답을 주고받는 방식
refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false)- 시간(ms)을 값으로 넣어주면 일정 시간마다 자동으로
refetch
- 시간(ms)을 값으로 넣어주면 일정 시간마다 자동으로
refetchIntervalInBackground: boolean- 브라우저에
focus되어 있지 않아도refetch를 시켜주는 것을 의미
- 브라우저에
retry: boolean | number | (failureCount: number, error: TError) => boolean- 쿼리가 실패하면
useQuery를 특정 횟수만큼 재요청하는 옵션 false인 경우, 실패한 쿼리는 기본적으로 다시 시도하지 않음true인 경우에는 실패한 쿼리에 대해서 무한 재요청을 시도- Default:
3
- 쿼리가 실패하면
select해당 쿼리 함수에서 반환된 데이터의 일부를 변환하거나 선택할 수 있는 옵션
importReactfrom"react";import{useQuery}from"@tanstack/react-query";importaxiosfrom"axios";constRQOdnBuoy=()=>{constgetBuoyData=async()=>{returnawaitaxios.get("https://api.odn-it.com/devices/");};const{ status, data, error }=useQuery(queryKey,// v4부터 배열의 형태로 작성 ex) ["buoy"]queryFn,// Promise를 반환하는 함수 ex) getBuoyData{// 기타 옵션 ex) cacheTime, staleTime, refetchOnWindowFocus, refetchOnMount, retry, ...select: (data)=>{constallData=data?.data.results.map((res)=>res);// 데이터 사용 시, allData.device_id로 비교적 짧은 코드로 작성할 수 있도록 변환returnallData;},});// ...};
useQuery여러 개constgetBuoyData=async(id)=>{returnawaitaxios.get(`https://api.odn-it.com/devices/${id}/`);};const{data: buoy10}useQuery(["buoy",10],()=>getBuoyData(10),{cacheTime: 5*60*1000,// 5분staleTime: 1*60*1000,// 1분refetchOnWindowFocus: true,// 다른 창을 갔다가 돌아왔을 시, refetchrefetchOnMount: true,retry: 2,// error시 fetch 재시도select: (data)=>{constdetailData=data?.data;returndetailData;},});const{data: buoy12}useQuery(["buoy",12],()=>getBuoyData(12),{cacheTime: 5*60*1000,// 5분staleTime: 1*60*1000,// 1분refetchOnWindowFocus: true,// 다른 창을 갔다가 돌아왔을 시, refetchrefetchOnMount: true,retry: 2,// error시 fetch 재시도select: (data)=>{constdetailData=data?.data;returndetailData;},});const{data: buoy14}useQuery(["buoy",14],()=>getBuoyData(14),{cacheTime: 5*60*1000,// 5분staleTime: 1*60*1000,// 1분refetchOnWindowFocus: true,// 다른 창을 갔다가 돌아왔을 시, refetchrefetchOnMount: true,retry: 2,// error시 fetch 재시도select: (data)=>{constdetailData=data?.data;returndetailData;},});const{data: buoy100}useQuery(["buoy",100],()=>getBuoyData(100),{cacheTime: 5*60*1000,// 5분staleTime: 1*60*1000,// 1분refetchOnWindowFocus: true,// 다른 창을 갔다가 돌아왔을 시, refetchrefetchOnMount: true,retry: 2,// error시 fetch 재시도select: (data)=>{constdetailData=data?.data;returndetailData;},});
useQueries의 Dynamic ParallelconstgetBuoyData=async(id)=>{returnawaitaxios.get(`https://api.odn-it.com/devices/${id}/`);};constdata=useQueries({queries: [10,12,14,100].map((id)=>{return{queryKey: ["buoy",id],queryFn: ()=>getOxygenData(id),cacheTime: 5*60*1000,// 5분staleTime: 1*60*1000,// 1분refetchOnWindowFocus: true,// 다른 창을 갔다가 돌아왔을 시, refetchrefetchOnMount: true,retry: 2,// error시 fetch 재시도enable: !!id,select: (data)=>{constdetailData=data?.data;returndetailData;},};}),});
여러 개의
useQuery를 선언하는 경우, 일반적으로 쿼리 함수들은 병렬로 요청돼서 처리됨쿼리 여러 개를 동시에 수행하는 경우, 렌더링이 거듭될 때마다 계속 쿼리가 수행되야 하는 경우가 발생
이러한 쿼리 수행 로직이 결국 hook 규칙에 어긋날 수도 있기 때문에
useQueries를 사용하여 코드의 가독성과 길이, 효율성을 극대화 할 수 있음
QueryClient인스턴스를 반환하며,QueryClient를 통해 캐시와 상호작용함
import{useQueryClient}from"react-query";constqueryClient=useQueryClient();쿼리에 대한 초기 데이터가 필요하기 전에 캐시에서 제공하여 초기값을 지정해주는 방법
useQuery의initialData옵션을 통해서 쿼리를 미리 채워 넣으므로써 초기 로드 상태를 건너 뛸 수 있음
exportconstuseBuoyDetail=(id)=>{constqueryClient=useQueryClient();returnuseQuery(["buoy-detail",id],()=>getBuoyData(id),{cacheTime: 5*60*1000,// 5분staleTime: 1*60*1000,// 1분refetchOnWindowFocus: true,// 다른 창을 갔다가 돌아왔을 시, refetchrefetchOnMount: true,retry: 2,// error시 fetch 재시도// refetchInterval: 5000, // polling (시간에 따라 refetch)// refetchIntervalInBackground: false,select: (data)=>{constdetailData=data?.data;returndetailData;},initialData: ()=>{constcacheData=queryClient.getQueryData(["buoy"])?.data?.results?.find((data)=>data.device_id===parseInt(id));if(cacheData){console.log({cacheData: cacheData});// {cacheData: {…}}return{data: cacheData};}else{console.log({cacheData: undefined});// {cacheData: undefined}returnundefined;}},});};위의 예시에서
queryClient.getQueryData메서드는 기존 쿼리의 캐싱된 데이터를 가져오기 위해 사용할 수 있는 동기 함수- 쿼리가 존재하지 않는다면
undefined를 반환
- 쿼리가 존재하지 않는다면
해당 데이터의 형태를 정확히 할아야
find를 통해 같은 id 값을 찾아 캐시를 불러와 채울 수 있음
- 해당 쿼리의 캐시를 무효화 할 수 있는 방법
import{useMutation,useQueryClient}from"@tanstack/react-query";importaxiosfrom"axios";constaddHeroData=async(hero)=>{returnawaitaxios.post("http://localhost:5000/superheroes",hero);};exportconstuseAddHero=()=>{constqueryClient=useQueryClient();returnuseMutation(addHeroData,{onSuccess: ()=>{queryClient.invalidateQueries("heroes");},});};해당 구현 코드는
useMutation을 사용한 CRUD 예제 중 하나해당 쿼리를 통해
queryKey가 "heroes"인 쿼리의 캐시를 무효화하여 실시간으로 데이터를 최신화
invalidateQueries와 같이 모든 캐시를 무효화 한 뒤 refetch를 하는 경우, 기존의 캐시된 데이터가 많다면 fetch 속도가 느려지는 문제가 생김
import{useMutation,useQueryClient}from"@tanstack/react-query";importaxiosfrom"axios";constaddHeroData=async(hero)=>{returnawaitaxios.post("http://localhost:5000/superheroes",hero);};exportconstuseAddHero=(pageNum)=>{constqueryClient=useQueryClient();returnuseMutation(addHeroData,{onSuccess: (data)=>{queryClient.setQueryData(["heroes",pageNum],(oldData)=>{return{
...oldData,data: [...oldData?.data,data?.data],};});// 쿼리의 캐시된 데이터를 즉시 업데이트하여 실시간으로 수정된 부분을 최신화 시켜주는 작업},});};수정된 부분의 쿼리 데이터를 수동으로 설정하여 쿼리 데이터 내부 로직 자체에서 업데이트하도록 설정하는 방법
정확한 의미로는 서버와 실시간으로 동기화 됐다고는 볼 수 없음
- 하지만, UX 부분에서 보자면 피드백이 즉각적으로 이루어지고 있는 것 처럼 보이기 때문에 유리한 부분이 분명 존재함
데이터 통신의 CRUD 기능을 가능하게 해줌 쉽게 생각해서 Create, Update, Delete는
useMutation를 사용하며, Read는useQuery를 사용여기서 사용할 예제는 부표 데이터가 아닌 json-server를 사용한 임의의 영웅 데이터를 통해 진행할 예정
(부표 데이터 API의 경우, GET을 통해 데이터 조회만 가능)
mutatemutation객체의mutate메서드를 통해 요청 함수를 호출하여 사용할 수 있음onSuccess / onError- 해당 메서드를 통해 성공했을 시,
response데이터를 핸들링할 수 있음
- 해당 메서드를 통해 성공했을 시,
C - Create
RQHeroes.jsximportReact,{useState}from"react";import{Link}from"react-router-dom";import{useHeroes}from"../hooks/useHeroes";import{useAddHero}from"../hooks/useMutation";constRQHeroes=()=>{const[name,setName]=useState("");const[alterEgo,setAlterEgo]=useState("");const{ status, data, error, isFetching, refetch }=useHeroes();const{mutate: addHero}=useAddHero();consthandleAddHero=()=>{consthero={ name, alterEgo };addHero(hero);};if(isFetching){return(// ...);}/** 아래 코드로 에러 핸들링 끝 */if(status==="error"){// status -> success, loading, error...return(// ...);}return(// ...);};exportdefaultRQHeroes;
useMutation.js→useAddHero()import{useMutation,useQueryClient}from"@tanstack/react-query";importaxiosfrom"axios";constaddHeroData=async(hero)=>{returnawaitaxios.post("http://localhost:5000/superheroes",hero);};exportconstuseAddHero=()=>{constqueryClient=useQueryClient();returnuseMutation(addHeroData,{onSuccess: ()=>{queryClient.invalidateQueries("heroes");// post, delete 시, 실시간으로 최신화 시켜주는 작업// 키가 여러 개라면, ["heroes", "detail", ...]},});};
R - Read
U - Update
RQHeroDetail.jsximportReact,{useState}from"react";import{useLocation,useParams}from"react-router-dom";import{useHeroDetail}from"../hooks/useHeroDetail";import{useUpdateHero}from"../hooks/useMutation";constRQHeroDetail=()=>{const{ id }=useParams();constlocation=useLocation();const{ status, data, error, isFetching }=useHeroDetail(id);const[updateValue,setUpdateValue]=useState({name: data?.name,alterEgo: data?.alterEgo,});constname=updateValue.name;constalterEgo=updateValue.alterEgo;consthero={ name, alterEgo };const{mutate: updateHero}=useUpdateHero(id,hero);const[updateToggle,setUpdateToggle]=useState(false);consthandleChange=(e)=>{const{ name, value }=e.target;setUpdateValue({ ...updateValue,[name]: value});};consthandleUpdate=()=>{updateHero(id,hero);};if(isFetching){return(// ...);}/** 아래 코드로 에러 핸들링 끝 */if(status==="error"){// status -> success, loading, error...return(// ...);}return(// ...);};exportdefaultRQHeroDetail;
useMutation.js→useUpdateHero()import{useMutation,useQueryClient}from"@tanstack/react-query";importaxiosfrom"axios";constupdateHeroData=async(id,hero)=>{returnawaitaxios.put(`http://localhost:5000/superheroes/${id}`,hero);};exportconstuseUpdateHero=(id,hero)=>{constqueryClient=useQueryClient();returnuseMutation(()=>updateHeroData(id,hero),{onSuccess: ()=>{console.log({updateValue: {id: id,hero: hero}});// updateValue: {id: '9', hero: {…}}queryClient.invalidateQueries("heroes");// post, delete 시, 실시간으로 최신화 시켜주는 작업// 키가 여러 개라면, ["heroes", "detail", ...]},});};
D - Delete
RQHeroes.jsximportReact,{useState}from"react";import{Link}from"react-router-dom";import{useHeroes}from"../hooks/useHeroes";import{useAddHero,useDeleteHero}from"../hooks/useMutation";constRQHeroes=()=>{const[name,setName]=useState("");const[alterEgo,setAlterEgo]=useState("");const{ status, data, error, isFetching, refetch }=useHeroes();const{mutate: addHero}=useAddHero();const{mutate: deleteHero}=useDeleteHero();consthandleAddHero=()=>{consthero={ name, alterEgo };addHero(hero);};consthandleDeleteHero=(id)=>{deleteHero(id);};if(isFetching){return(// ...);}/** 아래 코드로 에러 핸들링 끝 */if(status==="error"){// status -> success, loading, error...return(// ...);}return(// ...);};exportdefaultRQHeroes;
useMutation.js→useDeleteHero()import{useMutation,useQueryClient}from"@tanstack/react-query";importaxiosfrom"axios";constdeleteHeroData=async(id)=>{returnawaitaxios.delete(`http://localhost:5000/superheroes/${id}`);};exportconstuseDeleteHero=()=>{constqueryClient=useQueryClient();returnuseMutation(deleteHeroData,{onSuccess: ()=>{queryClient.invalidateQueries("heroes");// post, delete 시, 실시간으로 최신화 시켜주는 작업// 키가 여러 개라면, ["heroes", "detail", ...]},});};
해당 페이지네이션 구현을 위한
page,size,limit등 해당 API에서의 설정법에 따라 코드가 변경될 수 있음 React-Query에서 지원하는 것이 아닌,queryKey값을 통해 구현실제 공식 사이트의 사용법과 다르게 해당 API의 데이터인 next uri 데이터를 통해 다음 데이터의 존재 여부를 체크하는 식으로 구현
(다음 페이지 버튼 비활성화를 위함)
RQOdnBuoyOxygen.jsximportReact,{useState}from"react";import{useBuoyOxygen}from"../hooks/useBuoyOxygen";import{useLocation}from"react-router-dom";import{useNextOxygen}from"../hooks/useNextOxygen";constRQOdnBuoyOxygen=()=>{const[pageNum,setPageNum]=useState(1);constlocation=useLocation();const{ id, deviceID, serialNumber }=location.state;const{data: nextData}=useNextOxygen(id,pageNum);constnextPage=nextData?.next;// 다음 페이지에 데이터가 존재하는 지를 API 데이터를 통해 체크const{ status, data, error, isFetching }=useBuoyOxygen(id,pageNum);if(isFetching){return(// ...);}/** 아래 코드로 에러 핸들링 끝 */if(status==="error"){// status -> success, loading, error...return(// ...);}return(// ...);};exportdefaultRQOdnBuoyOxygen;
API 통신이기 때문에 다음 페이지에 대한 데이터가 존재하지 않아
404에러가 뜨는 것을 방지또한, 데이터를 전부 불러왔을 경우 다음 페이지 버튼을
disabled하기 위한 작업을 진행- 해당 API 데이터의 경우
nexturi와previousuri가 존재하기 때문에 위와 같이 사용이 가능
- 해당 API 데이터의 경우
useNextOxygen.jsimport{useQuery}from"@tanstack/react-query";importaxiosfrom"axios";constgetNextData=async({ queryKey })=>{constid=queryKey[1];constpageNum=queryKey[2];returnawaitaxios.get(`https://api.odn-it.com/devices/${id}/oxygens/?size=3&page=${pageNum}`);};exportconstuseNextOxygen=(id,pageNum)=>{returnuseQuery(["next-oxygen",id,pageNum],getNextData,{select: (data)=>{constnextPage=data?.data;returnnextPage;},});};
- 다음 페이지의 데이터가 존재하는지의 여부를 체크하는 코드
useBuoyOxygen.jsimport{useQuery}from"@tanstack/react-query";importaxiosfrom"axios";constgetOxygenData=async({ queryKey })=>{constid=queryKey[1];constpageNum=queryKey[2];returnawaitaxios.get(`https://api.odn-it.com/devices/${id}/oxygens/?size=3&page=${pageNum}`);};exportconstuseBuoyOxygen=(id,pageNum)=>{returnuseQuery(["oxygen",id,pageNum],getOxygenData,{cacheTime: 5*60*1000,// 5분staleTime: 1*60*1000,// 1분refetchOnWindowFocus: true,// 다른 창을 갔다가 돌아왔을 시, refetchrefetchOnMount: true,retry: 2,// error시 fetch 재시도select: (data)=>{constoxygenData=data?.data.results?.map((res)=>res);returnoxygenData;},});};
- 실제 용존산소 데이터 불러오는 코드
해당 API 데이터 구조상, 다음 페이지를 불러올 수 있도록 존재하는
next데이터를 통해 다음 페이지에 해당하는 uri를 체크하도록 데이터 통신을 추가했음다음 페이지가 존재하지 않을 경우, API 데이터에서 자동으로
next에 해당하는 데이터를null으로 출력하기 때문에, 다음 페이지가null인 경우를 체크하여 데이터의 최종 페이지를 식별
주로 무한 스크롤에 사용
data.pages: TData[]- 해당 데이터의 모든 데이터를 담은 배열
data.pageParams: unknown[]- 데이터를 불러올 때마다 해당 데이터의 params를 담은 배열
isFetchingNextPage: boolean- 다음 페이지를 fetching 중일 경우
true
- 다음 페이지를 fetching 중일 경우
isFetchingPreviousPage: boolean- 이전 페이지를 fetching 중일 경우
true
- 이전 페이지를 fetching 중일 경우
fetchNextPage: (options?: FetchNextPageOptions) => Promise<UseInfiniteQueryResult>- 다음 페이지의 데이터를 fetch하기 위해 사용
fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise<UseInfiniteQueryResult>- 이전 페이지의 데이터를 fetch하기 위해 사용
hasNextPage: boolean- 다음 페이지의 데이터가 fetch 되었는지를 체크하여 fetch되었을 시
true
- 다음 페이지의 데이터가 fetch 되었는지를 체크하여 fetch되었을 시
hasPreviousPage: boolean- 이전 페이지의 데이터가 fetch 되었는지를 체크함
pageParam- 해당 프로퍼티를 통해 무한 스크롤을 위한 페이지 값을 전달할 수 있음
- 아래의 예제의 경우,
getNextPageParam의 조건을 통해 페이지를 추가적으로 증가하도록 구현 - 반드시 기본값으로 초기 페이지 값을 설정해야 함
getNextPageParam: (lastPage, allPages) => unknown | undefined- 첫 번째 인자
lastPage는 fetch한 가장 최근에 가져온 페이지 목록 - 두 번째 인자
allPages는 현재까지 가져온 모든 페이지 데이터
- 첫 번째 인자
getPreviousPageParam: (firstPage, allPages) => unknown | undefinedgetNextPageParam의 반대의 속성을 가지고 있음
API 데이터의 특성을 이용하여 공식 설명 사이트의 예제와 구현하는 방식의 차이가 있음
(해당 데이터 중,
next/previousuri 데이터와 해당 데이터의 총 개수 데이터를 활용하여 구현)
RQOdnInfiniteOxygen.jsximportReact,{Fragment,useState}from"react";import{useLocation}from"react-router-dom";import{useInfiniteOxygen}from"../hooks/useInfinite";import{useNextOxygen}from"../hooks/useNextOxygen";constRQOdnInfiniteOxygen=()=>{const[pageNum,setPageNum]=useState(1);constlocation=useLocation();const{ id, deviceID, serialNumber }=location.state;const{data: nextData}=useNextOxygen(id,pageNum);constpageCount=nextData?.count;constnextPage=nextData?.next;const{ status, data, error, isFetching, isFetchingNextPage, fetchNextPage }=useInfiniteOxygen(id,pageCount);/** 아래 코드로 에러 핸들링 끝 */if(status==="error"){// status -> success, loading, error...return(// ...);}return(// ...);};exportdefaultRQOdnInfiniteOxygen;
useNextOxygen.jsimport{useQuery}from"@tanstack/react-query";importaxiosfrom"axios";constgetNextData=async({ queryKey })=>{constid=queryKey[1];constpageNum=queryKey[2];returnawaitaxios.get(`https://api.odn-it.com/devices/${id}/oxygens/?size=3&page=${pageNum}`);};exportconstuseNextOxygen=(id,pageNum)=>{returnuseQuery(["next-oxygen",id,pageNum],getNextData,{select: (data)=>{constnextPage=data?.data;returnnextPage;},});};
useInfinite.jsimport{useInfiniteQuery}from"@tanstack/react-query";importaxiosfrom"axios";constgetOxygenData=async({ queryKey, pageParam =1})=>{constid=queryKey[1];returnawaitaxios.get(`https://api.odn-it.com/devices/${id}/oxygens/?size=3&page=${pageParam}`);};exportconstuseInfiniteOxygen=(id,pageCount)=>{returnuseInfiniteQuery(["oxygen-infinite",id],getOxygenData,{cacheTime: 5*60*1000,// 5분staleTime: 1*60*1000,// 1분refetchOnWindowFocus: true,// 다른 창을 갔다가 돌아왔을 시, refetchrefetchOnMount: true,retry: 2,// error시 fetch 재시도getNextPageParam: (_lastPage,allPages)=>{returnallPages.length<pageCount&&allPages.length+1;},});};
해당 API 데이터 구조상, 다음 페이지를 불러올 수 있도록 존재하는
next데이터를 통해 다음 페이지에 해당하는 uri를 체크하도록 데이터 통신을 추가했음다음 페이지가 존재하지 않을 경우, API 데이터에서 자동으로
next에 해당하는 데이터를null으로 출력하기 때문에, 다음 페이지가null인 경우를 체크하여 데이터의 최종 페이지를 식별
서버 업데이트 시, UI에서도 업데이트를 할 것이라고 (낙관적으로) 가정해서 미리 UI를 업데이트를 시켜주고 서버를 통해 검증을 받아 업데이트 또는 롤백하는 방식
Optimistic Update는 인터넷 속도가 느리거나 서버가 느릴 때 굉장히 유용함
- 유저가 행한 액션을 기다릴 필요 없이 바로 업데이트가 되는 것처럼 보이기 때문에 사용자의 경험(UX) 측면에서 좋음
import{useMutation,useQueryClient}from"@tanstack/react-query";importaxiosfrom"axios";constaddHeroData=async(hero)=>{returnawaitaxios.post("http://localhost:5000/superheroes",hero);};exportconstuseAddHero=(pageNum)=>{constqueryClient=useQueryClient();returnuseMutation(addHeroData,{onMutate: async(newData)=>{// Optimistic Update(낙관적 업데이트)를 덮어쓰지 않기 위해 쿼리를 수동으로 삭제awaitqueryClient.cancelQueries(["heroes",pageNum]);// 이전 값constprevData=queryClient.getQueryData(["heroes",pageNum]);// 새로운 값으로 Optimistic Update(낙관적 업데이트) 진행queryClient.setQueryData(["heroes",pageNum],(oldData)=>{return{
...oldData,data: [
...oldData.data,{id: oldData?.data?.length+1, ...newData},],};});console.log({prevData: { prevData }});return{// 값이 들어있는 context 객체를 반환
prevData,};},// mutation 실패 시, onMutate에서 반환된 context를 사용하여 롤백 진행onError: (_error,_data,context)=>{console.log({prevData: context.prevData});queryClient.setQueryData(["heroes",pageNum],context.prevData);},// 오류 또는 성공 후, 항상 데이터 리프레쉬 진행onSettled: ()=>{console.log({onSettled: "Refresh"});queryClient.invalidateQueries(["heroes",pageNum]);},});};importReactfrom"react";import{useQuery}from"@tanstack/react-query";importaxiosfrom"axios";constRQOdnBuoy=()=>{constgetBuoyData=async()=>{returnawaitaxios.get("https://api.odn-it.com/devices/");};const{ status, data, error }=useQuery(queryKey,// v4부터 배열의 형태로 작성 ex) ["buoy"]queryFn,// Promise를 반환하는 함수 ex) getBuoyData{// 기타 옵션 ex) cacheTime, staleTime, refetchOnWindowFocus, refetchOnMount, retry, ...});// 에러 / 로딩 핸들링if(status==="loading"){return<h2>Loading...</h2>;}if(status==="error"){return<h2>Error : {error.message}</h2>;}// ...};