목차
lodaer활용하기
우리는 페이지를 불러오기 전에 데이터를 먼저 불러오고 그 데이터를 기반으로 페이지를 불러오고 싶은 경우가 있다.
그럴떄 loader를 이용하면 된다.
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import HomePage from "../pages/HomePage";
import Layout from "./components/Layout";
import EventsPages from "../pages/EventsPage";
import EventDetailPage from "../pages/EventDetailPage";
import EventLayout from "../pages/EventLayout";
import NewEventPage from "../pages/NewEventPage";
import EditEventPage from "../pages/EditEventPage";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: "/events",
element: <EventLayout />,
children: [
{
path: "/events",
element: <EventsPages />,
loader: async () => {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
} else {
const resData = await response.json();
return resData.events;
}
},
},
{
path: "/events/:id",
element: <EventDetailPage />,
},
{
path: "/events/:id/edit",
element: <EditEventPage />,
},
{
path: "/events/new",
element: <NewEventPage />,
},
],
},
],
},
]);
function RouterLecture() {
return <RouterProvider router={router}></RouterProvider>;
}
export default RouterLecture;
위와 같이 페이지를 라우터를 통해 이동한다고 하면 다음과 같이 응용할 수 있다.
EventsPages.jsx
import EventsList from "../src/components/EventsList";
import { useLoaderData } from "react-router-dom";
function EventsPages() {
const events = useLoaderData();
return <EventsList events={events}></EventsList>;
}
export default EventsPages;
주의해야 하는 점이 있다면 동급이하의 컴포넌트에서만 데이터를 불러올 수 있다는 점이다.
즉 위의 컴포넌트들 중에서 HomePage, EventLayout, Layout 컴포넌트에서는 데이터를 불러올 수 없다.
loader의 불러오는 위치 바꾸기
다음과 같이 loader를 작성하면 페이지를 모아놓은 컴포넌트가 비대해지게 된다.
따라서 다음과 같이 해결할 수 있다.
EventsPages.jsx
import EventsList from "../src/components/EventsList";
import { useLoaderData } from "react-router-dom";
function EventsPages() {
const events = useLoaderData();
return <EventsList events={events}></EventsList>;
}
export default EventsPages;
export async function loader() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
} else {
const resData = await response.json();
return resData.events;
}
}
이런식으로 loader를 불러오는 위치를 변경해서 간결하게 만들 수 있다.
...
import EventsPages, { loader as eventsLoader } from "../pages/EventsPage";
...
const router = createBrowserRouter([
{
...
children: [
{
path: "/events",
element: <EventsPages />,
loader: eventsLoader,
},
...
],
},
],
},
]);
function RouterLecture() {
return <RouterProvider router={router}></RouterProvider>;
}
export default RouterLecture;
위와 같이 코드를 작성하면 백엔드에서 데이터를 가져올때 우리는 컴포넌트에 빈 데이터가 들어오는지에 대해 고려할 필요가 없다. 데이터가 들어올때까지 기다렸다가 컴포넌트가 랜더링되기 때문이다.
그렇다면 우리는 현재 불러오고 있는 상태를 사용자에게 보여주고 싶다면 어떻게 하면 될까?
바로 useNavigation을 이용하는 것이다.
import { Outlet, useNavigation } from "react-router-dom";
import MainNavigation from "./MainNavigation";
function Layout() {
const navigation = useNavigation();
return (
<>
<MainNavigation />
<main>
{navigation.state === "loading" && <p>Loading...</p>}
<Outlet />
</main>
</>
);
}
export default Layout;
이렇게 하면 아래와 같이 로딩문구와 Homepage의 문구가 같이 뜨게 되는데 이동하기 전에
http://localhost:3000/ 이곳에 머무르기 때문이다.
loader를 통한 에러 핸들링하기
1. 에러 객체 전달하기
import EventsList from "../src/components/EventsList";
import { useLoaderData } from "react-router-dom";
import { useNavigation } from "react-router-dom";
function EventsPages() {
const data = useLoaderData();
const events = data.events;
if(data.isError){
return <p>{data.message}</p>
}
return <EventsList events={events}></EventsList>;
}
export default EventsPages;
export async function loader() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
return { isError: true, message: "could not fetch events" };
} else {
return response;
}
}
2. errorElement 페이지 활용하기
import EventsList from "../src/components/EventsList";
import { useLoaderData } from "react-router-dom";
function EventsPages() {
const data = useLoaderData();
const events = data.events;
return <EventsList events={events}></EventsList>;
}
export default EventsPages;
export async function loader() {
const response = await fetch("http://localhost:8080/eventsa");
if (!response.ok) {
console.log("error");
throw new Response(JSON.stringify({ message: "Could not fetch events" }), {
status: 500,
});
} else {
return response;
}
}
이런식으로 에러가 발생하면 에러요소가 있는 페이지가 나올때까지
라우터의 상위 요소로 가서 상위 요소의 에러 페이지를 발생 시킨다.
import ErrorPage from "../pages/ErrorPage";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: "/events",
element: <EventLayout />,
children: [
{
path: "/events",
element: <EventsPages />,
loader: eventsLoader,
},
],
...
},
]);
function RouterLecture() {
return <RouterProvider router={router}></RouterProvider>;
}
export default RouterLecture;
하지만 이렇게 하면 아래와 같이 json형식으로 바꾸고 파싱해야 하는 번거로움이 생긴다.
import PageContent from "../src/components/PageContent";
import { useRouteError } from "react-router-dom";
function ErrorPage() {
const error = useRouteError();
let title = "An error occurred!";
let message = "Something went wrong!";
if (error.status === 500) {
message = JSON.parse(error.data).message;
console.log(error);
}
if (error.status === 404) {
title = "Not found!";
message = "could not find resource or page";
}
return (
<PageContent title={title}>
<p>{message}</p>
</PageContent>
);
}
export default ErrorPage;
따라서 아래와 같이 json()을 이용해 바꿀 수 있다.
주의해야 할 것은 예외를 발생시키고 중단해야 하기 때문에 thorw가 아닌 return을 해주면 오류에 직면한다.
import EventsList from "../src/components/EventsList";
import { useLoaderData, json } from "react-router-dom";
function EventsPages() {
const data = useLoaderData();
const events = data.events;
return <EventsList events={events}></EventsList>;
}
export default EventsPages;
export async function loader() {
const response = await fetch("http://localhost:8080/eventsa");
if (!response.ok) {
// console.log("error");
// throw new Response(JSON.stringify({ message: "Could not fetch events" }), {
// status: 500,
// });
//throw 대신 return 해주면 오류남
throw json(
{ message: "Could not fetch events" },
{
status: 500,
}
);
} else {
return response;
}
}
동적인 페이지 예외처리하기
그렇다면 동적인 params를 이용한 페이지는 어떻게 예외처리를 해야할까?
바로 loader함수 자체에서 제공해주는 params를 이용하는 것이다.
import EventItem from "../src/components/EventItem";
import { useParams, json } from "react-router-dom";
function EventDetailPage() {
const params = useParams();
return (
<>
<h1>EventDetailPage</h1>
<EventItem></EventItem>
</>
);
}
export default EventDetailPage;
//요청 객체와 parmas를 포함하고 있음
export async function loader({ request, params }) {
const id = params.eventId;
const response = await fetch("http://localhost:8080/events/" + id);
if (!response.ok) {
throw json(
{ msessage: "Could not fetch details for selected event." },
{
status: 500,
}
);
} else {
return response;
}
}
loader사용 시 주의할 점 (useLoaderData)
최신 리액트에서는 부모에서의 loader를 자식들이 쓰지 못하기 때문에 하나하나 loader를 지정해주어야 한다.
사용 불가 코드
{
path: ":eventId",
loader: eventDetailLoader,
children: [
{
index: true,
element: <EventDetailPage />,
},
{
path: "edit",
element: <EditEventPage />,
},
],
},
올바른 코드
{
path: ":eventId",
children: [
{
index: true,
element: <EventDetailPage />,
loader: eventDetailLoader,
},
{
path: "edit",
element: <EditEventPage />,
loader: eventDetailLoader,
},
],
},
useRouteLoaderData
이렇게 하면 하나하나 써주어야 하는 번거로움이 있는데 이를 위해서 useRouteLoaderData를 써주면 된다.
이렇게 하면 id를 통해 부모의 loader의 접근이 가능하다.
{
path: ":eventId",
id: "event-detail",
loader: eventDetailLoader,
children: [
{
index: true,
element: <EventDetailPage />,
},
{
path: "edit",
element: <EditEventPage />,
},
],
},
import EventForm from "../src/components/EventForm";
import { useRouteLoaderData } from "react-router-dom";
function EditEventPage() {
const data = useRouteLoaderData("event-detail");
const event = data.event;
return (
<>
<EventForm event={event}></EventForm>
</>
);
}
export default EditEventPage;
action을 이용해서 데이터 post 요청 해보기
라우터에서 제공해주는 Form으로 바꾸고 method를 지정해 준다.
그러면 액션함수에서 request인자로 전달 받을 수 있다.
EventForm.jsx
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={method} className={classes.form}>
<p>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
name="title"
required
defaultValue={event?.title}
/>
</p>
<p>
<label htmlFor="image">Image</label>
<input
id="image"
type="url"
name="image"
defaultValue={event?.image}
required
/>
</p>
<p>
<label htmlFor="date">Date</label>
<input
id="date"
type="date"
name="date"
defaultValue={event?.date}
required
/>
</p>
<p>
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
rows="5"
defaultValue={event?.description}
required
/>
</p>
<div className={classes.actions}>
<button type="button" onClick={cancelHandler}>
Cancel
</button>
<button>Save</button>
</div>
</Form>
);
}
export default EventForm;
NewEventPage.jsx
loader와 비슷하게 인자로 받아주고 사용하는 것을 볼 수 있다.
redirect를 통해 action실행 후 페이지의 이동을 만들었다.
import EventForm from "../src/components/EventForm";
import { json, redirect } from "react-router-dom";
function NewEventPage() {
return <EventForm method={"POST"}></EventForm>;
}
export default NewEventPage;
export async function action({ request, params }) {
const data = await request.formData();
//Object.fromEntries()는 이 이터레이터를 일반 객체로 변환합니다.
//data.entries()는 FormData 객체의 모든 키-값 쌍을 이터레이터로 반환합니다.
const fd = Object.fromEntries(data.entries());
//json형식으로 바꾸어준다.
const jsonFd = JSON.stringify(fd);
const response = await fetch("http://localhost:8080/events", {
method: "POST",
body: jsonFd,
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw json(
{ message: "Could not save event" },
{
status: 505,
}
);
}
//리다이렉션을 통해 지정한 곳으로 이동한다.
return redirect("/events");
}
그렇다면 의문점이 생긴다? 굳이 useNavigate를 통해 이동하면 되는데 redirect를 써야할까?
일단 비동기 함수이기 때문에 컴포넌트 안이 아니라 useNavigate는 쓰지 못한다.
또한 아래 이유 때문에 쓴다.
- 페칭 전 이동 - 데이터를 가져오기 전에 경로를 변경하여 불필요한 리소스 로드를 방지할 수 있다.
- 선 처리 리다이렉트 - 리소스를 로드하기 전에 특정 조건을 기반으로 경로를 변경할 수 있다.
loader와 비슷하게 불러와준다.
import NewEventPage, { action as newEventAction } from "../pages/NewEventPage";
{
path: "new",
element: <NewEventPage />,
action: newEventAction,
},
action에서 useSubmit을 이용해 요청 제어하기
{
index: true,
element: <EventDetailPage />,
action: deleteEventAction,
},
아래와 같이 하면 method로 delete가 전달이 되면서 action함수에서 request.method로 사용가능할 수 있게 된다.
삭제이기 때문에 첫 인자는 null로 넣어주었다.
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) {
//첫번째는 우리가 제출할려는 데이터
//두번째는 method및 action을 통한 경로 설정
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;
이어서..
너무 내용이 길어져서 아래 링크로 가면 더 있습니다.
https://be-senior-developer.tistory.com/176
'코딩 정보 > React' 카테고리의 다른 글
[React] 로그인 시 토큰 기반 인증 만들어보기 (0) | 2024.08.24 |
---|---|
[React][Router] 라우터 활용을 통한 비동기 통신 (하) (0) | 2024.08.22 |
[React] 통신을 위한 Json과 response, error에 대해 알아보자 (0) | 2024.08.21 |
[React] 라우터의 개념과 활용 방법에 대해 알아보자 (1) | 2024.08.16 |
[React] 리덕스의 개념과 활용 방법에 대해 알아보자 (0) | 2024.08.11 |