좋다.
앞전의 loader 1과 loader 2에서 많은 데이터를 loader를 통해 로딩하고 패칭하여 접근까지 하는 방법을 살펴보고 학습했다.
이제는 데이터 패칭만이 아닌 사용자가 직접 어떤 양식이나 데이터를 보내보는 방법을 살펴볼 차례이다. (post 혹은 put/patch)
새롭게 만들어진 혹은 입력받은 데이터를 서버로 보내기 위해선 우선 양식이 필요할것이다.
NewEventPage.js
import EventForm from "../components/EventForm";
function NewEventPage() {
return (
<>
<h1>This page is New Event Page!</h1>
<EventForm />
</>
);
}
export default NewEventPage;
이 페이지는 사용자가 입력한 새로운 데이터들을 받아서 서버로 보내는 양식이 있는 페이지 이다.
폼 양식이 있는 EventForm컴포넌트를 렌더링 해야하며, 일반적인 방법을 사용해 이벤트 핸들러를 생성하여 http통신을 할 수도 있겠지만, 여기서도 Loader를 통해 서버로 보낼 수 있도록 만들어 보면 될 것 같다.
먼저 라우터를 정의한 곳에 가서 NewEventPage가 등록된 라우터에 action 프로퍼티를 추가한다.
이 action은 함수를 받으며, 해당 로직은 그 로직이 속해야할 컴포넌트에 가까이 두는 편이 좋다.
import EventForm from "../components/EventForm";
function NewEventPage() {
return (
<>
<h1>This page is New Event Page!</h1>
<EventForm />
</>
);
}
export default NewEventPage;
export async function action({ request, params }) {
fetch(`http://localhost:8080/events`, {
method: "POST",
headers: {
"Content-type": "applycation/json",
},
body:
});
}
action함수도 loader함수와 마찬가지로, 브라우저에서 실행되는 코드이므로 어떤 브라우저 API도 접근하게 할 수 있다.
그럼 fetch함수 안에서 body로 보내줄 데이터 접근은 어떻게 해야할까?
react-router-dom은 폼양식 제출 처리를 쉽게 해주고, 해당 폼에서 데이터를 추출하는것도 서브한다.
EventForm.js
import { Form, useNavigate } from "react-router-dom";
import classes from "./EventForm.module.css";
function EventForm({ method, event }) {
const navigate = useNavigate();
const 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;
그러기 위해선 폼 양식 컴포넌트에서 모든 input요소에 name속성이 있어야 한다. 그래야 나중에 데이터 추출할때 그것을 보고 할 수 있다.
그리고 나서 react-router-dom이 제공하는 Form이라는 컴포넌트로 일반 html태그와 교체 해주어야 한다.
이 Form컴포넌트는 벡엔드로 요청을 전송하는 브라우저 기본사항을 생략하고 전송되는 그 요청을 받아서 내가 지정하는 action함수에 준다.
그럼 제출된 모든 데이터가 포함되어 있기에 사용할 수 있다.
그렇기 때문에 우선 Form컴포넌트 안에 method프로퍼티를 추가해준다.
여기서 중요한 것은 자동으로 백엔드에 바로 전송되는것이 아닌 action으로 전송되는 것!!
리엑트라우터가 loader와 같이 action에도 요청에 대한 객체를 전달한다.
{request, params}
전에 상세페이지에서 사용되는 loader와는 다르게 params는 사용하지 않고, requset객체를 사용한다. 왜냐하면 그 request객체에 결국 입력받은 폼 데이터가 포함되어있을것이기 때문이다.
export async function action({ request, params }) {
const data = await request.formData(); // Form으로 받은 입력값들에 접근하기 위한 방법이다.
// data.get('')
// 다양한 입력양식을 받았다면 객체를 활용하자!
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",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify(eventData),
});
}
이를 사용하려면 formData메소드를 통해 접근해야한다. 그리고나서 이도 비동기로 받아온것이기에 await을 걸어주고 get메소드를 통해 제출된 다양한 입력 필드에 접근할 수 있게 되는것이다.
get에 아까 input의 name속성으로 지정한 식별자들을 넣어주면 해당하는 값이 추출된다.
이렇게 리엑트 라우터가 해당 action함수에 전달한 요청데이터를 추출할 수 있게 되었다.즉, 리엑트 라우터가 백엔드로 최종적으로 보내기 전, 프론트단에서 충분히 데이터 처리를 거치고 보내주는것이다.
이제는 이 데이터를 백엔드로 보내주어야한다.
import NewEventPage, { action as newEventAction } from "./pages/NewEventPage";
...
const router = createBrowserRouter([
{
path: "/",
element: <RootPage />,
errorElement: <ErrorPage />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: "events",
element: <EventsRootPage />,
children: [
{
index: true,
element: <EventsPage />,
loader: eventsLoader,
},
{
path: ":eventId",
id: "event-detail",
loader: eventDetailLoader,
children: [
{
index: true, // 인덱스 페이지기 때문에 loader함수가 해당 컴포넌트안에서 정의되어 있음
element: <EventDetailPage />,
},
{
path: "edit",
element: <EditEventPage />,
},
],
},
{
path: "new",
element: <NewEventPage />,
action: newEventAction,
},
],
},
],
},
]);
...
이렇게 새롭게 생성한 action함수를 라우터 정의한곳에 와서 등록해주면 된다.
그럼 이제 최종적으로 백엔드로 보낸 데이터에 대한 응답인 response를 살펴보고 리턴된 데이터를 오류를 잡거나 추출하는등 다양한 것들을 할 수가 있다.
import { json, redirect } from "react-router-dom";
export async function action({ request, params }) {
const data = await request.formData(); // Form으로 받은 입력값들에 접근하기 위한 방법이다.
// data.get('')
// 다양한 입력양식을 받았다면 객체를 활요하자!
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",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify(eventData),
});
if (!response.ok) {
throw json({ message: "Could not save new event!" }, { status: 500 });
}
return redirect("/events");
}
보통은 좋은 사용자 경험을 주기 위해 폼을 제출하고 나서 다른 페이지로 이동시켜주는게 좋다.
이 기능또한 리엑트 라우터에서 제공하는 redirect라는 기능이다.
이 redirect도 특수한 응답객체를 생성하는데 우리는 그냥 이동할 경로만 지정해주면 나머지는 알아서 리엑트 라우터가 처리한다. 매우 좋다^^
post요청을 해봤으니 이제 해당 데이터를 삭제하기 위한 헨들링도 해볼 필요가 있다.
비슷하게 action함수를 사용해야한다.
현재 상세페이지에서 해당 게시물을 삭제할 수 있는 버튼이 자리하고 있으므로, EventDetailPage에서 action을 생성해주고 생성한 action을 올바른 위치의 라우터에 등록해준다.
EventDetailPage.js
import { json, redirect, useRouteLoaderData } from "react-router-dom";
import EventItem from "../components/EventItem";
function EventDetailPage() {
const data = useRouteLoaderData("event-detail"); // data = response객체
const event = data.event;
return (
<>
{/* 백엔드에서 eventId 엔드포인트에 대한 응답줄때 객체 전체에서 event 프로퍼티에 응답 데이터를 포함시키고 있다. */}
<EventItem event={event} />
</>
);
}
export default EventDetailPage;
// 리엑트라우터가 백엔드로 바로 보내지않고 loader로 요청 객체를 전달해준다, 그걸 파라미터로 받아서 사용가능하다.
export async function loader({ request, params }) {
const id = params.eventId;
const response = await fetch(`http://localhost:8080/events/${id}`);
if (!response.ok) {
throw json(
{ message: "Could not fetch details for selected event." },
{ status: 500 }
);
}
return response;
}
export async function action({ request, params }) {
const id = params.eventId;
const response = await fetch(`http://localhost:8080/events/${id}`);
if (!response.ok) {
throw json({ message: "Could not delete it" }, { status: 500 });
}
return redirect("/events");
};
이렇게 action을 설정해 줬으면, 해당 이벤트가 트리거되어야하는(삭제 버튼이 있는) EventItem컴포넌트에서 설정해주어야 한다.
이때, 삭제할 헨들링에서는 사용자에게 한번의 피드백을 주기위한 조건을 작성하고 그 값이 true일때 그때가 되서야 삭제할 event가 정상적으로 제출되도록 해야한다.
그때 리엑트 라우터에서 제공하여 사용할 수 있는 훅이 바로 useSubmit훅이다.
useSubmit으로 만들어진 함수에 첫번째 인자로 제출하려는 데이터를 넣어주어야 하고, 두번째 인자로는 폼에 설정할 수 있는 값과 같은 값들을 설정할 수 있게 해준다.
지금 이 헨들링은 삭제를 하기 위함이니 method를 delete로 설정해준다.
다만, 첫번째 인자엔 null을 해줘야 하는데 이는 현재 여기선 데이터가 필요하진 않기 때문이다.
이를 통해 submit은 백엔드에 전송하기 전 action에 요청객체를 전달해 주는것이다!
EventItem.js
import { Link, useSubmit } from "react-router-dom";
import classes from "./EventItem.module.css";
function EventItem({ event }) {
const submit = useSubmit();
const startDeleteHandler = () => {
const dobbleCheck = window.confirm("Are you sure?");
if (dobbleCheck) {
// 만약 내가 트리거해야할 action이 다른 라우터에서 정의된거라면 submit() 두번째 인자에 따로 해당 action이 등록되어있는 경로를 추가해주어야한다. -> { method: "delete", path: "/" }
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;
위처럼 useSubmit훅을 통해 보낸 데이터는 요청 객체가 될것이며 이를 실제 action함수에서 request객체로 사용가능하다.
// 삭제 action
export async function action({ request, params }) {
const id = params.eventId;
const response = await fetch(`http://localhost:8080/events/${id}`, {
method: request.method,
headers: {
"Content-type": "application/json",
},
});
if (!response.ok) {
throw json({ message: "Could not delete it" }, { status: 500 });
}
return redirect("/events");
}
이렇게 action함수를 통해 삭제 헨들링도 구현을 해보았다.