관리 메뉴

거니의 velog

(50) 리액트 쿼리와 리코일 2 본문

SpringBoot_React 풀스택 프로젝트

(50) 리액트 쿼리와 리코일 2

Unlimited00 2024. 3. 11. 17:51

2. 상품목록 페이지

* 상품목록에서 데이터를 조회하는 기능은 동일하지만 페이지의 번호나 검색 조건 등이 변경될 수 있으므로 useQuery를 이용할 때 주의할 점이 생긴다.


(1) 중복적인 쿼리 키(key)

* 기존 components/products/ListComponent.js 에 useQuery를 이용해서 page와 size까지 쿼리 키(key)가 되도록 수정한다.

import React from "react";
import { getList } from "../../api/productsApi";
import useCustomMove from "../../hooks/useCustomMove";
import FetchingModal from "../common/FetchingModal";
import PageComponent from "../common/PageComponent";
import { API_SERVER_HOST } from "../../api/todoApi";
import useCustomLogin from "../../hooks/useCustomLogin";
import { useQuery } from "@tanstack/react-query";

const host = API_SERVER_HOST;

const initState = {
  (...)
};

const ListComponent = () => {
  const { page, size, moveToList, moveToRead } = useCustomMove();
  const { moveToLoginReturn } = useCustomLogin();
  const { isFetching, data, error, isError } = useQuery(
    ["products/list", { page, size }],
    () => getList({ page, size })
  );

  if (isError) {
    console.log(error);
    return moveToLoginReturn();
  }

  const serverData = data || initState;

  return (
    <div className="border-2 border-blue-100 mt-10 mr-2 ml-2">
      {isFetching ? <FetchingModal /> : <></>}

      (...)

      <PageComponent
        serverData={serverData}
        movePage={moveToList}
      ></PageComponent>
    </div>
  );
};

export default ListComponent;

* 코드에서는 page와 size가 쿼리 키(key)로 지정되었다. 만일 2페이지를 호출한다면 아래와 같이 쿼리 키(key)가 보관되는 것을 볼 수 있다.


[동일 페이지 갱신 문제]

* 현재 코드에서는 staleTime을 지정하지 않았기 때문에 서버에서 가져온 데이터를 사용한 후에 바로 stale한 상태가 된다. 따라서 잠시 브라우저를 벗어나 다른 프로그램을 클릭하거나 이용한 후에 다시 브라우저를 활성화하면 서버를 재호출하는 것을 확인할 수 있다.

* 조회 페이지와 달리 상품 목록 페이지는 아래 쪽에 페이지 번호를 클릭할 수 있다. 이 때 페이지 번호가 다르다면 useQuery()가 이용하는 쿼리 키(key) 값이 달라지므로 문제가 없겠지만, 동일한 페이지를 클릭하는 경우에 문제가 된다. 아래의 결과는 2페이지를 조회한 상태에서 지속적으로 2페이지를 클릭했지만, API 서버에 호출이 한 번벆에 일어나지 않은 결과이다.


(2) invalidateQueries()

* 동일한 쿼리 키(key)가 반복적으로 호출할 때 문제를 해결하는 가장 간단한 방법은 리액트 쿼리가 보관하는 데이터를 무효화(invalidate) 시키는 방법이다. 리액트 쿼리는 해당 키(key) 값의 데이터가 무효화되면 다시 서버를 호출해서 데이터를 조회하게 된다.

* ListComponent에는 현재 리액트 쿼리의 QueryClient를 가져오는 useQueryClient()를 선언하고 페이지 번호를 클릭했을 때 동작하는 handleClickPage()를 추가한다.

import React from "react";
import { getList } from "../../api/productsApi";
import useCustomMove from "../../hooks/useCustomMove";
import FetchingModal from "../common/FetchingModal";
import PageComponent from "../common/PageComponent";
import { API_SERVER_HOST } from "../../api/todoApi";
import useCustomLogin from "../../hooks/useCustomLogin";
import { useQuery, useQueryClient } from "@tanstack/react-query";

const host = API_SERVER_HOST;

const initState = {
  (...)
};

const ListComponent = () => {
  const { page, size, moveToList, moveToRead } = useCustomMove();
  const { moveToLoginReturn } = useCustomLogin();
  const { isFetching, data, error, isError } = useQuery(
    ["products/list", { page, size }],
    () => getList({ page, size })
  );

  const queryClient = useQueryClient(); // 리액트 쿼리 초기화를 위한 현재 객체

  const handleClickPage = (pageParam) => {
    if (pageParam.page === parseInt(page)) {
      queryClient.invalidateQueries("products/list");
    }

    moveToList(pageParam);
  };

  if (isError) {
    console.log(error);
    return moveToLoginReturn();
  }

  const serverData = data || initState;

  return (
    <div className="border-2 border-blue-100 mt-10 mr-2 ml-2">
      {isFetching ? <FetchingModal /> : <></>}

      (...)

      <PageComponent
        serverData={serverData}
        movePage={handleClickPage}
      ></PageComponent>
    </div>
  );
};

export default ListComponent;

* handleClickPage()는 동일한 페이지를 클릭한 경우에만 invalidateQueries()를 실행한다. 리액트 쿼리에서 invalidateQueries()는 해당 키로 시작하는 결과를 모두 무효화시킨다.

* 예를 들어 1페이지와 2페이지를 조회한다면 쿼리 키(key) 값들은 아래와 같이 보관된다.

* 동일한 페이지를 다시 클릭한다면 products/list 키로 시작하는 데이터들은 무효화되기 때문에 다시 API 서버를 호출하게 된다(임시로 API 서버가 몇 초 이후에 응답을 보내도록 조정하고 현재 페이지를 다시 클릭해 보자). 화면을 보면 freshing 처리가 되면서 매번 서버를 호출하는 것을 확인할 수 있다.


(3) refresh 활용

* invalidateQueries()를 이용하는 방식의 단점은 사용자가 짧은 시간 동안 동일한 페이지를 클릭하는 경우 매번 서버의 호출이 너무 많아진다는 점이다.

* 이런 상황을 피하고 싶다면 매번 변경되는 refresh 값과 staleTime을 같이 이용하면 일정 시간 동안은 서버를 반복적으로 호출하는 문제를 해결할 수 있다. staleTime을 이용해서 약간의 시간 동안 반복적으로 서버를 호출하는 것을 막고 refresh를 이용해서 동일한 페이지에 대한 쿼리 키(key) 값을 변경하는 방식이다.

const ListComponent = () => {
  const { page, size, moveToList, moveToRead, refresh } = useCustomMove();
  const { moveToLoginReturn } = useCustomLogin();
  const { isFetching, data, error, isError } = useQuery(
    ["products/list", { page, size, refresh }], // refresh 추가
    () => getList({ page, size }),
    { staleTime: 1000 * 5 } // staleTime 추가
  );

  // const queryClient = useQueryClient(); // 필요하지 않음

  const handleClickPage = (pageParam) => {
    // if (pageParam.page === parseInt(page)) {
    //   queryClient.invalidateQueries("products/list");
    // }

    moveToList(pageParam);
  };

* 아래 화면은 짧은 시간 동안 동일한 페이지를 여러 번 클릭했을 경우의 결과로 5초가 지난 후에만 한 번 호출이 되는 상황을 캡쳐한 것이다.

* 동일한 페이지의 호출은 useQuery()의 결과 중 refetch를 이용할 수도 있긴 하지만, 이 경우 다른 쿼리 스트링을
  변경하는 등의 처리는 어렵기 때문에 컴포넌트의 상태(state)를 이용하는 것이 좋다.