24/1/24

Laejun Kim·2024년 1월 24일
1

TIL

목록 보기
81/89

팀 프로젝트

refactoring

커스텀 훅

const reviewDelteMutate = useMutation({
    mutationFn: deleteReview,
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['reviews', review.place_id],
      });
      queryClient.invalidateQueries({
        queryKey: ['reviews', currentUserId],
      });
    },
    onError: () => {
      toastError('문제가 발생하여 삭제하지 못했습니다');
      return;
    },
  });
  const reviewDelete = () => {
    Swal.fire({
      icon: 'warning',

@@ -51,7 +40,7 @@ const ReviewUpperSection = ({
      confirmButtonColor: '#FFD029',
    }).then((result) => {
      if (result.isConfirmed) {
        reviewDelteMutate.mutate(review.id);
        router.back();
        toastSuccess('삭제 완료');
      }

다음과 같은 useMutation 코드가 어떤 컴포넌트 내부에 있는 상황을 생각해보자. 심지어 이게 하나만 있는 것도 아니고 CUD에 대하여 각각 하나씩 존재한다. 필연적으로 코드의 길이가 길어지고 가독성이 안좋아진다는 문제가 발생할 것이다.

이 문제를 해결하기 위해 커스텀 훅을 만들어서 mutation을 간결하게 사용해보도록 한다.

먼저 hooks 폴더 내에 어떤 항목과 관련된 mutation들을 모아둘 파일을 만든다. 지금은 Review 항목과 관련된 mutation들을 모아둘 생각이기 때문에 파일의 이름은 useReviews 가 적당할 것이다.

useReviews 의 모습은 아래와 같다

//src>hooks>useReviews.ts
import {
  deleteReview,
  insertNewReview,
  updateReviewContent,
} from '@/apis/reviews';
import { toastError } from '@/libs/toastifyAlert';
import {
  useMutation,
  useQueryClient,
  InvalidateQueryFilters,
} from '@tanstack/react-query';

export const useReviews = (
  setIsEditing?: React.Dispatch<React.SetStateAction<boolean>>,
  placeId?: string,
  currentUserId?: string,
  reviewId?: string,
) => {
  const queryClient = useQueryClient();

  const reviewDelteMutate = useMutation({
    mutationFn: deleteReview,
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['reviews', placeId],
      });
      queryClient.invalidateQueries({
        queryKey: ['reviews', currentUserId],
      });
    },
    onError: () => {
      toastError('문제가 발생하여 삭제하지 못했습니다');
      return;
    },
  });

  const updateReviewMutate = useMutation({
    mutationFn: updateReviewContent,
    onMutate: async (updateReviewParams) => {
      await queryClient.cancelQueries({ queryKey: ['review', reviewId] });
      const prevReview: object | undefined = queryClient.getQueryData([
        'review',
        reviewId,
      ]);
      const updatedReview = {
        ...prevReview,
        content: updateReviewParams.editValue,
      };
      queryClient.setQueryData(['review', reviewId], updatedReview);

      return { prevReview };
    },
    onError: (error, updateReviewParams, context) => {
      // Rollback to the previous review data in case of an error
      if (context?.prevReview) {
        queryClient.setQueryData(['review', reviewId], context.prevReview);
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['review', reviewId] });
      queryClient.invalidateQueries({ queryKey: ['reviews', placeId] });
      if (setIsEditing) {
        setIsEditing(false);
      }
    },
  });

  const insertReviewMutate = useMutation({
    mutationFn: insertNewReview,
    onSuccess: async () => {
      await queryClient.invalidateQueries([
        'reviews',
        placeId,
      ] as InvalidateQueryFilters);
    },
  });

  return {
    deleteReview: reviewDelteMutate.mutate,
    updateReview: updateReviewMutate.mutate,
    insertReview: insertReviewMutate.mutate,
  };
};

다소 길지만 전체적인 모습을 담기 위해 파일 전체를 가져왔다.
먼저 useReviews 라는 함수가 있고 이 함수가 리턴하는 것이 각 뮤테이션의 mutation이 들어있는 객체라는 것에 주목하자.

기존에 컴포넌트 내에 그대로 적혀있었던 mutation 코드들을 useReviews 함수 내에 옮겨서 적어주고 해당 상수에서 mutation 만 빼내서 리턴한다. 이때 이름을 더 간단하게 붙여주는 것도 유효하다.

각 뮤테이션에서 reviewId 나 placeId 같은 추가적인 정보나 mutation 내에서 실행할 별도의 함수를 필요로 하는 경우도 있는데 이런 것들은 useReviews에서 optional 인자로 받아 주어야 한다.

이렇게 만든 커스텀 훅을 컴포넌트 내에서 사용하는 모습은 아래와 같다

  const { deleteReview } = useReviews(
    undefined,
    review.place_id,
    currentUserId as string,
  );
(중략)
 deleteReview(review.id);

이전에 비해 훨씬 간결해 진 점이 눈에 띈다. 또한 delete mutation 을 하는데 필요한 placeId와 userId 도 함께 전달하고 있는 것도 주목할만 하다. useReviews의 첫번째 인자인 setIsEditing은 수정 mutation 에서 사용하는 것이고 delete mutation에서는 사용하지 않으므로 undefined 로 적어준다.

rpc 활용

이전에 rpc 를 활용한 부분은 reviewCard 컴포넌트였는데 이번에 placeCard 컴포넌트에도 적용하기로 결정, 리팩토링을 진행하였다.

   let query = supabase
        .from('places')
        .select('*')
        .ilike('place_name', `%${searchValue}%`);

로 되어 있던 코드를

   let query = supabase.rpc('search_places', {
        p_search_value: searchValue,
      });

위와같이 변경, rpc 로 호출할 함수는 미리 supabase 에 만들어 두었다.

원래는 rpc 호출 이후에 다른 supabase 메서드들을 사용하는 것이 불가능하다고 생각했었으나 .in() 이나 .range()같은 메서드를 활용하는것이 가능하다는 것으로 확인되어 기존에 존재하던 검색 로직에 끼워넣는 것이 가능했다.

그 결과는 아래와 같다

Before

After

무수히 많이 생성되던 다 관리하기도 어려운 쿼리키들이 전부 사라지는 멋진 결과를 얻었다!

0개의 댓글