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)
댓글 남기기