React Query Custom hook으로 사용하기

이수빈·2023년 2월 17일
10
post-thumbnail

useQuery, useMutation을 Custom hook으로 사용하기


Custom hook이란?

  • Custom hook은 컴포넌트 로직을 함수로 뽑아내어 재사용하기 위해 사용한다.

  • hook은 항상 컴포넌트의 최상위 코드에서만 호출되어야 한다. 이 규칙을 따라야 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것을 보장 할 수 있다.

  • Custom hook을 만들때 이름은 use로 시작해야한다. 이를 따르지 않으면 특정한 함수가 그 안에서 Hook을 호출하는지를 알 수 없기 때문에 Hook 규칙의 위반 여부를 자동으로 체크할 수 없다.


useQuery Custom hook으로 사용하기

  • useQuery는 서버에서 데이터를 가져오는데 사용하는 함수이다. queryFn과 queryKey라는 두가지 파라미터가 꼭 필요하다.

  • queryFn을 통해 서버에서 데이터를 가져오고, 이를 query Key값으로 저장하여 애플리케이션 내부에서 다시 가져오고, 캐싱하고 공유 할 수 있다.

  • query Key는 배열이여야 한다. string값이 query Key로 주어져도 내부적으로 길이가 1인 배열로 변환된다.

//useCommentQuery.ts

import * as API from '../../api/API';
import { useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { Tcomment } from '../../type/commentType';

const fetchComments = async (shopId: number) => {
  return await API.get(`/api/comments?shopId=${shopId}`);
};

export const useCommentQuery = (shopId: number) => {
  const {
    isLoading: isCommentLoading,
    isError: isCommentError,
    data: commentState,
    isSuccess,
  } = useQuery<Tcomment[], AxiosError>(['comment', shopId], () => fetchComments(shopId));

  return { isCommentLoading, isCommentError, commentState, isSuccess };
};
  • Custom hook으로 만들때는 먼저 쿼리별로 재사용을 하도록 파라미터와 에러타입 명시와 쿼리별로 hook을 작성하였고

  • 반환값 네이밍을 쿼리별로 다르게 해 사용하는 컴포넌트에서의 코드를 명확하게 했다.

// foodDetail.tsx

const FoodDetail = () => {
  const scrollRef = useRef<HTMLElement>(null);
  const shopId = Number(useParams().id);

  const { isCommentLoading, isCommentError, commentState } = useCommentQuery(shopId);
  const { isMenuLoading, isMenuError, menuState } = useMenuQuery(shopId);
  const { isShopLoading, isShopError, shopState } = useShopQuery(shopId);

  if (isCommentLoading || isMenuLoading || isShopLoading) {
    return <S.CommentContainer>로딩중</S.CommentContainer>;
  }

  if (isCommentError || isMenuError || isShopError) {
    return <S.CommentContainer>Error 발생</S.CommentContainer>;
  }

  return (
    <S.Pagecontainer ref={scrollRef}>
      <NavBar />
      <DetailSlider imageArr={makeImgArr(shopState, menuState)} />
      <Content shop={shopState} />
      <Comment shopId={shopState?.shopId} scrollRef={scrollRef} />
      <S.CommentContainer>
        {commentState?.map((comment) => (
          <CommentList key={comment.commentId} commentProp={comment} shopId={shopId} />
        ))}
      </S.CommentContainer>
      <Footer />
    </S.Pagecontainer>
  );
};

export default FoodDetail;
  • 쿼리의 상태가 로딩중이거나 에러가 존재할 수 있으므로, 이를 타입가드를 해주어야 데이터가 undefined 되지 않는다.

  • hook에서 각 쿼리별 상태를 받은 후 상태에 따라 타입가드를 진행했다.

const isLoading = commentLoading || menuLoading || shopLoading 
// isLoading을 사용하면 타입가드가 제대로 이루어지지 않음. 
  • 쿼리의 상태를 따로 변수로 만들면 타입스크립트의 컴파일러는 이를 타입가드 되었다고 인식하지 못하는 문제도 있었다.

useMutation Custom Hook으로 사용하기

  • 공식문서에서는 서버 데이터를 직접 변경할 경우 useQuery 대신 useMutation을 사용하는 것을 권장한다.

  • mutation에서는 api호출이 성공했을때 onSuccess라는 콜백함수를 호출하고, 에러가 발생했을때는 onError라는 콜백함수를 호출한다.

  • mutation Fn은 오직 하나의 오직 하나의 변수만 파라미터로 받을 수 있다. 여러개의 파라미터를 넘겨줄때는 객체형태로 넘겨줘야 한다.

  • 오직 하나의 파라미터만 받을 수 있는 이유는 Mutation Function은 Type으로 TVariable이라는 하나의 파라미터만 받을 수 있도록 정의되어 있다.

export type UseMutateFunction<
  TData = unknown,
  TError = unknown,
  TVariables = void,
  TContext = unknown,
> = (
  ...args: Parameters<MutateFunction<TData, TError, TVariables, TContext>>
) => void
  • useMutation은 mutation Fn을 받아서 mutation 객체를 반환한다. 이 객체의 mutate 메소드를 통해 이벤트 핸들러에서 서버데이터를 변경 할 수 있다.
//usePatchComment.ts

import { useMutation, useQueryClient } from '@tanstack/react-query';
import * as API from '../../api/API';
import { PostComment } from '../../type/commentType';

type PatchComment = {
  comment: PostComment;
  commentId: number;
};

const patchComment = async ({ comment, commentId }: PatchComment) => {
  return await API.patch(`/api/comments/${commentId}`, comment);
}; // mutation function은 오직 하나의 variable or object만 파라미터로 받을 수 있다. 

type UsePatchComment = {
  onSuccessCb: () => void;
  onErrorCb: () => void;
  shopId: number | undefined;
};

export const usePatchComment = ({ onSuccessCb, onErrorCb, shopId }: UsePatchComment) => {
  const queryClient = useQueryClient();
  const mutation = useMutation(patchComment, {
    onSuccess: () => {
      onSuccessCb();
      queryClient.invalidateQueries(['comment', shopId], { exact: true });
    },
    onError: () => {
      onErrorCb();
    },
  });

  return { mutation }; // mutation객체 반환
};
  • Custom hook 을 만들때는 mutation을 재사용하기 위해 컴포넌트에서 작성하는 로직을 최소화 하도록 분리하였다.
  • mutation 객체에서 Success와 Error 상태를 처리하기위해 onSuccessCb, onErrorCb라는 콜백 Fn을 컴포넌트에서 파라미터로 넘겨받았고, 쿼리 무효화(invalidateQueries)를 통해 데이터 변경 후 새로운 데이터를 다시 refetching 하도록 hook을 작성하였다.
//TextArea.tsx

interface TextAreaProps {
  commentId: number;
  shopId: number;
  commentStar: NullableNum;
  content: string;
  canRevise: boolean;
  updateRevise: (x: boolean) => void;
  updateReadOnly: (x: boolean) => void;
}

const TextArea = ({
  commentId,
  shopId,
  commentStar,
  content,
  canRevise,
  updateRevise,
  updateReadOnly,
}: TextAreaProps) => {
  const [textValue, setTextValue] = useState<string>(content);

  const onSuccessCb = () => {
    updateRevise(false);
    updateReadOnly(true);
  }; //요청이 성공시 호출됨

  const onErrorCb = () => {
    alert('요청에 실패하였습니다.');
  }; // 요청 실패시 호출됨
  const { mutation: patchComment } = usePatchComment({ onSuccessCb, onErrorCb, shopId });

  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setTextValue(e.target.value);
  };

  const reviseEnd = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (inValidateText(textValue, commentStar)) return;
    const revisedComment = {
      star: commentStar,
      content: textValue,
    };
    patchComment.mutate({ comment: reviseComment, commentId });
  }; // 이벤트핸들러에서 유효한 데이터라면 hook 실행됨.

  return (
    <S.TextContainer>
      <S.CommentArea
        value={textValue}
        onChange={handleChange}
        disabled={!canRevise}
        maxLength={100}
      />
      {canRevise && <S.Button onClick={reviseEnd}>수정완료</S.Button>}
    </S.TextContainer>
  );
};

ref)

profile
응애 나 애기 개발자

0개의 댓글