리액트 쿼리의 다양한 훅

김현준·2024년 8월 13일
0

리액트

목록 보기
8/11

리액트 쿼리는 데이터를 가져오고 변형하는 데 매우 유용한 다양한 훅을 제공한다. 각 훅은 특정한 역할을 수행하며, 이들을 잘 활용하면 비동기 데이터 관리를 쉽게 할 수 있다. 아래에서 주요 훅들을 소개한다.

Query 훅

useQuery

  • 데이터 페칭을 위한 기본 훅이다.
  • 사용 예:
     import { useQuery } from 'react-query';
     import axios from 'axios';
	//api 패칭 함수
     const fetchTodo = async () => {
       const { data } = await axios.get('/api/todo');
       return data;
     };
	
	//컴포넌트
     const TodoComponent = () => {
       const { data, error, isLoading } = useQuery('todo', fetchTodo);

       if (isLoading) return <div>Loading...</div>;
       if (error) return <div>Error: {error.message}</div>;

       return <div>{data.title}</div>;
     };

useInfiniteQuery

  • 무한 스크롤 또는 페이지네이션 데이터를 페칭하는 데 사용된다.
  • 사용 예:
 //api.ts
//characterId, page, size를 인자로 받는다.
export async function getCharacterComments(characterId: string, page: number, size: number) {
  return handleAxiosError<CharacterCommentResult>(
    axios
      .get(`/api/v1/character/comment/${characterId}`, {
        params: { page, size },
      })
      .then((res) => res.data)
  );
}

//CharacterComments.tsx
const PAGE_SIZE = 10;

function CharacterComments({ characterId }: { characterId: string }) {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery(
    ['characterComments', characterId],
    ({ pageParam = 0 }) => getCharacterComments(characterId, pageParam, PAGE_SIZE),
    {
      getNextPageParam: (lastPage, allPages) => {
        const morePagesExist = lastPage.data.length >= PAGE_SIZE;
        if (!morePagesExist) return undefined;
        return allPages.length;  // 다음 페이지 번호를 반환
      },
    }
  );
  
  /* 스크롤이 페이지 끝에 도달했을 때 데이터를 가져오도록 설정할 경우
  const observerElem = useRef(null);

  useEffect(() => {
    if (!observerElem.current) return;

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 1.0 } // 관찰 대상이 뷰포트에 완전히 들어왔을 때 트리거
    );

    observer.observe(observerElem.current);

    return () => observer.disconnect(); // cleanup
  }, [fetchNextPage, hasNextPage]); */

  if (status === 'loading') return <p>Loading...</p>;
  if (status === 'error') return <p>Error loading comments.</p>;

  return (
    <div>
      {data.pages.map((page, index) =>
        page.data.map((comment: ICharacterComment) => (
          <div key={`${comment.userId}-${index}`}>
            <p><strong>{comment.userNickname}:</strong> {comment.comment}</p>
          </div>
        ))
      )}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage
          ? 'Loading more...'
          : hasNextPage
          ? 'Load More'
          : 'No more comments'}
      </button>
	/* 스크롤이 페이지 끝에 도달했을 때 데이터를 가져오도록 설정할 경우
	<div ref={observerElem} style={{ height: '1px' }} />
      {isFetchingNextPage && <p>Loading more...</p>}
    </div>/*
  );
}
  • useInfiniteQuery: 무한 스크롤을 구현하기 위해 React Query에서 제공하는 훅
  • getNextPageParam: 다음 페이지가 있는지 여부를 결정하고, 다음 페이지를 가져오기 위해 사용되는 페이지 번호를 반환
getNextPageParam: (lastPage, allPages) => {
      // lastPage에서 nextPage를 가져온다.
      const nextPage = lastPage.nextPage;
      // nextPage가 있으면 그 값을 반환하고, 없으면 undefined를 반환하여 페이지네이션을 멈춘다.
      return nextPage ? nextPage : undefined;
    }

  • getNextPageParam의 인자
    • lastPage:
      가장 최근에 가져온 페이지의 데이터
      이 데이터에는 페이지와 관련된 정보(예: 현재 페이지 번호, 다음 페이지 여부 등)가 포함되어 있을 수 있다.
    • allPages:
      지금까지 가져온 모든 페이지의 배열
      이를 통해 여러 페이지를 참고하여 다음 페이지를 결정할 수 있다.
  • getNextPageParam의 반환값
    • 다음 페이지가 있는 경우:
      다음 페이지를 가져오기 위한 파라미터를 반환
      이 파라미터는 fetchFunction에 전달
    • 다음 페이지가 없는 경우:
      undefined를 반환하여 더 이상 페이지를 가져오지 않도록 한다.
  • 반환값
    • fetchNextPage: 다음 페이지의 데이터를 가져오기 위해 호출되는 함수
    • hasNextPage: 더 가져올 데이터가 있는지를 나타내는 값
    • isFetchingNextPage: 다음 페이지를 로딩 중인지 나타내는 값

useInfiniteQuery 훅을 사용할 때 반환되는 데이터 객체에는 pages와 pageParams라는 두 가지 주요 속성이 포함된다.
낙관적 업데이트를 할 때 주의하자
아래 캡처본은 현재 3번째 페이지까지 불러온 상황이다.

  • pages
    • useInfiniteQuery 훅이 성공적으로 가져온 각 페이지의 데이터 배열을 포함
    • 이 배열은 각 페이지의 응답 데이터를 순서대로 포함하며, useInfiniteQuery가 가져온 모든 페이지의 데이터를 추적

      예를 들어, 만약 3개의 페이지를 가져왔다면, pages는 다음과 같이 구성될 수 있다.

      pages: [
        // 첫 번째 페이지의 데이터
        [{ id: 1, name: "Item 1" }, { id: 2, name: "Item 2" }],
        // 두 번째 페이지의 데이터
        [{ id: 3, name: "Item 3" }, { id: 4, name: "Item 4" }],
        // 세 번째 페이지의 데이터
        [{ id: 5, name: "Item 5" }, { id: 6, name: "Item 6" }]
      ]
  • pageParams
    • useInfiniteQuery가 각 페이지의 데이터를 가져올 때 사용한 매개변수(pageParam)를 저장하는 배열
    • 이 배열에는 useInfiniteQuery가 getNextPageParam 함수에 의해 반환된 각 페이지의 매개변수가 순서대로 저장

      예를 들어, 다음 페이지를 가져오기 위해 getNextPageParam에서 각 페이지마다 다른 pageParam 값을 반환했다면, pageParams는 다음과 같이 구성될 수 있다.

      pageParams: [
        0,  // 첫 번째 페이지의 pageParam
        1,  // 두 번째 페이지의 pageParam
        2   // 세 번째 페이지의 pageParam
      ]

참고하면 좋은 자료


useQueries

개별 useQuery들의 타입을 배열로 묶어놓은 것이다.

  • 여러 개의 쿼리를 동시에 실행할 때 사용된다.

  • 사용 예:

    import { useQueries } from 'react-query';
    import axios from 'axios';
    
    const fetchUser = async (userId) => {
      const { data } = await axios.get(`/api/user/${userId}`);
      return data;
    };
    
    const UsersComponent = ({ userIds }) => {
      const results = useQueries(
        userIds.map(userId => ({
          queryKey: ['user', userId],
          queryFn: () => fetchUser(userId),
        }))
      );
    
      return (
        <div>
          {results.map(({ data, isLoading, error }, index) => (
            <div key={userIds[index]}>
              {isLoading ? (
                'Loading...'
              ) : error ? (
                `Error: ${error.message}`
              ) : (
                data.name
              )}
            </div>
          ))}
        </div>
      );
    };
    
    //구조분해 할당 예시
    const [boardDetailQuery, boardLikesQuery, boardCommentQuery] = useQueries([
    {
    	queryKey: ["boardDetail", boardId],
    	queryFn: () => getBoardDetail(boardId + ""),
    },
    {
    	queryKey: ["boardLikes", +boardId!, user?.id],
    	queryFn: () => getBoardLikes(+boardId!, String(user?.id)),
    },
    {
    	queryKey: ["boardComment", boardId],
    	queryFn: () => getBoardComment(boardId + ""),
    },
    ]);
    
    // 각 쿼리의 데이터와 로딩 상태를 구조 분해 할당으로 추출
    const { data: postData, isLoading: postLoading } = boardDetailQuery;
    const { data: likeData, isLoading: likeLoading } = boardLikesQuery;
    const { data: commentData, isLoading: commentLoading } = boardCommentQuery;

Mutation 훅

  1. useMutation
    • 데이터 변형(예: POST, PUT, DELETE 요청)을 처리하는 데 사용된다.
    • 사용 예:
     import { useMutation, useQueryClient } from 'react-query';
     import axios from 'axios';

     const addTodo = async (newTodo) => {
       const { data } = await axios.post('/api/todo', newTodo);
       return data;
     };

     const AddTodoComponent = () => {
       const queryClient = useQueryClient();
       const mutation = useMutation(addTodo, {
         onSuccess: () => { // onSuccess: (data, variables) 이런 식으로 각각 응답받는 데이터, muate에 전달한 데이터를 사용할 수도 있다.
           // 데이터를 새로고침
           queryClient.invalidateQueries('todos');
         },
         onError: (error) => {
      	   consol.log(error)//요청에 실패할 경우 error 객체가 응답 데이터가 된다.
         },
       });

       const handleAddTodo = () => {
         mutation.mutate({ title: 'New Todo' });
       };

       return (
         <div>
           <button onClick={handleAddTodo}>Add Todo</button>
           {mutation.isLoading && <div>Adding todo...</div>}
           {mutation.error && <div>Error adding todo: {mutation.error.message}</div>}
         </div>
       );
     };

낙관적 mutate 등 자세한 사용법

  • 서버의 데이터 업데이트가 성공하기 전에도 UI를 업데이트할 수 있게 함
  • 서버와의 데이터 동기화를 신경쓰지 않고 먼저 사용자에게 성공 시 UI를 보여준 후, 요청의 결과가 오면 성공/실패 여부에 따라 UI 업데이트
  • 사용자는 서버와의 통신 여부와 관계 없이 UI를 확인할 수 있게 됨
  • 예시: 인스타그램의 좋아요 기능, 카카오톡이 일단 전송된 후 성공, 취소/재전송 창이 나중에 뜨는 것

데이터를 서버로 보낼 경우의 코드

주의사항
useInfiniteQuery(무한 스크롤)는 pages와 pageParams라는 두 가지 주요 속성이 포함되기에 낙관적 업데이트에 주의해야 한다.
useInfiniteQuery에 적용한 예시

좋아요 예시

const toggleLike = async (postId) => {
  // 서버에 좋아요 상태를 토글하는 요청
  await axios.post(`/api/posts/${postId}/toggleLike`);
};

const PostComponent = ({ postId, initialLikes }) => {
  const queryClient = useQueryClient();
  
  // 좋아요 상태를 업데이트하는 뮤테이션 설정
  const mutation = useMutation({
    mutationFn: () => toggleLike(postId),
    onMutate: async (postId) => {
      // 기존 캐시 취소, '쿼리 키'로 진행 중인 refetch 취소하여 낙관적 업데이트를 덮어쓰지 않도록 함
      await queryClient.cancelQueries(['liked', postId]);

      // 이전 캐시 상태 가져오기
      const previousLikes = queryClient.getQueryData(['liked', postId]);

      // 캐시된 데이터를 낙관적 업데이트
      queryClient.setQueryData(['liked', postId], (old) => ({
        ...old,
        likes: old.likes + 1, // 낙관적으로 좋아요 수 증가
      }));

      // 오류 발생 시 되돌리기 위해 이전 상태 반환
      return { previousLikes };
    },
    onError: (err, postId, context) => {
      // 오류 발생 시 이전 상태로 복원
      queryClient.setQueryData(['liked', postId], context.previousLikes);
    },
    onSettled: (postId) => {
      // 성공, 실패 여부에 관계 없이 refetch(쿼리 무효화)
      queryClient.invalidateQueries(['liked', postId]);
    },
  });

  return (
    <button
      onClick={() => {
        mutation.mutate(postId);
      }}
    >
      좋아요
    </button>
  );
};

export default PostComponent;

기타 훅

useQueryClient

  • 쿼리 클라이언트를 사용하여 쿼리 캐시를 수동으로 조작할 때 사용된다.

  • 사용 예:

    import { useQueryClient } from 'react-query';
    
    const Component = () => {
      const queryClient = useQueryClient();
    
      const handleRefetch = () => {
        queryClient.invalidateQueries('todos');
      };
    
      return <button onClick={handleRefetch}>Refetch Todos</button>;
    };
  • invalidateQueries
    특정 쿼리를 무효화하여 데이터를 새로 가져올 수 있다.(코드에서 'todos'는 쿼 리 키)

    • 사용자가 데이터를 업데이트하고 해당 변경 사항을 실시간으로 반영해야 할 때.(게시판 목록에서 어떤 게시글을 작성(Post)하거나 게시글을 제거(Delete)했을 때 화면에 보여주는 게시판 목록을 실시간으로 최신화해야 할 때)
    • 데이터가 만료되었거나 잘못된 정보를 표시하는 경우 캐시를 강제로 갱신할 때.
    • 특정 동작 이후에 데이터를 다시 가져와야 할 때.
  • removeQueries
    쿼리를 캐시에서 완전히 제거(로그아웃 등 특정 데이터가 더 이상 필요하지 않을 때)

    • 예시: queryClient.removeQueries("member");

useIsFetching

  • 현재 활성화된 모든 쿼리의 페칭 상태를 확인할 수 있다.

  • 사용 예:

    import { useIsFetching } from 'react-query';
    
    const GlobalLoadingIndicator = () => {
      const isFetching = useIsFetching();
    
      return isFetching ? <div>Loading...</div> : null;
    };

리액트 쿼리는 이처럼 다양한 훅을 제공하여 데이터 페칭과 변형 작업을 효율적으로 관리할 수 있게 도와준다. 각 훅의 용도와 사용법을 잘 이해하고 적절히 활용하면, 비동기 데이터 관리가 훨씬 간편해질 것이다.

profile
기록하자

0개의 댓글

관련 채용 정보