import { Posts } from "./Posts";
import "./App.css";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
function App() {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<div className='App'>
<h1>Blog Posts</h1>
<Posts />
</div>
<ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider>
);
}
export default App;
react-query를 사용하기 위해서는 QueryClientProvider가 App내부의 컴포넌트를 감싸야 한다.
또한 Devtools의 사용을 위해서는 내부에 ReactQueryDevtools 컴포넌트를 넣어야 한다.
성공적으로 적용된 모습이다. 아직 query를 불러온 것이 없기 때문에 빈 모양이지만 불러왔을 경우, 서버 데이터의 stale, fresh 등의 상태나 cache상태도 체크가 가능하며 어떤 데이터가 있는지도 확인 가능하니 크롬devtools를 대체하여 사용하기에도 좋다.
활성, 비활성, 만료(stale)등의 모든 쿼리의 상태를 알려준다. 그리고 마지막으로 업데이트된 타임스탬프도 알려준다. 쿼리에 의해 반환된 데이터를 확인할 수 있는 데이터 탐색기도 있고 쿼리를 볼 수 있는 쿼리 탐색기도 있다.
공식 홈페이지의 devtools 의 내용 중 중요한 부분이 있다. 기본적으로 devtools는 프로덕션 번들에 포함되어 있지 않다는 점이다. 이는 NODE_ENV 변수에 따라 프로덕션 환경에 있는지 여부가 결정된다. React 앱은 npm run build
를 실행할 때만 NODE_ENV 변수를 production으로 설정한다. npm start
를 실행할 때는 development 또는 testing으로 설정된다.
따로 패키지를 관리할 필요가 없이 편리하게 devtools를 사용할 수 있다는 점이 큰 장점이라고 생각한다.
import { useState } from "react";
import { useQuery } from "react-query";
import { PostDetail } from "./PostDetail";
const maxPostPage = 10;
async function fetchPosts() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10&_page=0");
return response.json();
}
export function Posts() {
const [currentPage, setCurrentPage] = useState(0);
const [selectedPost, setSelectedPost] = useState(null);
// const data = [];
const { data } = useQuery("post", fetchPosts);
return (
<>
<ul>
{data.map((post) => (
<li key={post.id} className='post-title' onClick={() => setSelectedPost(post)}>
{post.title}
</li>
))}
</ul>
<div className='pages'>
<button disabled onClick={() => {}}>
Previous page
</button>
<span>Page {currentPage + 1}</span>
<button disabled onClick={() => {}}>
Next page
</button>
</div>
<hr />
{selectedPost && <PostDetail post={selectedPost} />}
</>
);
}
위의 코드에서 useQuery()의 첫번째 인자는 queryKey로써 고유한 query의 이름을 정하는 개념이다. v4 버전이라면 ["post"] 이러한 방식으로 작성해줘야한다.
두번째 인자는 데이터를 받아올 함수를 작성한다. 비동기 처리함수 fetchPosts 를 작성하여 데이터를 불러오도록 하자.
하지만 undefined 데이터는 map() 함수에 할당할 수 없다는 에러가 발생한다. 왜냐하면 비동기 처리함수의 return 값은 아직 도달하지 않았기 때문이다. 그렇다면 어떻게 해결해야할까?
if (!data) return <div />;
data가 undefined 일 경우 early return 을 통해 빈 div태그를 넘겨주면 해결이 된다. data에 비동기로 받은 데이터가 들어오게 된다면 if문은 지나갈 것이고 배열로 된 데이터는 정상적으로 화면에 나오게 될 것이다.
물론 이러한 방식보다는 더 세련된 방법이 있지만 차후에 다른 기능들과 함께 다시 작성할 예정이다.
데이터가 성공적으로 받아와졌다.
또한 Devtools에 queryKey를 기준으로 서버 데이터의 상태와 Data Explorer에 어떤 데이터가 들어와 있는지 보여주고, Query Explorer는 cachetime 이나 query의 설정들이 나와있다.
데이터를 받아오는 동안의 loading, 만약 데이터를 불러오는데 실패하면 error를 나타내는 방법에 대해 알아보자.
const { data, isLoading, isError } = useQuery("post", fetchPosts);
기존에 data를 구조분해할당한 곳에 isLoading, isError 를 추가해주자.
각 두개의 리턴값은 boolean이다.
그렇다면 아까 작성한 if(!data) 문을 더 세련되게 변경할 수 있다.
if (isLoading) return <h2>Loading...</h2>;
이렇게 수정하면 데이터를 받아오는 중에는 h2태그의 Loading... 이 보이게 될 것이다.
여기서 isFetching 이라는 함수도 있는데 둘의 기능은 비슷하지만 분명 차이점이 존재한다.
우선 isFetching은 isLoading의 상위 집합이다. isFetching은 비동기 쿼리가 해결되지 않았음을 의미한다. 이 경우에는 아직 Fetching을 완료하지 않았다는 의미이지만 쿼리가 Axios 또는 GraphQL일 수도 있다.
isLoading은 이에 대한 하위 집합이다. isLoading은 가져오는 상태에 있음을 의미하며 쿼리 함수가 아직 해결되지 않은 것이다. 또한 캐시된 데이터도 없다. 아예 query를 생성한 적이 없다는 것이다.
이것만으로는 큰 차이가 없어보이지만, Pagination을 사용할 때 캐시된 데이터가 있을 때와 없을 때의 차이가 있으며 이를 구분해야 할 필요가 있다. 차후에 포스팅할 내용에서 설명하겠다.
서버에 문제가 있어 데이터를 받아오지 못할 경우 isError를 사용할 수 있다.
작성한 if()문 아래에 하나를 추가해주자.
if (isError) return <h2>Error!</h2>;
이렇게 작성하면 react-query의 default 설정으로 3번의 데이터 요청이 보내지는데 모두 실패하게 된다면 isError의 리턴값이 true로 바뀌게된다. 그렇다면 h2 태그의 Error! 가 나타나게 된다.
또한 useQuery 내부의 함수중 error 기능도 있는데 이는 에러 객체를 리턴하는 기능이다.
const { data, isLoading, isError, error } = useQuery("post", fetchPosts);
if (isError)
return (
<>
<h2>Error!</h2>
<p>{error.toString()}</p>
</>
);
기존의 if 문에 추가를 해주면
에러 객체가 나타나게 된다.
개발자 도구로 query를 받아오면 데이터가 바로 stale 상태로 바뀐다. 이는 react-query의 default 설정이 staleTime이 0 으로 되어있기 때문이다. 이미 서버로부터 받아오는 데이터는 그 데이터가 변질되지 않은 데이터임을 증명할 수 없기 때문에 fresh 상태가 아닌 상해버린 즉 만료(stale)로 변경되는 것이다. react-query는 데이터 리페칭은 만료된 데이터에서만 실행된다. 또한 default 설정에서는 만료된 데이터 이외에도 데이터를 리페치하는 트리거들이 있다. 컴포넌트가 다시 마운트되거나, 윈도우가 다시 포커스 되었을 때다. 단, 데이터가 stale상태일 때만 실행된다.
결론적으로 staleTime은 데이터가 신선함을 증명하는 최대 시간
이라고 할 수 있다. 달리 말하면 데이터가 만료됐다고 판단하기 전까지 허용하는 시간이 staleTime 이다.
만약 웹사이트에 표시된 데이터가 10초까지는 그대로여도 괜찮다면 staleTime을 10000ms 로 설정하면 된다. 이는 데이터가 자주 리패치 되어야 하거나 아닌 경우에 따라 시간을 설정해주면 된다. 예시 코드의 블로그 포스트는 자주 리페치될 필요가 없으므로 더 길게 설정해도 될 것이다.
이를 설정하는 방법은
const { data, isLoading, isError, error } = useQuery("post", fetchPosts, { staleTime: 10000 });
useQuery의 인자에 설정들을 추가할 수 있다.
10초간 fresh 상태였다가 stale로 이동하는 것을 확인할 수 있으며, devtools 로 보게되면 staleTime이 변경된 것을 확인할 수 있다.
여기서 react-query의 개발자의 철학이 담긴 트윗도 있다.
default staleTime이 0 으로 되어있는 이유를 설명한 트윗이다. react-query는 서버에서 가져온 데이터와 클라이언트 단에서 보여주는 데이터의 일치를 보장하기위해 만든 라이브러리임을 보여주는 트윗이라고 생각한다.
staleTime은 데이터를 리페치할때 사용하는 부분이다.
그렇다면 cache는 나중에 다시 사용할 필요가 있는 데이터다.
특정 쿼리에 대한 활성(active) useQuery가 없는 경우에 그 데이터는 콜드 스토리지로 이동한다. 여기서 cacheTime이 지나면 캐시의 데이터는 만료되며 사라지게 된다. defaults는 5분(300000ms)이다.
cacheTime이 작동하는 시기는 특정 쿼리(queryKey)의 useQuery가 활성화 된 후 경과한 시간이다.
캐시가 만료되면 가비지콜렉터가 실행되고 데이터를 사용할 수 없게 된다. 여기서 캐시가 있는 동안에는 Fetching할 때만 사용될 수 있다. 데이터 페칭은 중지되지 않으므로 서버의 최신 데이터로 리프레시가 가능하게 된다.
만약 만료된 데이터가 위험(예를 들면 업비트나 다른 주식 차트를 보여주는 경우) cacheTime을 0으로 설정하면 된다.