[React Query] 2. Pagenation, Prefetching, Mutation

hzn·2023년 3월 9일
0

React Query

목록 보기
4/4
post-thumbnail

13. 쿼리 키 Query Key

PostDetail.jsx

const { data, isLoading, isError, error } = useQuery('comments', () =>
    fetchComments(post.id)
  );

1) 문제 발생

  • 처음 fetching한 게시물의 답글 데이터가 계속 똑같이 나오는 문제 발생 (comments 쿼리가 refetch 되지 않음)

2) 원인

👉🏽 (post.id에 따라) 각각 다른 데이터(query)를 받아오지만 같은 query key(comments)를 사용하고 있기 때문
👉🏽 이미 알려진(만들어진?)(known keys) 키의 쿼리 데이터는 특정한 트리거가 있어야 refetch된다.

트리거의 예

  • component remount
  • window refocus
  • running refetch function
  • automated refetch (지정된 간격으로 refetch 자동 실행)
  • query invalidation after a mutation

3) 해결 방법

❌ 새 게시물 클릭할 때마다 쿼리를 무효화해서 refetch 한다?

  • 캐시에서 이전 게시물의 쿼리(데이터)를 지우면 안됨
  • 게시물 1과 2를 클릭했을 때 서로 같은 쿼리를 실행하는 것이 아님 (서로 다른 쿼리!)
    => 같은 캐시 공간을 차지하지 않는다. 각 쿼리에 해당하는 캐시를 가지게 된다.

✅ 각 게시물의 쿼리에 label을 설정한다

  • query key에 문자열 대신 배열을 할당한다.

Query Key를 배열로 설정하기

PostDetail.jsx

const { data, isLoading, isError, error } = useQuery(['comments', post.id], () =>
    fetchComments(post.id)
  );
  • 쿼리 키를 의존성 배열처럼 다루는 것
  • 쿼리 키가 바뀌면( = post.id가 업데이트되면) 새로운 쿼리를 만든다.
  • 새로운 쿼리는 별개의 staleTime과 cacheTime을 가진다.

👉🏽 쿼리 함수에 있는 값(데이터를 구별할 때 쓰이는 값. 여기서는 post.id)이 쿼리 키(배열)에 포함돼야 한다!


  • 새로운 게시물을 클릭하면 이전 쿼리는 inactive 상태가 된다.
  • 가비지로 수집되기 전까지는 캐시에 남아있는다. (만약 cacheTime 동안 다시 사용되면 다시 활성화됨)

14. 페이지네이션 Pagenation

현재 페이지 보여주기

  • 페이지마다 다른 쿼리 키가 필요
    👉🏽 쿼리 키를 배열로 만들어준다.
    👉🏽 배열에 가져오는 페이지 번호를 포함

Post.jsx

...
const maxPostPage = 10; // 최대 10페이지로 임의로 설정해 줌 (json placeholder가 제공하는 api에 limit가 10으로 설정되어 있음..)

// 페이지에 따라 받아오는 데이터가 다른 쿼리 함수     
async function fetchPosts(pageNum) { // 매개변수로 pageNum 
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${pageNum}` // pageNum에 따라 다른 api 요청 
  );
  return response.json();
}
    
export function Posts() {
  const [currentPage, setCurrentPage] = useState(1); // 현재 페이지 state 만들기. 첫 페이지를 1페이지로 설정
  const [selectedPost, setSelectedPost] = useState(null);

  const { data, isError, error, isLoading } = useQuery(
    ['posts', currentPage], // currentPage를 넣어서 쿼리키를 의존성 배열처럼 만든다
    () => fetchPosts(currentPage), // 인자를 가지는 쿼리함수
    {
      staleTime: 2000,
    }
  );

이동 버튼 만들기 / 페이지 이동하기

  • Next Page, Previous Page 버튼 클릭
    👉🏽 currentPage state를 업데이트
    👉🏽 React Query가 바뀐 쿼리 키를 감지하고 새로운 쿼리를 실행해서 새로운 페이지가 표시된다.

Posts.jsx

...
<button
     disabled={currentPage <= 1} // 현재 페이지가 1 이하면 비활성화
     onClick={() => {
     setCurrentPage((previousValue) => previousValue - 1); // 클릭하면 현재페이지 -1
     }}>Previous page</button>
     <span>Page {currentPage}</span>
<button
    disabled={currentPage >= maxPostPage}
    onClick={() => {
    setCurrentPage((previousValue) => previousValue + 1); // 클릭하면 현재페이지 +1
    }}>Next page</button>

15. 데이터 프리페칭 Pre-fetching

  • 다음 페이지가 캐시에 없기 때문에 Next Page 버튼을 누를 때마다 로딩 인디케이터가 보이는 현상...
    👉🏽 Pre-fetching으로 (데이터를 미리 가져와서 캐시에 넣어서) 이러한 현상을 없애줄 수 있다.

pre-fetch의 목적

  • 일단 캐시된 데이터를 표시해 주면서
  • 백그라운드에서 데이터의 업데이트 여부를 조용히 서버에서 확인하는 것
  • 만약 데이터가 업데이트 됐을 경우 해당 데이터를 페이지에 보여줌.
    QQQ) 그럼 두 번 통신해서 비효율적인 것 아닌지.
  • 데이터를 캐시에 추가
  • 자동으로 stale 상태가 됨 (설정할 수 있지만 stale이 기본값) (inactive..)
  • re-fetching 하는 동안 stale 상태의 데이터를 보여줌 (캐시가 만료되지 않았다는 가정 하에! 만약 사용자가 cacheTime보다 오래 페이지에 머물렀다면 캐시가 없기 때문에 다시 로딩 인디케이터가 나타남)
  • 추후 사용자가 사용할 법한 모든 데이터에 Pre-fetching을 사용

queryClient.prefetchQuery

  • prefetching을 수행하는 queryClient의 메서드

Post.jsx

  • Next Page를 prefetching
import { useQuery, useQueryClient } from 'react-query'; // useQueryClient 불러오기
...
export function Posts() {
 ...
  const queryClient = useQueryClient(); // queryClient 사용

// currentPage가 바뀔 때마다 pre-fetching 하기 (useEffect와 의존성 배열 사용) 
  useEffect(() => {
    if (currentPage < maxPostPage) { // Next Page 클릭에 대한 prefetching을 만들 것이므로
      const nextPage = currentPage + 1;
      queryClient.prefetchQuery(['posts', nextPage], () =>
        fetchPosts(nextPage) // 해당 포스트(다음 페이지)를 fetch
      );
    }
  }, [currentPage, queryClient]);

참고 : Previous Page의 데이터를 캐시에 유지하기

  • keepPreviousData: true : 쿼리 키가 변경되어서 새로운 데이터를 fetching 하는 동안에도 마지막으로 fetch 되었던 데이터 값을 유지한다. (이전 페이지로 이동했을 때 해당 데이터가 캐시에 있도록)

Post.jsx

export function Posts() {
  const { data, isError, error, isLoading } = useQuery(
    ['posts', currentPage],
    () => fetchPosts(currentPage),
    {
      staleTime: 2000,
      keepPreviousData: true, // Previous Page 데이터 유지하기
    }
  );

16. isLoading vs isFetching

  • isFetching : 데이터 가져오는 중 (쿼리 함수 완료 전) (데이터 존재 여부 상관 X)
  • isLoading : isFetching + 캐시된 쿼리 데이터 없음 (데이터 새로 가져오는 중)
  • 보통 로딩 인디케이터를 만들 때는 isLoading 사용 (데이터 없어서 새로 가져올 때만 보여줄 용도로 사용되므로)

🐥 (어떤 상태이든) 캐시가 있다는 것...
=> fetching 중일 때 보여줄 수 있는 데이터가 있다는 것 (fetching은 다시 해야 함)

17. Mutation

  • 데이터를 변경하기 위해 서버에 네트워크 호출을 실시

참고 : 강의에서 사용하는 jsonplaceholder는 실제 서버 데이터를 변경할 수는 없음. mutation 요청 보내는 건 가능하지만...

useMutation

  • mutate 함수를 리턴 (..?) (객체의 속성 함수로...)
  • query key가 필요 없음 (데이터를 저장하지 않으므로)
  • isLoading은 있지만 isFetching은 없음 (캐시되는 항목이 없으므로)
  • retry(재시도) 기본값 없음. (자동 재시도를 적용하고 싶다면 설정은 가능)

useMutation과 useQuery의 차이점

  • useQuery의 queryFn은 매개변수 가질 수 없지만
  const { data, isLoading, isError, error } = useQuery(
    ['comments', post.id],
    () => fetchComments(post.id) // 쿼리함수 (매개변수 x)
  );
  • useMutation의 mutationFn은 매개변수를 가질 수 있다.
const deleteMutation = useMutation((postId) => deletePost(postId)); // 변이함수 (매개변수 O)

mutate 함수 사용

  • mutate 함수의 인자(여기서는 post.id)로 넣으면
      <button onClick={() => deleteMutation.mutate(post.id)}>
            Delete
          </button>
  • mutationFn의 인자(여기서는 postId)로 전달된다
    const deleteMutation = useMutation((postId) => deletePost(postId));

PostDetail.jsx

  • Delete 버튼 누르면 삭제
import { useQuery, useMutation } from 'react-query';

// 삭제 mutationFn
async function deletePost(postId) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/postId/${postId}`,
    { method: 'DELETE' }
  );
  return response.json();
}

const deleteMutation = useMutation((postId) => deletePost(postId));

   return (
    <>
      ...
      <button onClick={() => deleteMutation.mutate(post.id)}>Delete</button> // mutate의 인자로 삭제할 post id를 전달
      {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>
      )}
    ...

isError, isLoading, isSuccess

  • useMutation의 반환 객체에 들어있음..

PostDetail.jsx

 return (
    <>
      <h3 style={{ color: 'blue' }}>{post.title}</h3>
      <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>
      )}
      <button>Update title</button>
      <p>{post.body}</p>
      <h4>Comments</h4>
      {data.map((comment) => (
        <li key={comment.id}>
          {comment.email}: {comment.body}
        </li>
      ))}
    </>
  );

20. 정리

  • useQuery: 서버에서 데이터를 가져오고 최신 상태인지 확인하는 훅

  • staleTime : 데이터가 사용 가능한 상태로 유지되는 시간 (특정 트리거에 의해 re-fetch 되어 시작됨)

  • cacheTime : 데이터가 비활성화 된 후 남아있는 시간

  • 쿼리 키가 변경되면 useQuery hook은 쿼리를 다시 실행함 (re-fetch)
    ( => 데이터 함수(쿼리 함수?)가 바뀌면 쿼리 키도 바뀜(바뀌어야 함). 데이터가 바뀌면 다시 실행될 수 있도록)

0개의 댓글