리액트 쿼리란?
우리는 다른 서버와의 API 통신과 비동기 데이터 관리를 위해 Redux-thunk, Redux-Saga 등 미들웨어를 채택해서 사용할 수 있어요. 하지만 다음과 같은 문제가 있습니다.
어떤 데이터에 대한 요청을 의미해요!
axios의 경우 get 요청과 비슷합니다.
const response = await axios.get(’http://localhost:3000/todos’)
어떤 데이터를 변경하는 거에요.
어떤 데이터라 함은, 데이터 그룹 그 자체를 의미한답니다.
바꾼다는 것은 추가, 수정, 삭제를 의미해요. CRUD 중, CUD에 해당해요.
(Create, Update, Delete)
axios의 경우 post, put, patch, delete 요청과 비슷합니다.
axios.post(’http://localhost:3000/todos’., newTodo); axios.patch(
http://localhost:3000/todos/${id}
, {isDone: true});
- (3) Query Invalidation
위에서 보았던 Query를 invalidation. 즉, 무효화 시킨다는 의미입니다.
무효화 시킨다는 것이 무슨 의미일까요?
기존에 가져온 Query는 서버 데이터이기 때문에, 언제든지 변경이 있을 수 있어요. 그렇기 때문에 ‘최신 상태가 아닐 수’ 있습니다. 그런 경우, 기존의 쿼리를 무효화 시킨 후 최신화 시켜야 하겠죠.
이런 과정을 React Query에서는 알아서 해준답니다. 그 유용한 기능이 바로 Query Invalidation
이에요.
아래 명령어로 react-query를 설치해주세요.
yarn add react-query
$ npm i @tanstack/react-query
# or
$ pnpm add @tanstack/react-query
# or
$ yarn add @tanstack/react-query
App.jsx
import {
QueryClient,
QueryClientProvider,
useQuery,
} from 'react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
)
}
useQuery를 이용하여 조회기능을 구현해볼게요. 먼저 src > api 폴더를 만들어주시고 그 아래에 todos 관련 api를 관리할 파일을 만들겠습니다.
src > api > todos.js
import axios from "axios";
// 모든 todos를 가져오는 api
const getTodos = async () => {
const response = await axios.get("http://localhost:3000/todos");
return response;
};
export { getTodos };
TodoList 컴포넌트의 코드도 변경해주겠습니다.
Todolist.jsx
import React from "react";
import { StyledDiv, StyledTodoListHeader, StyledTodoListBox } from "./styles";
import Todo from "../Todo";
import { __getTodosThunk } from "../../modules/todosSlice";
import { getTodos } from "../../../api/todos";
import { useQuery } from "react-query";
/**
* 컴포넌트 개요 : 메인 > TODOLIST. 할 일의 목록을 가지고 있는 컴포넌트
* 2022.12.16 : 최초 작성
*
* @returns TodoList 컴포넌트
*/
function TodoList({ isActive }) {
const { isLoading, isError, data } = useQuery({queryKey:["todos"], getTodos});
if (isLoading) {
return <p>로딩중입니다....!</p>;
}
if (isError) {
return <p>오류가 발생하였습니다...!</p>;
}
return (
<StyledDiv>
<StyledTodoListHeader>
{isActive ? "해야 할 일 ⛱" : "완료한 일 ✅"}
</StyledTodoListHeader>
<StyledTodoListBox>
{data
.filter((item) => item.isDone === !isActive)
.map((item) => {
return <Todo key={item.id} todo={item} isActive={isActive} />;
})}
</StyledTodoListBox>
</StyledDiv>
);
}
export default TodoList;
| const { isLoading, isError, data } = useQuery("todos", getTodos);
이 부분이 React Query가 가지고 있는 큰 장점이라 할 수 있어요. Thunk를 이용하면 isLoading, isError등을 개발자가 직접 만들어줬어야 했죠. state에서요.
React Query는 서버 데이터를 위한 표준을 이미 제시하고 있기 때문에 개발자들 마다의 특성에 따라 바뀔 염려가 없습니다.
return 문에 도착하기 전에 isLoading 또는 isError에 따라 별도의 처리를 해주기 때문에 대기상태 처리 / 오류 처리에 대한 부분도 아주 쉽게 해결이 되었어요 😎
todo를 추가하는 부분을 함께 구현해볼게요!
src > api > todos.js
import axios from "axios";
// 공통으로 뺐어요(물론 .env를 쓰는게 더 바람직해요)
const SERVER_URI = "http://localhost:4000";
const getTodos = async () => {
const response = await axios.get(`${SERVER_URI}/todos`);
return response.data;
};
const addTodo = async (newTodo) => {
await axios.post(`${SERVER_URI}/todos`, newTodo);
};
export { getTodos, addTodo };
Input.jsx
...
import { addTodo } from "../../../api/todos";
import { QueryClient, useMutation } from "react-query";
...
function Input() {
...
const queryClient = new QueryClient();
const mutation = useMutation(addTodo, {
onSuccess: () => {
// Invalidate and refresh
// 이렇게 하면, todos라는 이름으로 만들었던 query를
// invalidate 할 수 있어요.
queryClient.invalidateQueries("todos");
},
});
[invalidate의 과정]
Input.jsx에서 값 입력으로 인해 서버 데이터가 변경됨
→ onSuccess가 일어나면 기존의 Query인 “todos”는 무효화
→ 새로운 데이터를 가져와서 “todos”를 최신화시킴
→ TodoList.jsx를 갱신함
따라서 계속해서 리액트 앱은 최신 상태의 서버 데이터를 유지할 수 있게 되는거에요.
import { useQuery } from 'react-query';
import { fetchTodoList } from '../api/fetchTodoList';
function App() {
const info = useQuery('todos', fetchTodoList);
}
‘Query Keys’에 대해 좀 더 자세히 알아볼까요?
QK는 위 예제처럼 한 단어일 수도 있구요. 배열의 형태일 수도 있고, 심지어는 nested 객체일 수도 있어요. Key라는 말이 의미하듯, 모든 Query keys는 Unique해야 함을 잊지 마세요!
const query1 = useQuery('qk', api); // unique
const query2 = useQuery('qk2', api); // not unique
const query3 = useQuery('qk2', api); // not unique
단어 한 개로 이루어진 Query Keys
만일 다음과 같은 코드가 있다고 한다면
useQuery('todos', ...)
내부적으로는 다음과 같이 해석돼요(배열 형태로 갖고있어요)
queryKey === ['todos']
배열 형태의 Query Keys
정보를 유일하게 식별하기 위해 하나의 단어보다 더 많은 ‘표현’이 필요하다면 문자, 숫자, object 등등 여러가지를 조합한 배열 형태의 key도 사용이 가능하겠죠!
공식 문서에서는 다음과 같은 예시를 제시하고 있네요.
// 💥주의! key는 표현이 그렇다는거지, api 로직과는 관련이 없어요!
// ID가 5인 todo 아이템 1개
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]
// ID가 5인 todo 아이템 1개인데, preview 속성은 true야
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]
// todolist 전체인데, type은 done이야
useQuery(['todos', { type: 'done' }], ...)
// queryKey === ['todos', { type: 'done' }]
문제! 다음 Query Keys는 Unique 한가요?
1번 문제
useQuery(['todos', { status, page }], ...)
useQuery(['todos', { page, status }], ...)
// useQuery(['todos', { page, status, other: undefined }], ...)
2번 문제
useQuery(['todos', status, page], ...)
useQuery(['todos', page, status], ...)
// useQuery(['todos', undefined, page, status], ...)
‘Query Keys’에 대해 좀 더 자세히 알아볼까요?
b. 두 번째 인자, ‘fetchTodoList’. 우린 이걸 쿼리 함수(Query Functions)라고 불러요.
쿼리 함수는 promise 객체를 return 합니다.
promise 객체는 반드시 data를 resolve하거나 에러를 내야 해요.
resolve는 정상적으로 통신이 되었음을 의미해요
원했던 상황이 아닌 경우. 즉, 오류가 발생한 경우에는 그에 맞는 적절한 오류 처리 관련 로직을 삽입해서 처리를 해줘야만 해요. axios, fetch, graphql 중 어떤 방법을 이용하던지 적절한 오류 처리를 통해 사용자가 혼란에 빠지지 않도록 해줘야만 해요.
useQuery의 결과물에 대해
a. useQuery를 통해 얻은 결과물은 객체(object)에요 객체!
b. 그 안에는 여러분이 ‘조회’를 요청한 결과에 대한 거의 모든 정보가 들어있고 그 과정에 대한 정보도 다음과 같이 들어있어요.
i. 시~작 하면, isLoading이 true
가 돼요.
ii. 조회 결과 오류가 나면 isError가 true
가 돼요. isLoading은 false
가 되겠죠. error 객체를 통해 좀 더 상세한 오류 내용을 확인할 수 있어요.
iii. 조회 결과 정상이 되면 isSuccess가 true
가 돼요. isLoading은 false
가 된답니다. data 객체를 통해 좀 더 상세한 조회 결과를 확인할 수 있어요.
// [출처] : 공식문서
function App() {
const mutation = useMutation(newTodo => {
return axios.post('/todos', newTodo)
})
return (
<div>
{mutation.isLoading ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
a. mutation.mutate( 인자 )
i. 인자는 반드시 한 개의 변수 또는 객체여야 해요.
ii. mutation.mutate(인자1, 인자2) → 오류
b. 결과를 객체(object 형태로) 갖고 있습니다.
c. 그 결과물 객체는 항상 어느 상태 중 하나
에 속해요.
i. isIdle
ii. isLoading
iii. isError
1. error 객체를 항상 품고 있음을 명심!
iv. isSuccess(query에만 있는게 아니에요)
1. data 객체를 항상 품고 있음을 명심