[React-Query] 리액트 쿼리로 댓글 등록 구현하기 - useMutation & invalidateQueries

Woonil·2024년 1월 4일
1

리액트 쿼리

목록 보기
2/2
post-thumbnail
post-custom-banner

리액트 쿼리의 invalidateQueries 를 사용하면 캐싱된 데이터의 재조회를 통해 클라이언트 측 상태를 업데이트 할 수 있다. 프로젝트 내 리뷰 등록의 예시를 들어 설명한다. api 요청은 post로 진행한다.

Before

기존에는 댓글목록 정보를 상위 컴포넌트로부터 prop으로 받아와 업데이트 해준다. api 요청을 보내는 순간 isLoading 상태가 되어 로딩스피너를 띄운다. 정상적인 응답이 오면 댓글목록을 업데이트하여 보여준다.

import { useState, useRef } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";
import CircularProgress from "@mui/material/CircularProgress";
import { useSelector } from "react-redux";

const ReviewRegistration = ({ reviews, setReviews }) => {
  const { postId } = useParams();

  const user = useSelector((state) => state.user);

  const [isLoading, setIsLoading] = useState(false);
  const [newReview, setNewReview] = useState("");
  const [newReviewData, setNewReviewData] = useState({
    content: newReview,
    user_id: user.id,
    recipe_id: postId,
    last_name: user.last_name,
  });

  const reviewInputRef = useRef(null);

  const handleReviewInputChange = () => {
    setNewReview(reviewInputRef.current.value);
    setNewReviewData({
      content: reviewInputRef.current.value,
      user_id: user.id,
      recipe_id: postId,
      last_name: user.last_name,
    });
  };

  const handleReviewSubmit = (event) => {
    event.preventDefault();
    if (!user.id) {
      alert("로그인 후 리뷰를 작성해주세요.");
      
      return;
    }
    setIsLoading(true);

    // REST: 리뷰 등록
    const reviewPostURL = `${process.env.REACT_APP_SERVER}/api/review/`;
    axios
      ?.post(reviewPostURL, newReviewData, {
        headers: {
          "Content-Type": "application/json",
        },
      })
      ?.then((res) => {
        if (res.status === 200) {
          setIsLoading(false);
          setNewReview("");
          setReviews([...reviews, newReviewData]);
        }
      });
  };

  return (
    <>
      <form className="flex-auto mr-2" onSubmit={handleReviewSubmit}>
        {isLoading ? (
          <div className="w-full h-full flex justify-center items-center">
            <CircularProgress color="success" />
          </div>
        ) : (
          <input
            onChange={handleReviewInputChange}
            value={newReview}
            className="w-full h-full py-0 pl-10 pr-24 bg-[#efefef] border-none rounded-3xl outline-2 outline-primary text-xl text-black/90"
            type="text"
            placeholder="리뷰 추가"
            ref={reviewInputRef}
          />
        )}
      </form>
    </>
  );
};

export default ReviewRegistration;

After

우선 상위 컴포넌트에서 api 요청을 통해 리뷰 목록을 불러올 때, query key를 "recipe-reviews"로 지정하여 캐싱한다. 아래 코드를 보기 전 리액트 쿼리 훅 중 useMutationinvalidateQueries에 대해 이해한다.
React-Query Document > useMutation

  • useMutation
    • mutationFn
      api 요청을 통해 새로운 댓글을 본문에 실어보낸다. axios는 요청에 대한 응답을 프로미스로 반환한다. 이 때, 인자값으로 variables를 받을 수 있는데 이는 useMutation이 제공하는 함수인 mutate가 이 함수에 전달하는 값이다. 폼 제출시 실행하는 핸들러인 handleReviewSubmitcreateReview()(mutate) 에 전달하는 값이 바로 그것이다.
    • [onSuccess]
      'mutation'이 성공적으로 수행되는 경우 실행되며, 'mutation'의 결과 (mutationFn의 결과)를 인자로 받는다. (이 코드에서는 인자는 사용하지 않았다.)
  • queryClient.invalidateQueries
    해당하는 query key로 캐시된 하나 또는 그 이상의 쿼리를 무효화하고 다시 가져온다. 여기서는 캐싱된 댓글 목록에 대한 데이터를 다시 불러오는데 사용된다. onSuccess와 결합하여 'mutation'이 성공적으로 일어난 경우에 수행한다.
... 위와 동일한 import문 생략
import { useQueryClient, useMutation } from "@tanstack/react-query";

const ReviewRegistration = () => {
  const { postId } = useParams();

  const user = useSelector((state) => state.user);

  const [newReview, setNewReview] = useState("");
  const [newReviewData, setNewReviewData] = useState({
    content: newReview,
    user_id: user.id,
    recipe_id: postId,
    last_name: user.last_name,
  });

  const reviewInputRef = useRef(null);
  const handleReviewInputChange = () => {
    setNewReview(reviewInputRef.current.value);
    setNewReviewData({
      content: reviewInputRef.current.value,
      user_id: user.id,
      recipe_id: postId,
      last_name: user.last_name,
    });
  };

  const { mutate: createReview, isPending } = usePostReview();

  const handleReviewSubmit = (event) => {
    event.preventDefault();
    if (!user.id) {
      alert("로그인 후 리뷰를 작성해주세요.");
      event.preventDefault();

      return;
    }
    createReview(newReviewData);
    setNewReview("");
  };

  return (
    <>
      <form className="flex-auto mr-2" onSubmit={handleReviewSubmit}>
        {isPending ? (
          <div className="w-full h-full flex justify-center items-center">
            <CircularProgress color="success" />
          </div>
        ) : (
          <input
            onChange={handleReviewInputChange}
            value={newReview}
            className="w-full h-full py-0 pl-10 pr-24 bg-[#efefef] border-none rounded-3xl outline-2 outline-primary text-xl text-black/90"
            type="text"
            placeholder="리뷰 추가"
            ref={reviewInputRef}
          />
        )}
      </form>
    </>
  );
};

// REST: 리뷰 등록
const usePostReview = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (newReviewData) => {
      const reviewPostURL = `${process.env.REACT_APP_SERVER}/api/review/`;
      return await axios.post(reviewPostURL, newReviewData);
    },
    onSuccess: () => {
      queryClient.invalidateQueries(["recipe-reviews"]);
    },
  });
};

export default ReviewRegistration;
profile
우니리개발일지
post-custom-banner

0개의 댓글