React Query (1)
다양한 데이터들을 관리하기 위해서 React Context를 통해 전역 데이터 관리를 할 수 있다.
Prop Drilling과 같은 문제점은 해결할 수 있지만, 데이터의 변화 흐름을 알기 어렵다는 단점은 여전히 존재한다.
따라서 이를 해결하기 위해 Redux, Recoil과 같은 다양한 상태 관리 라이브러리들이 등장했는데, Redux와 Recoil 같은 경우 클라이언트 상태 관리에 초점이 맞춰져 있어 서버 상태를 관리하기엔 잘 맞지 않는 부분이 있다.
따라서 이를 해결하기 위해 서버 상태 데이터만을 집중적으로 관리하는 리액트 쿼리가 등장했다.
클라이언트 상태 데이터
웹사이트의 어떤 메뉴가 열렸는지 닫혔는지, 혹은 사용자가 어떤 버튼을 눌렀는지 아닌지와 같은 UI 상태 값이나, 유저가 입력 폼에 입력한 데이터 등 서버와는 상관없이 웹 브라우저 안에서만 사용하는 데이터
서버 상태 데이터
서버에서 가져오는 데이터, 예를 들면 네이버에 접속했을 때 우리가 보는 뉴스 기사나 각종 글 등등
서버 상태 (Server State)
서버 상태 데이터의 특징
- 데이터를 받아오기까지 걸리는 시간이 존재, 비동기식으로 구현할 필요가 있다
- 데이터를 받아오는 과정에서 발생하는 에러를 어떻게 처리하고 어떤 식으로 유저에게 안내할 것인지 고민해야 한다
- 최신으로 유지되어야 한다. (사이트의 성격에 따라 얼마나 최신으로 데이터를 유지해야 하는가 결정 ex.주식사이트)
Redux 같은 라이브러리에서는 이러한 서버 데이터들의 특성에 맞게 구현하고 관리하는게 쉽지 않았는데, 이를 해결하기 위해 리액트 쿼리가 등장하게 되었다.
React Query
리액트 쿼리는 데이터를 가져오는 과정에서 로딩과 에러 처리를 쉽게 구현할 수 있도록 여러 값을 제공해주고, 정해진 시간 혹은 조건마다 서버 상태 데이터를 최신으로 가져오는 작업도 알아서 해준다.
그뿐만 아니라, cache를 사용해서 매번 서버에서 데이터를 가져올 필요 없이 유저에게 더 빠르게 데이터를 보여주기도 한다.
그 외에도 리액트 쿼리는 페이지네이션이나 Infinite Query, Optimistuc Update와 같은 웹사이트들에서 자주 사용하는 기능을 손쉽게 구현할 수 있도록 해준다.
리액트 쿼리 설치
npm install @tanstack/react-query
Home.js
function HomePage() {
return <div>홈페이지</div>;
}
export default HomePage;
App.js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import HomePage from './HomePage';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<HomePage />
</QueryClientProvider>
);
}
export default App;
@tanstack/react-query 패키지에서 QueryClient를 import해서 새로운 쿼리 클라이언트를 만들어 주기
쿼리 클라이언트를 QueryClientProvider를 통해 App 컴포넌트의 자손 컴포넌트로 전달
→ ReactContext에서 데이터를 전역으로 사용하기 위해 Context Provider 설정하는 것과 비슷
QueryClientProvider를 통해 쿼리 클라이언트를 제공해 줘야 그 안에 있는 자손 컴포넌트에서 리액트 쿼리를 사용할 수 있게 된다.
⚡️ react-query가 아닌 @tanstack/react-query에서 import하기
react-query는 리액트 쿼리 3 버전이고 @tanstack/react-query가 가장 최신 버전
리액트 쿼리 개발자 도구 (React Query Devtools)
npm install @tanstack/react-query-devtools
App.js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import HomePage from './HomePage';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<HomePage />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export default App;
initialIsOpen은 리액트 쿼리 개발자 도구가 열려있는 상태로 실행할 것인가를 선택하는 옵션!
npm start하면 아래에 플로팅 버튼이 뜨고, 버튼 클릭하면 개발자 도구 화면이 보인다.
쿼리란?
백엔드로부터 데이터를 받아오기 위해 useQuery()라는 훅을 사용한다.
여기서 쿼리란 '문의하다, 질문하다'라는 뜻을 가지고 있는 단어로, 데이터베이스에서 우리가 필요한 데이터를 요청하는 것을 말한다.
다시 말해, useQuery()는 필요한 데이터를 백엔드에서 요청해 받아오는 React Hook
useQuery()
getPost() 함수: 백엔드로부터 모든 포스트 목록을 받아오는 함수
api.js
const BASE_URL = 'https://learn.codeit.kr/api/codestudit';
export async function getPosts() {
const response = await fetch(`${BASE_URL}/posts`);
return await response.json();
}
HomePage 컴포넌트에서 useQuery()를 import한 다음 실행, 결과를 콘솔에 출력
import { useQuery } from '@tanstack/react-query';
import { getPosts } from './api';
function HomePage() {
const result = useQuery({ queryKey: ['posts'], queryFn: getPosts });
console.log(result);
return <div>홈페이지</div>;
}
export default HomePage;
useQuery() 리턴 값 살펴보기
데이터
data: 백엔드에서 받아온 데이터들이 들어있다.
위의 예시를 보면 response body로 받은 데이터가 객체로 오고, 페이지네이션에 필요한 정보들과 함께 results란 항목에 실에 포스트 데이터가 배열로 들어가 있다.
데이터를 받아온 시간
dataUpdatedAt: 현재의 데이터를 받아온 시간을 나타내는 항목
이 시간을 기준으로 언제 데이터를 refetch할 것인지 등을 정하게 된다.
다양한 상태 정보
isError, isFetched, isPending, isPaused, isSuccess와 같은 다양한 정보 확인 가능
React Query의 status
- query status : 실제로 받아 온 data 값이 있는지 없는지 나타내는 상태 값 - useQuery() 결괏값에서 status 값 통해 확인
- fetch status : quertFn() 함수가 현재 실행되는 중인지 아닌지를 나타내는 상태 값 - fetchStatus 값 통해 확인
Query Status
- pending : 아직 데이터를 받아오지 못했을 때
- error : 데이터를 받아오는 중에 에러가 발생
- success : 데이터를 성공적으로 받아왔을 때
useQuery()의 결괏값을 콘솔에 찍었을 때 결괏값이 두 번 출력된다.
첫번째 결괏값의 status: pending
가장 처음 컴포넌트가 마운트되고(DOM 트리에 추가되고) useQuery()가 실행되면서, 데이터를 아직 받아오기 전이므로 pending 상태가 되는 것.
두번째 결괏값의 status: success
Fetch Status
queryFn으로 등록한 쿼리 함수의 실행 상태를 말해주는 값
- fetching : 현재 쿼리 함수가 실행되는 중일 때
- paused : 쿼리 함수가 시작은 했는데 실제로 실행되고 있지는 않을 때 (네트워크가 오프라인이 된 경우)
- idle : 쿼리 함수가 어떤 작업도 하고 있지 않은 상황
- 컴포넌트가 마운트되어 useQuery()가 실행 - 아직 데이터를 못 받아옴(query status - pending)
- 쿼리 함수가 실행 - (fetch status - fetching)
- 만약 네트워크 상태가 오프라인이라면 (fetch status - paused)
데이터를 성공적으로 받아왔다면 (query status - success)
데이터를 받아오는 과정에서 에러 발생했다면 (query status - error) - fetch status는 데이터를 성공적으로 가져왔는지 여부에 상관없이, 쿼리 함수의 실행이 끝나면 idle 상태
- 데이터를 서버에서 다시 받아오는 refetch 작업 발생 시 쿼리함수가 재실행 - (fetch status - fetching)
리액트 쿼리 status 정리
query status와 fetch status는 독립적인 상태이기 때문에 상황에 따라 다양한 조합의 형태가 나타날 수 있다.
이상적인 상황에서는 "pending & fetching" 상태에서 "success & idle" 상태가 되겠지만,
에러가 발생하는 경우 "error & idle" 상태가 될 수도 있다.
"success & idle" 상태에서 데이터를 refetch하게 되면 "success & fetching" 상태가 되기도 한다.
이처럼 query status와 fetch status 값을 잘 활용하면, 다양한 상황에 맞춰 디테일한 구현이 가능하다.
캐시 (Cache)
내 컴퓨터에 있는 데이터를 가져오는 것에 비해 백엔드에서 데이터를 가져오는 일은 시간이 많이 걸린다.
유저가 방금 전에 확인한 데이터를 반복해서 보는 경우 똑같은 데이터를 매번 백엔드에서 받아 와야만 할까?
유저가 자주 보는 이 데이터를 어딘가에 저장해 두었다가 백엔드에 요청할 필요 없이 바로 보여주는데 사용하는 것이 캐시(cache)이다.
캐시 (cache) : 데이터를 미리 복사해 놓는 임시 장소
특징 : 저장 공간의 크기는 작지만, 데이터를 가져오는 속도는 매우 빠르다
웹브라우저의 캐시 : 사이트에 접속했을 때 받아 온 데이터를 캐시 형태로 저장해서, 사용자가 같은 사이트에 접속하면 서버에 매번 데이터를 다시 요청하는 게 아니라 저장해놓은 데이터를 유저에게 보여준다. 이렇게 캐시를 사용하는 걸 '캐싱'이라고 한다.
React Query의 캐시
언제 캐시에 저장되어 있는 데이터를 보여주는지 이해하려면 우선 리액트 쿼리의 데이터 라이프 사이클을 알아야 한다.
function HomePage() {
const result = useQuery({ queryKey: ['posts'], queryFn: getPosts });
console.log(result);
return <div>홈페이지</div>;
}
HomePage라는 컴포넌트가 렌더링되면 useQuery()가 실행되고, 여기서 쿼리 함수로 설정한 getPosts() 함수를 통해 백엔드로부터 포스트 데이터를 받아왔다. 그런데 useQuery()를 사용한다고 무조건 쿼리함수가 실행되어 백엔드로부터 데이터를 받아오는 것은 아니다.
useQuery()의 동작
useQuery()에서 queryFn(쿼리 함수) 말고도 queryKey(쿼리 키)를 설정해줬다.
['post']라는 쿼리 키로 받아 온 포스트 데이터가 캐시에 저장되어 있다.
- useQuery()는 먼저 전달받은 쿼리 키로 캐시에 저장된 데이터가 있는지 확인한다.
- 만약, 저장되어 있는 데이터가 없으면 쿼리 함수를 실행해 데이터를 백엔드로부터 받아오게 된다. 그런 다음에 쿼리 키(['post'])로 데이터를 캐시에 저장한다.
- ['post']라는 쿼리 키로 저장된 데이터가 캐시에 있다면
- 데이터가 fresh 상태 : useQuery는 캐시에 저장된 데이터를 리턴하고 끝난다.
- 데이터가 stale 상태 : 백그라운드에서 refetch를 진행한다. 그리고 백엔드에서 새로 받아온 데이터로 기존의 ['post']로 저장되어 있는 데이터를 갱신한다.
refetch를 진행하게 되는 네가지 상황
1️⃣ 새로운 쿼리 인스턴스가 마운트 되는 상황 (refetchOnMount)
2️⃣ 브라우저창에 다시 포커스가 가는 상황 (refetchOnWindowFocus)
3️⃣ 네트워크가 다시 연결되는 상황 (refetchOnReconnect)
4️⃣ 미리 설정해둔 refetch interval 시간이 지난 상황 (refetchInterval)
🚀 리액트 쿼리에서 정의하는 데이터의 상태
fresh : 백엔드에서 이제 막 데이터를 받아와 캐시에 저장된 데이터 상태 (신선한 상태)
stale : stale time이라고 불리는 특정 시간이 지난 데이터의 상태 (신선하지 않은 상태)
inactive : 컴포넌트가 언마운트되어서(DOM트리에서 제거되어서) 해당 데이터가 쓰이지 않는 상태
Stale Time
리액트 쿼리에서는 디폴트 값으로 stale time이 0으로 설정되어 있다.
그래서 사실상 모든 데이터는 백엔드에서 막 받아왔어도 바로 stale 상태가 되고, 따라서 매번 데이터가 필요할 때마다 refetch를 하게 된다. 구현하려는 사이트의 특성에 따라 매번 refetch할 필요가 없는 상황에서는 stale time 값을 적절히 변경해주면 된다.
Garbage Collection Time
캐시에 저장된 데이터는 영영 캐시에 남아있는 걸까? 그렇지 않다.
캐시는 한정된 공간이기 때문에 필요 없는 데이터는 삭제해서 다른 데이터가 사용할 수 있는 공간을 마련해줘야 한다.
리액트 쿼리는 필요없는 데이터를 삭제하는 것도 알아서 해준다. 쿼리 컴포넌트가 언마운트 되어 해당 데이터가 쓰이지 않는 상황이 되면 데이터는 inactive 상태가 된다고 했는데, inactive 상태의 데이터는 garbage collection time이 지나면 캐시에서 삭제 된다.
가비지 컬렉션 타임은 기본적으로 5분으로 설정되어 있으며, 변경 가능하다.
라이프 사이클 살펴보기
- useQuery()가 실행되는 컴포넌트가 마운트되면 useQuery()를 통해 쿼리 함수가 실행되고 데이터를 받아온다.
- 받아 온 데이터는 useQuery()에서 지정해 줬던 쿼리 키를 이용해 캐싱 (캐시에 저장)
- 캐시에 저장된 데이터는 fresh 상태에서 staleTime이 지나면 stale 상태로 변경
- 유저가 데이터를 요청하게 되면 캐시된 데이터를 먼저 보여준다
- 이때 데이터가 fresh 상태 : 추가적인 refetch 진행하지 않는다
- stale 상태 : 백그라운드에서(자체적으로 알아서) refetch 진행 -> 새로운 데이터로 유저에게 보여준다
- 컴포넌트가 언마운트되어 데이터가 inactive 상태가 되면 gcTime(가비지 컬렉션 타임)동안 캐시에 저장되어 있다가 그 이후에 가비지 콜렉터에 의해 삭제
라이프 사이클 시간 설정하기
리액트 쿼리에서의 디폴트값 : staleTime은 0, gcTime은 5분
function HomePage() {
const result = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
staleTime: 60 * 1000,
gcTime: 60 * 1000 * 10,
});
console.log(result);
return <div>홈페이지</div>;
}
staleTime 값을 1분으로 변경 (처음 데이터를 받아와도 1분 간은 fresh 상태로 유지, 1분 후 stale 상태로 변경)
gcTime 값을 통해 가비지 컬렉션 타임은 10분으로 변경
*staleTime과 gcTime은 밀리초(ms)가 기준이므로, 1000이 곧 1초를 의미