본문 바로가기

온라인 강의(유데미, 인프런 등)/React-Query(유데미)

쿼리 키 & 페이지네이션 & 데이터 pre-fetching & isLoading / isFetching & Mutation

반응형

쿼리 키

 

쿼리 키를 표현하는 방식에는 두 가지가 있다.

  1. 문자열 방식 => "post"
  2. 배열 방식 => ["comments", post.id]
  const { data, isLoading, isError, error } = useQuery(
    ['comments', post.id],
    () => fetchComments(post.id)
  );

만약 배열 방식으로 쿼리 키를 표현한다면 쿼리 키를 쿼리에 대한 종속성 배열로 취급하게 된다. 따라서 쿼리 키가 변경되면 새 쿼리를 생성하여 각각의 staleTime, cacheTime 을 가지게 된다.

배열에 속해있는 값들이 전부 같으면 같은 쿼리라고 인식하여 cache 에 저장되어 있는 데이터를 이용한다.

따라서 데이터를 가져올 때 사용하는 쿼리 함수에 있는 값이 쿼리 키에 포함되어야 한다. 

 

페이지네이션(무한 스크롤 x)

 

이전 페이지, 다음 페이지 기능을 만들 땐 페이지마다 각 다른 쿼리 키가 필요하다. 따라서 쿼리 키를 배열로 업데이트하여 가져오는 페이지 번호를 포함시켜야 한다.

ex) 쿼리 키 : ["post", currentPage] <- currentPage은 state 값으로 관리

 

데이터 pre-fetching

 

페이지네이션을 통해 다음 페이지로 이동할 때 다음 페이지에 대한 데이터가 캐시에 없으므로 사용자는 잠시 로딩 인디케이터를 보게 되고 이는 사용자 경험에 좋지 않은 영향을 준다. 그래서 데이터를 미리 가져와 캐시에 넣어 사용자가 기다릴 필요가 없도록 만들어야 한다.

이는 페이지네이션 뿐만 아니라 통계적으로 사용자의 다음 행동을 예측하여 필요한 데이터를 가져오는 데에도 사용할 수 있다.

import { useEffect, useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';

import { PostDetail } from './PostDetail';

const maxPostPage = 10;

async function fetchPosts(pageNum) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${pageNum}`
  );
  return response.json();
}

export function Posts() {
  const [currentPage, setCurrentPage] = useState(1);
  const [selectedPost, setSelectedPost] = useState(null);

  // prefetchQuery를 통해 nextPage 정보를 사전 페칭하여 페이지 이동시 사용자 경험을 향상
  const queryClient = useQueryClient();

  // currentPage가 바뀔 때마다 로직이 재실행
  // currentPage 변경으로 nextPage도 바뀌게 되고 그에 따라 prefetchQuery로 가져오는 nextPage 쿼리키 변경
  useEffect(() => {
    // 범위 외의 데이터를 가져오지 않도록
    if (currentPage < maxPostPage) {
      const nextPage = currentPage + 1;
      queryClient.prefetchQuery(['posts', nextPage], () =>
        fetchPosts(nextPage)
      );
    }
  }, [currentPage, queryClient]);

  const { data, isLoading, error, isError, isFetching } = useQuery(
    ['posts', currentPage],
    () => fetchPosts(currentPage),
    {
      staleTime: 2000,
      keepPreviousData: true, // 쿼리키가 바꿔어도 이전 데이터 유지(이전 페이지로 돌아갈 경우 해당 데이터가 캐시에 있음)
    }
  );

 
  if (isError)
    return (
      <>
        <h3>Oops, somthing went wrong!</h3>
        <p>{error.toString()}</p>
      </>
    );

  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={() => {}}> */}
        <button
          disabled={currentPage <= 1}
          onClick={() => {
            setCurrentPage((prevValue) => prevValue - 1);
          }}
        >
          Previous page
        </button>
        {/* <span>Page {currentPage + 1}</span> */}
        <span>Page {currentPage}</span>
        {/* <button disabled onClick={() => {}}> */}
        <button
          disabled={currentPage >= maxPostPage}
          onClick={() => {
            setCurrentPage((prevValue) => prevValue + 1);
          }}
        >
          Next page
        </button>
      </div>
      <hr />
      {selectedPost && <PostDetail post={selectedPost} />}
    </>
  );
}

 

isLoading /  isFetching

 

  1. isFetching : async 쿼리 함수가 해결되지 않은 경우, 즉 아직 데이터를 가져오는 중 <- 뒷 면에서 서버 데이터가 업데이트되었는지 조용히 확인
  2. isLoading : isFetching이 true 이면서 아직 캐시된 데이터가 없고 데이터를 가져오는 중 <- 사용자에게 보여줄 데이터가 없을 때 보여주기

 

Mutation(변이)

 

변이는 서버에 데이터를 업데이트할 수 있도록 네트워크를 call 하는 것이다. 

업데이트 된 데이터를 사용자에게 보여주거나 진행된 변경 내용을 등록하여 사용자에게 보여주는 방법은 다음과 같다(실시간 데이터 업데이트 하는 법).

  1. 낙관적 업데이트라고 하는 방법으로, 서버에서 모든 것이 잘 진행되었다고 가정하고 데이터를 먼저 바꿔놓고, 아니라면 다시 원상태로 롤백한다.
  2. 변이 호출을 실행할 때 서버에서 받는 데이터를 취하고, 업데이트된 해당 데이터로 캐시를 업데이트 한다.
  3. 관련 쿼리를 무효화한다. 무효화 하는 경우 서버에서 데이터를 리페치하여 업데이트 된 데이터를 가져온다.

useMutation는 일부 예외를 제외하고 useQuery와 비슷하다.

  1. mutate 함수를 반환한다.
  2. 데이터를 저장하지 않으므로 쿼리 키가 필요없다.
  3. isLoading은 있지만 isFetching은 없다(캐시가 없기 때문).
  4. retry하지 않는다(useQuery는 기본적으로 3번 retry 된다).
import { useQuery, useMutation } from 'react-query';

async function fetchComments(postId) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/comments?postId=${postId}`
  );
  return response.json();
}

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

async function updatePost(postId) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/postId/${postId}`,
    { method: 'PATCH', data: { title: 'REACT QUERY FOREVER!!!!' } }
  );
  return response.json();
}

export function PostDetail({ post }) {
  const { data, isLoading, isError, error } = useQuery(
    ['comments', post.id],
    () => fetchComments(post.id)
  );
  
  // useMutation은 객체를 반환
  const deleteMutation = useMutation((postId) => deletePost(postId));
  const updateMutation = useMutation((postId) => updatePost(postId));

  if (isLoading) return <h2>Loading...</h2>;
  if (isError)
    return (
      <>
        <h2>Something went wrong...</h2>
        {/* <p>{error.toString()}</p> */}
        <p>{error.toString().split(': ')[1]}</p>
      </>
    );

  // console.log('data: ', data);

  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: 'blue' }}>Deleting the post</p>
      )}
      {deleteMutation.isSuccess && (
        <p style={{ color: 'green' }}>Post has (not) been deleted</p>
      )}
      <button onClick={() => updateMutation.mutate(post.id)}>
        Update title
      </button>
      {updateMutation.isError && (
        <p style={{ color: 'red' }}>Error updating the post</p>
      )}
      {updateMutation.isLoading && (
        <p style={{ color: 'blue' }}>Updating the post</p>
      )}
      {updateMutation.isSuccess && (
        <p style={{ color: 'green' }}>Post has (not) been updated</p>
      )}
      <p>{post.body}</p>
      <h4>Comments</h4>
      {data.map((comment) => (
        <li key={comment.id}>
          {comment.email}: {comment.body}
        </li>
      ))}
    </>
  );
}

 

반응형