# React Query란?
(1) 개념
react-query는 서버의 값을 클라이언트에 가져오거나, 캐싱, 값 업데이트, 에러핸들링 등 비동기 과정을 더욱 편하게 하는데 사용된다.
(2) 기존 미들웨어의 한계
다른 서버와의 API 통신과 비동기 데이터 관리를 위해 Redux-thunk, Redux-Saga 등 미들웨어를 채택해서 사용할 수 있다.
하지만 다음과 같은 문제가 있다.
- 보일러 플레이트 : 코드량이 너무 많음.
- 규격화 문제 : Redux가 비동기 데이터 관리를 위한 전문 라이브러리가 아님(규격화 문제)
(3) 리액트 쿼리의 강점 : 쉽고, 책임에서 자유롭다.
- 보일러 플레이트 만들다가 오류날 일이 없음
- 내가 만든 부분 아니기 때문에 잘못이 일어난들 내 잘못이 아님
- 사용방법이 기존 thunk 대비 너무 쉽고, 직관적이다.
# 주요 키워드
(1) Query
어떤 데이터에 대한 요청을 의미한다.
axios의 경우 get 요청과 비슷합니다.
const response = await axios.get(’http://localhost:4001/todos’)
(2) Mutation
어떤 데이터를 변경하는 것을 의미한다.
어떤 데이터라 함은, 데이터 그룹 그 자체를 의미한다.
바꾼다는 것은 추가, 수정, 삭제를 의미한다. CRUD 중, CUD에 해당함(Create, Update, Delete)
axios의 경우 post, put, patch, delete 요청과 비슷하다.
axios.post(’http://localhost:4001/todos’., newTodo);
axios.patch(http://localhost:4001/todos/${id}, {isDone: true});
(3) Query Invalidation
위에서 보았던 Query를 invalidation. 즉, 무효화 시킨다는 의미이다.
무효화 시킨다는 것이 무슨 의미일까?
기존에 가져온 Query는 서버 데이터이기 때문에, 언제든지 변경이 있을 수 있다. 그렇기 때문에 ‘최신 상태가 아닐 수’ 있습니다. 그런 경우, 기존의 쿼리를 무효화 시킨 후 최신화 시켜야 한다.
이런 과정을 React Query에서는 알아서 해준다. 그 유용한 기능이 바로 Query Invalidation이다.
# 실습
(1) 조회기능 구현 : 기존 Todolist 프로젝트 변경해보기
아래 명령어로 react-query를 설치
yarn add react-query
json server 와 axios도 설치해야됨
App.jsx
import React from "react";
import Router from "./shared/Router";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Router />;
</QueryClientProvider>
);
};
export default App;
QueryClientProvider : 데이터를 읽어오는 기능(QueryClient)을 애플리케이션 전체에 주입하도록 하는 API
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.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("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에 따라 별도의 처리(자동)를 해주기 때문에 대기상태 처리 / 오류 처리에 대한 부분도 아주 쉽게 해결이 되었다.
(2) 추가 기능 구현
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를 갱신함
따라서 계속해서 리액트 앱은 최신 상태의 서버 데이터를 유지할 수 있게 되는 것이다.
# 조금 더 나아가기
(1) useQuery
1. useQuery hook의 사용 방법에 대해 다시한번 짚어보자.
import { useQuery } from 'react-query';
import { fetchTodoList } from '../api/fetchTodoList';
function App() {
const info = useQuery('todos', fetchTodoList);
}
2. useQuery의 인자에 대해
# 첫 번째 인자 ‘todos’. 우린 이걸 쿼리의 키(Query Keys)라고 부른다.
- refetching 하는 데에 쓰인다.
- 캐싱(caching) 처리를 하는 데에도 쓰인다.
- 애플리케이션 전체 맥락에서 이 쿼리를 공유하는 방법으로 쓰인다. —> 그 어느 컴포넌트 곳곳에 촥촥 뿌려져 있어도 같은 key면 같은 쿼리 및 데이터가 보장된다.
‘Query Keys’에 대해 좀 더 자세히 알아보자
1. 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
2. 단어 한 개로 이루어진 Query Keys
만일 다음과 같은 코드가 있다고 한다면
useQuery('todos', ...)
내부적으로는 다음과 같이 해석된다(배열 형태로 갖고있는다)
queryKey === ['todos']
3. 배열 형태의 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' }]
4. 다음 Query Keys는 Unique 할까?
useQuery(['todos', { status, page }], ...)
useQuery(['todos', { page, status }], ...)
// useQuery(['todos', { page, status, other: undefined }], ...)
객체는 키값을 기준으로 하므로 1번,2번은 QK는 유니크하지 않다.
3번은 other가 추가되어있으므로 유니크하다고 볼 수 있다.
useQuery(['todos', status, page], ...)
useQuery(['todos', page, status], ...)
// useQuery(['todos', undefined, page, status], ...)
배열은 순서를 기준으로 하므로 1,2번은 유니크하다고 볼 수 있다.
# 두 번째 인자, ‘fetchTodoList’. 우린 이걸 쿼리 함수(Query Functions)라고 부른다.
- 쿼리 함수는 promise 객체를 return 한다.
- promise 객체는 반드시 data를 resolve하거나 에러를 내야 한다.
1. resolve는 정상적으로 통신이 되었음을 의미한다.
2. 원했던 상황이 아닌 경우. 즉, 오류가 발생한 경우에는 그에 맞는 적절한 오류 처리 관련 로직을 삽입해서 처리를 해줘야만 한다.
axios, fetch, graphql 중 어떤 방법을 이용하던지 적절한 오류 처리를 통해 사용자가 혼란에 빠지지 않도록 해줘야만 한다.
# useQuery의 결과물에 대해
1. useQuery를 통해 얻은 결과물은 객체(object)이다.
2. 그 안에는 ‘조회’를 요청한 결과에 대한 거의 모든 정보가 들어있고 그 과정에 대한 정보도 다음과 같이 들어있다.
- 시작 하면, isLoading이 true가 된다.
- 조회 결과 오류가 나면 isError가 true가 된다. isLoading은 false가 된다. error 객체를 통해 좀 더 상세한 오류 내용을 확인할 수 있다.
- 조회 결과 정상이 되면 isSuccess가 true가 된다. isLoading은 false가 된다. data 객체를 통해 좀 더 상세한 조회 결과를 확인할 수 있다.
# mutations
1. 위에서 언급했듯이, query와는 다르게 mutation은 CUD에서 사용된다.
// [출처] : 공식문서
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>
)
}
2. useMutation은 hook이다. 함수이고, API이다.
(1) mutation.mutate( 인자 )
- 인자는 반드시 한 개의 변수 또는 객체여야 한다.
- mutation.mutate(인자1, 인자2) → 오류
(2) 결과를 객체(object 형태로) 갖고 있다.
(3) 그 결과물 객체는 항상 어느 상태 중 하나에 속한다.
- isIdle
- isLoading
- isError(error 객체를 항상 품고 있음을 명심)
- isSuccess(query에만 있는게 아니다, data 객체를 항상 품고 있음을 명심)
'코딩 > React' 카테고리의 다른 글
인증/인가(쿠키, 세션, 토큰, JWT) (0) | 2023.05.01 |
---|---|
throttling & debouncing (0) | 2023.05.01 |
Custom Hooks (0) | 2023.04.29 |
Thunk 2 (0) | 2023.04.29 |
Thunk (1) | 2023.04.28 |
댓글