React

[React] React Query v5 - 페이지네이션과 프리페칭(pre-fetching)

밍띠이 2024. 4. 20. 23:00
반응형

블로그 게시물을 선택할 경우 해당 게시물에 달린 댓글을 페칭해오는 과정에서

게시물을 선택해도  연관된 댓글으로 리프레쉬가 안되는 이슈가 발생하였다.

관련하여 해결한 내용들을 정리하고자 한다.

0. Problem?

stale 상태인데도 리프레시 되지 않음

모든 댓글 쿼리가 같은 ['comments'] 쿼리 키를 사용하고 있었음.

이는 트리거가 있을 때만 새로 가져옴

트리거?

- 컴포넌트 리마운트

- 윈도우 리포커스

- 리페치 함수 실행

- 자동 리페치

- mutation 후에 쿼리 무효화

등 이러한 트리거를 통해 클라이언트의 데이터가 서버의 데이터와 다르다고 알게됨

-> 하지만 이 트리거에 해당하는 상황이 아님

해결 방법?

1. 새 블로그 포스트 제목을 클릭할때마다 데이터를 무효화(삭제) 하는 등의 방식으로 프로그래밍 한다

-> 쉽지않고, 원하는 방향이 아님, 데이터를 지울 필요도 없음

게시물 1에관한 댓글을 불러오는 쿼리와 게시물 2에 관한 댓글들을 불러오는 쿼리는 사실 같은 쿼리가 아니기 때문에

캐시에서 같은 공간을 차지할 필요도 없다. -> 쿼리마다 게시물 ID를 가지고 있기 때문에, 쿼리마다 캐시를 할수 있다.

-> 2. 각 포스트에 대한 쿼리를 별도로 라벨링 해서 수행할 것

1. 쿼리키 as 의존성 배열(Dependency Array)

 

의존성 배열(Dependency Array)이란?

의존성 배열(Dependency Array)은 useEffect, useCallback, useMemo 등의 Hook에서 사용되는 배열로, Hook이 불필요하게 반복해서 실행되는 것을 방지하여 성능을 최적화하기 위해서 사용한다.

의존성 배열의 주요 목적은 다음과 같다.

  • Hook의 재실행 조건 설정: 의존성 배열에 포함된 값들이 변경될 때만 훅이 재실행되도록 조건을 설정한다. 이를 통해 불필요한 실행을 방지할 수 있다.
  • 최신 상태 유지: 의존성 배열을 사용하면 Hook이 항상 최신 상태의 값을 참조할 수 있다. 이를 통해 불필요한 연산을 방지할 수 있다.
  • 의존성 관리의 명확성: 의존성 배열을 사용하면 훅이 어떤 값에 의존하고 있는지 명확하게 파악할 수 있다. 이는 코드의 가독성을 높이고, 의도하지 않은 의존성 관계를 방지할 수 있다.

모든 댓글 쿼리가 같은 ['comments'] 쿼리 키를 사용하고 있었음. 여기에 두번째 인자로 의존성 배열을 추가하여 post.id가 업데이트 될때 값을 리페칭 할 수 있도록 한다.

- queryFn 의 값(value)는 키의 일부분이어야 한다.

현재 선택된 post 의 댓글

개별 post 마다 comment 를 개별 쿼리로 사용할 수 있게 된다.

현재는 화면에 보여지지 않는 댓글

inactive 상태인 댓글의 경우 gcTime이 적용되어 해당 시간 이후에 화면에서 사라지게 된다.

 

2. 페이지네이션(Pagination)

댓글이나 게시글 수가 많아짐에 따라 페이징 처리를 해야하는 상황이 있었다.

이는 리액트 쿼리에서 제공하는 페이지네이션 기능과 무한 스크롤 기능으로 처리해 줄 수 있었는데

정적인 게시판에서는 페이지네이션을 통해 리스트를 나누어 보여주기로 하였다.

-> 이전 및 다음 페이지로 이동하는 버튼 추가

-> 페이지이동 처리

-> 컴포넌트 상태에서 현재 페이지 추적(currentPage)

-> 각 페이지마다 다른 쿼리키를 가져야 함

- 이동 버튼 클릭 시 현재 페이지를 업데이트 시키고 새로운 쿼리를 실행한다.

const { data, isError, error, isLoading } = useQuery({
    queryKey: ["posts", currentPage],
    queryFn: () => fetchPosts(currentPage),
    staleTime: 2000,
  });

 

<button
  disabled={currentPage <= 1}
  onClick={() => {
    setCurrentPage((previousValue) => previousValue - 1);
  }}
>
  ⬅️
</button>
<span>📖 페이지 : {currentPage}</span>
<button
  disabled={currentPage >= maxPostPage}
  onClick={() => {
    setCurrentPage((previousValue) => previousValue + 1);
  }}
>
  ➡️
</button>

이전 페이지로 가는 버튼은 첫 페이지가 1 페이지 이기 때문에 currentPage 가 1 이하일 경우 눌리지 않게(disabled) 처리하였고,

다음 페이지로 가는 버튼은 마지막 페이지 이상일 경우 눌리지 않게 처리하였다.

클릭 시 간단하게 setCurrentPage((previouseValue) => previouseValue +- 1 ) 해주어 이전 값에서 한페이지 씩 이동 하도록 처리하였다. 이렇게 하면 쿼리 키가 달라지기 때문에 자동으로 쿼리를 실행하여 리스트를 불러오게 된다.

편하다..........!!!!!!!!!!

하지만 여기서 발생한 문제가 있었다. 바로 새로운 페이지로 이동하려면 쿼리 실행 후 데이터를 받아오기 까지 '로딩중' 이라는 메시지가 나오는데 꽤 신경 쓰이는 부분이 있었다. 사용자 경험을 위해 수정해야겠다고 생각했다.

어떻게 처리해야 하나 찾아보다 데이터 프리페칭이 가능하다는 것을 알게되었다.

 

3. 데이터 프리페칭(Pre-feching)

https://tanstack.com/query/latest/docs/framework/react/guides/prefetching

프리페치란 캐시에 데이터를 추가해놓는 메서드다.

자동으로 stale 상태로 들어가지만 리페치 되는 동안에 캐시에 담겨있는 데이터를 보여줄 수 있기 때문에 사용자 경험이 훨씬 향상될 것이다.

다만 캐시가 만료되면 보이지 않기 때문에 만료 시점은 미리 고려해야 할 사항으로 보인다.

페이지네이션 뿐만 아니라 예를들어 웹사이트 플로우 상 사용자가 로그인 후 게시판으로 넘어갈 확률이 많다면 로그인 시에 미리 게시판의 데이터를 프리페치 하여 사용자들로 하여금 게시판의 stale 데이터들을 보여주는 등으로 사용 가능하다.

 const queryClient = useQueryClient();

  useEffect(() => {
    // 프리페치 처리
    const nextPage = currentPage + 1;
    const prefetch = () => {
      queryClient.prefetchQuery({
        queryKey: ["posts", nextPage],
        queryFn: () => fetchPosts(nextPage),
      });
    };
    if (maxPostPage > nextPage) {
      prefetch();
    }
  }, [currentPage, queryClient]); // queryClient 도 종속성 배열에 추가

 

useEffect를 이용하여 현재 페이지가 변경 될 경우 다음 페이지를 프리페치하여 사용자는 로딩 없이 페이지가 바로 볼 수 있게끔 할 수 있다.

현재 2페이지 이지만 개발도구를 통해 3페이지 데이터도 미리 불러오는것을 확인할 수 있다.

 

4. isLoading vs. isFetching

isFetching은 비동기 쿼리 함수가 아직 해결되지 않았을 때 'true' 값을 반환한다.

isLoading은 isFetching 안에 포함된 관계로써, 비동기 쿼리 함수가 해결되지 않았고(isFetching = true), 캐시에 데이터가 없는 경우 'true'값을 반환한다.

따라서 prefetching을 사용할 경우 백그라운드에서는 현재페이지가 다음 페이지로 이동할 때마다 해당 데이터는 stale 상태이기 때문에 새로 fetch는 진행하지만, 사용자 관점에서 캐시에 데이터를 가지고 있기 때문에 캐시에 저장된 데이터를 보여주고, 만약 fetch 해 온 데이터가 변동이 있을 경우 해당 데이터가 화면에 보여지게 된다. 그렇기 때문에 항상 최신 데이터를 유지하는 것으로 보일 수 있다.

5. Mutation 이란?

블로그에 포스트를 추가하거나 삭제 하거나 예를들어 게시물 제목을 변경하는 경우 데이터가 변경이 되는데 이때 서버에 데이터가 변경됨을 네트워크 호출 해 적용하게 하는 과정이다.

useMutation 훅을 이용하면 된다.

uesQuery와 비슷하다. mutate 함수를 반환하고 쿼리키는 필요하지 않다

isLoading은 있으나 isFetching은 없다

기본적으로 재시도는 하지 않는다

6. useMutation으로 게시물 삭제하기

 

deletePost api 는 상위 post컴포넌트에서 useMutation으로 정의한다.

// post.jsx
 const deleteMutation = useMutation({
    mutationFn: (postId) => deletePost(postId),
  });
  
  // ...
          <PostDetail post={selectedPost} deleteMutation={deleteMutation} />

 // ...

 

하위 상세 페이지의 실제 삭제 버튼에 해당 mutate 를 호출해 준다.

// PostDetail.jsx
export function PostDetail({ post, deleteMutation }) {
 // ...
  <button onClick={() => deleteMutation.mutate(post.id)}>🗑️ 삭제</button>
  // ...

 

현재 jsonPlaceholder 에서 제공하는 데이터는 수정, 삭제가 안되기 때문에 

별개로 로딩과 에러 처리를 해줄 수 있다.

useMutation은 캐시가 없기 때문에 간단하다.

기타 속성에 대해 알아보자.

7. 기타 useMutation 속성

https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation

`isPending` , `isError`, `isSuccess` 등을 이용하여 상태 처리를 해줄 수 있다.

<button onClick={() => deleteMutation.mutate(post.id)}>🗑️ 삭제</button>
{deleteMutation.isPending && (
<p className="loading">🔥 게시물을 삭제중입니다...</p>
)}
{deleteMutation.isError && (
<p className="error">
  ⚠️ 문제가 발생했어요 + {deleteMutation.error}...
</p>
)}
{deleteMutation.isSuccess && (
<p className="success">🥲 게시물을 삭제했어요</p>
)}

+ 상위 컴포넌트에서 다른 post를 클릭할 경우 Mutation 상태를 리셋 해주어야 한다.

그렇지 않으면 다른 포스트도 삭제되었다고 나온다..! deleteMutation.reset() 을 사용하면 된다.

// Post.jsx
// ...
 {data.map((post) => (
          <li
            key={post.id}
            className="post-title"
            onClick={() => {
              deleteMutation.reset();
              setSelectedPost(post);
            }}
          >
            🔗 {post.title}
          </li>
        ))}
        // ...

 

수정하는 방법도 동일하게 만들 수 있다.

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

 

8. React Query 기초 정리

 

리덕스를 대체하여 리액트 쿼리를 사용할 수 있을 뿐만아니라, 더 나은 기능들을 제공하기 때문에 더 유용한 것 같다.

- 패키지 설치, 쿼리클라이언트 생성, 쿼리프로바이더 추가

- useQuery : isLoading, isFetching , error 반환 및 데이터

- staleTime : 얼마 동안 리페치 대상인지 아닐지

- gcTime: inactivity 상태로 얼마나 캐시 데이터를 유지할건지

- 쿼리키를 의존성 배열로 사용하기

- 페이지네이션, 프리페칭

- useMutation for server-side-effects

 

 

반응형