useQuery, useMutation 사용해보기

조바이블·2024년 3월 30일

useQuery 사용해보기

듣고 있는 강의 과제에 대한 나의 풀이

export function PostDetail({ post }) {
  const { isLoading, data, isError, error } = useQuery({
    queryKey: ["comments"],
    queryFn: () => fetchComments(post.id),
  });  

  if (isLoading) {
    return <h3>Loading...</h3>;
  }  

  if (isError) {
    return (
      <>
        <h2>Error</h2>
        <p>{error.toString()}</p>
      </>
    );
  }

이렇게 하니 문제가 발생했다.

  • 다른 포스트를 아무리 클릭해도 comment 값이 변경되지 않고 유지
  • 아무리 클릭해도 어떤 업데이트를 받지 못했다.

원인

  • 모든 쿼리가 같은 comments 쿼리 키를 사용

React Query는 아래와 같은 상황에서 데이터를 자동으로 새로고침하거나 갱신한다.

  • 컴포넌트 재마운트
  • window focusing
  • useQuery에서 반환된 refetch 함수를 수동으로 실행
  • 지정된 간격에서 자동으로 재검색
  • 데이터 변경을 알리는 뮤테이션이 성공한 후, 관련 쿼리를 무효화(invalidate)하여 새로운 데이터로 갱신

포스트 제목을 클릭할때는 위의 상황중 단 하나도 일어나지 않기 때문에 데이터가 변경되지 않았다.

Solution

  • 블로그 포스트 제목을 클릭할때 마다 데이터 삭제

  • 포스트 2르 ㄹ누를때 블로그 포스트 1에 대한 코멘트를 캐시에서 제거해야할 필요가 없음
    - 같은 쿼리를 수행하지도 않음(포스트 클릭, 코멘트 가져오)
    - 굳이 캐 시에서 같은 공간을 차지할 필요도 없음

  • 쿼리에는 PostId를 가지고 있기 때문에 쿼리마다 캐시를 할수 있다.

  • 어떠한 댓글 쿼리에 대해서도 캐시를 공유할 필요 없다.

  • 각 포스트에 대한 쿼리를 별도로 라벨링 해서 처리가 가능하다.

쿼리키 (데이터 식별자 및 종속성 배열)

우리는 아래와 같이 처리가 가능하다.
['comments', post.id]

  • 리액트 쿼리는 키가 변경될때 새로운 쿼리를 생성한다.
  • 이 경우 post.id가 업데이트 될때 새로운 쿼리를 생성
  • 각 쿼리는 개별적인 stale time, 개별적인 캐시 시간을 갖는다.
  • 이렇게 종속성 배열이 다르면 완전히 다른 쿼리로 간주한다.

이렇기 때문에 데이터를 가져올때 쓰는 쿼리 함수의 모든 값은 키의 일부여야 한다.

  const { isLoading, data, isError, error } = useQuery({
    queryKey: ["comments", post.id],
    queryFn: () => fetchComments(post.id),
  });

실제로 이렇게 변경하니 정상적으로 comment들이 변경되었다.

 const { isLoading, data, isError, error } = useQuery({
    queryKey: ["comments"],
    queryFn: () => fetchComments(post.id),
  });  

useQuery내용을 살펴보면 post.id 와 상관없이 같은 쿼리키를 사용하고 있었고 실제 불러오는 데이터가 달라도 react query는 캐시에 데이터가 있다고 판단하고 새로운 데이터를 가져오지 않았던 것이다.

Pagination

  • 컴포넌트 상태로 currentPage를 만든다.
  • 쿼리키에 page number을 추가 한다. ["posts", currentPage]
  • 사용자가 버튼을 클릭하여 next Page, Previous Page button을 클릭하면
    - update currentPage
    - 새로운 쿼리를 날린다.
const { data, isError, isLoading, error } = useQuery({
    queryKey: ["posts", currentPage],
    queryFn: () => fetchPosts(currentPage),
    staleTime: 2000, // 2 Seconds
  });
  <div className="pages">
	<button
	  disabled={currentPage <= 1}
	  onClick={() => {
		setCurrentPage((prev) => prev - 1);
	  }}        >
	  Previous page
	</button>

	<span>Page {currentPage}</span>
	<button
	  disabled={currentPage >= maxPostPage}
	  onClick={() => {
		setCurrentPage((prev) => prev + 1);
	  }}
>
	  Next page
	</button>
  </div>

이렇게 구현을 하고 사용해보니 다음 이나 이전 버튼을 누를때까지 Loading 화면이 노출되었다.
왜냐면 캐시에 페이지가 없었기 때문이다.
이번 강의에서는 prefetch로 캐시에 넣어 사용자가 기다리게 하지 않는다.

Prefetching

  • 데이터를 캐시에 넣는다
  • 데이터는 기본적으로 stale로 간주된다.
  • 그 후에 데이터를 사용할 때 데이터는 여전히 stale 상태여서 다시 데이터를 가져와야 한다.
  • 데이터를 가져오는 동안 react query는 캐시에 있는 데이터를 제공한다.
  • 그렇게 새로고침 될 때까지 캐시에 있는 데이터를 표시할 수 있다.

사용자가 특정 포스트 페이지에서 캐시 시간 보다 오래 머무르고 다음 페이지를 클릭했을때 캐시가 만료되어 아무 데이터가 없을 수도 있다.

이러한 prefetching은 페이지네이션 뿐만 아니라 사용자가 원하는 모든 데이터에 사용할 수 있다.

prefetchQuery

  • 쿼리 클라이언트의 메소드로 useQueryClient라는 훅을 사용해 쿼리클라이언트를 가져올 수있다.
import { useQuery, useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();

상태 업데이트는 비동기적이라 이 업데이트가 이미 적용되었는지 여부를 확인할수 없기 때문에
다음 버튼에 넣기는 부적합하다.

그래서 우리는 useEffect를 사용한다.

  const queryClient = useQueryClient();
  useEffect(() => {
    const nextPage = currentPage + 1;
    if (nextPage < maxPostPage) {
      queryClient.prefetchQuery({
        queryKey: ["posts", nextPage],
        queryFn: () => fetchPosts(nextPage),
      });
    }
  }, [currentPage, QueryClient]);
  • prefetchQuery를 사용하기 위해 쿼리 키가 필요하다.
  • useQuery에 사용된것과 같은 형태여야 한다.
  • 리액트 쿼리가 캐시에 이미 데이터가 있는지 확인할때 이 쿼리키를 기준으로 검색한다.

prefetch 작동방식

  • 현재 페이지가 업데이트 될때 useQuery가 새 페이지를 쿼리키로 사용하는 것

  • 만약 현재 3페이지인 경우
    - 캐시에 4페이지에 대한 내용 , 즉 prefetch 된 내용이 있지만 자동으로 stale 된 상태이다.
    - stale time은 기본 값인 0이기 때문이다.
    - useQuery 데이터 stale 시간은 2초이지만 prefetching은 0초이다.

  • 데이터가 업데이트인 경우 prefetch 데이터 stale 시간이 0초이므로 "posts"stale시간은 0초이므로 캐시데이터는 stale 상태이다.

    "Stale" 상태란 일반적으로 쿼리나 데이터의 상태가 더 이상 유효하지 않거나 최신 정보를 반영하지 않는 상태를 의미

  • 따라서 useQuery가 다시 작동한다.

  • 캐시에 있는 데이터가 표시되지만 stale 상태이기 때문에 다시 요청한다.

  • 그리고 새로 요청하여 받은 데이터를 반환하면 데이터가 업데이트 된다.

  • 데이터를 불러왔을때 결과가 캐시된 데이터와 같으면 사용자는 우리가 서버를 확인해 데이터가최신인지 여부를 확인했는지 알수 없다.

Mutations

서버에 네트워크 호출을 통해 실제 데이터를 업데이트하거나 새로운 포스트를 추가하거나 삭제하며 제목을 변경하는 등의 행동을 수행할 수 있습니다.

1. 낙관적 업데이트와 실패 처리

낙관적 업데이트는 서버 호출이 성공적으로 수행될 것으로 예상되는 경우, 실제 서버 응답을 기다리지 않고 사용자 인터페이스를 빠르게 업데이트하는 것입니다. 하지만 서버 호출이 실패하면 이를 처리해야 합니다. 실패한 변이를 되돌리거나 사용자에게 알리는 등의 방법을 사용하여 오류를 관리할 수 있습니다.

2. 리액트 캐시 업데이트

서버에서 받은 데이터를 사용하여 리액트 캐시를 업데이트할 수 있습니다. 이를 통해 사용자 인터페이스가 최신 정보를 반영하고 데이터의 일관성을 유지할 수 있습니다. 예를 들어, 변이가 성공하면 새로운 데이터를 사용하여 컴포넌트 상태를 업데이트하고, 이를 통해 리렌더링하여 사용자에게 즉시 변경 사항을 보여줄 수 있습니다.

3. 쿼리 무효화와 서버 데이터 동기화

변이가 서버의 데이터를 변경하면 클라이언트 캐시의 일부 쿼리는 더 이상 유효하지 않을 수 있습니다. 따라서 쿼리를 무효화하고 클라이언트의 데이터를 서버의 최신 데이터와 동기화해야 합니다. 이를 통해 애플리케이션의 상태를 정확하게 유지하고 사용자에게 최신 정보를 제공할 수 있습니다.

4. 최적화된 네트워크 호출

Mutation를 수행할 때는 네트워크 호출을 최적화하여 성능을 향상시킬 수 있습니다. 예를 들어, GraphQL을 사용하여 변이에 필요한 데이터만 서버로부터 요청하거나, HTTP 요청을 최소화하여 네트워크 부하를 줄일 수 있습니다.

5. 비동기 처리와 에러 핸들링

네트워크 호출은 비동기적으로 이루어지므로 변이의 성공 또는 실패를 적절히 처리해야 합니다. 이를 위해 Promise나 async/await와 같은 비동기 처리 메커니즘을 사용하고, 오류 핸들링을 통해 사용자에게 적절한 피드백을 제공해야 합니다.

useMutation

  • 리액트 쿼리에서의 useMutation 훅을 사용
  • useQuery 와 사용법이 비슷하나 몇가지 차이점이 있다.
    - useMutationmutate 함수를 반환
    - 실제로 서버에 변경 사항을 호출할 때 사용한다.
    - query key가 필요하지 않는다.
    - 실제로 데이터를 저장하지 않기 때문
    - isLoading은 있지만 isFetching은 없다.
    - 캐시를 활용하지 않기 때문에 의미가 없다.
    - 변이와 관련된 캐시도 없고 기본적으로 재시도도 없다.
    - 원한다면 설정을 통해 재시도는 가능하다.
  const deleteMutation = useMutation({
    mutationFn: (postId) => deletePost(postId),
  });
  //deleteMutation.mutate
   {selectedPost && (
        <PostDetail post={selectedPost} deleteMutation={deleteMutation} />
      )}

export function PostDetail({ post, deleteMutation }) {
//...
	<button onClick={() => deleteMutation.mutate(post.id)}>
        Delete
      </button>
}
  • deleteMutation.reset(); 모든 상세 속성 초기화, 이걸 하지 않으면 다른 포스트에서 삭제때 사용된 속성들이 유지되서 리셋해준다.
<ul>
	{data.map((post) => (
	  <li
	   key={post.id}
		className="post-title"
		onClick={() => {
		  deleteMutation.reset();
		  setSelectedPost(post);
		}}
>
		{post.title}
	  </li>
	))}
  </ul>
profile
개발 공부를 해보자.. 취업은.. 어렵겠지만 그래도 공부는 해보자

0개의 댓글