React Query는 서버 상태 관리를 위한 강력한 라이브러리로, 효율적인 데이터 패칭, 캐싱, 그리고 동기화 기능을 제공한다. 이번 포스팅에서는 React Query의 주요 개념과 고급 기능을 정리하고, React Router Data API(이하 RRD의 loader)와의 콤비네이션 활용법도 함께 소개한다.
1. Query Key에 대한 이해
React Query의 queryKey는 데이터를 식별하는 고유한 키다. 데이터 캐싱, 무효화, 갱신 등의 작업은 모두 queryKey를 기준으로 이루어진다.
- 구조화된 Query Key: queryKey는 배열 형태로 작성하며, 데이터를 체계적으로 관리할 수 있다.
const { data } = useQuery({
queryKey: ["events", userId], // 배열 형태로 작성
queryFn: fetchEvents,
});
- 위 코드에서:
- "events"는 데이터 종류를 나타낸다.
- userId는 특정 사용자와 관련된 데이터를 식별한다.
- 중첩된 Key 사용: Key를 계층적으로 구성하면 특정 조건에 따라 데이터를 무효화하거나 다시 가져올 때 유리하다.
queryClient.invalidateQueries(["events"]); // 모든 이벤트 무효화
queryClient.invalidateQueries(["events", userId]); // 특정 사용자 데이터만 무효화
2. 낙관적 업데이트 (Optimistic Update)
낙관적 업데이트는 사용자가 실행한 변경 사항이 서버에서 처리되기 전에 UI에 즉시 반영되도록 한다. 이를 통해 빠른 사용자 경험을 제공할 수 있다.
- onMutate로 낙관적 업데이트 구현
const mutation = useMutation({ mutationFn: updateEvent, onMutate: async (updatedEvent) => { await queryClient.cancelQueries(["events"]); const previousData = queryClient.getQueryData(["events"]); queryClient.setQueryData(["events"], (old) => old.map((event) => event.id === updatedEvent.id ? { ...event, ...updatedEvent } : event ) ); return { previousData }; // 이전 데이터를 반환 }, onError: (error, updatedEvent, context) => { queryClient.setQueryData(["events"], context.previousData); // 에러 시 롤백 }, onSettled: () => { queryClient.invalidateQueries(["events"]); // 완료 후 데이터 재패칭 }, });
3. 에러 발생 시 롤백 (onError)
onMutate를 통해 낙관적 업데이트를 적용했지만, 요청이 실패하면 데이터를 롤백해야 한다. 이를 위해 onError를 활용한다.
- 에러 발생 시 이전 상태 복원
- onMutate에서 반환한 context 객체를 사용하여 상태를 복원한다.
onError: (error, updatedEvent, context) => { queryClient.setQueryData(["events"], context.previousData); console.error("Update failed:", error.message); },
4. onSettled()의 정확한 기능
onSettled는 Mutation이 성공하든 실패하든, 요청이 완료된 후 항상 실행된다. 이를 활용하면 데이터 무효화 같은 공통 작업을 한 곳에 처리할 수 있다.
- 예제
onSettled: () => { queryClient.invalidateQueries(["events"]); // 항상 데이터 새로 고침 },
5. 최대 데이터 수 제한하기 (최근 리스트 최대 개수 설정)
React Query에서 최근 데이터 리스트를 관리할 때, 보여지는 데이터의 최대 개수를 제한할 수 있다. 이를 통해 예를 들어 최근 리스트에 표시되는 데이터를 최신 3개로 제한하는 기능을 구현할 수 있다.
- 구현 예제
import { useQuery, useQueryClient } from "@tanstack/react-query";
function RecentEventList() {
const queryClient = useQueryClient();
const { data } = useQuery({
queryKey: ["recentEvents"],
queryFn: fetchRecentEvents,
onSuccess: (data) => {
// 최근 데이터의 최대 개수를 3개로 제한
queryClient.setQueryData(["recentEvents"], data.slice(0, 3));
},
});
return (
<ul>
{data?.map((event) => (
<li key={event.id}>{event.name}</li>
))}
</ul>
);
}
코드 설명
- onSuccess에서 데이터 제한
- onSuccess 콜백을 사용해 데이터를 가져온 후, slice 메서드를 이용하여 최대 3개의 데이터만 남긴다.
- queryClient.setQueryData를 활용해 데이터를 다시 저장한다.
- 최대 데이터 수 조정
- slice(0, 3)는 데이터 배열의 첫 번째부터 세 번째 데이터까지만 반환한다.
- 필요에 따라 개수를 조정하거나 조건을 추가할 수 있다.
추가: 최근 데이터 업데이트 및 캐싱
React Query의 데이터 갱신 메커니즘을 활용하면, 새로운 데이터가 추가될 때마다 최근 리스트를 동적으로 갱신할 수 있다.
- Mutation과 조합
const mutation = useMutation({ mutationFn: addNewEvent, onSuccess: (newEvent) => { queryClient.setQueryData(["recentEvents"], (old) => { const updated = [newEvent, ...(old || [])]; // 새 데이터 추가 return updated.slice(0, 3); // 최대 3개로 제한 }); }, }); const handleAddEvent = (event) => { mutation.mutate(event); };
설명
- 새로운 이벤트가 추가되면, 기존 데이터에 새 데이터를 맨 앞에 추가한 뒤 최대 3개로 제한한다.
- 이를 통해 항상 최신 데이터가 리스트에 표시된다.
이 코드를 적용하면 최근 리스트가 항상 최대 3개의 최신 데이터만 표시된다. 이 방법은 유저 인터페이스에서 보여지는 데이터를 제한하거나, 최근 활동 목록과 같은 기능을 구현할 때 매우 유용하다. 🎯
6. RRD의 Loader와 React Query의 조합
React Router Data API(RRD의 loader)와 React Query는 데이터를 초기 로딩과 동기화하는 데 훌륭한 조합을 이룬다.
- Loader에서 초기 데이터 제공 RRD의 loader를 사용해 서버에서 데이터를 가져온다.
// routes.js
export async function loader() {
const response = await fetch("http://localhost:3000/events");
return response.json();
}
- React Query와 결합 Loader에서 가져온 데이터를 React Query의 초기 데이터로 사용할 수 있다.
import { useLoaderData } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
function EventList() {
const loaderData = useLoaderData(); // Loader에서 가져온 초기 데이터
const { data } = useQuery({
queryKey: ["events"],
queryFn: fetchEvents,
initialData: loaderData, // 초기 데이터로 설정
});
return (
<ul>
{data.map((event) => (
<li key={event.id}>{event.name}</li>
))}
</ul>
);
}
또한 아래의 예시 코드에 내가 직접 사용한 구체적인 예시와 주석이 포함되어 있다.
import { Link, redirect, useNavigate, useNavigation, useParams, useSubmit } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { fetchEvent, queryClient, updateEvent } from "../../util/http.js";
import Modal from "../UI/Modal.jsx";
import EventForm from "./EventForm.jsx";
export default function EditEvent() {
const navigate = useNavigate();
const params = useParams();
const submit = useSubmit();
const { state } = useNavigation();
//이걸 계속 유지해도 좋은 이유는 캐시를 사용하기 때문에 다른 페이지에 갔다 다시 돌아와도 데이터를 다시 요청하지 않고 캐시를 사용해서 빠르게 데이터를 가져올 수 있음. 다만 loader에서도 불러오고 있으므로 중복을 방지하기 위해서 stale time을 사용함.
const { data } = useQuery({
queryKey: ["events", { id: params.id }],
queryFn: ({ signal }) => {
fetchEvent({ signal, id: params.id });
},
staleTime: 10000,
});
// const { mutate } = useMutation({
// mutationFn: updateEvent,
// onMutate: async (data) => {
// const newEvent = data.event;
// await queryClient.cancelQueries(["events", { id: params.id }]);
// const previousEvent = queryClient.getQueryData(["events", { id: params.id }]);
// queryClient.setQueryData(["events", { id: params.id }], newEvent);
// return { previousEvent };
// },
// onError: (error, variables, context) => {
// queryClient.setQueryData(["events", { id: params.id }], context.previousEvent);
// },
// onSettled: () => {
// queryClient.invalidateQueries(["events", { id: params.id }]);
// },
// });
function handleSubmit(formData) {
// mutate({ event: formData, id: params.id });
// navigate("../");
submit(formData, { method: "PUT" });
}
function handleClose() {
navigate("../");
}
return (
<Modal onClose={handleClose}>
<EventForm inputData={data} onSubmit={handleSubmit}>
{state === "submitting" ? (
<p>Updating event...</p>
) : (
<>
<Link to="../" className="button-text">
Cancel
</Link>
<button type="submit" className="button">
Update
</button>
</>
)}
</EventForm>
</Modal>
);
}
export function loader({ params }) {
return queryClient.fetchQuery({
queryKey: ["events", { id: params.id }],
queryFn: ({ signal }) => fetchEvent({ signal, id: params.id }),
});
}
export async function action({ request, params }) {
const formData = await request.formData();
const updatedEventData = Object.fromEntries(formData);
await updateEvent({ event: updatedEventData, id: params.id });
await queryClient.invalidateQueries(["events"]);
return redirect("../");
}
loader를 사용했음에도 useQuery를 지속시킨 이유는 캐시를 사용하기 위해서이고, 다만 데이터의 중복 요청을 방지하기 위해서
staleTime을 걸어줬고 이러면 다른 페이지에 다녀왔을때 데이터를 요청하고 캐시를 유지해서 해당 페이지의 값을 유지시킨다.
정리
React Query는 데이터 패칭과 상태 관리를 단순화하면서 강력한 기능을 제공한다.
- Query Key는 데이터 관리의 중심이며, 구조화된 Key를 활용해 유연성을 높일 수 있다.
- 낙관적 업데이트와 롤백 기능은 빠른 사용자 경험을 제공하면서 데이터 일관성을 유지한다.
- onSettled와 max 설정을 통해 작업 완료 시 후속 처리와 캐싱 정책을 제어할 수 있다.
- RRD와의 조합으로 React Router의 초기 로딩과 React Query의 상태 관리를 유기적으로 연결할 수 있다.
이 모든 기능을 적절히 활용하면 유지보수성과 확장성이 뛰어난 애플리케이션을 개발할 수 있다. 🚀
'Frontend Study' 카테고리의 다른 글
Typescript를 알아보자 (1) | 2025.01.16 |
---|---|
Query key에 대한 추가 설명 (0) | 2025.01.15 |
React _ Tanstack react Query (0) | 2025.01.13 |
React _ Tanstack은 무엇일까? (0) | 2025.01.13 |
React _ react-router-dom의 기능을 활용한 auth 관리 (0) | 2025.01.12 |