본문 바로가기
코딩 정보/React

[React][Tanstack Query] 개념과 활용 방법에 대해 알아보자

by 꽁이꽁설꽁돌 2024. 8. 26.
728x90
반응형

목차

     

    간단한 설명은 아래 페이지에 있다.

    참고

    https://be-senior-developer.tistory.com/94

     

    [React] React-Query 라이브러리가 무엇인지 알아보자

    목차 오늘은 프로젝트를 하면서 리액트 쿼리에 대해 접하게 되었다. 말하는 감자인 나는 당연히 모르기 때문에 이번 기회에정리해보고자 한다.. 리액트 쿼리의 필요성 캐시동일한 데이터에

    be-senior-developer.tistory.com

     

    다음과 같이 QueryClientProvider를 사용해 주자

    useQuery로 데이터 불러오기

     

    main.jsx

    import {
      Navigate,
      RouterProvider,
      createBrowserRouter,
    } from "react-router-dom";
    ...
    import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
    const router = createBrowserRouter([
      {
        path: "/",
        element: <Navigate to="/events" />,
      },
     ...
    ]);
    
    const queryClient = new QueryClient();
    
    function Query() {
      return (
        <QueryClientProvider client={queryClient}>
          <RouterProvider router={router} />
        </QueryClientProvider>
      );
    }
    
    export default Query;

     

    여기서  queryFn에 들어가는 함수는 비동기 함수여야 하며,

    따라서 async 키워드를 사용하거나 프로미스를 반환해야 한다.

     

    NewEventsSection.jsx

    import LoadingIndicator from "../UI/LoadingIndicator.jsx";
    import ErrorBlock from "../UI/ErrorBlock.jsx";
    import EventItem from "./EventItem.jsx";
    //자체적으로 작동해서 필요한 데이터를 가져오고 로딩 상태에 대한 정보를 제공한다.
    import { useQuery } from "@tanstack/react-query";
    import { fetchEvents } from "../../util/http.js";
    
    export default function NewEventsSection() {
      //쿼리키를 통해 요청으로 생성된 데이터를 캐시 처리한다.
      const { data, isPending, isError, error } = useQuery({
        queryKey: ["events"],
      //그 전에 캐시 처리한 데이터를 다시 사용하고 업데이트된 요청 확인
        queryFn: fetchEvents,
      });
    
      let content;
    
      if (isPending) {
        content = <LoadingIndicator />;
      }
    
      if (error) {
        content = (
          <ErrorBlock
            title="An error occurred"
            message={error.info?.message || "Failed to fetch events."}
          />
        );
      }
    
      if (data) {
        content = (
          <ul className="events-list">
            {data.map((event) => (
              <li key={event.id}>
                <EventItem event={event} />
              </li>
            ))}
          </ul>
        );
      }
    
      return (
        <section className="content-section" id="new-events-section">
          <header>
            <h2>Recently added events</h2>
          </header>
          {content}
        </section>
      );
    }

     

    아래 보이는 것과 같이 데이터 요청 비동기 함수이다.

     

    fetchEvents.js 

    export async function fetchEvents() {
      const response = await fetch("http://localhost:8080/events");
    
      if (!response.ok) {
        const error = new Error("An error occurred while fetching the events");
        error.code = response.status;
        error.info = await response.json();
        throw error;
      }
    
      const { events } = await response.json();
    
      return events;
    }

     

    이런식으로 캐시 사을 중지 하고 다시 페이지로 오면 모든 리소스를 다시 불러오는 것을 볼 수 있다.

     

    쿼리를 이용한 캐시 사용을 허용했을때도 똑같이 요청이 보내지지만 이 요청은 다르다.

    내부적으로 요청을 전송해서 업데이트 된 요소가 있는지 확인을 위함이다.

     

    그렇다면 이 요청을 제어할려면 어떻게 하면 좋을까?

     

    staleTime

    바로 staleTime을 이용하면 된다. 쉽게 말하면 전송 요청 지연 시간으로 설정 시간 이내로 컴포넌트가 랜더링 될 경우 

    요청을 보내지 않는다.

     

    수정한 코드

      //쿼리키를 통해 요청으로 생성된 데이터를 캐시 처리한다.
      const { data, isPending, isError, error } = useQuery({
        queryKey: ["events"],
        //그 전에 캐시 처리한 데이터를 다시 사용하고 업데이트된 요청 확인
        queryFn: fetchEvents,
        //캐시에 데이터가 있을 때 업데이트 된 데이터를 가져오기 위한 전송요청 지연시간
        staleTime: 10000,
        //캐싱 데이터 유지 시간
        gcTime: 30000
      });

     

     

    우리는 동적으로 값을 얻고 싶을 때가 있다. 이럴때 유동적으로queryKey를 넣어주면 된다.

    import { useRef, useState } from "react";
    import { useQuery } from "@tanstack/react-query";
    import { fetchEvents } from "../../util/http";
    import LoadingIndicator from "../UI/LoadingIndicator";
    import ErrorBlock from "../UI/ErrorBlock";
    import EventItem from "./EventItem";
    
    export default function FindEventSection() {
    	//useRef를 통해 하면 바로 컴포넌트에 반영되지 않기 때문에 useState를 써주어야함
      const searchElement = useRef();
      const [searchTerm, setSearchTerm] = useState("");
      
      //함수 인자를 전달해야 하기 떄문에 화살표함수로 바꿈
      const { data, isPending, isError, error } = useQuery({
        queryKey: ["events", { search: searchTerm }],
        queryFn: ({ signal }) => fetchEvents({ signal, searchTerm }),
      });
      function handleSubmit(event) {
        event.preventDefault();
        setSearchTerm(searchElement.current.value);
      }
    
      let content = <p>Please enter a search term and to find events.</p>;
    
      if (isPending) {
        content = <LoadingIndicator />;
      }
      if (isError) {
        content = (
          <ErrorBlock
            title={"An error occured"}
            message={error.info?.message || "Failed to fetch events"}
          />
        );
      }
      if (data) {
        content = (
          <ul className="events-list">
            {data.map((event) => (
              <li key={event.id}>
                <EventItem event={event}></EventItem>
              </li>
            ))}
          </ul>
        );
      }
      return (
        <section className="content-section" id="all-events-section">
          <header>
            <h2>Find your next event!</h2>
            <form onSubmit={handleSubmit} id="search-form">
              <input
                type="search"
                placeholder="Search events"
                ref={searchElement}
              />
              <button>Search</button>
            </form>
          </header>
          {content}
        </section>
      );
    }

     

    위의 코드를 보면 signal를 함수 인자로 주었는데 요청이 완료되기 전에

    사용자가 페이지에 나갈 경우 요청을 취소할 수 있도록 주는 신호이다.

     

    따라서 요청을 주는 함수도 다음과 같이 수정할 수 있다.

    fetchEvents.jsx

    export async function fetchEvents({ signal, searchTerm }) {
      let url = "http://localhost:8080/events";
      if (searchTerm) {
        url += "?search=" + searchTerm;
      }
      console.log(signal);
      const response = await fetch(url, { signal: signal });
    
      if (!response.ok) {
        const error = new Error("An error occurred while fetching the events");
        error.code = response.status;
        error.info = await response.json();
        throw error;
      }
    
      const { events } = await response.json();
    
      return events;
    }

     

    isLoading vs isPending

    • isLoading은 쿼리가 비활성화 되었다고 해서 true가 반환되지 않는다.
    • isPending은 쿼리가 비활성화 되었을 경우 true를 반환한다.

     

    그렇다면 다음 코드는 검색을 위한 코드인데

    우리가 원하는 동작은 다음과 같다.

    초기에는 검색을 하지 않았으니 아무것도 뜨지 않는다.
    검색을 한다.
    그 후 아무것도 넣지 않고 검색하면 모든 요소가 검색되게 한다.

     

    따라서 enabled를 통해 다음과 같이 쿼리를 조작해 주면 된다.

     

    1. 초기에는 state에 아무값도 넣지 않아 udefined를 만들어 준다.

     

    2. 그 후 값에 빈 문자열을 넣게 되면 undefined가 아니므로 작동한다.

     

    3. isPending 대신 isLoading을 넣어 쿼리 비활성화시에 로딩창이 뜨지 않도록 한다.

    import { useRef, useState } from "react";
    import { useQuery } from "@tanstack/react-query";
    import { fetchEvents } from "../../util/http";
    import LoadingIndicator from "../UI/LoadingIndicator";
    import ErrorBlock from "../UI/ErrorBlock";
    import EventItem from "./EventItem";
    export default function FindEventSection() {
      const searchElement = useRef();
      //초기값은 undefined
      const [searchTerm, setSearchTerm] = useState();
    
      const { data, isLoading, isError, error } = useQuery({
        queryKey: ["events", { search: searchTerm }],
        queryFn: ({ signal }) => fetchEvents({ signal, searchTerm }),
        //쿼리 활성화를 위한 코드
        enabled: searchTerm !== undefined,
      });
      function handleSubmit(event) {
        event.preventDefault();
        setSearchTerm(searchElement.current.value);
      }
    
      let content = <p>Please enter a search term and to find events.</p>;
    
      if (isLoading) {
        content = <LoadingIndicator />;
      }
      if (isError) {
        content = (
          <ErrorBlock
            title={"An error occured"}
            message={error.info?.message || "Failed to fetch events"}
          />
        );
      }
      if (data) {
        content = (
          <ul className="events-list">
            {data.map((event) => (
              <li key={event.id}>
                <EventItem event={event}></EventItem>
              </li>
            ))}
          </ul>
        );
      }
      return (
        <section className="content-section" id="all-events-section">
          <header>
            <h2>Find your next event!</h2>
            <form onSubmit={handleSubmit} id="search-form">
              <input
                type="search"
                placeholder="Search events"
                ref={searchElement}
              />
              <button>Search</button>
            </form>
          </header>
          {content}
        </section>
      );
    }

     

    useMutation을 통해 데이터 전송하기

    import { Link, useNavigate } from "react-router-dom";
    import { createNewEvent } from "../../util/http.js";
    import Modal from "../UI/Modal.jsx";
    import EventForm from "./EventForm.jsx";
    import { useMutation } from "@tanstack/react-query";
    import ErrorBlock from "../UI/ErrorBlock.jsx";
    import { queryClient } from "../../util/http.js";
    export default function NewEvent() {
      const navigate = useNavigate();
    
      //변형은 응답 데이터를 캐시 처리하지 않기 때문에 꼭 키가 필요한 것은 아님
      //mutate를 통해 실행 시점을 결정해 주어야 함
      const { isError, isPending, error, mutate } = useMutation({
        mutationFn: createNewEvent,
        onSuccess: () => {
          //데이터를 만료시켜서 새로운 데이터 트리거를 유도함
          //events를 포함하는 모든 데이터를 만료 -> 특정 데이터만 하고 싶으면 exact: true를 추가한다.
          queryClient.invalidateQueries({ queryKey: ["events"] });
          navigate("/events");
        },
      });
      function handleSubmit(formData) {
        mutate({ event: formData });
      }
    
      return (
        <Modal onClose={() => navigate("../")}>
          <EventForm onSubmit={handleSubmit}>
            {isPending && "Submitting..."}
            {!isPending && (
              <>
                <Link to="../" className="button-text">
                  Cancel
                </Link>
                <button type="submit" className="button">
                  Create
                </button>
              </>
            )}
          </EventForm>
          {isError && (
            <ErrorBlock
              title={"Failed to create event"}
              message={error.info?.message || "Failed to create event. "}
            ></ErrorBlock>
          )}
        </Modal>
      );
    }

     

    이런식으로 queryClient에 접근가능하게끔 만들어 주자

    import { QueryClient } from "@tanstack/react-query";
    export const queryClient = new QueryClient();

     

    삭제 구현 시 주의점

    refetchType: none으로 설정을 해주게 되면 페이지 삭제를 하고 쿼리 무효화를 할 시

    그 페이지에서 다시 쿼리를 불러오지 않는다.

     const { mutate } = useMutation({
        mutationFn: deleteEvent,
        onSuccess: () => {
          queryClient.invalidateQueries({
            queryKey: ["events"],
            refetchType: "none",
          });
    
          navigate("/events");
        },
      });

     

    낙관적 업데이트 하기

    종종 데이터 수정을 할때 유효한 값을 넣지 않으면 다시 롤백해야 하는 경우가 있다.

    이럴 경우에 어떻게 해야 할지 알아보자

    const {
        isError: editIsError,
        isPending: editLoading,
        error: editError,
        mutate,
      } = useMutation({
        mutationFn: updateEvent,
        //mutate가 실행하자마자 작동함
        onMutate: async (data) => {
          const newEvent = data.event;
          
          //특정 쿼리키의 데이터를 가져옴(롤백용)
          const previousEvent = queryClient.getQueryData(["events", { id: id }]);
          
          //특정 키의 모든 활성 쿼리를 취소
          //해당 쿼리 응답 데이터와 낙관적 업데이트 쿼리가 충돌 x
          await queryClient.cancelQueries({ queryKey: ["events", { id: id }] });
    
      
          queryClient.setQueryData(["events", { id: id }], newEvent);
          //return한 값은 context에 제공
          return { previousEvent };
        },
        //에러가 날 경우
        onError: async (error, data, context) => {
          queryClient.setQueryData(["events", { id: id }], context.previousEvent);
        },
        //성공에 상관없이 mutation이 완료될때마다 실행
        //항상 백엔드와 프론트엔드의 데이터 동기화
        onSettled: () => {
          queryClient.invalidateQueries({ queryKey: ["events", { id: id }] });
        },
      });

     

    반응형