https://macaronics-react-udemy-ex21-2.netlify.app/


1.App
동적라우트 설정
- 1)객체 배열 방식
import { createBrowserRouter , RouterProvider} from 'react-router-dom';
import HomePage from './pages/HomePage';
import EventsPage , {loader as eventLoader }from './pages/events/Events';
import EventDetailPage ,
{ loader as eventDetailLoader ,
action as deleteEventAction
} from './pages/events/EventDetailPage';
import NewEventPage from './pages/events/NewEventPage';
import EditEventPage from './pages/events/EditEventPage';
import RootLayout from './pages/RootLayout';
import ErrorPage from './pages/Error';
import EventsRootLayout from './pages/events/EventsRootLayout';
import { action as manipulateEventAction } from './components/EventForm';
import NewsletterPage , {action as newsletterAction} from './pages/newsletter/Newsletter';
// 라우트 정보를 담은 객체 배열
const router =createBrowserRouter( [
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorPage/>,
children: [
{ index: true, element: <HomePage /> },
{
path: 'events',
element: <EventsRootLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: eventLoader
},
{
path: 'new',
element: <NewEventPage /> ,
action: manipulateEventAction,
},
{
path: ':eventId',
id:'event-detail',
loader: eventDetailLoader,
children:[
{
index: true,
element: <EventDetailPage />,
action:deleteEventAction,
},
{
path: 'edit',
element: <EditEventPage /> ,
action: manipulateEventAction,
}
]
},
]
}
,
{
path: 'newsletter',
element: <NewsletterPage />,
action: newsletterAction,
},
]
},
]);
function App() {
return <RouterProvider router={router} /> ;
}
export default App;
2)
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import HomePage from './pages/HomePage';
import EventsPage from './pages/events/Events';
import EventDetailPage from './pages/events/EventDetailPage';
import NewEventPage from './pages/events/NewEventPage';
import EditEventPage from './pages/events/EditEventPage';
import RootLayout from './pages/RootLayout';
import ErrorPage from './pages/Error';
import EventsRootLayout from './pages/events/EventsRootLayout';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<RootLayout />} >
<Route index element={<HomePage />} />
<Route path="events" element={<EventsRootLayout />} >
<Route index element={<EventsPage />} />
<Route path="new" element={<NewEventPage />} />
<Route path=":eventId" element={<EventDetailPage />} />
<Route path=":eventId/edit" element={<EditEventPage />} />
</Route>
</Route>
<Route path="*" element={<ErrorPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
2. RootLayout
1)전체 페이지 레이아웃 설정
import React from 'react'
import MainNavigation from '../components/MainNavigation'
import { Outlet } from 'react-router-dom'
function RootLayout() {
return (
<>
<MainNavigation />
<main>
<Outlet />
</main>
</>
)
}
export default RootLayout
2) 서브페이지 레이웃 EventsRootLayout.js
import React from 'react'
import EventsNavigation from '../../components/EventsNavigation';
import { Outlet } from 'react-router-dom';
function EventsRootLayout() {
return (
<>
<EventsNavigation />
<Outlet />
</>
)
}
export default EventsRootLayout
3.ErrorPage
import React from 'react'
import PageContent from './../components/PageContent';
import { useRouteError } from 'react-router-dom';
import MainNavigation from '../components/MainNavigation';
function ErrorPage() {
const error =useRouteError();
let title="에러 발생됨!";
let message="에러가 발생했습니다.";
if(error.status === 500){
message=error.data.message;
}
if(error.status === 404){
title="404 Error";
message='페이지를 찾을 수 없습니다.';
}
return (
<>
<MainNavigation />
<PageContent title={title}>
<p>{message}</p>
</PageContent>
</>
)
}
export default ErrorPage
4. Events && EventDetailPage
동적라우트 파라미터 받기, 뒤로가기
1)Events
import React from "react";
import { Link } from "react-router-dom";
import classes from "./Event.module.css";
const DUMMYDATA = [
{
id: 1,
title: "Event 1",
description: "This is the first event",
date: "2021-10-10T17:00:00.000Z",
},
{
id: 2,
title: "Event 2",
description: "This is the second event",
date: "2021-10-10T17:00:00.000Z",
},
{
id: 3,
title: "Event 3",
description: "This is the third event",
date: "2021-10-10T17:00:00.000Z",
},
];
const EventsPage = () => {
return (
<div>
<h1>EventsPage</h1>
<p>
<ul>
{DUMMYDATA &&
DUMMYDATA.map((event) => {
return (
<li>
<Link key={event.id} to={"/events/" + event.id}>
{event.title}
</Link>
</li>
);
})}
</ul>
</p>
</div>
);
};
export default EventsPage;
2)EventDetailPage
import React from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
function EventDetailPage() {
let {eventId} =useParams();
const navigate=useNavigate();
return (
<div>
<h1>EventDetailPage : {eventId} </h1>
<button onClick={()=>navigate(-1)}>뒤로가기</button>
<br/> <br/> <br/>
<Link to=".."> 점두개로 뒤로가기</Link>
</div>
)
}
export default EventDetailPage
=========================================================================================================================
react-router-dom의 loader() 함수와 서버 사이드 렌더링(SSR)은 모두 웹 애플리케이션에서 데이터를 로드하고
렌더링하는 방법에 대한 다른 접근 방식입니다.
둘의 공통점과 차이점은 다음과 같습니다.
1.공통점:
둘 다 웹 애플리케이션에서 데이터를 로드하고 페이지를 렌더링하는 데 사용됩니다.
둘 다 사용자 경험을 향상시키기 위해 설계되었습니다.
2.차이점:
1)데이터 로딩 위치: SSR은 서버에서 데이터를 로드하고 HTML을 생성한 후 클라이언트에 전송합니다.
반면에 loader() 함수는 클라이언트 측에서 데이터를 로드합니다.
2)렌더링 시점: SSR은 서버에서 페이지를 렌더링하고, 이를 클라이언트에 전송합니다.
loader() 함수는 데이터를 로드한 후 클라이언트에서 페이지를 렌더링합니다.
loa
der() 함수가 SEO에 친화적인지 여부는 다양한 요인에 따라 달라집니다.
일반적으로, 클라이언트 측 렌더링은 검색 엔진 최적화(SEO)에 덜 유리할 수 있습니다.
이는 검색 엔진 크롤러가 JavaScript를 실행하지 않거나 제한적으로 실행하기 때문입니다.
그러나 최근에는 많은 검색 엔진들이 JavaScript를 더 잘 처리하고 있으며,
이로 인해 클라이언트 측 렌더링의 SEO 문제가 점점 줄어들고 있습니다.
그러나, 웹에서 중요한것이 seo 검색엔진 적용이다.
따라서, nextjs 프레임워크를 통해서만 ssr 적용이 가능하므로
react-router-dom 버전 6 이상 나오는 다음과 같은 loader() , action(), useFetcher(), defer() 등의 함수들은
그냥 이런것이 있다 정도 만 생각하면 될것 같다.
=========================================================================================================================
5. loader() 함수사용하기
react-router-dom 버전 6 이상에서 loader() 함수는 서버에서 데이터를 가져온 후 컴포넌트를 렌더링할 수 있게 도와주는 기능입니다.
다음은 loader() 함수의 사용 방법에 대한 간략한 설명입니다.
1.데이터를 가져오는 페이지(컴포넌트)의 route definition에 loader property를 추가합니다.
{
path: 'url',
element: <컴포넌트 />,
loader: () => {
// 데이터 가져오기
return data;
}
}
loader는 함수를 값으로 가집니다.
loader의 return 값은 element 컴포넌트와 해당 컴포넌트를 필요로 하는 모든 컴포넌트들(children)에 전달됩니다.
2.원하는 컴포넌트에서 loader에서 return한 data를 사용합니다.
const data명 = useLoaderData();
아래는 loader() 함수를 사용하는 예시입니다.
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import RootLayout from './pages/Root';
import Homepage from './pages/Home';
import EventsPage, { loader as eventsLoader } from './pages/Events';
import EventDetailPage from './pages/EventDetail';
import EditEventPage from './pages/EditEvent';
import NewEventPage from './pages/NewEvent';
import EventsRootLayout from './pages/EventsRoot';
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{ index: true, element: <Homepage /> },
{
path: 'events',
element: <EventsRootLayout />,
children: [
{ index: true, element: <EventsPage />, loader: eventsLoader },
{ path: ':eventId', element: <EventDetailPage /> },
{ path: 'new', element: <NewEventPage /> },
{ path: ':eventId/edit', element: <EditEventPage /> },
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
그리고 EventsPage 컴포넌트에서 useLoaderData()를 사용하여 loader에서 return한 데이터를 사용하는 예시입니다.
import React from 'react';
import EventsList from '../components/EventsList';
import { useLoaderData } from 'react-router-dom';
import { getEvents } from '../plugins/eventAxios';
function EventsPage() {
const events = useLoaderData();
return <>{<EventsList events={events} />}</>;
}
export default EventsPage;
export async function loader() {
const data = await getEvents();
return data.result;
}
이렇게 loader() 함수를 사용하면 비동기 데이터를 더 효과적으로 처리할 수 있습니다. 이 기능을 활용하면 사용자 경험을 향상시키는 데 도움이 될 것입니다.
6. loader() 함수사용하기
react-router-dom 버전 6에서는 json() 유틸리티 함수를 사용하여 데이터를 쉽게 처리할 수 있습니다.
이 함수는 주로 loader에서 사용되며, 컴포넌트가 렌더링 될 때 자동으로 response.json()을 호출하므로, 컴포넌트에서 데이터를 파싱할 필요가 없습니다.
json() 함수 사용법
import { json } from "react-router-dom";
const loader = async () => {
const data = getSomeData();
return json(data);
};
위의 코드에서 getSomeData()는 데이터를 가져오는 함수를 나타냅니다.
이 함수는 실제 애플리케이션에서 필요한 데이터를 가져오는 함수로 대체해야 합니다.
이렇게 하면 loader 함수는 json(data)를 반환하게 되며, 이는 react-router-dom이 자동으로 response.json()을 호출하게 됩니다.
async function loadEvents() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
// throw new Response(JSON.stringify({message:'이벤트를 가져올 수 없습니다.'}),
// { status:505}
// );
return json({ message: "데이터를 가져올 수 없습니다." }, { status: 500 });
} else {
// 다음과 같이 response 로 직접 반환 처리를 해도 다음과정이 생략되어 전달된다. 그러나, defer 사용시에는 원래데로 작성해 야 한다.
// const resData = await response.json();
// return {events: resData.events, isError: false};
const resData = await response.json();
return resData.events;
}
}
7. useRouteLoaderData 사용하기
react-router-dom 버전 6에서는 useRouteLoaderData 훅을 사용하여 현재 렌더링된 라우트의 데이터를 트리의 어디에서나 사용할 수 있습니다
import { useRouteLoaderData } from "react-router-dom";
function SomeComp() {
const user = useRouteLoaderData("root");
// ...
}
위의 코드에서 root는 라우트의 ID를 나타냅니다.
이 ID는 라우트를 생성할 때 정의됩니다.
이렇게 하면 useRouteLoaderData 훅은 root 라우트의 데이터를 반환하게 됩니다1. 이 데이터는 앱의 어디에서나 사용할 수 있습니다.
라우트 객체를 생성할 때 id 속성을 사용하여 라우트에 ID를 할당할 수 있습니다.
이 ID는 문자열이어야 하며, 라우트를 고유하게 식별하는 데 사용됩니다.
이 ID는 useRouteLoaderData 훅에서 사용되어 특정 라우트의 데이터에 액세스할 수 있게 합니다.
다음은 라우트에 ID를 할당하는 예시입니다.
const router = createBrowserRouter([
{
id: "root", // 라우트 ID 설정
element: <Team />,
path: "/teams/:teamId",
loader: async ({ request, params }) => {
return fetch(`/fake/api/teams/${params.teamId}.json`, {
signal: request.signal,
});
},
action: async ({ request }) => {
return updateFakeTeam(await request.formData());
},
errorElement: <ErrorBoundary />,
},
]);
위의 코드에서 root는 라우트의 ID를 나타냅니다.
이 ID는 라우트를 생성할 때 정의됩니다.
이렇게 하면 useRouteLoaderData 훅은 root 라우트의 데이터를 반환하게 됩니다.
이 데이터는 앱의 어디에서나 사용할 수 있습니다.
8. action() 사용하기
react-router-dom 버전 6에서 action() 함수는 라우트 로더의 "읽기"에 대한 "쓰기"를 제공합니다.
이 함수는 앱이 간단한 HTML 및 HTTP 의미론을 사용하여 데이터 변형을 수행할 수 있게 해주며, React Router는 비동기 UI와 재검증의 복잡성을 추상화합니다.
action() 함수는 앱이 라우트로 비-GET 제출(“post”, “put”, “patch”, “delete”)을 보낼 때마다 호출됩니다.
이는 몇 가지 방법으로 발생할 수 있습니다.
// 폼
<Form method="post" action="/songs" />;
<fetcher.Form method="put" action="/songs/123/edit" />;
// 명령형 제출
let submit = useSubmit();
submit(data, { method: "delete", action: "/songs/123" });
fetcher.submit(data, { method: "patch", action: "/songs/123/edit" });
action() 함수는 라우트로 보내는 Fetch Request 인스턴스를 받습니다.
가장 일반적인 사용 사례는 요청에서 FormData를 파싱하는 것입니다.
<Route action={async ({ request }) => {
let formData = await request.formData();
// ...
}} />
action() 함수에서는 웹 Response를 반환할 수도 있습니다.
useActionData에서 액세스할 수 있습니다.
EventForm.js
import { Form, useNavigate, useNavigation , useActionData, json, redirect } from 'react-router-dom';
import classes from './EventForm.module.css';
function EventForm({ method, event }) {
/**
*1.useActionData 사용자 입력 검증하고 검증 오류 출력하기
*
*1)폼 검증은 백엔드에서 실행한다.
*2)백엔드에서 반환상태값 예를 들어 422 status 로 반환처리한다.
*3)action 에서 다음과 같이 422 응답처리를 받으면 리턴한다.
* if(response.status===422){
* return response;
* }
*4)useActionData 에서 감지하여 백엔드에서 받은 오류데이터를 받는다
*
*/
const checkeData=useActionData();
const navigate = useNavigate();
const navigation=useNavigation();
//2.useNavigation
// react-router-dom 의 navigation 에서 실시간 상태값을 반환된다.
//따라서 다음과 같이 이용할수 있다.
const isSubmitting=navigation.state==='submitting';
function cancelHandler() {
navigate('..');
}
/**
* 3.action
* action='/any-other-path'
* action 값이 없다면 자동으로 해당 경로 /events/new 의 action 에서 데이터를 처리하게 된다.
*/
return (
<Form method={method} className={classes.form}>
{/* 1.useActionData 사용자 입력 검증하고 검증 오류 출력하기 */}
{checkeData&& checkeData.errors &&
<ul>
{Object.values(checkeData.errors).map(err=><li key={err}>{err}</li>)}
</ul>
}
<p>
<label htmlFor="title">제목</label>
<input id="title" type="text" name="title" required
defaultValue={event ? event.title : ''}
/>
</p>
<p>
<label htmlFor="image">이미지 주소</label>
<input id="image" type="url" name="image" required defaultValue={event ? event.image : ''} />
</p>
<p>
<label htmlFor="date">날짜</label>
<input id="date" type="date" name="date" required defaultValue={event ? event.date : ''} />
</p>
<p>
<label htmlFor="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.toUpperCase()==="PATCH"){
const eventId=params.eventId;
console.log("업데이트 처리 아이디 :", eventId);
url="http://localhost:8080/events/"+eventId;
}
const response= await fetch(url, {
method:method,
headers:{
'Content-Type':'application/json'
},
body:JSON.stringify(eventData)
});
//사용자 입력 검증하고 검증 오류 출력하기
if(response.status===422){
return response;
}
if(!response.ok){
throw json({message :'이벤트를 저장할 수 없습니다.'}, {status:500});
}
return redirect('/events');
}
9. useFetcher() 사용하기
react-router-dom 버전 6에서 useFetcher() 훅은 데이터 변형과 로드를 모델링하는 데 사용됩니다.
이 훅은 네비게이션 외부에서 로더를 호출하거나, URL을 변경하지 않고 액션을 호출하고 페이지의
데이터를 재검증하거나, 동시에 여러 변형을 비행 상태로 유지하는 등의 작업을 수행할 때 유용합니다.
import { useFetcher } from "react-router-dom";
function SomeComponent() {
const fetcher = useFetcher();
// call submit or load in a useEffect
React.useEffect(() => {
fetcher.submit(data, options);
fetcher.load(href);
}, [fetcher]);
// build your UI with these properties
fetcher.state;
fetcher.formData;
fetcher.json;
fetcher.text;
fetcher.formMethod;
fetcher.formAction;
fetcher.data;
// render a form that doesn't cause navigation
return <fetcher.Form />;
}
위의 코드에서 data와 options는 submit() 함수에 전달되는 데이터와 옵션을 나타내며, href는 load() 함수에 전달되는 URL을 나타냅니다1fetcher 객체는 다양한 속성을 가지고 있으며, 이를 사용하여 UI를 구성할 수 있습니다1fetcher.Form 컴포넌트를 사용하면 네비게이션을 발생시키지 않는 폼을 렌더링할 수 있습니다
NewsletterSignup
import { useFetcher } from 'react-router-dom';
import classes from './NewsletterSignup.module.css';
import { useEffect } from 'react';
function NewsletterSignup() {
/**
* https://reactrouter.com/en/main/hooks/use-fetcher
* useFetcher\
*1.공통되게 처리
*2.다른 페이지로 이동되지 않게 처리
3.loader 나 액션이 속한 페이지 또는 라우트를 로딩하지 않고, 그것을 트리거하고 싶을 때 사용해야 한다.
useFetcher는 React Router에서 제공하는 훅 중 하나로,
UI를 액션과 로더에 연결할 수 있게 해주는 기능입니다.
이 훅은 네비게이션 없이 로더를 호출하거나, URL을 변경하지 않고 액션을 호출하고 페이지의 데이터를 재검증하고 싶을 때 유용합니다.
또한, 여러 개의 변형을 동시에 처리해야 하는 경우에도 사용할 수 있습니다.
useFetcher를 사용하면, 다음과 같은 작업을 수행할 수 있습니다.
UI 경로와 관련이 없는 데이터를 가져오기 (팝오버, 동적 폼 등)
네비게이션 없이 액션에 데이터를 제출하기 (뉴스레터 가입과 같은 공유 컴포넌트)
목록에서 여러 개의 제출을 동시에 처리하기 (여러 버튼을 클릭하고 모두 동시에 대기 상태가 되어야 하는 “할 일” 앱 목록)
무한 스크롤 컨테이너 등
*/
const fetcher=useFetcher();
const {data, state}=fetcher;
useEffect(()=>{
//console.log("useFetcher data :", data);
if(state==='idle' && data && data.message) {
window.alert(data.message);
}
}, [data, state]);
return (
<fetcher.Form
method="post"
action='/newsletter'
className={classes.newsletter}>
<input
type="email"
placeholder="뉴스레터를 신청하세요..."
aria-label="뉴스레터 신청"
/>
<button>가입하기</button>
</fetcher.Form>
);
}
export default NewsletterSignup;
10. defer() 사용하기
react-router-dom 버전 6에서 defer() 함수는 데이터를 로드하는 동안 페이지를 즉시 렌더링하도록 합니다.
이는 라우트 로더에서 데이터를 가져오는 데 시간이 오래 걸리는 경우에 유용합니다12. defer() 함수를 사용하면 페이지가 Response 객체가 반환되기 전에 이미 로드되므로,
응답이 fetch(url)을 반환한 것처럼 자동으로 처리되지 않습니다.
다음은 defer() 함수를 사용하는 방법에 대한 예시입니다
import { Await, defer, useLoaderData } from "react-router-dom";
import { getPackageLocation } from "./api/packages";
async function loader({ params }) {
const packageLocationPromise = getPackageLocation(params.packageId);
return defer({
packageLocation: packageLocationPromise,
});
}
function PackageRoute() {
const data = useLoaderData();
const { packageLocation } = data;
return (
<main>
<h1>Let's locate your package</h1>
<p>
Your package is at {packageLocation.latitude} lat and{" "}
{packageLocation.longitude} long.
</p>
</main>
);
}
위의 코드에서 getPackageLocation()는 패키지 위치를 가져오는 함수를 나타냅니다.
이 함수는 실제 애플리케이션에서 필요한 데이터를 가져오는 함수로 대체해야 합니다.
defer() 함수를 사용하면 loader 함수는 defer({ packageLocation: packageLocationPromise })를 반환하게 되며,
이는 react-router-dom이 페이지를 즉시 렌더링하게 됩니다12. 이렇게 하면 컴포넌트에서 데이터를 파싱할 필요가 없어집니다
EventDetailPage.jsx
import React, { Suspense } from 'react'
import { json, useRouteLoaderData, redirect, defer, Await } from 'react-router-dom'
import EventItem from './../../components/EventItem';
import EventsList from '../../components/EventsList';
function EventDetailPage() {
const {event, events}=useRouteLoaderData('event-detail');
//const data=useLoaderData();
console.log("events :", event);
return (
<>
<Suspense fallback={<p className='center'>로딩중...</p> } >
<Await resolve={event}>
{ (loadEvent) =><EventItem event={loadEvent} />}
</Await>
</Suspense>
<Suspense fallback={<p className='center'>로딩중...</p> } >
<Await resolve={events}>
{(loadEvents) => <EventsList events={loadEvents} />}
</Await>
</Suspense>
</>
)
}
export default EventDetailPage;
async function loadEvents() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
return json({ message: "데이터를 가져올 수 없습니다." }, { status: 500 });
} else {
const resData = await response.json();
return resData.events;
}
}
async function loadEvent(params) {
const response=await fetch('http://localhost:8080/events/' +params.eventId);
if(!response.ok){
return json({message:'이벤트 상세 데이터를 가져올 수 없습니다.'}, {status:500});
}
const resData = await response.json();
return resData.event;
}
//로더
export async function loader ( {request, params} ){
return defer({
event: await loadEvent(params),
events: loadEvents(),
});
}
export async function action({request, params}){
const eventId=params.eventId;
const response=await fetch('http://localhost:8080/events/'+eventId ,{
method:request.method,
headers:{
'Content-Type':'application/json'
}
});
if(!response.ok){
throw json({message:'이벤트 삭제에 실패 했습니다'}, {status:500});
}
return redirect('/events');
}
1번 기본 소스를 참조로 해서 2번 소스를 볼것
소스
1) https://github.dev/braverokmc79/macaronics-react-udemy-ex21-1
2) https://github.dev/braverokmc79/macaronics-react-udemy-ex21-2














댓글 ( 0)
댓글 남기기