목차
간단한 설명은 아래 페이지에 있다.
참고
https://be-senior-developer.tistory.com/94
다음과 같이 QueryClientProvider를 사용해 주자
useQuery로 데이터 불러오기
main.jsx
import {
Navigate,
RouterProvider,
createBrowserRouter,
} from "react-router-dom";
...
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
const router = createBrowserRouter([
{
path: "/",
element: <Navigate to="/events" />,
},
...
]);
const queryClient = new QueryClient();
function Query() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
export default Query;
여기서 queryFn에 들어가는 함수는 비동기 함수여야 하며,
따라서 async 키워드를 사용하거나 프로미스를 반환해야 한다.
NewEventsSection.jsx
import LoadingIndicator from "../UI/LoadingIndicator.jsx";
import ErrorBlock from "../UI/ErrorBlock.jsx";
import EventItem from "./EventItem.jsx";
//자체적으로 작동해서 필요한 데이터를 가져오고 로딩 상태에 대한 정보를 제공한다.
import { useQuery } from "@tanstack/react-query";
import { fetchEvents } from "../../util/http.js";
export default function NewEventsSection() {
//쿼리키를 통해 요청으로 생성된 데이터를 캐시 처리한다.
const { data, isPending, isError, error } = useQuery({
queryKey: ["events"],
//그 전에 캐시 처리한 데이터를 다시 사용하고 업데이트된 요청 확인
queryFn: fetchEvents,
});
let content;
if (isPending) {
content = <LoadingIndicator />;
}
if (error) {
content = (
<ErrorBlock
title="An error occurred"
message={error.info?.message || "Failed to fetch events."}
/>
);
}
if (data) {
content = (
<ul className="events-list">
{data.map((event) => (
<li key={event.id}>
<EventItem event={event} />
</li>
))}
</ul>
);
}
return (
<section className="content-section" id="new-events-section">
<header>
<h2>Recently added events</h2>
</header>
{content}
</section>
);
}
아래 보이는 것과 같이 데이터 요청 비동기 함수이다.
fetchEvents.js
export async function fetchEvents() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
const error = new Error("An error occurred while fetching the events");
error.code = response.status;
error.info = await response.json();
throw error;
}
const { events } = await response.json();
return events;
}
이런식으로 캐시 사을 중지 하고 다시 페이지로 오면 모든 리소스를 다시 불러오는 것을 볼 수 있다.
쿼리를 이용한 캐시 사용을 허용했을때도 똑같이 요청이 보내지지만 이 요청은 다르다.
내부적으로 요청을 전송해서 업데이트 된 요소가 있는지 확인을 위함이다.
그렇다면 이 요청을 제어할려면 어떻게 하면 좋을까?
staleTime
바로 staleTime을 이용하면 된다. 쉽게 말하면 전송 요청 지연 시간으로 설정 시간 이내로 컴포넌트가 랜더링 될 경우
요청을 보내지 않는다.
수정한 코드
//쿼리키를 통해 요청으로 생성된 데이터를 캐시 처리한다.
const { data, isPending, isError, error } = useQuery({
queryKey: ["events"],
//그 전에 캐시 처리한 데이터를 다시 사용하고 업데이트된 요청 확인
queryFn: fetchEvents,
//캐시에 데이터가 있을 때 업데이트 된 데이터를 가져오기 위한 전송요청 지연시간
staleTime: 10000,
//캐싱 데이터 유지 시간
gcTime: 30000
});
우리는 동적으로 값을 얻고 싶을 때가 있다. 이럴때 유동적으로queryKey를 넣어주면 된다.
import { useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { fetchEvents } from "../../util/http";
import LoadingIndicator from "../UI/LoadingIndicator";
import ErrorBlock from "../UI/ErrorBlock";
import EventItem from "./EventItem";
export default function FindEventSection() {
//useRef를 통해 하면 바로 컴포넌트에 반영되지 않기 때문에 useState를 써주어야함
const searchElement = useRef();
const [searchTerm, setSearchTerm] = useState("");
//함수 인자를 전달해야 하기 떄문에 화살표함수로 바꿈
const { data, isPending, isError, error } = useQuery({
queryKey: ["events", { search: searchTerm }],
queryFn: ({ signal }) => fetchEvents({ signal, searchTerm }),
});
function handleSubmit(event) {
event.preventDefault();
setSearchTerm(searchElement.current.value);
}
let content = <p>Please enter a search term and to find events.</p>;
if (isPending) {
content = <LoadingIndicator />;
}
if (isError) {
content = (
<ErrorBlock
title={"An error occured"}
message={error.info?.message || "Failed to fetch events"}
/>
);
}
if (data) {
content = (
<ul className="events-list">
{data.map((event) => (
<li key={event.id}>
<EventItem event={event}></EventItem>
</li>
))}
</ul>
);
}
return (
<section className="content-section" id="all-events-section">
<header>
<h2>Find your next event!</h2>
<form onSubmit={handleSubmit} id="search-form">
<input
type="search"
placeholder="Search events"
ref={searchElement}
/>
<button>Search</button>
</form>
</header>
{content}
</section>
);
}
위의 코드를 보면 signal를 함수 인자로 주었는데 요청이 완료되기 전에
사용자가 페이지에 나갈 경우 요청을 취소할 수 있도록 주는 신호이다.
따라서 요청을 주는 함수도 다음과 같이 수정할 수 있다.
fetchEvents.jsx
export async function fetchEvents({ signal, searchTerm }) {
let url = "http://localhost:8080/events";
if (searchTerm) {
url += "?search=" + searchTerm;
}
console.log(signal);
const response = await fetch(url, { signal: signal });
if (!response.ok) {
const error = new Error("An error occurred while fetching the events");
error.code = response.status;
error.info = await response.json();
throw error;
}
const { events } = await response.json();
return events;
}
isLoading vs isPending
- isLoading은 쿼리가 비활성화 되었다고 해서 true가 반환되지 않는다.
- isPending은 쿼리가 비활성화 되었을 경우 true를 반환한다.
그렇다면 다음 코드는 검색을 위한 코드인데
우리가 원하는 동작은 다음과 같다.
따라서 enabled를 통해 다음과 같이 쿼리를 조작해 주면 된다.
1. 초기에는 state에 아무값도 넣지 않아 udefined를 만들어 준다.
2. 그 후 값에 빈 문자열을 넣게 되면 undefined가 아니므로 작동한다.
3. isPending 대신 isLoading을 넣어 쿼리 비활성화시에 로딩창이 뜨지 않도록 한다.
import { useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { fetchEvents } from "../../util/http";
import LoadingIndicator from "../UI/LoadingIndicator";
import ErrorBlock from "../UI/ErrorBlock";
import EventItem from "./EventItem";
export default function FindEventSection() {
const searchElement = useRef();
//초기값은 undefined
const [searchTerm, setSearchTerm] = useState();
const { data, isLoading, isError, error } = useQuery({
queryKey: ["events", { search: searchTerm }],
queryFn: ({ signal }) => fetchEvents({ signal, searchTerm }),
//쿼리 활성화를 위한 코드
enabled: searchTerm !== undefined,
});
function handleSubmit(event) {
event.preventDefault();
setSearchTerm(searchElement.current.value);
}
let content = <p>Please enter a search term and to find events.</p>;
if (isLoading) {
content = <LoadingIndicator />;
}
if (isError) {
content = (
<ErrorBlock
title={"An error occured"}
message={error.info?.message || "Failed to fetch events"}
/>
);
}
if (data) {
content = (
<ul className="events-list">
{data.map((event) => (
<li key={event.id}>
<EventItem event={event}></EventItem>
</li>
))}
</ul>
);
}
return (
<section className="content-section" id="all-events-section">
<header>
<h2>Find your next event!</h2>
<form onSubmit={handleSubmit} id="search-form">
<input
type="search"
placeholder="Search events"
ref={searchElement}
/>
<button>Search</button>
</form>
</header>
{content}
</section>
);
}
useMutation을 통해 데이터 전송하기
import { Link, useNavigate } from "react-router-dom";
import { createNewEvent } from "../../util/http.js";
import Modal from "../UI/Modal.jsx";
import EventForm from "./EventForm.jsx";
import { useMutation } from "@tanstack/react-query";
import ErrorBlock from "../UI/ErrorBlock.jsx";
import { queryClient } from "../../util/http.js";
export default function NewEvent() {
const navigate = useNavigate();
//변형은 응답 데이터를 캐시 처리하지 않기 때문에 꼭 키가 필요한 것은 아님
//mutate를 통해 실행 시점을 결정해 주어야 함
const { isError, isPending, error, mutate } = useMutation({
mutationFn: createNewEvent,
onSuccess: () => {
//데이터를 만료시켜서 새로운 데이터 트리거를 유도함
//events를 포함하는 모든 데이터를 만료 -> 특정 데이터만 하고 싶으면 exact: true를 추가한다.
queryClient.invalidateQueries({ queryKey: ["events"] });
navigate("/events");
},
});
function handleSubmit(formData) {
mutate({ event: formData });
}
return (
<Modal onClose={() => navigate("../")}>
<EventForm onSubmit={handleSubmit}>
{isPending && "Submitting..."}
{!isPending && (
<>
<Link to="../" className="button-text">
Cancel
</Link>
<button type="submit" className="button">
Create
</button>
</>
)}
</EventForm>
{isError && (
<ErrorBlock
title={"Failed to create event"}
message={error.info?.message || "Failed to create event. "}
></ErrorBlock>
)}
</Modal>
);
}
이런식으로 queryClient에 접근가능하게끔 만들어 주자
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient();
삭제 구현 시 주의점
refetchType: none으로 설정을 해주게 되면 페이지 삭제를 하고 쿼리 무효화를 할 시
그 페이지에서 다시 쿼리를 불러오지 않는다.
const { mutate } = useMutation({
mutationFn: deleteEvent,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["events"],
refetchType: "none",
});
navigate("/events");
},
});
낙관적 업데이트 하기
종종 데이터 수정을 할때 유효한 값을 넣지 않으면 다시 롤백해야 하는 경우가 있다.
이럴 경우에 어떻게 해야 할지 알아보자
const {
isError: editIsError,
isPending: editLoading,
error: editError,
mutate,
} = useMutation({
mutationFn: updateEvent,
//mutate가 실행하자마자 작동함
onMutate: async (data) => {
const newEvent = data.event;
//특정 쿼리키의 데이터를 가져옴(롤백용)
const previousEvent = queryClient.getQueryData(["events", { id: id }]);
//특정 키의 모든 활성 쿼리를 취소
//해당 쿼리 응답 데이터와 낙관적 업데이트 쿼리가 충돌 x
await queryClient.cancelQueries({ queryKey: ["events", { id: id }] });
queryClient.setQueryData(["events", { id: id }], newEvent);
//return한 값은 context에 제공
return { previousEvent };
},
//에러가 날 경우
onError: async (error, data, context) => {
queryClient.setQueryData(["events", { id: id }], context.previousEvent);
},
//성공에 상관없이 mutation이 완료될때마다 실행
//항상 백엔드와 프론트엔드의 데이터 동기화
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["events", { id: id }] });
},
});
'코딩 정보 > React' 카테고리의 다른 글
[React][React-Query] React-Query를 통해 fallback을 구현해 보자 (0) | 2024.08.27 |
---|---|
[React][React Pattern] 컴파운드 컴포넌트에 대해 알아보자 (0) | 2024.08.26 |
[React] 배포 과정에 대해서 알아보자 (0) | 2024.08.24 |
[React] 로그인 시 토큰 기반 인증 만들어보기 (0) | 2024.08.24 |
[React][Router] 라우터 활용을 통한 비동기 통신 (하) (0) | 2024.08.22 |