install
npm install react-query
App.jsx
import {QueryClient, QueryClientProvider} from 'react-query';
const queryClient = new QueryClient();
function App() {
return (
// provide React Query client to App
<QueryClientProvider client={queryClient}>
<div className="App">
<Posts />
</div>
</QueryClientProvider>
);
}
export default App;
import {QueryClient, QueryClientProvider} from 'react-query';
를 임포트해서 App 을 감싼다
그리고 이걸 사용해줄 Posts 컴포넌트에 가서
import { useQuery } from "react-query"; // -> 쿼리를 임포트해준다
async function fetchPosts() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=10&_page=0"
);
return response.json();
}
const data = [];
로 하드코딩 된 배열을 useQuery로 바꿔줄 수 있다.
const {data} = useQuery("posts", fetchPosts) ;
// useQuery(쿼리키(쿼리의 이름),쿼리함수(이 쿼리에 대한 데이터를 가져오는 방법, 비동기여야함))
useQuery는 다양한 속성을 가진 개체를 반환
이제 매핑하는 데이터는 fetchPosts에서 반환된 데이터가 되고
이 HTTP 요청에서 반환된 JSON 이 된다.
export function Posts() {
const [currentPage, setCurrentPage] = useState(0);
const [selectedPost, setSelectedPost] = useState(null);
const {data} = useQuery("posts", 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} />}
</>
);
}
이렇게 해줬을 경우 오류가 남
매핑을 시도했지만 맵은 배열 전용이기 때문에 현제 데이터가 정의되지 않았다고 나오는 것인데
바로 fetchPosts 가 비동기식이라서 그렇다
일단 간단히 해결하기 위해서
const {data} = useQuery("posts", fetchPosts) ;
if (!data) return <div/>
이렇게 코드를 작성해주면 fetchPost가 해결이 될 때까지 데이터는 거짓이 된다.
하지만 해결되면 데이터에 배열이 포함되고 컴포넌트가 다시 렌더링되어 매핑이 된다.
useQuery 에는
isLoading, isError, isFetching 과 같은 속성이 있다
이걸 사용해서 데이터가 정의되지 않았을 때의 상태를 관리해보자
const {data, isError, isLoading} = useQuery("posts", fetchPosts) ;
if (isLoading) return <h3>Loading...</h3>
로딩중일 경우 Loading 을 띄워주는 방법이다.
로딩중으로 표시된 다음 데이터가 표시된다.
isFetching 은 아직 패칭을 완료하지 않았다는 의미이지만
쿼리가 Axios 호출 또는 GraphQL 호출일 수도 있다
isLoading
if (isError) return <h3>Oops, something went wrong</h3>
리액트 쿼리는 기본값으로 3번 시도한 후에 해당 데이터를 가져올 수 없다고 결정한다.
isError 불리언을 반환하는 것 외에도
반환객체에는 쿼리함수에서 전달하는 오류도 있다.
const {data, isError,error, isLoading} = useQuery("posts", fetchPosts) ;
if (isLoading) return <h3>Loading...</h3>
if (isError)
return (
<>
<h3>Oops, something went wrong</h3>
<p>{error.toString()}</p>
</>
);
리액트 쿼리는 개발자 도구를 따로 제공해준다.
사용법은 간단하다
App.jsx 에서
QueryClientProvider안에 ReactQueryDevtools을 넣어주면 된다
import {ReactQueryDevtools} from 'react-query/devtools'
...
<QueryClientProvider client={queryClient}>
...
<ReactQueryDevtools/>
</QueryClientProvider>
화면에서 보면 꽃모양이 생긴 걸 볼 수 있는데 그걸 클릭해주면 리액트 쿼리 개발자 도구를 볼 수가 있다.
여기서 stale 에 대해 알아보자
stale 이란, '만료' 를 뜯한다.
Stale Data (만료된 데이터)
stale Data 란 뭘까
데이터가 만료됐다는게 뭘까?
데이터 refetching 은 만료된 데이터에서만 실행된다.
데이터 리패칭 실행에는 만료된 데이터 외에도 여러 트리거가 있다.
예를들면, 컴포넌트가 다시 마운트되거나 윈도우가 가시 포커드 되었을 때가 있다.
Stale Time 은 데이터를 허용하는 '최대 나이'라고 할 수 있다
혹은 데이터가 만료됐다고 판단하기 전까지 허용하는 시간이 staleTime 이다
웹사이트에 표시된 데이터가 10초까지는 그대로여도 괜찮다면
staleTime을 10초로 설정한다.(데이터의 성격에 따라 다르다)
업데이트 하는 방법은
useQuery를 불러올때 3번째 인수를 불러오는 것이고 이는 option 이라고 한다.
업데이트 할 옵션은 stalTime 이고 1/1000초 단위이다.
블로그 게시물이 2초마다 만료되도록 설정해보자
const {data, isError,error, isLoading} = useQuery("posts", fetchPosts, {staletime:2000}) ;
왜 staleTime의 기본값은 0 일까?
데이터는 항상 만료상태이니깐 항상 서버에서 다시 가져와야한다 라고 가정하는 것
-> 이렇게 되면 사용자에게 만료된 데이터를 제공할 가능성이 훨씬 줄어든다!
staleTime
cache / cacheTime
이제 블로그 게시글을 클릭했을 때 해당 포스트에 대한 댓글을 불러오는 쿼리를 작성해보자
//postDatail.jsx
import { useQuery } from "react-query"; // 임포트
async function fetchComments(postId) { //댓글 데이터 패치함수
const response = await fetch(
`https://jsonplaceholder.typicode.com/comments?postId=${postId}`
);
return response.json();
}
포스트에서 해준 것과 비슷하게 작성하였다.
export function PostDetail({ post }) {
// replace with useQuery
const {data, isLoading, isError, error} = useQuery("comments",()=>fetchComments(post.id));
// const {data, isLoading, isError, error} = useQuery(['comments', post.id],()=>fetchComments(post.id));
if (isLoading) return <h3>Loading...</h3>;
if (isError) return (
<>
<h3>Error!</h3>
<p>{error.toString()}</p>
</>
)
잘 불러와지는데 문제점이 있다.
다른 포스트를 눌러도 계속 같은 커멘트 데이터를 불러오는 문제점이 생겼다.
왜 이런 문제가 생긴거냐면
모든 쿼리가 같은 comments 쿼리 키를 사용하고 있기 때문이다.
해결방법
쿼리는 데이터에 대한 id를 가지기 때문에 쿼리별로 캐시를 남길 수 있으며
comments 쿼리에 대한 캐시를 공유하지 않아도 된다.
각 쿼리에 해당하는 개시를 가지게 될 것이기 때문에 각 게시물에 대한 쿼리에
라벨을 설정하면 된다.
=> 쿼리 키에 문자열(String) 대신 배열(Array)을 전달하면 가능하다!
['comments', post.id]
const {data, isLoading, isError, error} = useQuery(['comments', post.id],()=>fetchComments(post.id));
이렇게 해주면 배열의 첫번째요소로 문자열 'comments'를 가지고 두번째 요소로 post.id 를 가지는데, 쿼리키를 쿼리에 대한 의존성 배열로 취급하게 된다.
따라서 쿼리 키가 변경되면(즉 post.id가 업데이트되면) React Query가 새 쿼리를 생성해서 staleTime과 cacheTime을 가지게 되고 의존성 배열이 다르다면 완전히 다른 것으로 간주한다.
따라서 데이터를 가져올 때 사용하는 쿼리 함수에 있는 값이 쿼리 키에 포함되어야 한다. 이렇게 해주면 모든 comments 쿼리가 같은 쿼리로 간주되는 상황을 막고 각기 다른 쿼리로 다뤄진다!
확인해보면 잘 불러와지는 것을 볼 수 있다.
여기서 보면 다른 게시물을 클릭하자마자 이전에 클릭했던 요소가 비활성화 되는데 이는 캐시로 남아있고 cacheTime동안 사용하지 않으면 가비지컬렉터에 들어간다.
페이지 네이션을 구현하였는데 next button 을 누를 때마다 페이지가 로딩되길 기다려야 하고 , 그 기다리는 동안 'Loading' 이 뜬다. 이건 좀 사용자 경험에 좋지 못한 생김새이다..
그래서 이걸 해결해주기 위해서는 데이터를 미리 가져와 캐시에 넣어서 사용자가 기다릴 필요가 없게 해보겠다.
Prefetching 이란
데이터에 캐시를 추가하며 구성할 수 있긴 하지만 기본값으로 만료(stale) 상태이다.
즉 데이터를 사용하고자 할 때 만료 상태에서 데이터를 다시 가져온다.
데이터를 다시 가져오는 중에는 캐시에 있는 데이터를 이용해 앱에 나타낸다.
(캐시가 만료되지 않았다는 가정하에)
사용해 줄 post.jsx 에서 useQueryClient를 임포트해온다.
import { useQuery, useQueryClient } from "react-query";
//사용할 컴포넌트 안에 queryClient 생성
const queryClient = useQueryClient();
이 훅을 기능 컴포넌트에 실행해야 하는데
다음 페이지로 onClick시 실행하는건 no.. 왜냐면 상태업데이트가
비동기식으로 일어나기 때문에 이미 업데이트가 진행됐는지 알 방법이 없음 .
그래서 useEffect 로 현재 페이지에 생기는 변경 사항을 활용한다.
const queryClient = useQueryClient();
useEffect(()=>{
//알고 있는 범위 외의 데이터를 가져오기 않도록 조건을 추가
if(currentPage < maxPostPage){
const nextPage = currentPage +1;
//다음 페이지가 무엇이든 데이터를 미리 가져오기
queryClient.prefetchQuery(['posts', nextPage], () => fetchPosts(nextPage));
}
},[currentPage, queryClient])
const {data, isError,error, isLoading} = useQuery(["posts",currentPage], () => fetchPosts(currentPage), {
staletime:2000,
//쿼리키가 바뀔때도 지난 데이터를 유지
keepPreviousData:true,
}
) ;
확인해보자아
7번째껄 클릭했을 때 8이 미리 불러와지는걸 볼 수 있음 .
미리 데이터를 불러와서 캐시에 저장해두는 것 확인 !
덕분에 9페이지에 갔을때 페이지는 바로 보이게 된다
변이는 서버에 데이터를 업데이트하도록 서버에 네트워크 호출을 실시한다.
import { useMutation } from 'react-query';
//postDetail.jsx
export function PostDetail({ post }) {
const { data, isLoading, isError, error } = useQuery(
['comments', post.id],
() => fetchComments(post.id)
);
const deleteMutation = useMutation((postId) => deletePost(postId));
return 객체를 구조분해하지 않았다. 왜냐?
위에서 useQuery 를 써줄 대 이미 구조 분해를 진행하면서 일부 확보했기 때문에.
deleteMutation을 작성해주었다.
그리고 보다시피 useQuery랑 비슷하지만 쿼리키는 작성하지 않았다. 왜냐?
쿼리키와 관련있는 캐시 내부의 데이터와는 상관 없기 때문에
함수만 작성해줌
이 useMutation에서 객체는 변이 함수를 반환하게 된다.
즉 누군가 Delete 버튼을 클릭할 때 이 변이 함수를 실행하려는 것
<button onClick={() => deleteMutation.mutate(post.id)}>Delete</button>
{deleteMutation.isError && (
<p style={{ color: 'red' }}>Error deleting the post</p>
)}
{deleteMutation.isLoading && (
<p style={{ color: 'purple' }}>deleting the post</p>
)}
{deleteMutation.isSuccess && (
<p style={{ color: 'green' }}>Post has been deleted</p>
)}
객체를 반환하는 deleteMutation 과 속성함수인 mutate을 실행하게 되며
props에서 받은 postId가 무엇이든 상관없이 실행하게 된다.
변이함수를 호출할 때면 인수가
deleteMutation.mutate(post.id)
의 post.id 에 할당된다
그럼 이 post.id 를 가져다가
변이함수 const deleteMutation = useMutation((postId) => deletePost(postId));
delete 포스트로 전달하게 된다
조건부 문단들을 통해 현재 상태를 보여지게끔도 추가해줬다!
자 여기까지 !!
반환 객체에 변이 속성을 실행하여 변이의 호출을 조절할 수 있었고
쿼리에서 진행한 것과 유사한 방식으로 주기 (isError, isLoading, isSuccess)로 처리할 수 있었다.