티스토리 뷰
[React-Query] react-query 는 무엇이고 어떻게 사용하나요? + React.suspense
blueprint-12 2022. 10. 24. 22:11Redux, zustand, Recoil은 클라이언트의 상태를 관리한다면 React Query는 서버의 상태를 관리합니다.
프론트에서 사용하는 데이터는 크게 두 가지로 분류할 수 있는데
정말 프론트에서만 쓰는 view를 위한 데이터가 있고, API에서 가지고 온 데이터(실제로는 서버에 존재하는 데이터) 이렇게 두 가지 상대값이 존재합니다. 리액트 쿼리는 이 두 가지 중 서버의 상태를 관리하기 위한 상태관리 라이브러리 입니다.
React-query 는 서버 상태를 관리하기 위한 상태 관리 라이브러리입니다.
우리가 어떤 데이터를 서버에 요청하고 나서부터 요청을 받은 후까지 데이터를 받아오기 전까지 참조 못하게 하는 기능, 게시글 목록 중 한 데이터 수정 api를 호출했다면 게시글 목록 자체를 리패칭하기 등 직접할 일이 참 많습니다. 리액트 쿼리는 이런 비동기 처리를 편하게 할 수 있도록 많은 기능을 제공합니다.
리액트 쿼리와 리덕스를 비교하면 상태관리의 흐름이 다릅니다.
-> 리액트 쿼리는 API요청을 보내는 것에 특화되어 있습니다.
React Query의 상태 관리 흐름
- fetching: 데이터 요청 중
- fresh(신선한 데이터): 데이터를 갓 받아온 직후/ 컴포넌트의 상태가 변하더라도 데이터 재요청하지 않음
- stale(상한 데이터): 데이터 만료/ 최신화가 필요한 데이터
- inactive: 쿼리가 언마운트된 상태 (더는 사용하지 않는 상태)
- 🔥주의🔥 쿼리가 언마운트된다고해서 비동기 요청이 취소되는 것은 아닙니다. 프로미스가 일단 만들어지고 언마운트된 거라면 데이터는 캐시에 살아있을 수 있습니다.
- delete: 완전히 삭제된 상태(캐시 데이터가 메모리에서 삭제)
주요 개념
- query(쿼리): 쿼리 키 + 쿼리 함수
- query key(쿼리 키): 쿼리를 구분하기 위한 특정 값(문자, 배열, 딕셔너리 등)
- query function(쿼리 함수): 서버에서 데이터를 요청하고 Promise를 리턴하는 함수(= 비동기 요청)
- data: 쿼리 함수가 리턴한 Promise가 resolve된 값
- staleTime: 쿼리 데이터가 fresh에서 stale로 전환되는데 걸리는 시간. 기본 값: 0
- cacheTime: unused 또는 inactive 캐시 데이터가 메모리에서 유지될 시간. 기본 값은 5분이며 설정한 시간을 초과하면 메모리에서 제거된다.
리액트 쿼리에서 가장 중요한 개념은 Query(쿼리)입니다. -> 리덕스에서 Store 정도의 개념이라고 보면 된다.
Store는 직접 무슨 데이터를 바꿔야만 했지만(임의로 Store의 값을 변경해야 데이터가 바뀜) 쿼리는 쿼리 키(Query Key)값과 쿼리 펑션(Query Function)이 있습니다.
*여기서 Query function은 보통 API를 요청하는 함수입니다.
쿼리 키에 같은 쿼리 키를 주고 어디선가 쿼리 키가 호출되면 이 키를 갖고 있는 요소가 뭔가 변했을 때 쿼리 펑션을 통해 API호출을 다시하게 된다. 즉, Store과 다르게 키 값에 엮여있는 무언가가 바뀌게 되면 다시 API에 요청을 해서 같은 키 값에 있는 데이터를 바꿉니다.
리액트 쿼리를 사용하려면 당연히 API가 있어야합니다. (서버: ㅎㅇ)
저는 이전에 만들어둔 mock API를 사용하겠습니다.
먼저, yarn(패키지 매니저)을 통해 axios 와 react-query를 설치해줍니다.
API요청시 axios를 사용하고 서버데이터 관리를 실습해볼 것이기 때문에 당연히 react-query를 설치해야겠죠?
리덕스가 Store를 통해 데이터를 주입하는 것을 기억하시나요?
리액트 쿼리도 동일하게 데이터를 주입해주는 단계가 필요합니다.
src/App.js
🙋♂️리액트 쿼리는 자체적으로 dev tool을 제공합니다. (따로 추가로 설치할 필요 없어요!)
아래와 같이 react-query/devtool 에서 ReactQueryDevtools 컴포넌트를 가져오고
initialIsOpen의 값을 true로 주면 화면에 react query devtool이 활성화됩니다.
dev tool을 사용하면, 데이터의 상태를 알 수 있습니다. fresh/ fetching/stale/inactive
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { ReactQueryDevtools } from "react-query/devtools";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={true} />
<App />
</QueryClientProvider>
);
데이터를 가져오는 기능은 useQuery라는 훅을 사용하고, 데이터를 수정할 때는 useMutation을 사용합니다.
- React-query가 API 통신과 밀접한 것을 기억하면 GET 요청은 useQuery훅, POST PUT DELETE 요청은 보통 useMutation훅을 사용합니다.
useQuery() 훅 사용하여 데이터 가져오기
- useQuery를 사용하여 쿼리 인스턴스를 만들어봅시다.
src/App.js
import React from "react";
import "./App.css";
import { useQuery } from "react-query";
import axios from "axios";
const getDummyList = () => {
return axios.get("http://localhost:5001/DUMMY_ROOM_LIST");
};
function App() {
//✅useQuery 훅은 첫번째 인자로 쿼리 키를 받는다.
//✅두번째 인자로 API 요청 함수입니다. getData(), 프라미스 객체 이런거
//✅세번째 인자로는 옵션객체(요청 성공시 머할지, 실패시 등등) 를 넘길 수 있습니다.
const dummy_query = useQuery("dummy_list", getDummyList, {
//dummy_query를 5번호출하면 getDummyList함수는 1번실행되는데
//옵션에 넣은 것들은 5번 연속실행된다. 이 점을 염두해둬야 한다.
onSuccess: (data) => {
console.log(data);
},
//fresh -> stale로 가는 과정의 시간을 10초로 늘려줬다.
//데이터가 stale상태가 되면 다른 탭에서 원래 탭으로 다시 돌아올 때 API호출을 자동으로 하기 때문(기본옵션때문임)
staleTime: 10000,
});
return <div className="App"></div>;
}
export default App;
console.log(dummy_query.data.data);
/* 해보면 우리가 API로 가져오고 싶은 데이터가 들어있다.
하지만 맨 처음 상태, 즉 status가 loading일 때에는 dummyList.data.data에 접근할 수 없기 때문에
따로 처리해줘야 한다. */
//리액트 쿼리로 가져온 데이터는 isLoading이라는 state를 따로 만들 필요가 없다.
//아래와 같이 바로 사용하면 된다.
if(dummy_query.isLoading){
return null;
}
useMutation() 훅 사용하여 데이터 수정하기
import React, { useRef } from "react";
import "./App.css";
import { useQuery, useMutation } from "react-query";
import axios from "axios";
const getDummyList = () => {
return axios.get("http://localhost:5001/DUMMY_ROOM_LIST");
};
const addSleepData = (data) => {
return axios.post("http://localhost:5001/DUMMY_ROOM_LIST", data);
};
function App() {
const day_input = useRef("");
const time_input = useRef("");
//useQuery 훅은 첫번째 인자로 쿼리 키를 받는다.
//두번째 인자로 API 요청 함수입니다. getData(), 프라미스 객체 이런거
//세번째 인자로는 옵션객체(요청 성공시 머할지, 실패시 등등) 를 넘길 수 있습니다.
const dummy_query = useQuery("dummy_list", getDummyList, {
//dummyList를 5번호출하면 getDummyList함수는 1번실행되는데
//옵션에 넣은 것들은 5번 연속실행된다. 이 점을 염두해둬야 한다.
onSuccess: (data) => {
console.log(data);
},
});
//useQuery와 다르게 유니크 키값이 필요없이 바로 첫번째인자로 함수를 바로 넣어주고
//두번째인자로 옵션이 들어갑니다.
// mutate라는 함수를 구조분해할당으로 받아와야 합니다.
//보통은 mutate함수를 이름을 따로 붙여서 뽑아내는데 그냥해도된다.
// const { mutate: addSleepDataMutate } = useMutation(addSleepData);
const { mutate } = useMutation(addSleepData);
//isLoading은 기본 프로퍼티로 제공된다. 따로 만들 필요가 없다.
if (dummy_query.isLoading) {
return null;
}
return (
<div className="App">
{dummy_query.data.data.map((el) => {
return (
<div key={el.id}>
<p>{el.day}</p>
<p>{el.sleep_time}</p>
</div>
);
})}
<input ref={day_input} />
<input ref={time_input} />
<button
onClick={() => {
const data = {
day: day_input.current.value,
sleep_time: time_input.current.value,
};
mutate(data);
}}
>
데이터 추가하기
</button>
</div>
);
}
export default App;
- 데이터를 추가할 때, 데이터가 화면에 바로 반영되지 않는 이유는 캐싱된 데이터를 후처리해주지 않기 때문입니다.
- 즉, 현재 쿼리가 가지고 있는 값은 stale된 값, 업데이트가 되기 전의 상한 데이터 이기 때문에 이 데이터를 최신형으로 업데이트하려면 해당 쿼리를 무효화시키면 됩니다.
- useMutate의 2번째 인자인 옵션객체에 성공 시 후처리 메소드를 만들어주면 되는데 onSuccess시 invalidateQueries를 호출해주면 됩니다.
- useQueryClient라는 훅을 사용하면 어떤 유니크한 키값을 가진 쿼리를 무효화시킬 수 있는 함수(invalidateQueries)를 사용할 수 있습니다. invalidateQueries의 인자로는 지우고자하는 쿼리의 유니크 키를 넣어주면 되는데 아무 값도 넣어주지 않으면 모든 캐싱된 쿼리들이 날라가게되니 주의해야 합니다.
import React, { useRef } from "react";
import "./App.css";
import { useQuery, useMutation, useQueryClient } from "react-query";
import axios from "axios";
const getDummyList = () => {
return axios.get("http://localhost:5001/DUMMY_ROOM_LIST");
};
const addSleepData = (data) => {
return axios.post("http://localhost:5001/DUMMY_ROOM_LIST", data);
};
function App() {
const day_input = useRef("");
const time_input = useRef("");
//현재 쿼리데이터를 무효화시켜주는 hook useQueryClient
//이 queryClient의 invalidateQueries라는 함수가 쿼리(유니크한 쿼리 키를 가지고 있는 쿼리)를 무효화 시켜준다.
const queryclient = useQueryClient();
//useQuery 훅은 첫번째 인자로 쿼리 키를 받는다.
//두번째 인자로 API 요청 함수입니다. getData(), 프라미스 객체 이런거
//세번째 인자로는 옵션객체(요청 성공시 머할지, 실패시 등등) 를 넘길 수 있습니다.
const dummy_query = useQuery("dummy_list", getDummyList, {
//dummyList를 5번호출하면 getDummyList함수는 1번실행되는데
//옵션에 넣은 것들은 5번 연속실행된다. 이 점을 염두해둬야 한다.
onSuccess: (data) => {
console.log(data);
},
});
//useQuery와 다르게 유니크 키값이 필요없이 바로 첫번째인자로 함수를 바로 넣어주고
//두번째인자로 옵션이 들어갑니다.
// mutate라는 함수를 구조분해할당으로 받아와야 합니다.
//보통은 mutate함수를 이름을 따로 붙여서 뽑아내는데 그냥해도된다.
// const { mutate: addSleepDataMutate } = useMutation(addSleepData);
const { mutate } = useMutation(addSleepData, {
onSuccess: () => {
//데이터 목록을 다시 불러오면 ok!
//invalidateQueries의 인자로 무효화시킬 쿼리의 키값을 넘겨주면된다.
//쿼리 키를 안넘기게 되면 모든 쿼리가 날라갑니다.
queryclient.invalidateQueries("dummy_list");
//성공 시 input값 비워주기
day_input.current.value = "";
time_input.current.value = "";
},
});
//isLoading은 기본 프로퍼티로 제공된다. 따로 만들 필요가 없다.
if (dummy_query.isLoading) {
return null;
}
return (
<div className="App">
{dummy_query.data.data.map((el) => {
return (
<div key={el.id}>
<p>{el.day}</p>
<p>{el.sleep_time}</p>
</div>
);
})}
<input ref={day_input} />
<input ref={time_input} />
<button
onClick={() => {
const data = {
day: day_input.current.value,
sleep_time: time_input.current.value,
};
mutate(data);
}}
>
데이터 추가하기
</button>
</div>
);
}
export default App;
+
🐱👤Suspense 사용해보기(React v18)
원래도 리액트에 있던 기능이지만 test버전에서 진짜 기능으로 리액트 18부터 사용이 가능합니다.
- 서스펜스는 로딩 중, 에러가 난 경우 처럼 어떤 상황마다 화면 분기 처리를 할 때 편히하기 위해 탄생한 컴포넌트입니다.
- 서스펜스는 클라이언트에서만 사용하는 것이 아니라 서버 컴포넌트와 같이 조합해서 쓰면 강력한 기능을 구사할 수 있다고 합니다.
이전 코드에 isLoading이라는 프로퍼티를 사용하여, 비동기 데이터가 아직 완료되지 않은 경우 return null로 에러가 뜨지않게 했던 것을 기억하시나요? 이 부분을 Suspense를 통해 간편하게 대체할 수 있습니다.
if (dummy_query.isLoading) {
return null;
}
Suspense 사용방법
React.suspsense를 사용하고 싶은 범위의 상위에 위치시켜 컴포넌트를 감싸줍니다.
index.js
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.Suspense fallback={<div>로딩 중입니다.</div>}>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={true} />
<App />
</QueryClientProvider>
</React.Suspense>
);
- 이렇게만 하면 우리가 지정한 fallback 컴포넌트가 loading중에 보이지 않습니다. 아래와 같은 작업을 추가로 해주어야 합니다.
기존에 client값으로 넘겨준 QueryClient객체의 초기값으로 객체를 넘겨줍니다.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { ReactQueryDevtools } from "react-query/devtools";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
},
},
});
- 이렇게하면 오류 대신 데이터가 fetching되는 중에 <div>로딩중입니다.</div>라는 fallback이 대신 보이게 됩니다.
🤸♀️ 리액트 쿼리와 서스펜스가 같이 사용하기 좋은 이유는 바로 defaultOptions의 queries 의 suspense가 기본적으로 속성으로 들어가 있기 때문입니다. 그렇기 때문에 suspense 의 값을 true로 설정해주면 그제서야 화면에 loading시 표시하려고 했던 fallback 컴포넌트가 보이게 됩니다.
'Frontend > react.js' 카테고리의 다른 글
[React | Issue] 로컬 스토리지가 동기처리라면 API호출 함수에서 문제가 없을까? (0) | 2023.01.15 |
---|---|
[React | Issue] 리액트 컴포넌트의 DB 값 변동 시, 겪은 이슈(with json-server DELETE 메소드) (0) | 2022.12.08 |
[React] 무한스크롤 기능 구현( feat. react-query & react-intersection-observer) (2) | 2022.10.23 |
[React] Portal (0) | 2022.10.22 |
[React | axios] axios interceptor 사용하기 (0) | 2022.10.22 |
- Total
- Today
- Yesterday
- 항해99추천비추천
- 타입스크립트 DT
- 틸드와 캐럿
- nvm 설치순서
- nvm경로 오류
- 원티드 FE 프리온보딩 챌린지
- 항해99프론트
- 형제 요소 선택자
- float 레이아웃
- getStaticPaths
- aspect-ratio
- Prittier
- 부트캠프항해
- 원티드 프리온보딩 FE 챌린지
- 프리렌더링확인법
- getServerSideProps
- 프리온보딩 프론트엔드 챌린지 3월
- grid flex
- && 셸 명령어
- 타입스크립트 장점
- ~ ^
- 항해99프론트후기
- tilde caret
- 원티드 프리온보딩 프론트엔드 챌린지 3일차
- text input pattern
- is()
- reactAPI
- fs모듈 넥스트
- D 플래그
- 원티드 3월 프론트엔드 챌린지
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |