
React Query는 데이터 Fetching, 동기화, Caching 서버 데이터를 받아와서 핸들링, 및 비동기과정 관리등을 쉽게 해주는 라이브러리 이다.
클라이언트 상태와 서버 상태를 명확히 구분하기 위해 만들어졌다.
* 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는 캐시 와 상호작용이 가능하다고 한다.그렇다면 이제 useQuery 에 대해서 자세하게 알아보자.
useQuery 에서는 (알다시피) queryKey 와 queryFn이 필수로 들어가야하는 인자이다.
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 페이지의 데이터를 요청한다.)
queryFn 은 Promise를 반환하는 함수가 들어간다.
나는 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에는 주요 옵션들이 있다. 몇가지를 알아보자!
먼저 가장 기본적인 staleTime gcTime 에 대해서,,
staleTime & gcTimestaleTime
- 데이터가 최신(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는 데이터가 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 실행XrefetchOnWindowFocus도 refetchOnMount 와 비슷한데, 차이점은 refetchOnWindowFocus는 윈도우가 포커싱 될때마다 refetch를 실행한다는것이다.
refetchOnMount와 같이 기본값은 true이고 always 로 설정시 윈도우가 포커싱 될때마다 실행한다.(실제로 사용해보진 못했다. 형태는 위에 refetchOnMount와 똑같다.)
윈도우 포커싱이 뭔지 잘 몰랐는데, 간단히 설명하면 크롬에 a,b 창을 켜놓고 a를 보다가 b를 클릭후 다시 a로 돌아가는 경우라고 한다.
(놀랐던 점은 개발자 도구 창을 켜서 개발자 도구창에서 작업하다가 다시 페이지 내부를 클릭해도 해당된다고 한다.)
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는 특정 프로퍼티들이 변경되었을때만 리렌더링이 발생하도록 설정할 수 있다.
별도로 설정하지 않으면 컴포넌트에 접근한 값이 변경되었을때 리렌더가 발생한다고 한다.
const [currentPage, setCurrentPage] = useState(1);
const { data, dataUpdatedAt } = useQuery({
queryKey: ["todos", currentPage],
queryFn: () => getTodo({ page: currentPage }),
notifyOnChangeProps: ["data"],
});
위 코드에서는 data 값이 변경될때에만 리렌더가 발생하고, 만약 설정값을 주지 않았다면 data 와 dataUpdatedAt 중 어느 하나가 변경되면 리렌더링 된다.
Polling 이란 실시간 웹에서 특정 시간/주기를 가지고 서버와 응답을 주고받는것을 의미한다.
기본적으로 이 옵션을 사용하기 위해서는 refetchInterval 와 refetchIntervalInBackground 를 설정해주면 된다.
const [currentPage, setCurrentPage] = useState(1);
const { data, dataUpdatedAt } = useQuery({
queryKey: ["todos", currentPage],
queryFn: () => getTodo({ page: currentPage }),
refetchInterval: 2000,
refetchIntervalInBackground: true,
});
이쯤 알아보도록 하고 이제 실제로 사용해보자.
사실 Signup/in => TodoList 페이지를 만들면서 다 완성하고싶었는데, 아직 useQuery로 Api 연결 후 데이터 뿌리는 과정밖에 해보지 못했다.
계속해서 해보고 내용 추가하겠다..!
간단히 정리하자면 TodoList 만들기는 이정도가 되겠다.
TodoList Api 연결 후 데이터 불러오기 (GET)- TodoList 에 해야할일 추가하기 (POST)
- TodoList 에 해야할일 수정하기 (PATCH)
- TodoList 에 해야할일 삭제하기 (DELETE)
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번을 해보고 벨로그에 업데이트 하겠다..!!