React Query - useQuery

최하영·2023년 12월 27일
0

React

목록 보기
2/6
post-thumbnail

🎀React Query란?

React Query는 데이터 Fetching, 동기화, Caching 서버 데이터를 받아와서 핸들링, 및 비동기과정 관리등을 쉽게 해주는 라이브러리 이다.
클라이언트 상태와 서버 상태를 명확히 구분하기 위해 만들어졌다.

🎀React Query 기능

  • 캐싱을 통해 애플리케이션 속도를 향상시킨다.
  • 동일한 데이터에 대한 중복 요청 => 단일 요청으로 만들어줌
  • 페이지네이션 및 데이터 지연 로드와 같은 성능 최적화
  • 오래된 데이터의 상태를 파악하여 update를 해줌
  • 리액트훅과 비슷한 인터페이스를 제공한다.
  • 서버 상태의 메모리 및 가비지 수집 관리

🎀React Query 설치 및 셋팅

* react프로젝트생성 (폴더명: reactquery)

{npx create-react-app reactquery}

* 만든 폴더로 이동 후 리액트쿼리 install 및 실행

cd reactquery
npm install react-query
npm start

* QueryClientProvider 사용
react-query 를 사용하기 위해서는 QueryClientProvider를 최상단에 감싸주고 QueryClient인스턴스를 client props로 넣어 애플리케이션에 연결해야 한다.

//App.js

import React from "react";
import { QueryClient, QueryClientProvider } from "react-query";
import { RouterProvider } from "react-router-dom";
import { ThemeProvider } from "styled-components";

import { COLORS } from "./components/designToken/color";
import { SIZES } from "./components/designToken/size";
import GlobalStyle from "./components/styles/globalStyle";
import router from "./libs/routes/routes";

const theme = {
    colors: COLORS,
    sizes: SIZES,
};
function App() {
    const queryClient = new QueryClient();
    return (
        <QueryClientProvider client={queryClient}>
            <ThemeProvider theme={theme}>
                <GlobalStyle />
                <RouterProvider router={router} />
            </ThemeProvider>
        </QueryClientProvider>
    );
}

export default App;
  • queryClient캐시 와 상호작용이 가능하다고 한다.
    (queryClient에서 모든 query 나 mutation에 기본옵션 추가 가능)
  • App.js에 QueryClientProvider로 감싸고, client props에 queryClient를 연결함으로써, 비동기 요청을 알아서 처리하는 background가 된다.

그렇다면 이제 useQuery 에 대해서 자세하게 알아보자.

🎀useQuery

✨useQuery의 기본

  • useQuery 에서는 (알다시피) queryKeyqueryFn이 필수로 들어가야하는 인자이다.

        const [currentPage, setCurrentPage] = useState(1);
        const { data, isLoading, isError, error } = useQuery({
            queryKey: ["todos", currentPage], 
            queryFn: () => getTodo({ page: currentPage }),
        });
  • queryKey 는 데이터 key값으로 배열로 지정해줘야하는데, 내 코드에서는 뒤에 currentPage라는 변수를 추가한것을 볼 수 있다.
    ==> 이는 page라는 인자가 currentPage라는 변수에 의존 하기때문에 추가해 준것인데, 이렇게해야 제대로 쿼리에 접근할 수 있다고 한다.
    (PS. getTodo 는 axios를 활용하여 데이터를 가져오는 함수로 page 라는 todolist 페이지의 데이터를 요청한다.)

  • queryFnPromise를 반환하는 함수가 들어간다.
    나는 getTodo 를 사용해야해서 위 사진과 같이 작성하였다.

필수값인 queryFn과 queryKey 를 알아보았다.

그외에 useQuery 에서 사용되는 주요 리턴 데이터를 간단하게 아래에 정리한다.

  • data: 쿼리 함수가 리턴한 Promise에서 resolved된 데이터
  • error: 쿼리 함수에 오류가 발생한 경우, 쿼리에 대한 오류 객체
  • status: data, 쿼리 결과값에 대한 상태를 표현하는 status는 문자열 형태로 3가지의 값이 존재한다.
    • pending: 쿼리 데이터가 없고, 쿼리 시도가 아직 완료되지 않은 상태
      { enabled: false } 상태로 쿼리가 호출되면 이 상태로 시작
    • error: 에러 발생했을 때 상태
    • success: 쿼리 함수가 오류 없이 요청 성공하고 데이터를 표시할 준비가 된 상태
  • fetchStatus: queryFn에 대한 정보
  • fetching: 쿼리가 현재 실행 중인 상태
  • paused: 쿼리를 요청했지만, 잠시 중단된 상태 (network mode와 연관)
  • isLoading: 처음 실행된 쿼리일 때 로딩 여부에 따라 true/false로 반환
    =>이미 캐싱 된 데이터가 있다면 로딩 여부에 상관없이 false를 반환
  • isFetching: 캐싱 된 데이터가 있더라도 쿼리가 실행되면 로딩 여부에 따라 true/false로 반환
  • isSuccess: 쿼리 요청이 성공하면 true
  • isError: 쿼리 요청 중에 에러가 발생한 경우 true
  • refetch: 쿼리를 수동으로 다시 가져오는 함수

✨useQuery의 주요 옵션

useQuery에는 주요 옵션들이 있다. 몇가지를 알아보자!

먼저 가장 기본적인 staleTime gcTime 에 대해서,,

🤚staleTime & gcTime

staleTime

  • 데이터가 최신(fresh) -> 오래된 상태(stale) 가 되기까지 걸리는 시간
  • 기본값은 0 이다.

gcTime

  • 데이터가 사용햐지 않거나 inactive일때 캐싱된 상태로 남아있는 시간
  • 쿼리 인스턴스가 unmount 되면 데이터는 inactive 상태로 변경되며, 캐시는 gcTime만큼 유지
  • staleTime과 관계없이 무조건 inactive 된 시점을 기준으로 캐시 데이터 삭제
  • 기본값이 5분이고, SSR 환경에서는 Infinity

코드에서 staleTime 과 gcTime을 설정해봤다.

const TodoPage = () => {
   const [currentPage, setCurrentPage] = useState(1);
   const { data, isLoading, isError, error } = useQuery({
       queryKey: ["todos", currentPage],
       queryFn: () => getTodo({ page: currentPage }),
       staleTime: 5 * 60 * 1000, //5분
       gcTime: 5 * 60 * 1000, // 5분
   });

여기서 keyPoint는 두 옵션의 기본값이 각각 0분과 5분 이라는것인데, 보편적으로 staleTiem < gcTime 이여야 한다. => 설정을 잘 해줘야한다.

🤚refetchOnMount

refetchOnMount는 데이터가 stale 상태일 경우 마운트시마다 refetch를 실행하는 옵션이다. 아래와 같이 사용가능하다.

const TodoPage = () => {
  const [currentPage, setCurrentPage] = useState(1);
  const { data, isLoading, isError, error } = useQuery({
      queryKey: ["todos", currentPage],
      queryFn: () => getTodo({ page: currentPage }),
      staleTime: 5 * 60 * 1000, //5분
      gcTime: 5 * 60 * 1000, // 5분
      refetchOnMount: true,
  });
  • true : 기본값
  • always : 마운트시 마다 매번 실행
  • false : 최초 fetch 이후 refetch 실행X

🤚refetchOnWindowFocus

refetchOnWindowFocus도 refetchOnMount 와 비슷한데, 차이점은 refetchOnWindowFocus는 윈도우가 포커싱 될때마다 refetch를 실행한다는것이다.

refetchOnMount와 같이 기본값은 true이고 always 로 설정시 윈도우가 포커싱 될때마다 실행한다.(실제로 사용해보진 못했다. 형태는 위에 refetchOnMount와 똑같다.)

윈도우 포커싱이 뭔지 잘 몰랐는데, 간단히 설명하면 크롬에 a,b 창을 켜놓고 a를 보다가 b를 클릭후 다시 a로 돌아가는 경우라고 한다.
(놀랐던 점은 개발자 도구 창을 켜서 개발자 도구창에서 작업하다가 다시 페이지 내부를 클릭해도 해당된다고 한다.)

🤚retry

retry는 쿼리 실패시 useQuery를 설정한 횟수만큼 재시도 한다.
기본값은 클라이언트 환경에서 3 , 서버환경에서는 0 이다.
false 로 설정한다면 다시 시도하지 않고, true 인 경우에는 무한으로 재요청을 시도한다고 한다.
아래와 같이 사용하면 된다.

    const [currentPage, setCurrentPage] = useState(1);
    const { data, isLoading, isError, error } = useQuery({
        queryKey: ["todos", currentPage], 
        queryFn: () => getTodo({ page: currentPage }),
        staleTime: 5 * 60 * 1000, //5분
        retry: 3,
    });

🤚notifyOnChangeProps

notifyOnChangeProps는 특정 프로퍼티들이 변경되었을때만 리렌더링이 발생하도록 설정할 수 있다.
별도로 설정하지 않으면 컴포넌트에 접근한 값이 변경되었을때 리렌더가 발생한다고 한다.

    const [currentPage, setCurrentPage] = useState(1);
    const { data, dataUpdatedAt } = useQuery({
        queryKey: ["todos", currentPage], 
        queryFn: () => getTodo({ page: currentPage }),
		notifyOnChangeProps: ["data"],
        
    });

위 코드에서는 data 값이 변경될때에만 리렌더가 발생하고, 만약 설정값을 주지 않았다면 datadataUpdatedAt 중 어느 하나가 변경되면 리렌더링 된다.

🤚Polling

Polling 이란 실시간 웹에서 특정 시간/주기를 가지고 서버와 응답을 주고받는것을 의미한다.
기본적으로 이 옵션을 사용하기 위해서는 refetchIntervalrefetchIntervalInBackground 를 설정해주면 된다.

    const [currentPage, setCurrentPage] = useState(1);
    const { data, dataUpdatedAt } = useQuery({
        queryKey: ["todos", currentPage], 
        queryFn: () => getTodo({ page: currentPage }),
		refetchInterval: 2000,
  		refetchIntervalInBackground: true,
        
    });
  • refetchInterval 에는 숫자 (시간값)을 지정해줘서 일정 시간마다 자동으로 refetch를 시켜준다.
  • refetchIntervalInBackground 에는 boolean 값이 들어가고, 탭이 백그라운드에 있는동안 자동으로 refetch 시켜준다. (브라우저를 안보고 있어도 refetch 시켜줌)

이쯤 알아보도록 하고 이제 실제로 사용해보자.

✨useQuery 실제로 사용해보기

사실 Signup/in => TodoList 페이지를 만들면서 다 완성하고싶었는데, 아직 useQuery로 Api 연결 후 데이터 뿌리는 과정밖에 해보지 못했다.
계속해서 해보고 내용 추가하겠다..!

간단히 정리하자면 TodoList 만들기는 이정도가 되겠다.

  • TodoList Api 연결 후 데이터 불러오기 (GET)
  • TodoList 에 해야할일 추가하기 (POST)
  • TodoList 에 해야할일 수정하기 (PATCH)
  • TodoList 에 해야할일 삭제하기 (DELETE)

🎏 1. API 연결 후 데이터 불러오기 (feat. Todolist)

axios 파일 -> todo 관련 api 생성

//axiosInstance 파일 생성
import axios from "axios";

import { getSessionToken } from "./storage-manager";

export const axiosInstance = axios.create({
    baseURL: process.env.REACT_APP_BASE_URL,
    headers: {
        "Content-Type": "application/json",
    },
    withCredentials: true,
});

//token 이 있으면 자동으로 헤더에 토큰 추가
axiosInstance.interceptors.request.use(
    (config) => {
        const token = getSessionToken();

        if (token) {
            config.headers["Authorization"] = `Bearer ${token}`;
        }
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);
//axios -> todo.js 파일 생성
import { axiosInstance } from "./axiosInstance";

//todo 목록 추가
export const postAddTodo = async ({ title, content }) => {
    try {
        const response = await axiosInstance.post("/todo", {
            title,
            content,
        });
        return response.data;
    } catch (error) {
        console.error(error);
    }
};
//todo 데이터 불러오기
export const getTodo = async ({ page }) => {
    try {
        const response = await axiosInstance.get("/todo", {
            params: {
                page: page,
            },
        });
        return response.data;
    } catch (error) {
        console.error(error);
    }
};
//todo 수정
export const patchTodo = async ({ title, content, state, todoId }) => {
    try {
        const response = await axiosInstance.patch(`./todo?${todoId}`, {
            title,
            content,
            state,
        });
        return response.data;
    } catch (error) {
        console.error(error);
    }
};
//todo삭제
export const deleteTodo = async ({ todoId }) => {
    try {
        const response = await axiosInstance.delete(`/todo/${todoId}`);
        return response.data;
    } catch (error) {
        console.error(error);
    }
};

이렇게 해두고 TodoPage.jsx 를 만든다.

import { useState } from "react";
import { useQuery } from "react-query";
import styled from "styled-components";
import TodoItem from "../components/commons/todo/todoItem";
import TodoTemplate from "../components/commons/todo/todoTemplate";
import { COLORS } from "../components/designToken/color";
import { getTodo } from "../libs/axios/todo";
const TodoPage = () => {
    const [currentPage, setCurrentPage] = useState(1);
    const { data, isLoading, isError, error } = useQuery({
        queryKey: ["todos", currentPage], //currentPage라는 상태에 의존하므로 추가해줘야함
        queryFn: () => getTodo({ page: currentPage }),
        staleTime: 5 * 60 * 1000, //5분
        retry: 3,
    });
    if (isLoading) return <div>Loading</div>;
    if (isError) return <div>{error.message}</div>;
    console.log(data, "data"); //데이터가 어떻게 들고와지는지 확인
    const dataArr = Object.values(data).filter((items) => {
        return typeof items === "object" && items.hasOwnProperty("title");
        //"title"속성을 가지고있는지확인
    });
    console.log(dataArr, "arr");
 return (
        <>
            <TodoTemplate>
                TodoList
                <TodoListBlock>
                    {dataArr &&
                        dataArr?.map((todo) => (
                            <TodoItem
                                key={todo.idx} // 고유한 식별자인 todoId를 key로 사용
                                todoId={todo.idx}
                                title={todo.title}
                                content={todo.content}
                                state={todo.state}
                            />
                        ))}
                    <button>ADD</button>
                </TodoListBlock>
            </TodoTemplate>
        </>
    );
};
export default TodoPage;
const TodoListBlock = styled.div`
    flex: 1;
    padding: 20px 32px;
    padding-top: 70px;
    padding-bottom: 48px;
    overflow-y: auto;
    background: ${COLORS.PALLETE.gray.base};
    `;

저기에서 해맸던 부분은 data가 뿌려지지 않았던 점이다.
문제가 뭔가 하고 보니 ,,
console.log(data, "data"); 콘솔에 데이터의 형태가 아래 사진과 같았다.

0: {...} , 1: {...} 이부분은 내가 Thunder로 임의로 내용을 추가해서 만든 투두고,
그 아래 pagination은 투두페이지네이션 구현을 위해 필요한 부분이였다.
근데 나는 일단 들고오는것이 목적이니, 저 페이지네이션이 필요없었다.

그래서, 객체 형태의 data 값을 Object.values(data) 를 통해 객체를 배열형태로 바꾼후, filter 를 통해 오로지 투두리스트 항목에 들어갈 부분만 걸러내었다.

이렇게 대충 투두리스트가 만들어졌다.
이제 2번을 해보고 벨로그에 업데이트 하겠다..!!

0개의 댓글