728x90
728x90
리액트 쿼리(Tanstack Query, React Query)
들어가며
- React Query라고 불린 Tanstack Query 라이브러리에 대해 정리해본다.
Tanstack Query(React Query)
개념
- 리액트 애플리케이션에서 서버 상태(데이터)를 쉽게 관리하고 조작할 수 있도록 도와주는 라이브러리
- React Query가 2022년 초에 Tanstack Query로 이름이 변경되었다.
- React Query가 포함된 Tanstack 생태계를 확장하고, 다양한 프레임워크와 플랫폼에서 사용할 수 있는 범용 데이터 페칭 및 상태 관리 라이브러리를 목표로 하기 위해서 였다고 한다.
- Tanstack Query는 이제 React뿐만 아니라 Vue, Svelte, Solid와 같은 다른 프레임워크에 대한 지원도 포함하고 있으며, Tanstack Query로의 이름 변경은 React Query의 기능적 변화보다는 브랜딩과 생태계 확장을 반영한 것이라고 한다.
- 자동 백그라운드 리페칭, 캐싱, 상태 관리, 에러 처리, 간단한 페이지네이션, 무한 스크롤 기능을 구현하는데 유용하다.
설치
- 터미널에 아래 명령을 실행하여 설치할 수 있다.
$ npm install @tanstack/react-query # yarn add @tanstack/react-query
주요 기능
데이터 페칭(Fetching Data)
- 데이터를 서버에서 가져와서 리액트 컴포넌트와 동기화하는 작업을 간단하게 처리할 수 있다.
- @useQuery@ 훅을 사용하여 데이터를 가져오고, 로딩 상태와 오류 상태를 쉽게 관리할 수 있다.
데이터 변조(Mutating Data)
- 데이터를 서버에 보내서 생성, 수정, 삭제하는 작업을 수행할 수 있다.
- @useMutation@ 훅을 사용하여 이러한 변조 작업을 간편하게 수행할 수 있다.
자동 리페치(Automatic Refetching)
- 사용자가 탭을 전환하거나 네트워크 상태가 변경될 때 데이터를 자동으로 다시 가져올 수 있다.
- 특정 이벤트나 시간 간격에 따라 자동으로 데이터를 새로 고칠 수 있다.
캐싱(Caching)
- 가져온 데이터를 캐시에 저장하여 동일한 데이터 요청 시 불필요한 네트워크 요청을 줄일 수 있다.
- 캐시된 데이터를 사용하면서 배경에서 새로운 데이터를 가져오는 기능을 제공한다.
병렬 및 의존 쿼리(Parallel and Dependent Queries)
- 여러 쿼리를 병렬로 실행하거나, 특정 쿼리가 완료된 후에 다른 쿼리를 실행할 수 있다.
쿼리 무효화(Query Invalidation)
- 특정 이벤트가 발생할 때 캐시된 데이터를 무효화하고, 데이터를 새로 가져오게 할 수 있다.
낙관적 업데이트(Optimistic Updates)
- 데이터를 변경할 때, 서버의 응답을 기다리지 않고 UI를 즉시 업데이트하여 사용자 경험을 향상시킬 수 있다.
Tanstack Query를 사용해야 하는 이유?
간편함
- 복잡한 상태 관리 로직을 단순화하여 코드의 가독성과 유지 보수성을 높인다.
- 내장된 훅을 사용하여 데이터 페칭과 상태 관리를 쉽게 처리할 수 있다.
효율성
- 캐싱과 자동 리페치 기능을 통해 불필요한 네트워크 요청을 줄이고, 애플리케이션의 성능을 향상시킨다.
확장성
- 병렬 및 의존 쿼리, 쿼리 무효화 등 고급 기능을 통해 복잡한 요구사항도 손쉽게 처리할 수 있다.
사용자 경험 향상
- 낙관적 업데이트, 자동 리페치 등의 기능을 통해 사용자에게 더욱 반응성 있는 UI를 제공할 수 있다.
설정 방법
- @main.jsx@ 파일에 다음과 같이 추가한다.
/src/main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
사용 방법
/src/utils/utils.js
import axios from 'axios';
const customFetch = axios.create({
baseURL: 'http://localhost:5001/api/tasks',
});
export default customFetch;
① @useQuery@ : GET
- 다음과 같이 @useQuery@를 @@tanstack/react-query@에서 import 한 후 사용한다.
- @useQuery@는 React Query에서 서버의 데이터를 가져오는 작업을 관리하는 데 사용하는 훅이다.
- 주로 서버에서 데이터를 읽기하는 작업(GET 요청)을 수행하고, 그 데이터를 컴포넌트에서 사용할 수 있게 해준다.
- @queryKey@
- 고유키(Unique Key)
- 애플리케이션 전반적으로 리페칭, 캐싱, 쿼리 공유 등을 위해 사용된다.
- @queryFn@
- 프로미스(Promise) 객체를 반환하는 함수
- 해당 함수는 데이터를 @resolve@ 하거나, 에러를 @throw@를 해야 한다.
import { useQuery } from '@tanstack/react-query';
const result = useQuery({
queryKey: ['tasks'],
queryFn: () => customFetch.get('/'),
});
console.log(result);
사용 예제
- 데이터가 불러와지는 중에는 @Loading...@이라는 문자가 표시되고, 데이터가 모두 불러와졌을 때 불러와진 데이터가 표시된다.
- @isError@는 에러가 발생했다는 메시지를 표시할 때 사용할 수 있고, @error@는 실제 에러 메시지의 내용을 표시할 때 사용할 수 있다.
import { useQuery } from '@tanstack/react-query';
import customFetch from '../utils/utils';
import SingleItem from './SingleItem';
const Items = () => {
// cf. React Query V5부터 isLoading 대신 isPending을 사용한다.
const { isPending, data, isError, error } = useQuery({
queryKey: ['tasks'],
queryFn: async () => {
const { data } = await customFetch.get('/');
return data;
},
});
if (isPending) {
return <p style={{ marginTop: '1rem' }}>Loading...</p>;
}
// 에러가 발생했다는 메시지 표시하기
if (isError) {
return <p style={{ marginTop: '1rem' }}>There was an error...</p>;
}
// 실제 에러 메시지 표시하기
// if (error) {
// return <p style={{ marginTop: '1rem' }}>{error.response.data}</p>;
// }
let fetchedData = data.taskList;
return (
<div className="items">
{fetchedData.map((item) => {
return <SingleItem key={item.id} item={item} />;
})}
</div>
);
};
export default Items;
② @useMutation@ : POST, PATCH, DELETE
- 다음과 같이 @useMutation@을 @@tanstack/react-query@에서 import 한 후 사용한다.
- @useMutation@은 React Query에서 데이터의 변경(생성, 수정, 삭제)과 같은 변경성 작업(Mutation)을 처리하는 데 사용하는 훅이다.
- 주로 서버에 데이터를 보내거나, 서버의 상태를 변경하는 요청을 보낼 때 사용된다.
- @useMutation@은 데이터를 읽기하는 @useQuery@와 달리, 수정/변경하는 작업을 관리한다.
- @mutationFn@
- 실제 데이터를 변경하는 함수
- @onSuccess@
- Helper Option
- 요청 성공 시 수행할 내용들 넣기
- @onError@
- Helper Option
- 요청 실패 시 수행할 내용들 넣기
import { useMutation } from '@tanstack/react-query';
const { mutate, isPending } = useMutation({
mutationFn: () => customFetch.post('/', { data }),
onSuccess: () => { ... },
onError: () => { ... },
});
사용 예제 : POST
- @useQueryClient@를 import 하여 다른 컴포넌트에서 @useQuery@를 통해 캐싱한 데이터에 접근할 수 있다.
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
const { mutate: createTask, isPending } = useMutation({
// ...
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
// ...
});
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import customFetch from '../utils/utils';
const Form = () => {
const [newItemName, setNewItemName] = useState('');
const queryClient = useQueryClient();
const { mutate: createTask, isPending } = useMutation({
mutationFn: (taskTitle) => {
return customFetch.post('/', { title: taskTitle });
},
onSuccess: () => {
if (newItemName) {
// 기존의 캐시된 데이터를 무효화하고, 해당 쿼리에 대한 데이터를 새로고침하기
queryClient.invalidateQueries({ queryKey: ['tasks'] });
// 성공 메시지 표시하기
toast.success('task added.');
// 입력창 비우기 및 화면 리랜더링
setNewItemName('');
}
},
onError: (error) => {
// 에러 메시지 표시하기
toast.error(error.response.data.msg);
},
});
const handleSubmit = async (e) => {
e.preventDefault();
// 작업 추가하기
createTask(newItemName);
};
return (
<form onSubmit={handleSubmit}>
<h4>task bud</h4>
<div className="form-control">
<input
type="text "
className="form-input"
value={newItemName}
onChange={(event) => setNewItemName(event.target.value)}
/>
<button type="submit" className="btn" disabled={isPending}>
add task
</button>
</div>
</form>
);
};
export default Form;
queryClient.invalidateQueries({ queryKey: ['tasks'] });는 React Query에서 캐시된 데이터를 무효화하고, 해당 쿼리에 대한 데이터를 새로고침하도록 지시한다.
사용 예제 : PATCH, DELETE
import { useMutation, useQueryClient } from '@tanstack/react-query';
import customFetch from '../utils/utils';
const SingleItem = ({ item }) => {
const queryClient = useQueryClient();
// PATCH : 수정
const { mutate: editTask } = useMutation({
mutationFn: ({ taskId, isDone }) => {
return customFetch.patch(`/${taskId}`, { isDone });
},
onSuccess: () => {
// 기존의 캐시 무효화 및 새로고침
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
// DELETE : 삭제
const { mutate: deleteTask, isPending } = useMutation({
mutationFn: ({ taskId }) => {
return customFetch.delete(`/${taskId}`);
},
onSuccess: () => {
// 기존의 캐시 무효화 및 새로고침
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
return (
<div className="single-item">
<input
type="checkbox"
checked={item.isDone}
onChange={() => editTask({ taskId: item.id, isDone: !item.isDone })}
/>
<p
style={{
textTransform: 'capitalize',
textDecoration: item.isDone && 'line-through',
}}
>
{item.title}
</p>
<button
className="btn remove-btn"
type="button"
disabled={isPending}
onClick={() => deleteTask({ taskId: item.id })}
>
delete
</button>
</div>
);
};
export default SingleItem;
(참고) 커스텀 훅으로 만들어서 사용하기
- 위의 코드들을 다음과 같이 커스텀 훅으로 만들어서 사용할 수 있다.
/src/hooks/reactQueryCustomHooks.jsx
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import customFetch from '../utils/utils';
// GET : 데이터 불러오기
export const useFetchTasks = () => {
const { isPending, data, isError, error } = useQuery({
queryKey: ['tasks'],
queryFn: async () => {
const { data } = await customFetch.get('/');
return data;
},
});
return { isPending, isError, error, data };
};
// POST : 데이터 생성
export const useCreateTask = () => {
const queryClient = useQueryClient();
const { mutate: createTask, isPending: createTaskLoading } = useMutation({
mutationFn: (taskTitle) => {
return customFetch.post('/', { title: taskTitle });
},
onSuccess: () => {
// 기존의 캐시된 데이터를 무효화하고, 해당 쿼리에 대한 데이터를 새로고침하기
queryClient.invalidateQueries({ queryKey: ['tasks'] });
// 성공 메시지 표시하기
toast.success('task added.');
},
onError: (error) => {
// 에러 메시지 표시하기
toast.error(error.response.data.msg);
},
});
return { createTask, createTaskLoading };
};
// PATCH : 데이터 수정
export const useEditTask = () => {
const queryClient = useQueryClient();
const { mutate: editTask } = useMutation({
mutationFn: ({ taskId, isDone }) => {
return customFetch.patch(`/${taskId}`, { isDone });
},
onSuccess: () => {
// 기존의 캐시 무효화 및 새로고침
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
return { editTask };
};
// DELETE : 데이터 삭제
export const useDeleteTask = () => {
const queryClient = useQueryClient();
const { mutate: deleteTask, isPending: deleteTaskLoading } = useMutation({
mutationFn: ({ taskId }) => {
return customFetch.delete(`/${taskId}`);
},
onSuccess: () => {
// 기존의 캐시 무효화 및 새로고침
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
return { deleteTask, deleteTaskLoading };
};
- 나머지 코드 확인하기
더보기
/src/components/Items.jsx
import { useFetchTasks } from '../hooks/reactQueryCustomHooks';
import SingleItem from './SingleItem';
const Items = () => {
// 커스텀 훅 불러오기
const { isPending, isError, error, data } = useFetchTasks();
if (isPending) {
return <p style={{ marginTop: '1rem' }}>Loading...</p>;
}
// 에러가 발생했다는 메시지 표시하기
if (isError) {
return <p style={{ marginTop: '1rem' }}>There was an error...</p>;
}
// 실제 에러 메시지 표시하기
// if (error) {
// return <p style={{ marginTop: '1rem' }}>{error.response.data}</p>;
// }
const fetchedData = data.taskList;
return (
<div className="items">
{fetchedData.map((item) => {
return <SingleItem key={item.id} item={item} />;
})}
</div>
);
};
export default Items;
/src/components/Form.jsx
import { useState } from 'react';
import { useCreateTask } from '../hooks/reactQueryCustomHooks';
const Form = () => {
const [newItemName, setNewItemName] = useState('');
const { createTaskLoading, createTask } = useCreateTask();
const handleSubmit = (e) => {
e.preventDefault();
// 작업 추가
// -> createTask의 두 번째 인자에 작업 수행 후 수행할 작업을 추가할 수 있다.
createTask(newItemName, {
onSuccess: () => {
setNewItemName('');
},
});
};
return (
<form onSubmit={handleSubmit}>
<h4>task bud</h4>
<div className="form-control">
<input
type="text "
className="form-input"
value={newItemName}
onChange={(event) => setNewItemName(event.target.value)}
/>
<button type="submit" className="btn" disabled={createTaskLoading}>
add task
</button>
</div>
</form>
);
};
export default Form;
⇒ @mutate(value, {})@에서 두 번째 인자(@{}@)에 mutate 작업이 수행된 후, 수행할 작업들을 추가할 수 있다.
/src/components/SingleItem.jsx
import { useDeleteTask, useEditTask } from '../hooks/reactQueryCustomHooks';
const SingleItem = ({ item }) => {
const { editTask } = useEditTask();
const { deleteTask, deleteTaskLoading } = useDeleteTask();
return (
<div className="single-item">
<input
type="checkbox"
checked={item.isDone}
onChange={() => editTask({ taskId: item.id, isDone: !item.isDone })}
/>
<p
style={{
textTransform: 'capitalize',
textDecoration: item.isDone && 'line-through',
}}
>
{item.title}
</p>
<button
className="btn remove-btn"
type="button"
disabled={deleteTaskLoading}
onClick={() => deleteTask({ taskId: item.id })}
>
delete
</button>
</div>
);
};
export default SingleItem;
예제 코드
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';
// QueryClient 인스턴스 생성
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyComponent />
</QueryClientProvider>
);
}
function MyComponent() {
// 데이터 페칭
const { data, error, isLoading } = useQuery('fetchData', fetchData);
if (isLoading) return <span>Loading...</span>;
if (error) return <span>Error: {error.message}</span>;
return <div>Data: {JSON.stringify(data)}</div>;
}
async function fetchData() {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
참고
- React Query V4까지 사용되었던 @isLoading@ 속성이 V5부터는 @isPending@으로 바뀌었다.
- Tanstack Query의 사용은 필수가 아니다. @useEffect@와 @fetch@를 이용하여 동일한 기능을 언제든지 만들 수 있다.
참고 사이트
728x90
728x90
'Programming > React' 카테고리의 다른 글
[React.js] Compound Component 패턴 (0) | 2024.08.12 |
---|---|
[React.js] Framer Motion 라이브러리 (0) | 2024.08.11 |
[React.js] Suspense 컴포넌트 (0) | 2024.08.06 |
[React.js] 낙관적 업데이트(Optimistic Updates) (React Query) (0) | 2024.07.10 |
[React.js] 지연 로딩(Lazy Loading) (0) | 2024.07.08 |
[React.js] useSearchParams 훅 (React Router) (0) | 2024.07.08 |
[React.js] defer() (React Router) (0) | 2024.07.07 |
[React.js] useFetcher 훅 (React Router) (0) | 2024.07.07 |