관리 메뉴

거니의 velog

(52) 리액트 쿼리와 리코일 4 본문

SpringBoot_React 풀스택 프로젝트

(52) 리액트 쿼리와 리코일 4

Unlimited00 2024. 3. 11. 19:08

4. 상품 수정 처리

* 상품 수정은 조회와 등록 기능이 같이 존재하기 때문에 useQuery()와 useMutation()을 같이 사용해야 한다. 상품의 수정 처리는 기본적으로 다음의 흐름을 따라서 처리한다.

- useQuery()를 이용해서 상품 데이터를 가져온 후 컴포넌트의 상태 값으로 지정한다.

- <input>을 이용해서 컴포넌트의 상태로 유지되는 데이터를 수정한다.

- 수정이나 삭제를 처리한 후 화면을 이동하게 된다.

* 리액트 쿼리 3 버전까지는 useQuery() 의 파라미터로 지정되는 옵션에는 onSuccess를 지정해서 서버의 데이터를 직접 useState()가 사용하는 상태로 변경할 수 있었지만 5 버전 부터는 deprecated 되므로 다른 방법을 사용해야 한다.


(1) 조회 및 상태 처리

* components/products/ModifyComponent 에서는 우선 useQuery()를 이용해서 상품 데이터를 가져오는 코드를 작성해야 한다.

* 상품 데이터를 가져오는 코드를 추가하면 아래와 같이 된다.

import React, { useEffect, useState, useRef } from "react";
import { getOne, putOne, deleteOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import { API_SERVER_HOST } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";
import ResultModal from "../common/ResultModal";
import { useQuery } from "@tanstack/react-query";

const initState = {
  (...)
};

const host = API_SERVER_HOST;

const ModifyComponent = ({ pno }) => {
  (...)

  const query = useQuery(["products", pno], () => getOne(pno));

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      (...)
    </div>
  );
};

export default ModifyComponent;

* useQuery()가 성공했을 경우 setProduct()를 하기 위해서 과거에는 onSuccess를 옵션으로 지정할 수 있었지만, onSuccess를 지정하는 방식은 deprecated 되었으므로 상태를 이용해서 처리해야 한다.

* 때문에 useQuery() 결과를 직접 setProduct()로 지정해야 하는데 이 경우 무한히 컴포넌트의 상태가 변경되었기 때문에 무한히 반복적인 코드가 된다.

  const query = useQuery(["products", pno], () => getOne(pno));

  // 절대로 실행하면 안되는 무한 반복
  if (query.isSuccess) {
    setProduct(query.data);
  }

* 이 문제를 해결하기 위해서는 useEffect()를 이용해서 온전히 데이터가 존재하고 성공했을 경우에만 setProduct()를 호출하도록 조정한다. 또한, 조회와 달리 수정 중간에 다시 API 서버를 호출하지 않도록 staleTime을 무한(infinity)으로 설정한다.

  const query = useQuery(["products", pno], () => getOne(pno), {
    staleTime: Infinity,
  });

  useEffect(() => {
    if (query.isSuccess) {
      setProduct(query.data);
    }
  }, [pno, query.data, query.isSuccess]);

* ModifyComponent에서 모달창이나 이벤트 처리를 제외한 코드는 아래와 같다.

import React, { useEffect, useState, useRef } from "react";
import { getOne, putOne, deleteOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import { API_SERVER_HOST } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";
import { useQuery } from "@tanstack/react-query";

const initState = {
  pno: 0,
  pname: "",
  pdesc: "",
  price: 0,
  delFlag: false,
  uploadFileNames: [],
};

const host = API_SERVER_HOST;

const ModifyComponent = ({ pno }) => {
  const { moveToList, moveToRead } = useCustomMove();
  const [product, setProduct] = useState(initState);
  const uploadRef = useRef();

  const query = useQuery(["products", pno], () => getOne(pno), {
    staleTime: Infinity,
  });

  useEffect(() => {
    if (query.isSuccess) {
      setProduct(query.data);
    }
  }, [pno, query.data, query.isSuccess]);

  const handleChangeProduct = (e) => {
    product[e.target.name] = e.target.value;
    setProduct({ ...product });
  };

  const deleteOldImages = (imageName) => {
    const resultFileNames = product.uploadFileNames.filter(
      (fileName) => fileName !== imageName
    );
    product.uploadFileNames = resultFileNames;
    setProduct({ ...product });
  };

  const handleClickModify = () => {
    const files = uploadRef.current.files;
    const formData = new FormData();
    for (let i = 0; i < files.length; i++) {
      formData.append("files", files[i]);
    }

    //other data
    formData.append("pname", product.pname);
    formData.append("pdesc", product.pdesc);
    formData.append("price", product.price);
    formData.append("delFlag", product.delFlag);

    for (let i = 0; i < product.uploadFileNames.length; i++) {
      formData.append("uploadFileNames", product.uploadFileNames[i]);
    }

    // 기존 처리 관련 코드 삭제
  };

  const handleClickDelete = () => {
    // 기존 처리 코드 삭제
  };

  const closeModal = () => {
    // 기존 코드 삭제
  };

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      {/* 기존에 모달 창 부분 삭제 */}

      (...)
    </div>
  );
};

export default ModifyComponent;

* 브라우저는 ModifyComponent의 동작 과정에서 API 서버를 여러 번 호출하지 않는지 확인한다.

* 조회 과정에서 FetchingModal을 보여주는 코드를 추가한다(staleTime을 Infinity로 지정한 이유는 상품 수정 과정에서 시간이 오래 걸리는 것을 대비하기 위함이다).

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      {query.isFetching ? <FetchingModal /> : <></>}

      (...)
    </div>
  );

(2) 삭제 처리

* 삭제 처리에는 useMutation을 활용해야 한다. 또한 삭제 후 결과를 보여주는 ResultModal을 활용해야 한다.

* 코드에서는 delMutation을 선언하고 delMutation의 isSuccess를 이용해서 삭제 후에 리액트 쿼리의 데이터를 삭제하고 이동하도록 처리한다.

import React, { useEffect, useState, useRef } from "react";
import { getOne, putOne, deleteOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import { API_SERVER_HOST } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import ResultModal from "../common/ResultModal";

(...)

  const delMutation = useMutation((pno) => deleteOne(pno));
  const queryClient = useQueryClient();
  const handleClickDelete = () => {
    delMutation.mutate(pno);
  };

  const closeModal = () => {
    if (delMutation.isSuccess) {
      queryClient.invalidateQueries(["products", pno]);
      queryClient.invalidateQueries(["products/list"]);
      moveToList();
    }
  };

* 화면에서는 delMutation의 isLoading과 isSuccess를 이용해서 모달창을 처리한다(ResultModal에 대한 import 필요).

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      {query.isFetching || delMutation.isLoading ? <FetchingModal /> : <></>}
      {delMutation.isSuccess ? (
        <ResultModal
          title={"처리 결과"}
          content={"정상적으로 처리되었습니다"}
          callbackFn={closeModal}
        ></ResultModal>
      ) : (
        <></>
      )}
      
      (...)
      
    </div>
  );

* 브라우저에서는 삭제 처리 동안 FetchingModal이 보이고 삭제된 후에는 ResultModal을 이용할 수 있게 된다. ResultModal이 종료되면 다시 목록 화면으로 이동하게 된다.


(3) 수정 처리

* 수정 처리 역시 useMutation()을 이용해서 수정 처리한 후에 이동하도록 작성한다.

  const modMutation = useMutation((product) => putOne(pno, product));
  const handleClickModify = () => {
    const files = uploadRef.current.files;
    const formData = new FormData();
    for (let i = 0; i < files.length; i++) {
      formData.append("files", files[i]);
    }

    //other data
    formData.append("pname", product.pname);
    formData.append("pdesc", product.pdesc);
    formData.append("price", product.price);
    formData.append("delFlag", product.delFlag);

    for (let i = 0; i < product.uploadFileNames.length; i++) {
      formData.append("uploadFileNames", product.uploadFileNames[i]);
    }

    modMutation.mutate(formData);
  };

* 수정된 후의 모달창이 닫히면 조회 화면으로 이동하게 한다. 이때 리액트 쿼리에 보관된 데이터는 삭제하기 위해서 invalidateQueries()를 이용한다.

  const closeModal = () => {
    if (delMutation.isSuccess) {
      queryClient.invalidateQueries(["products", pno]);
      queryClient.invalidateQueries(["products/list"]);
      moveToList();
      return;
    }

    if (modMutation.isSuccess) {
      queryClient.invalidateQueries(["products", pno]);
      queryClient.invalidateQueries(["products/list"]);
      moveToRead(pno);
    }
  };

* 모달창들이 보이는 조건에 modMutation 관련 조건들을 추가한다.

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      {query.isFetching || delMutation.isLoading || modMutation.isLoading ? (
        <FetchingModal />
      ) : (
        <></>
      )}
      {delMutation.isSuccess || modMutation.isSuccess ? (
        <ResultModal
          title={"처리 결과"}
          content={"정상적으로 처리되었습니다"}
          callbackFn={closeModal}
        ></ResultModal>
      ) : (
        <></>
      )}

* 브라우저를 통해서 상품정보가 수정되면 자동으로 조회 화면으로 이동하는지 확인한다.