[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)
    ( => 데이터 함수(쿼리 함수?)가 바뀌면 쿼리 키도 바뀜(바뀌어야 함). 데이터가 바뀌면 다시 실행될 수 있도록)

1개의 댓글

comment-user-thumbnail
2025년 5월 26일

Great sharing knowledge to update and learn to improve programming skills. More access to advanced programs. Explore scratch games to learn and build together to develop skills. Establish and create quality products and programs.

답글 달기