🔗 레파지토리에서 커밋 히스토리 순으로 보기
📌데이터 제출
📖 action() 사용하기
import { useNavigate, Form } from "react-router-dom";
import classes from "./EventForm.module.css";
function EventForm({ method, event }) {
const navigate = useNavigate();
function cancelHandler() {
navigate("..");
}
return (
<Form method="post" className={classes.form}>
<p>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
name="title"
required
defaultValue={event ? event.title : ""}
/>
</p>
<p>
<label htmlFor="image">Image</label>
<input
id="image"
type="url"
name="image"
required
defaultValue={event ? event.image : ""}
/>
</p>
<p>
<label htmlFor="date">Date</label>
<input
id="date"
type="date"
name="date"
required
defaultValue={event ? event.date : ""}
/>
</p>
<p>
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
rows="5"
required
defaultValue={event ? event.description : ""}
/>
</p>
<div className={classes.actions}>
<button type="button" onClick={cancelHandler}>
Cancel
</button>
<button>Save</button>
</div>
</Form>
);
}
export default EventForm;
Form은 백엔드로 요청하는 브라우저 기본값을 생략하게 만들고 대신에 전송되었을 요청들을 받아서 액션(action)에 준다. 이때, 각 input에 name 속성이 있어야한다.
💎 NewEventPage.js
import EventForm from "../components/EventForm";
import { json, redirect } from "react-router-dom";
function NewEventPage() {
return <EventForm />;
}
export default NewEventPage;
export async function action({ request, params }) {
const data = await request.formData();
const eventData = {
title: data.get("title"),
image: data.get("image"),
date: data.get("date"),
description: data.get("description"),
};
const response = await fetch("http://localhost:8080/events", {
method: "POST",
body: JSON.stringify(eventData),
headers: {
"Content-Type": "application/json",
},
});
console.log(response);
if (!response.ok) {
throw json(
{ message: "데이터를 전송하는데 실패했습니다." },
{ status: 500 }
);
}
return redirect("/events");
}
action()로 로더 함수처럼 리액트 라우터에 의해서 실행되고 유용한 프로퍼티(request, params)들이 포함된 객체를 받는다.
💎 App.js
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import RootPage from "./pages/RootPage";
import HomePage from "./pages/HomePage";
import EventsPage, { loader as eventsLoader } from "./pages/Events";
import EventDetailPage, {
loader as eventDetailLoader,
} from "./pages/EventDetailPage";
import NewEventPage, { action as newEventAction } from "./pages/NewEventPage";
import EditEventPage from "./pages/EditEventPage";
import EventsRootLayout from "./pages/EventRoot";
import ErrorPage from "./pages/Error";
const router = createBrowserRouter([
{
path: "/",
element: <RootPage />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventsRootLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: eventsLoader,
},
{
path: ":id",
id: "event-detail",
loader: eventDetailLoader,
children: [
{
index: true,
element: <EventDetailPage />,
},
{ path: "edit", element: <EditEventPage /> },
],
},
{ path: "new", element: <NewEventPage />, action: newEventAction },
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;

📖 프로그램적으로 데이터 제출하기(삭제하기) | action()을 트리거하는 또다른 방법
💎 EventDetailPage.js
import {
useRouteLoaderData,
json,
useParams,
redirect,
} from "react-router-dom";
import EventItem from "../components/EventItem";
function EventDetailPage() {
const data = useRouteLoaderData("event-detail");
return <EventItem event={data.event} />;
}
export default EventDetailPage;
export async function loader({ request, params }) {
const id = params.id;
const response = await fetch("http://localhost:8080/events/" + id);
if (!response.ok) {
throw json(
{ message: "이벤트 디테일에 대한 정보를 받아올 수 없습니다." },
{ status: 500 }
);
} else {
return response;
}
}
export async function action({ request, params }) {
const id = params.id;
const method = request.method;
const response = await fetch("http://localhost:8080/events/" + id, {
method: method,
});
if (!response.ok) {
throw json(
{ message: "이벤트를 삭제하는데 실패했습니다." },
{ status: 500 }
);
}
return redirect("/events");
}
- action 함수를 적고, 해당 액션의 method를 받아와서 동작하도록 하였다.
💎 App.js
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import RootPage from "./pages/RootPage";
import HomePage from "./pages/HomePage";
import EventsPage, { loader as eventsLoader } from "./pages/Events";
import EventDetailPage, {
loader as eventDetailLoader,
action as deleteEventAction,
} from "./pages/EventDetailPage";
import NewEventPage, { action as newEventAction } from "./pages/NewEventPage";
import EditEventPage from "./pages/EditEventPage";
import EventsRootLayout from "./pages/EventRoot";
import ErrorPage from "./pages/Error";
const router = createBrowserRouter([
{
path: "/",
element: <RootPage />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventsRootLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: eventsLoader,
},
{
path: ":id",
id: "event-detail",
loader: eventDetailLoader,
children: [
{
index: true,
element: <EventDetailPage />,
action: deleteEventAction,
},
{ path: "edit", element: <EditEventPage /> },
],
},
{ path: "new", element: <NewEventPage />, action: newEventAction },
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
💎 EventItem.js
import classes from "./EventItem.module.css";
import { Link, useSubmit } from "react-router-dom";
function EventItem({ event }) {
const submit = useSubmit();
function startDeleteHandler() {
const proceed = window.confirm("Are you sure?");
if (proceed) {
submit(null, { method: "delete" });
}
}
return (
<article className={classes.event}>
<img src={event.image} alt={event.title} />
<h1>{event.title}</h1>
<time>{event.date}</time>
<p>{event.description}</p>
<menu className={classes.actions}>
<Link to="edit">Edit</Link>
<button onClick={startDeleteHandler}>Delete</button>
</menu>
</article>
);
}
export default EventItem;
- 버튼이 눌렸을 때
startDeleteHandler 함수가 동작한다.
- 사용자가 정말로 삭제를 원하는지 한번 더 물어본다 (proceed)
- proceed가 true 이면, 삭제 동작을 한다. 이때, useSubmit 훅을 사용한다.
- submit 함수에서 우리는 삭제만을 원하기 때문에 별도의 데이터를 전달하지 않고 null을 전달한다.
- submit 함수에서 메서드와 액션 키를 통해서 동작을 제어할 수 있다. 만일 action이 다른 라우트 경로에서 정의되었다면 다른 경로로 지정할 수 있으나 우리의 경우, EventItem과 action이 같은 라우트 내에 정의 되었다.

📖 폼의 제출 상태를 이용하여 UI 상태 업데이트 하기
import { useNavigate, Form, useNavigation } from "react-router-dom";
import classes from "./EventForm.module.css";
function EventForm({ method, event }) {
const navigate = useNavigate();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
function cancelHandler() {
navigate("..");
}
return (
<Form method="post" className={classes.form}>
<p>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
name="title"
required
defaultValue={event ? event.title : ""}
/>
</p>
<p>
<label htmlFor="image">Image</label>
<input
id="image"
type="url"
name="image"
required
defaultValue={event ? event.image : ""}
/>
</p>
<p>
<label htmlFor="date">Date</label>
<input
id="date"
type="date"
name="date"
required
defaultValue={event ? event.date : ""}
/>
</p>
<p>
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
rows="5"
required
defaultValue={event ? event.description : ""}
/>
</p>
<div className={classes.actions}>
{}
<button type="button" onClick={cancelHandler} disabled={isSubmitting}>
취소하기
</button>
<button disabled={isSubmitting}>
{isSubmitting ? "저장 중..." : "저장하기"}
</button>
</div>
</Form>
);
}
export default EventForm;
- 작성한 폼을 제출 중(Save)이라면 버튼 disabled하고 '저장 중' 이라는 문구 띄우기

📖 사용자 입력을 검증하고 검증 요류 출력하기
💎 NewEventPage.js
import EventForm from "../components/EventForm";
import { json, redirect } from "react-router-dom";
function NewEventPage() {
return <EventForm />;
}
export default NewEventPage;
export async function action({ request, params }) {
const data = await request.formData();
const eventData = {
title: data.get("title"),
image: data.get("image"),
date: data.get("date"),
description: data.get("description"),
};
const response = await fetch("http://localhost:8080/events", {
method: "POST",
body: JSON.stringify(eventData),
headers: {
"Content-Type": "application/json",
},
});
if (response.status === 422) {
return response;
}
if (!response.ok) {
throw json(
{ message: "데이터를 전송하는데 실패했습니다." },
{ status: 500 }
);
}
return redirect("/events");
}
- 로더와 마찬가지로 리턴된 action 데이터도 페이지와 컴포넌트에서 사용할 수 있다.
import {
useNavigate,
Form,
useNavigation,
useActionData,
} from "react-router-dom";
import classes from "./EventForm.module.css";
function EventForm({ method, event }) {
const data = useActionData();
const navigate = useNavigate();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
function cancelHandler() {
navigate("..");
}
return (
<Form method="post" className={classes.form}>
{}
{data && data.errors && (
<ul>
{Object.values(data.errors).map((error) => (
<li key={error}>{error}</li>
))}
</ul>
)}
<p>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
name="title"
required
defaultValue={event ? event.title : ""}
/>
</p>
<p>
<label htmlFor="image">Image</label>
<input
id="image"
type="url"
name="image"
required
defaultValue={event ? event.image : ""}
/>
</p>
<p>
<label htmlFor="date">Date</label>
<input
id="date"
type="date"
name="date"
required
defaultValue={event ? event.date : ""}
/>
</p>
<p>
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
rows="5"
required
defaultValue={event ? event.description : ""}
/>
</p>
<div className={classes.actions}>
<button type="button" onClick={cancelHandler} disabled={isSubmitting}>
취소하기
</button>
<button disabled={isSubmitting}>
{isSubmitting ? "저장 중..." : "저장하기"}
</button>
</div>
</Form>
);
}
export default EventForm;
useActionData : action이 리턴한 데이터에 엑세스 할 수 있다.

📖 액션 재사용하기 | EditEventPage
- EditEvent는 새로운 NewEvent를 생성하는 action과 꽤 비슷하다.
- 이 액션을 재사용하면 좋을 듯 하다.
💎 NewEventPage.js
import EventForm from "../components/EventForm";
function NewEventPage() {
return <EventForm method="post" />;
}
export default NewEventPage;
- action 함수를 EventForm.js로 이동.
import {
useNavigate,
Form,
useNavigation,
useActionData,
json,
redirect,
} from "react-router-dom";
import classes from "./EventForm.module.css";
function EventForm({ method, event }) {
const data = useActionData();
const navigate = useNavigate();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
function cancelHandler() {
navigate("..");
}
return (
<Form method={method} className={classes.form}>
{data && data.errors && (
<ul>
{Object.values(data.errors).map((error) => (
<li key={error}>{error}</li>
))}
</ul>
)}
<p>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
name="title"
required
defaultValue={event ? event.title : ""}
/>
</p>
<p>
<label htmlFor="image">Image</label>
<input
id="image"
type="url"
name="image"
required
defaultValue={event ? event.image : ""}
/>
</p>
<p>
<label htmlFor="date">Date</label>
<input
id="date"
type="date"
name="date"
required
defaultValue={event ? event.date : ""}
/>
</p>
<p>
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
rows="5"
required
defaultValue={event ? event.description : ""}
/>
</p>
<div className={classes.actions}>
<button type="button" onClick={cancelHandler} disabled={isSubmitting}>
취소하기
</button>
<button disabled={isSubmitting}>
{isSubmitting ? "저장 중..." : "저장하기"}
</button>
</div>
</Form>
);
}
export default EventForm;
export async function action({ request, params }) {
const method = request.method;
const data = await request.formData();
const eventData = {
title: data.get("title"),
image: data.get("image"),
date: data.get("date"),
description: data.get("description"),
};
let url = "http://localhost:8080/events";
if (method === "PATCH") {
const eventId = params.id;
url = "http://localhost:8080/events/" + eventId;
}
const response = await fetch(url, {
method: method,
body: JSON.stringify(eventData),
headers: {
"Content-Type": "application/json",
},
});
if (response.status === 422) {
return response;
}
if (!response.ok) {
throw json(
{ message: "데이터를 전송하는데 실패했습니다." },
{ status: 500 }
);
}
return redirect("/events");
}
- 해당 Form을 재사용 가능하게 하기 위해선 Form method를 변경할 필요가 있다. New 이벤트를 만들기 위해서의 method는 POST, Edit 이벤트를 위해서 method는 PATCH이다.
- Form의 method를 동적으로 받아오기 위해서 NewEventPage, EditEventPage에 method 속성을 전달 받는다.
- action은 request를 이용해 method를 받아오고 해당 method가 PATCH이면 fetch 시, url을 변경할 수 있도록 한다.
import { useRouteLoaderData } from "react-router-dom";
import EventForm from "../components/EventForm";
function EditEventPage() {
const data = useRouteLoaderData("event-detail");
const event = data.event;
return <EventForm event={event} method="patch" />;
}
export default EditEventPage;
💎 App.js
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import RootPage from "./pages/RootPage";
import HomePage from "./pages/HomePage";
import EventsPage, { loader as eventsLoader } from "./pages/Events";
import EventDetailPage, {
loader as eventDetailLoader,
action as deleteEventAction,
} from "./pages/EventDetailPage";
import NewEventPage from "./pages/NewEventPage";
import EditEventPage from "./pages/EditEventPage";
import EventsRootLayout from "./pages/EventRoot";
import ErrorPage from "./pages/Error";
import { action as manipulateEventAction } from "./components/EventForm";
const router = createBrowserRouter([
{
path: "/",
element: <RootPage />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventsRootLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: eventsLoader,
},
{
path: ":id",
id: "event-detail",
loader: eventDetailLoader,
children: [
{
index: true,
element: <EventDetailPage />,
action: deleteEventAction,
},
{
path: "edit",
element: <EditEventPage />,
action: manipulateEventAction,
},
],
},
{
path: "new",
element: <NewEventPage />,
action: manipulateEventAction,
},
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
- EventForm으로부터 action을 받아와 NewEvent, EditEvent의 action에 사용한다.

📌 추가
📖 useFetcher()를 이용한 배후 작업
💎 NewsletterSignup.js
import { useEffect } from "react";
import classes from "./NewsletterSignup.module.css";
import { useFetcher } from "react-router-dom";
function NewsletterSignup() {
const fetcher = useFetcher();
const { data, state } = fetcher;
useEffect(() => {
if (state === "idle" && data && data.message) {
window.alert("등록 성공");
}
}, [data, state]);
return (
<fetcher.Form
method="post"
action="/newsletter"
className={classes.newsletter}
>
<input
type="email"
placeholder="Sign up for newsletter..."
aria-label="Sign up for newsletter"
/>
<button>Sign up</button>
</fetcher.Form>
);
}
export default NewsletterSignup;
useFetcher : 훅이 실행되면 객체를 주고, 이 객체에는 유용한 프로퍼티와 메서드가 있다.
Form 컴포넌트 -> 실제로 액션을 트리거. 하지만 라우트 전환을 시작하지 않는다.
fetcher는 액션을 트리거하거나 fetcher.load의 도움으로 로더를 트리거하지만 실제로 그 loader가 속한 페이지 또는 그 action이 속한 페이지로 이동하지 않을 때 사용해야한다.
action="/newsletter" → newsletter 라우트의 액션을 트리거한다.
- 즉 Event 창에서 입력하고 버튼을 눌르면 transition(전환)되지 않고 폼을 제출하고 있다.
useFetcher 은 전환하지 않은 채로 액션이나 로더와 상호작용하려는 경우에 사용해야하는 툴이다.(라우트 변경을 트리거 하지 않는 경우)
📖 defer() 함수로 데이터 가져오기를 연기하는 방법
- 데이터가 로딩되는 때를 연기할 수 있게 하는 기능이다.
- 데이터가 다 도착하지 않았어도 컴포넌트를 미리 렌더링하여 사용자 경험 개선할 수 있다.
💎 Events.js
import { useLoaderData, json, defer, Await } from "react-router-dom";
import EventsList from "../components/EventsList";
import { Suspense } from "react";
function EventsPage() {
const { events } = useLoaderData();
console.log(events);
return (
<Suspense fallback={<p style={{ textAlign: "center" }}>Loading...</p>}>
<Await resolve={events}>
{}
{(loadedEvents) => <EventsList events={loadedEvents} />}
</Await>
</Suspense>
);
}
export default EventsPage;
async function loadEvents() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
throw json({ message: "이벤트를 가져올 수 없습니다." }, { status: 500 });
} else {
const resData = await response.json();
return resData.events;
}
}
export function loader() {
return defer({
events: loadEvents(),
});
}
- loader에서 fetch동작을 따로 loadEvents 함수로 분리시킨다. 이때, 바로
response를 반환하는 것이 아니다!
- loadEvents함수는 Promise를 리턴한다.
- 기존의 loader 함수 안에
defer()를 이용하여 loadEvents()의 결과값을 불러오고 defer()는 객체를 입력받는다. 해당 객체 안에는 해당 페이지에서 오갈 수 있는 모든 HTTP 요청을 넣어줘야한다.
- loadEvents 함수로 받아온 프로미스를 defer 안의 events 키에 저장된다.

📖 연기해야할 데이터를 제어하는 방법
💎 EventDetailPage.js
import {
useRouteLoaderData,
json,
redirect,
defer,
Await,
} from "react-router-dom";
import EventItem from "../components/EventItem";
import EventsList from "../components/EventsList";
import { Suspense } from "react";
function EventDetailPage() {
const { event, events } = useRouteLoaderData("event-detail");
return (
<>
<Suspense fallback={<p style={{ textAlign: "center" }}>Loading...</p>}>
<Await resolve={event}>
{(loadedEvent) => <EventItem event={loadedEvent} />}
</Await>
<Await resolve={events}>
{(loadedEvents) => <EventsList events={loadedEvents} />}
</Await>
</Suspense>
</>
);
}
export default EventDetailPage;
async function loadEvent(id) {
const response = await fetch("http://localhost:8080/events/" + id);
if (!response.ok) {
throw json(
{ message: "이벤트 디테일에 대한 정보를 받아올 수 없습니다." },
{ status: 500 }
);
} else {
const resData = await response.json();
return resData.event;
}
}
async function loadEvents() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
throw json({ message: "이벤트를 가져올 수 없습니다." }, { status: 500 });
} else {
const resData = await response.json();
return resData.events;
}
}
export async function loader({ request, params }) {
const id = params.eventId;
console.log(id);
return defer({
event: await loadEvent(id),
events: loadEvents(),
});
}
export async function action({ request, params }) {
const id = params.eventId;
const method = request.method;
const response = await fetch("http://localhost:8080/events/" + id, {
method: method,
});
if (!response.ok) {
throw json(
{ message: "이벤트를 삭제하는데 실패했습니다." },
{ status: 500 }
);
}
return redirect("/events");
}