Tanstack Query : HTTP 요청을 전송하고 프론트엔드 사용자 인터페이스를 백엔드 데이터와 동기화된 상태로 유지하는데 이용하는 라이브러리.
import { useEffect, useState } from "react";
import LoadingIndicator from "../UI/LoadingIndicator.jsx";
import ErrorBlock from "../UI/ErrorBlock.jsx";
import EventItem from "./EventItem.jsx";
export default function NewEventsSection() {
const [data, setData] = useState();
const [error, setError] = useState();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
async function fetchEvents() {
setIsLoading(true);
const response = await fetch("http://localhost:3000/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;
}
fetchEvents()
.then((events) => {
setData(events);
})
.catch((error) => {
setError(error);
})
.finally(() => {
setIsLoading(false);
});
}, []);
let content;
if (isLoading) {
content = <LoadingIndicator />;
}
if (error) {
content = (
<ErrorBlock title="An error occurred" 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>
);
}
useEffect, fetch를 이용해도 되지만 많은 양의 코드를 작성해야 한다.npm i @tanstack/react-queryexport async function fetchEvents() {
const response = await fetch("http://localhost:3000/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;
}
import { useQuery } from "@tanstack/react-query";
import { fetchEvents } from "../../util/http.js";
import LoadingIndicator from "../UI/LoadingIndicator.jsx";
import ErrorBlock from "../UI/ErrorBlock.jsx";
import EventItem from "./EventItem.jsx";
export default function NewEventsSection() {
const { data, isPending, isError, error } = useQuery({
queryKey: ["events"], // 모든 쿼리(전송하는 모든 GET HTTP 요청)에는 쿼리 키가 있다.
queryFn: fetchEvents, // 해당 함수를 이용해 실제 요청을 전송할 때 실행할 코드를 정의.
}); // 자체적으로 http 요청을 전송하고 해당 섹션에 필요한 이벤트 데이터를 가져오고 로딩 상태에 대한 정보를 제공한다.
let content;
if (isPending) {
content = <LoadingIndicator />;
}
if (isError) {
content = (
<ErrorBlock
title="An error occurred"
message={error.info?.message || "Failed to fetch event."}
/>
);
}
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>
);
}
QueryFn
QueryKey
useQuery로 부터 반환받는 객체에는
data 속성 : 실제 응답 데이터가 값으로 들어있다.isPending : 요청이 여전히 실행 중인지, 응답을 받았는지에 대한 속성isError : 오류 응답을 받은 경우 true. 요청에 대한 응답에 오류 상태 코드가 있는 경우 오류를 발행하도록 해야한다.error : 발생한 오류에 대한 정보가 포함된 속성(ex. 에러 메시지)refetch : 해당 함수를 수동으로 호출해 사용자가 버튼을 눌렀을 때 동일한 쿼리를 다시 전송할 수 있다.
useQuery 훅을 사용하려면 이러한 기능들을 사용할 컴포넌트를 Tanstack 쿼리가 제공하는 특수한 프로바이더 컴포넌트로 래핑해야한다.import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}

useQuery 훅을 이용해서 데이터를 받아오고 있다.const { data, isPending, isError, error } = useQuery({
queryKey: ["events"],
queryFn: fetchEvents,
staleTime: 5000,
gcTime: 30000,
});
staleTime : 캐시에 데이터가 있을 때 업데이트된 데이터를 가져오기 위한 요청을 자체적으로 전송하기 전에 기다릴 시간을 설정한다.(기본값 0 : 데이터를 업데이트하기 위한 자체적인 요청을 항상 전송)gcTime : garbage collection time → 데이터와 캐시를 얼마나 오랫동안 보관할 지를 제어한다. (기본값은 5분)export async function fetchEvents(searchTerm) {
let url = "http://localhost:3000/events";
if (searchTerm) {
url += "?search=" + searchTerm;
// 백엔드에서 검색을 위한 동적으로 해당 쿼리(?search=)는 useQuery에서 검색에 대한 쿼리동작이 구현되었을 때 사용되어야한다.
}
const response = await fetch(url);
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;
}
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();
const [searchTerm, setSearchTerm] = useState();
const { data, isPending, isError, error } = useQuery({
// searchTerm이 변경되면 다른 쿼리가 전송될 수 있도록 함.
queryKey: ["events", { search: searchTerm }],
queryFn: () => fetchEvents(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 event."}
/>
);
}
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="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>
);
}

useQuery 훅에서 발생하였다.const { data, isPending, isError, error } = useQuery({
queryKey: ["events"],
queryFn: fetchEvents,
staleTime: 5000,
});
useQuery 훅은 여기에 정의한 쿼리 함수에 기본 데이터를 전달하고 있다.export async function fetchEvents({ signal, searchTerm }) {
//...
}
signal : 요청 전송이 취소되는 것을 파악할 수 있다. 예를 들어 사용자가 페이지를 벗어나면 리액트 쿼리는 전송 중인 요청을 취소하려 할 것이다.const { data, isPending, isError, error } = useQuery({
queryKey: ["events", { search: searchTerm }],
queryFn: ({ signal }) => fetchEvents({ signal, searchTerm }),
});

useQuery를 FindEventSection으로 전송하지 않도록 하는 것이 좋다. 즉, 검색어를 입력할 때까지 쿼리를 비활성화하는 것이다.const { data, isLoading, isError, error } = useQuery({
queryKey: ["events", { search: searchTerm }],
queryFn: ({ signal }) => fetchEvents({ signal, searchTerm }),
enabled: searchTerm !== undefined, // false: 비활성화, true: 활성화(기본값)
});
if (isLoading) {
content = <LoadingIndicator />;
}
isLoading : 쿼리가 비활성화됐다고 해서 enabled 속성이 true가 되지는 않는다.(isPending과는 다른 점)
useMutation을 사용하여 데이터 변경 | 데이터 전송하기useQuery는 데이터를 가져올 때만 사용하고 이번엔 데이터를 전송하는 것이니까 useMutation를 사용한다.useQuery를 이용할 수는 있다.useMutation은 데이터를 변경하는 쿼리에 최적화 되어있다.useMutation은 useQuery와는 다르게 컴포넌트가 렌더링될 때 자동으로 요청을 보내지 않는다. 대신 요청을 언제 실행할 것인지를 차후에 반환할 mutate 함수로 지정해줘야 한다.export async function createNewEvent(eventData) {
const response = await fetch("http://localhost:3000/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(eventData),
});
if (!response.ok) {
const error = new Error("An error occurred while creating the event");
error.code = response.status;
error.info = await response.json();
throw error;
}
const { event } = await response.json();
return event;
}
import { Link, useNavigate } from "react-router-dom";
import { useMutation } from "@tanstack/react-query";
import { createNewEvent } from "../../util/http.js";
import Modal from "../UI/Modal.jsx";
import EventForm from "./EventForm.jsx";
import ErrorBlock from "../UI/ErrorBlock.jsx";
export default function NewEvent() {
const navigate = useNavigate();
const { mutate, isPending, isError, error } = useMutation({
mutationFn: createNewEvent, // mutationKey도 있으나 mutation 동작은 캐시 처리를 하지는 않는다.
});
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. Plz check your inputs and try again later."
}
/>
)}
</Modal>
);
}
useMutation에서 반환된 객체에는data : 전송된 요청의 응답으로 반환된 데이터mutate : 해당 훅을 사용하는 컴포넌트에서 어디서든 mutate 함수를 호출해 요청을 전송할 수 있다.isPending : true/falseisError : useQuery에서와 같다.error : 오류의 세부정보useMutation 테스트하기 | 이미지 추가하기export async function fetchSelectableImages({ signal }) {
const response = await fetch("http://localhost:3000/events/images", {
signal,
});
if (!response.ok) {
const error = new Error("An error occurred while fetching the images");
error.code = response.status;
error.info = await response.json();
throw error;
}
const { images } = await response.json();
return images;
}
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { fetchSelectableImages } from "../../util/http.js";
import ImagePicker from "../ImagePicker.jsx";
import ErrorBlock from "../UI/ErrorBlock.jsx";
export default function EventForm({ inputData, onSubmit, children }) {
const [selectedImage, setSelectedImage] = useState(inputData?.image);
const { data, isPending, isError } = useQuery({
queryKey: ["events-images"],
queryFn: fetchSelectableImages,
});
function handleSelectImage(image) {
setSelectedImage(image);
}
function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = Object.fromEntries(formData);
onSubmit({ ...data, image: selectedImage });
}
return (
<form id="event-form" onSubmit={handleSubmit}>
<p className="control">
<label htmlFor="title">Title</label>
<input
type="text"
id="title"
name="title"
defaultValue={inputData?.title ?? ""}
/>
</p>
{isPending && <p>Loading selectable images...</p>}
{isError && (
<ErrorBlock
title="Failed to load selectable images"
message="Plz try agiain later."
/>
)}
{data && (
<div className="control">
<ImagePicker
images={data}
onSelect={handleSelectImage}
selectedImage={selectedImage}
/>
</div>
)}
<p className="control">
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
defaultValue={inputData?.description ?? ""}
/>
</p>
<div className="controls-row">
<p className="control">
<label htmlFor="date">Date</label>
<input
type="date"
id="date"
name="date"
defaultValue={inputData?.date ?? ""}
/>
</p>
<p className="control">
<label htmlFor="time">Time</label>
<input
type="time"
id="time"
name="time"
defaultValue={inputData?.time ?? ""}
/>
</p>
</div>
<p className="control">
<label htmlFor="location">Location</label>
<input
type="text"
id="location"
name="location"
defaultValue={inputData?.location ?? ""}
/>
</p>
<p className="form-actions">{children}</p>
</form>
);
}

useMutation 성공 시의 동작 및 쿼리 무효화 | 새로운 이벤트 작성 완료import { Link, useNavigate } from "react-router-dom";
import { useMutation } from "@tanstack/react-query";
import { createNewEvent } from "../../util/http.js";
import Modal from "../UI/Modal.jsx";
import EventForm from "./EventForm.jsx";
import ErrorBlock from "../UI/ErrorBlock.jsx";
import { queryClient } from "../../util/http.js";
export default function NewEvent() {
const navigate = useNavigate();
const { mutate, isPending, isError, error } = useMutation({
mutationFn: createNewEvent, // mutationKey도 있으나 mutation 동작은 캐시 처리를 하지는 않는다.
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["events"],
});
navigate("/events"); // 성공했을 때 이동.
}, //mutation이 성공하면 해당 함수가 실행. => mutation이 성공할 때까지 화면에 계속 머묾
});
function handleSubmit(formData) {
mutate({ event: formData });
}
//...
}
onSuccess:()=>{} : mutation이 성공할 때까지 새로운 이벤트를 생성하는 모달에 계속 머물게 한다.
새로운 이벤트가 생성됨과 동시에 바로 데이터를 가져와서 화면에 렌더링해야한다. → 리액트 쿼리에서 제공하는 메서드를 이용해 하나 이상의 쿼리를 무효화하는 것이다.
즉, 데이터가 오래되었으니 업데이트가 필요함을 알려야한다.
queryClient.invalidateQueries({}) : 쿼리를 무효화한다. 현재 화면에 표시된 컴포넌트와 관련된 쿼리가 실행된 경우 특정 쿼리로 가져왔던 데이터가 오래되었으니 만료로 표시하고 즉시 다시 가져오라고 리액트 쿼리에게 알린다.queryKey: ['events'] : 이 키가 포함된 모든 쿼리를 무효화한다. events 라는 키워드가 있는 키는 모두 무효화.exact: true로 설정한다면 위에서의 queryKey가 events로 정확히 일치하는 쿼리만 무효화된다.// http.js
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient();
// App.jsx
import { queryClient } from "./util/http.js";
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
