🔗 레파지토리에서 커밋 히스토리별로 보기
🔗 리액트 라우터 홈페이지
App.js의 설명대로 라우터 구성하기(가능하면 마지막 보너스 과제도 풀어보기!)
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import RootPage from "./pages/RootPage";
import HomePage from "./pages/HomePage";
import EventsPage from "./pages/EventsPage";
import EventDetailPage from "./pages/EventDetailPage";
import NewEventPage from "./pages/NewEventPage";
import EditEventPage from "./pages/EditEventPage";
const router = createBrowserRouter([
{
path: "/",
element: <RootPage />,
errorElement: <p>Error</p>,
children: [
{ index: true, element: <HomePage /> },
{ path: "events", element: <EventsPage /> },
{ path: "events/:id", element: <EventDetailPage /> },
{ path: "events/new", element: <NewEventPage /> },
{ path: "events/:id/edit", element: <EditEventPage /> },
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
import MainNavigation from "../components/MainNavigation";
import { Outlet } from "react-router-dom";
function RootPage() {
return (
<>
<MainNavigation />
<Outlet />
</>
);
}
export default RootPage;
import classes from "./MainNavigation.module.css";
import { NavLink } from "react-router-dom";
function MainNavigation() {
return (
<header className={classes.header}>
<nav>
<ul className={classes.list}>
<li>
<NavLink
to="/"
className={({ isActive }) =>
isActive ? classes.active : undefined
}
end
>
Home
</NavLink>
</li>
<li>
<NavLink
to="events"
className={({ isActive }) =>
isActive ? classes.active : undefined
}
>
Events
</NavLink>
</li>
</ul>
</nav>
</header>
);
}
export default MainNavigation;
import { Link } from "react-router-dom";
const DUMMY_EVENTS = [
{
id: "event1",
title: "Event Page 1",
},
{
id: "event2",
title: "Event Page 2",
},
{
id: "event3",
title: "Event Page 3",
},
{
id: "event4",
title: "Event Page 4",
},
{
id: "event5",
title: "Event Page 5",
},
];
function EventsPage() {
return (
<>
<h1>EventsPage</h1>
<ul>
{DUMMY_EVENTS.map((event) => (
<li key={event.id}>
<Link to={event.id}>{event.title}</Link>
</li>
))}
</ul>
</>
);
}
export default EventsPage;
import { useParams } from "react-router-dom";
function EventDetailPage() {
const params = useParams();
return (
<>
<h1>EventDetailPage</h1>
<p>{params.id}</p>
</>
);
}
export default EventDetailPage;

<EventNavigation> component above all /events... page componentsimport { createBrowserRouter, RouterProvider } from "react-router-dom";
import RootPage from "./pages/RootPage";
import HomePage from "./pages/HomePage";
import EventsPage from "./pages/EventsPage";
import EventDetailPage from "./pages/EventDetailPage";
import NewEventPage from "./pages/NewEventPage";
import EditEventPage from "./pages/EditEventPage";
import EventsNavigation from "./components/EventsNavigation";
const router = createBrowserRouter([
{
path: "/",
element: <RootPage />,
errorElement: <p>Error</p>,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventsNavigation />,
children: [
{ index: true, element: <EventsPage /> },
{ path: ":id", element: <EventDetailPage /> },
{ path: "new", element: <NewEventPage /> },
{ path: ":id/edit", element: <EditEventPage /> },
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
import classes from "./EventsNavigation.module.css";
import { Outlet } from "react-router-dom";
function EventsNavigation() {
return (
<>
<header className={classes.header}>
<nav>
<ul className={classes.list}>
<li>
<a href="/events">All Events</a>
</li>
<li>
<a href="/events/new">New Event</a>
</li>
</ul>
</nav>
</header>
<Outlet />
</>
);
}
export default EventsNavigation;

loader()를 이용한 데이터 가져오기useEffect, useState, fetch 사용하기// pages/Events.js
import { useEffect, useState } from "react";
import EventsList from "../components/EventsList";
function EventsPage() {
const [isLoading, setIsLoading] = useState(false);
const [fetchedEvents, setFetchedEvents] = useState();
const [error, setError] = useState();
useEffect(() => {
async function fetchEvents() {
setIsLoading(true);
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
setError("Fetching events failed.");
} else {
const resData = await response.json();
setFetchedEvents(resData.events);
}
setIsLoading(false);
}
fetchEvents();
}, []);
return (
<>
<div style={{ textAlign: "center" }}>
{isLoading && <p>Loading...</p>}
{error && <p>{error}</p>}
</div>
{!isLoading && fetchedEvents && <EventsList events={fetchedEvents} />}
</>
);
}
export default EventsPage;

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import RootPage from "./pages/RootPage";
import HomePage from "./pages/HomePage";
import EventsPage from "./pages/Events";
import EventDetailPage from "./pages/EventDetailPage";
import NewEventPage from "./pages/NewEventPage";
import EditEventPage from "./pages/EditEventPage";
import EventsRootLayout from "./pages/EventRoot";
const router = createBrowserRouter([
{
path: "/",
element: <RootPage />,
errorElement: <p>Error</p>,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventsRootLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: async () => {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
// ...
} else {
const resData = await response.json();
return resData.events; // EventsPage에 제공해 줄 것이다.
}
},
},
{ path: ":id", element: <EventDetailPage /> },
{ path: "new", element: <NewEventPage /> },
{ path: ":id/edit", element: <EditEventPage /> },
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
loaderimport { useLoaderData } from "react-router-dom";
import EventsList from "../components/EventsList";
function EventsPage() {
const events = useLoaderData(); // events는 resData.event가 된다.
return <EventsList events={events} />;
}
export default EventsPage;
useLoaderData : 가장 가까운 loader 데이터에 엑세스 하기 위해 실행할 수 있는 특수한 훅.loader() 데이터의 다양한 활용법EventsList 컴포넌트에서도 useLoaderData를 사용할 수 있다.import classes from "./EventsList.module.css";
import { useLoaderData } from "react-router-dom";
function EventsList() {
const events = useLoaderData();
return (
<div className={classes.events}>
<h1>All Events</h1>
<ul className={classes.list}>
{events.map((event) => (
<li key={event.id} className={classes.item}>
<a href="...">
<img src={event.image} alt={event.title} />
<div className={classes.content}>
<h2>{event.title}</h2>
<time>{event.date}</time>
</div>
</a>
</li>
))}
</ul>
</div>
);
}
export default EventsList;
import EventsList from "../components/EventsList";
function EventsPage() {
return <EventsList />;
}
export default EventsPage;
useLoaderData는 로더가 정의된 라우트보다 더 높은 상위에서 사용할 수 없다.useLoaderData를 사용하기 위해서는 loader를 추가한 컴포넌트(라우트)와 같은 수준이거나 더 낮은 수준에 있는 컴포넌트에서 사용가능하다.loader() 코드를 저장해야하는 위치loader()로 인해 fetch가 간단해졌지만 App의 규모가 커졌다.loader() 코드를 필요로 하는 컴포넌트 파일에 해당 코드를 넣는 것이 좋다. 즉, 여기서는 pages/Events가 있는 곳에 넣으면 된다.import { useLoaderData } from "react-router-dom";
import EventsList from "../components/EventsList";
function EventsPage() {
const events = useLoaderData(); // events는 resData.event가 된다.
return <EventsList events={events} />;
}
export default EventsPage;
export async function loader() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
// ...
} else {
const resData = await response.json();
return resData.events;
}
}
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 from "./pages/EventDetailPage";
import NewEventPage from "./pages/NewEventPage";
import EditEventPage from "./pages/EditEventPage";
import EventsRootLayout from "./pages/EventRoot";
const router = createBrowserRouter([
{
path: "/",
element: <RootPage />,
errorElement: <p>Error</p>,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventsRootLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: eventsLoader,
},
{ path: ":id", element: <EventDetailPage /> },
{ path: "new", element: <NewEventPage /> },
{ path: ":id/edit", element: <EditEventPage /> },
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
loader() 함수가 실행되는 시기import MainNavigation from "../components/MainNavigation";
import { Outlet, useNavigation } from "react-router-dom";
function RootPage() {
// useNavigation : 리액트 라우터가 제공해주는 훅.
// 현재 전환이 진행 중인지, 데이터를 전달하는 중인지 또는 전환이 진행되고 있지 않는지를 알 수 있다.
const navigation = useNavigation();
return (
<>
<MainNavigation />
<main>
{navigation.state === "loading" && <p>Loading...</p>}
<Outlet />
</main>
</>
);
}
export default RootPage;
loader()에서 응답 리턴하기import { useLoaderData } from "react-router-dom";
import EventsList from "../components/EventsList";
function EventsPage() {
const data = useLoaderData();
const events = data.events;
return <EventsList events={events} />;
}
export default EventsPage;
export async function loader() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
// ...
} else {
return response;
// const resData = await response.json();
// return resData.events; // 숫자,텍스트,객체 등 다 리턴할 수 있다.
}
}
fetch는 Response 객체의 프로미스를 리턴한다.useLoaderData혹은 자동으로 프로미스에서 데이터를 추출해주기 때문에 위와같이 코드를 작성해도 된다.loader()로 가는 코드의 종류localStorage, Cookie, ...import { useLoaderData } from "react-router-dom";
import EventsList from "../components/EventsList";
function EventsPage() {
const data = useLoaderData();
if (data.isError) {
return <p>{data.message}</p>;
}
const events = data.events;
return <EventsList events={events} />;
}
export default EventsPage;
export async function loader() {
const response = await fetch("http://localhost:8080/eventsss");
if (!response.ok) {
return { isError: true, message: "이벤트를 가져올 수 없습니다." };
} else {
return response;
}
}

errorElement 이용하기// Events.js
import { useLoaderData } from "react-router-dom";
import EventsList from "../components/EventsList";
function EventsPage() {
const data = useLoaderData();
if (data.isError) {
return <p>{data.message}</p>;
}
const events = data.events;
return <EventsList events={events} />;
}
export default EventsPage;
export async function loader() {
const response = await fetch("http://localhost:8080/eventsss");
if (!response.ok) {
throw new Error({ message: "이벤트를 가져올 수 없습니다." });
} else {
return response;
}
}
// Error.js
function ErrorPage() {
return <h1>오류가 발생했습니다.</h1>;
}
export default ErrorPage;
// Events.js
import { useLoaderData } from "react-router-dom";
import EventsList from "../components/EventsList";
function EventsPage() {
const data = useLoaderData();
if (data.isError) {
return <p>{data.message}</p>;
}
const events = data.events;
return <EventsList events={events} />;
}
export default EventsPage;
export async function loader() {
const response = await fetch("http://localhost:8080/eventsss");
if (!response.ok) {
throw new Response(
JSON.stringify({ message: "이벤트를 가져올 수 없습니다." }),
{ status: 500 }
);
} else {
return response;
}
}
// Error.js
import MainNavigation from "../components/MainNavigation";
import PageContent from "../components/PageContent";
import { useRouteError } from "react-router-dom";
function ErrorPage() {
const error = useRouteError();
// error 객체는 Response를 throw하거나 또는 다른 종류의 객체 혹은 데이터를 throw하는지에 달려있다.
let title = "오류가 발생했습니다";
let message = "Something went wrong!";
if (error.status === 500) {
message = JSON.parse(error.data).message;
}
if (error.status === 404) {
title = "Not Found";
message = "리소스나 페이지를 찾을 수 없습니다.";
}
return (
<>
<MainNavigation />
<PageContent title={title}>
<p>{message}</p>
</PageContent>
</>
);
}
export default ErrorPage;

json() 유틸리티 함수import { useLoaderData, json } from "react-router-dom";
import EventsList from "../components/EventsList";
function EventsPage() {
const data = useLoaderData();
if (data.isError) {
return <p>{data.message}</p>;
}
const events = data.events;
return <EventsList events={events} />;
}
export default EventsPage;
export async function loader() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
return json({ message: "이벤트를 가져올 수 없습니다." }, { status: 500 });
} else {
return response;
}
}
// Error.js
function ErrorPage() {
const error = useRouteError();
let title = "오류가 발생했습니다";
let message = "Something went wrong!";
if (error.status === 500) {
// message = JSON.parse(error.data).message;
message = error.data.message;
}
}
json() : json 형식의 데이터가 포함된 Response 객체를 포함하는 함수이다.loader() - EventDetailPageimport classes from "./EventsList.module.css";
import { Link } from "react-router-dom";
// import { useLoaderData } from "react-router-dom";
function EventsList({ events }) {
// const events = useLoaderData();
return (
<div className={classes.events}>
<h1>All Events</h1>
<ul className={classes.list}>
{events.map((event) => (
<li key={event.id} className={classes.item}>
<Link to={event.id}>
<img src={event.image} alt={event.title} />
<div className={classes.content}>
<h2>{event.title}</h2>
<time>{event.date}</time>
</div>
</Link>
</li>
))}
</ul>
</div>
);
}
export default EventsList;
<a>를 Link로 대체import { useLoaderData, json, useParams } from "react-router-dom";
import EventItem from "../components/EventItem";
function EventDetailPage() {
// const params = useParams();
const data = useLoaderData();
return <EventItem event={data.event} />;
}
export default EventDetailPage;
export async function loader({ request, params }) {
const id = params.id; // '/events/:id'
const response = await fetch("http://localhost:8080/events/" + id);
if (!response.ok) {
throw json(
{ message: "이벤트 디테일에 대한 정보를 받아올 수 없습니다." },
{ status: 500 }
);
} else {
return response;
}
}
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 eventsDetailLoader,
} from "./pages/EventDetailPage";
import NewEventPage 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",
element: <EventDetailPage />,
loader: eventsDetailLoader, // loader 함수 전달
},
{ path: "new", element: <NewEventPage /> },
{ path: ":id/edit", element: <EditEventPage /> },
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;

useRouteLoaderData 훅 및 다른 라우트의 데이터에 엑세스하기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 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, // 공통 loader
children: [
{
index: true,
element: <EventDetailPage />,
},
{ path: "edit", element: <EditEventPage /> },
],
},
{ path: "new", element: <NewEventPage /> },
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
import { useRouteLoaderData, json, useParams } 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; // '/events/:id'
const response = await fetch("http://localhost:8080/events/" + id);
if (!response.ok) {
throw json(
{ message: "이벤트 디테일에 대한 정보를 받아올 수 없습니다." },
{ status: 500 }
);
} else {
return response;
}
}
useRouteLoaderData : 부모의 데이터를 받기 위해 사용되는 훅. useLoaderData와 비슷하지만 부모 라우트에서 설정된 아이디값이 필요하다.useRouterLoaderData 훅을 사용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} />;
}
export default EditEventPage;
import { useNavigate } from "react-router-dom";
import classes from "./EventForm.module.css";
function EventForm({ method, event }) {
const navigate = useNavigate();
function cancelHandler() {
navigate("..");
}
return (
<form 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;
