학습 목표
- 기존 Thunk 가 가지고 있었던 한계를 이해합니다.
- React Query 의 개념과 사용방법을 실습을 통해 익힙니다.
- 서버 사이드 데이터와 클라이언트 사이드 데이터를 이해합니다.
React Quert 란?
react-query 는 서버의 값을 클라이언트에 가져오거나, 캐싱, 값, 업데이트, 에러핸들링 등 비동기 과정을 편하게 하는 데 사용합니다.
기존 미들웨어의 한계
다른 서버와의 API 통신과 비동기 데이터 관리를 위해 Redux-toolkit, Redux-Saga 등 미들웨어를 채택해서 사용할 수 있었습니다. 하지만, 다음과 같은 문제가 있습니다.
- 보일러 플레이트 : 코드량이 너무 많습니다.
- 규격화 문제 : Redux 가 비동기 데이터 관리를 위한 전문 라이브러리가 아닙니다.
React Query 의 강점
- 보일러 플레이트 만들면서 생기는 휴먼에러를 줄일 수 있습니다.
- 사용방법이 기존 thunk 대비 굉장히 쉽고, 직관적입니다.
- React 애플리케이션 내에서 데이터 패칭, 캐싱, 동기적, 그리고 서버의 상태의 업데이트를 좀 더 용이하게 만들어줍니다.
주요 키워드
useQuery
react-query 에서 제공하는 훅으로 데이터를 가져오는 목적으로 사용되며 API 나 다른 데이터 소스로부터 데이터를 가져오고 처리하는 작업을 간편하고 호율적으로 처리할 수 있는 선언적인 방식을 제공합니다. 데이터 조회가 아닌 데이터 변경 작업을 할 때는 useMutation 을 사용합니다.
useQuery 훅은 두 개의 매개변수를 받습니다.
첫 번째는 고유한 키인 "쿼리 키" 이고, 두 번째는 데이터를 가져오는 fetchData 함수입니다.
키는 쿼리를 식별하고 캐시하기 위해 사용되며, fetchData 함수는 데이터를 가져오는 로직을 수행합니다. React Query 는 캐싱, refetch 등을 자동으로 처리합니다.
쿼리함수(fetchData)
함수는 쿼리를 실행하거나 리패치해야할 때마다 호출합니다. 비동기 함수나 프로미스 기반 함수로 작성할 수 있으며, 데이터를 가져오는 로직을 처리합니다.
쿼리 상태
useQuery 훅은 data, isLoading, error 등의 속성을 반환합니다. 이러한 속성은 쿼리의 상태를 나타냅니다. 예를 들어 data 는 가져온 데이터를 담고 있고, isLoading 은 쿼리가 진행 중인지를 나타내며, error 는 발생한 오류를 포함합니다.
쿼리 구성
useQuery 에는 쿼리를 구성하기 위해 추가적인 옵션을 제공할 수도 있는ㄴ데, 이 옵션을 사용해 캐시 유지 시간 설정, 캐시 무효화 제어, 쿼리의 의존성 정의 등을 할 수 있습니다. 이러한 옵션을 사용해 쿼리의 동작을 필요에 맞게 커스터마이징할 수 있습니다.
useMutation
useMutation 훅은 변이(mutations) / 데이터 업데이트, 삭제를 처리하는 데 사용됩니다. mutate 는 서버의 데이터를 수정하거나 업데이트하는 작업을 의미하죠. useMutation 훅은 뮤테이션 요청을 보내고, 변이 상태를 관리하며, 낙관적 업데이트를 처리하는 등 뮤테이트를 처리하는 과정을 간편하게 해줍니다.
아래 예시에서 useMutation 훅은 createData 함수를 받습니다. createData 함수는 뮤테이션을 수행하는 역할을 담당합니다. mutate 함수를 사용해 createData 함수를 호출하고, 필요한 데이터를 전달할 수 있습니다.
useMutation 훅은 mutate, isLoading, isError, error 등의 속성을 반환합니다. 이러한 속성을 사용해 컴포넌트에서 로딩 상태, 오류 상태, 성공 상태를 처리할 수 있습니다.
import { useMutation } from 'react-query';
const MyComponent = () => {
const { mutate, isLoading, isError, error } = useMutation(createData);
const handleSubmit = (formData) => {
mutate(formData);
};
if (isLoading) {
return <div>Creating...</div>;
}
if (isError) {
return <div>Error: {error.message}</div>;
}
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit">Create</button>
</form>
);
const createData = async (formData) => {
try {
// Make a request to the server to create the data
const response = await axios.post('/api/data', formData);
return response.data; // Return the created data
} catch (error) {
throw new Error('Failed to create data.'); // Throw an error if the request fails
}
};
낙관적 업데이트 :
React Query의 낙관적 업데이트는 실제 서버 응답이 수신되기 전에 성공에 대한 낙관적 가정으로 사용자 인터페이스가 즉시 업데이 트는 걸 말합니다. 사용자에게 즉각적인 피드백을 제공하여 보다 원활하고 반응이 빠른 사용자 경험을 제공할 수 있습니다.
하지만 만약 서버 업데이트가 실패할 경우는 업데이트 이전의 데이터로 변경해야 되기 때문에 useMutation 훅에 onMutate함수를 사용하여 뮤테이션 되기 전에 실행될 콜백함수를 지정하여 해당 이슈를 방지할 수 있습니다.
const { mutate } = useMutation(createData, { onMutate: onMutate, }); const handleSubmit = (formData) => { // Perform pre-mutation tasks using onMutate onMutate(formData); // Perform the actual mutation mutate(formData); };
실습
조회기능 구현 : 기존 TodoList 프로젝트 변경해보기
실습을 위해 다음 github respository 에서 clone 을 받아왔습니다.
https://github.com/wonee09/redux-basic-todo.git
GitHub - wonee09/redux-basic-todo
Contribute to wonee09/redux-basic-todo development by creating an account on GitHub.
github.com
위 기본 redux 프로젝트를 react-query 로 변경해보았습니다.
우선, 아래 명령어를 통해 react-query 를 설치합니다.
yarn add react-query
App.jsx
import React from "react";
import Router from "./shared/Router";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = useQueryClient();
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Router />;
</QueryClientProvider>
);
};
export default App;
json server 의 설정이 안되었다면, 먼저 설정을 해주어야 합니다.(yarn add axios)
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("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 에 따라 별도의 처리를 해주기 때문에 대기상태 처리 / 오류 처리에 대한 부부도 아주 쉽게 해결이 되었습니다.
추가 기능 구현(: invalidateQueries )
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");
},
});
[invalidateQueries 의 과정]
Input.jsx 에서 값 입력으로 인해 서버 데이터가 변경되었다고 가정했을 때,
- onSuccess 가 일어나면 기존의 Query 인 "todos" 는 무효화합니다.
- 새로운 데이터를 가져와서 "todos" 를 최신화시킵니다.
- TodoList.jsx 를 갱신합니다.
따라서 계속해서 리액트 앱은 최신 상태의 서버 데이터를 유지할 수 있게 되는 거죠.
useQuery 에 대해
useQuery hook 의 사용방법에 대해
import { useQuery } from 'react-query';
import { fetchTodoList } from '../api/fetchTodoList';
function App() {
const info = useQuery('todos', fetchTodoList);
}
useQuery 인자에 대해
첫 번째 인자, 'todos' 를 쿼리의 키(Query Keys)라고 부릅니다.
- refetching 하는 데에 쓰이고,
- 캐싱(caching) 처리를 하는 데에도 쓰입니다.
- 애플리케이션 전체 맥락에서 이 쿼리를 공유하는 방법으로 쓰입니다. ▶ 어느 컴포넌트에 뿌려져 있어도 같은 key 면, 같은 쿼리 및 데이터가 보장되죠.
Query Keys 에 대해
1. 쿼리 키는 위 예제처럼 한 단어일 수도 있고, 배열의 형태일 수도 있고, 심지어 nested 객체일 수도 있습니다. Key 라는 말이 의미하듯, 모든 쿼리 키는 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' }]
두 번째 인자, 'fetchTodoList' 를 쿼리 함수(Query Function) 이라고 부릅니다.
1. 쿼리 함수는 promise 객체를 return 합니다.
2. promise 객체는 반드시 data 를 resolve 하거나 에러를 내야 합니다.
resolve 는 정상적으로 통신이 되었음을 의미하는데요. 원했던 상황이 아닌 경우, 즉 오류가 발생한 경우에는 그에 맞는 적절한 오류 처리 관련 로직을 삽입해서 처리를 해줘야만 합니다. axios, fetch, graphql 중 어떤 방법을 이용하던지 적절한 오류 처리를 통해 사용자가 혼란에 빠지지 않도록 해줘야 합니다.
3. useQuery 의 결과물에 대해
useQuery 를 통해 얻은 결과물은 객체(object)입니다. 그 안에는 '조회' 를 요청한 결과에 대한 거의 모든 정보가 들어있고, 그 과정에 대한 정보도 다음과 같이 들어있습니다.
- 시작하면, isLoading 이 true 가 됩니다.
- 조회 결과 오류가 나면, isError 가 true 가 됩니다. isLoading 은 false 가 되겠죠. error 객체를 통해 좀 더 상세한 오류 내용을 확인할 수 있습니다.
- 조회 결과 정상이 되면, isSuccess 가 true 가 됩니다. isLoading 은 false 가 되겠죠. data 객체를 통해 좀 더 상세한 조회 결과를 확인할 수 있습니다.
mutaitions
위에서 언급했듯이, query 와는 다르게 mutation 은 CUD 에서 사용됩니다.
useMutation 예제를 다시 한 번 짚어보겠습니다.
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>
)
}
useMutation 은 hook 이고, 함수이자 API 입니다.
- mutation.mutate(인자)
- 인자는 반드시 한 개의 변수 또는 객체여야 합니다.
- mutation.mutate(인자1, 인자2) → 오류
- 결과를 객체(object 형태로) 갖고 있습니다.
- 그 결과물 객체는 항상 어느 상태 중 하나에 속합니다.
- isIdle : 가동되지 않는
- isLoading
- isError : error 객체를 항상 품고 있습니다.
- isSuccess : data 객체를 항상 품고 있습니다.
'항해 16기 > Today I Learned' 카테고리의 다른 글
[항해 37일차] TIL_React 심화주차: 인증/인가(쿠키, 세션, 토큰, JWT) (0) | 2023.09.13 |
---|---|
[항해 36일차] TIL_React 심화주차: throttling & debouncing (0) | 2023.09.13 |
[항해 33일차] TIL_React 심화주차: Custom Hooks (0) | 2023.09.12 |
[항해 32일차] TIL_React 심화주차: Thunk 2 (0) | 2023.09.11 |
[항해 31일차] TIL_React 심화주차: Thunk (0) | 2023.09.11 |