
react-router-dom 패키지는 동적 라우팅과 함께 데이터를 처리하는 Form 컴포넌트와 action 함수를 제공합니다.
이를 활용해 사용자 입력을 효율적으로 처리하고 서버로 전송할 수 있습니다.
먼저, 사용자에게 이벤트 데이터를 입력받을 EventForm 컴포넌트를 구현합니다.
이 컴포넌트는 Form 컴포넌트를 사용하여 폼 데이터를 리액트 라우터의 action 함수로 직접 전달합니다.
import { useNavigate, Form } from 'react-router-dom';
import classes from './EventForm.module.css';
function EventForm({ 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;
그러기 위해선 폼 양식 컴포넌트에서 모든 input요소에 name속성이 있어야 한다. 그래야 나중에 데이터 추출할때 그것을 보고 할 수 있다.
그리고 나서 react-router-dom이 제공하는 Form이라는 컴포넌트로 일반 html태그와 교체 해주어야 한다.
그렇기 때문에 우선 Form컴포넌트 안에 method프로퍼티를 추가해준다.
⭐ 여기서 중요한 것은 자동으로 백엔드에 바로 전송되는것이 아닌 action으로 전송되는 것!!
먼저 라우터를 정의한 곳에 가서 NewEventPage가 등록된 라우터에 action 프로퍼티를 추가한다.
이 action은 함수를 받으며, 해당 로직은 그 로직이 속해야할 컴포넌트에 가까이 두는 편이 좋다.
import { json, redirect } from 'react-router-dom';
export async function action({ request }) {
const data = await request.formData();// Form으로 받은 입력값들에 접근하기 위한 방법이다.
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 event.' }, { status: 500 });
}
return redirect('/events');
}
이를 사용하려면 formData메소드를 통해 접근해야한다. 그리고나서 이도 비동기로 받아온것이기에 await을 걸어주고 get메소드를 통해 제출된 다양한 입력 필드에 접근할 수 있게 되는것이다.
get에 아까 input의 name속성으로 지정한 식별자들을 넣어주면 해당하는 값이 추출된다.
이렇게 리엑트 라우터가 해당 action함수에 전달한 요청데이터를 추출할 수 있게 되었다.즉, 리엑트 라우터가 백엔드로 최종적으로 보내기 전, 프론트단에서 충분히 데이터 처리를 거치고 보내주는것이다.
이제는 이 데이터를 백엔드로 보내주어야한다.'
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import EditEventPage from './pages/EditEvent';
import ErrorPage from './pages/Error';
import EventDetailPage, {
loader as eventDetailLoader,
} from './pages/EventDetail';
import EventsPage, { loader as eventsLoader } from './pages/Events';
import EventsRootLayout from './pages/EventRoot';
import HomePage from './pages/Home';
import NewEventPage, { action as newEventAction } from './pages/NewEvent';
import RootLayout from './pages/Root';
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <HomePage /> },
{
path: 'events',
element: <EventsRootLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: eventsLoader,
},
{
path: ':eventId',
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 함수는 리액트 라우터의 기능 중 하나로, 사용자의 폼 제출과 같은 특정 액션을 처리하고 그 결과를 서버에 전달하며, 그 응답에 따라 적절한 라우트 변경이나 데이터 업데이트를 수행합니다.
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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(eventData),
});
if (!response.ok) {
throw json({ message: 'Could not save event.' }, { status: 500 });
}
return redirect('/events');
}
post요청을 해봤으니 이제 해당 데이터를 삭제하기 위한 헨들링도 해볼 필요가 있다.
비슷하게 action함수를 사용해야한다.
현재 상세페이지에서 해당 게시물을 삭제할 수 있는 버튼이 자리하고 있으므로, EventDetailPage에서 action을 생성해주고 생성한 action을 올바른 위치의 라우터에 등록해준다.
import { useRouteLoaderData, json, redirect } from 'react-router-dom';
import EventItem from '../components/EventItem';
function EventDetailPage() {
const data = useRouteLoaderData('event-detail');
{/* 백엔드에서 eventId 엔드포인트에 대한 응답줄때 객체 전체에서 event 프로퍼티에 응답 데이터를 포함시키고 있다. */}
return <EventItem event={data.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,
}
);
} else {
return response;
}
}
export async function action({params,request}){
const eventId = params.eventId;
const response = await fetch('http://localhost:8080/events/'+eventId,{
method: request.method,
});
if (!response.ok) {
throw json(
{ message: 'Could not delete event.' },
{
status: 500,
}
);
}
return redirect('/events');
}
이렇게 action을 설정해 줬으면, 해당 이벤트가 트리거되어야하는(삭제 버튼이 있는) EventItem컴포넌트에서 설정해주어야 한다.
이때, 삭제할 헨들링에서는 사용자에게 한번의 피드백을 주기위한 조건을 작성하고 그 값이 true일때 그때가 되서야 삭제할 event가 정상적으로 제출되도록 해야한다.
그때 리엑트 라우터에서 제공하여 사용할 수 있는 훅이 바로 useSubmit훅이다.
useSubmit으로 만들어진 함수에 첫번째 인자로 제출하려는 데이터를 넣어주어야 하고, 두번째 인자로는 폼에 설정할 수 있는 값과 같은 값들을 설정할 수 있게 해준다.
지금 이 헨들링은 삭제를 하기 위함이니 method를 delete로 설정해준다.
다만, 첫번째 인자엔 null을 해줘야 하는데 이는 현재 여기선 데이터가 필요하진 않기 때문이다.
eventitem.js
import { Link, useSubmit } from 'react-router-dom';
import classes from './EventItem.module.css';
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;
위처럼 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");
}
https://velog.io/@ksa199653/React-%EB%A6%AC%EC%97%91%ED%8A%B8-%EB%9D%BC%EC%9A%B0%ED%84%B0-loader-3