
loader() 프로퍼티를 학습하여 출력한 예시
먼저 loader() 라는 프로퍼티를 학습하여 사용하기 전에 원래 사용했던 예시를 살펴보자.
더미 데이터들을 백엔드로부터 가져오는 코드는 다음과 같다.
Event.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;
위와 같은 코드는 다음과 같은 방식으로 되어있다.
이벤트 페이지에 도달 => 이벤트 페이지 컴포넌트 전체가 렌더링 => 백엔드로부터 데이터 요청 전송
이와 같은 로직으로 이루어져 있다.
그런데 데이터 요청을 전송하기전에 컴포넌트 전체를 렌더링하는것은 그다지 좋지않다고 볼 수 있다.
그렇다면 다음과 같이 컴포넌트를 렌더링하기 이전에 데이터를 가져올 방법은 없는것일까?
이벤트 페이지 도달 => 백엔드로부터 데이터 요청 전송 => 이벤트 페이지 컴포넌트 전체가 렌더링
리액트 라우터의 데이터 가져오기를 사용하면 손쉽게 해결할 수 있다.
loader()프로퍼티를 사용하여 JSX코드가 렌더링되기 직전에 loader()함수를 리액트 라우터에 의해 트리거되고 실행된다.
App.js
const router = createBrowserRouter([
{
path: '/', element : <RootLayout/>,
children : [
{ index : true, element: <HomePage /> },
{ path : 'events',
element: <EventRootLayout/>,
children : [
// loader 프로퍼티를 사용하여 JSX 코드가 렌더링되기이전에 데이터를 가져오자.
{ 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;
}
}},
{ path : ':eventId', element : <EventDetailPage/>},
{ path : 'new', element : <NewEventPage />},
{ path : ':eventId/edit', element: <EditEventPage/>}
],
},
],
},
]);
function App() {
return <RouterProvider router={router}/>
}
export default App;
이렇게 loader 함수를 정의해놓고 loader 함수가 리턴한 데이터를 액세스해보자.
기존의 Event.js 코드의 대부분을 지우고, 가까운 loader 데이터에 액서스 할 수 있는 특수 훅 "useLoaderData"를 사용한다.
Event.js
// 가장 가까운 loader 데이터에 액세스하기 위해 실행할 수 있는 특수 훅 "useLoaderData"
// 가장 가까운 데이터란 loader 함수가 리턴한 resData를 말한다.
import { useLoaderData } from 'react-router-dom';
import EventsList from '../components/EventsList';
function EventsPage() {
//userLoaderData()를 호출하여 data 라는 상수 이름으로 받기
const data = useLoaderData();
return (
<>
<EventsList events={fetchedEvents} />
</>
);
}
export default EventsPage;
동일하게 작동하는것을 볼 수 있다.
loader를 추가한 라우트보다 같은 수준이나 더 낮은 수준에 있는 컴포넌트에서 액세스하여 작동 할 수 있지만, loader를 추가한 라우트보다 높은 수준에 있는 컴포넌트에서는 액세스하여 작동 할 수 없다.
예를들어
path: '/', element : <RootLayout/>,
children : [
{ index : true, element: <HomePage /> },
// 이벤트 레이아웃 만들기
{ path : 'events',
element: <EventRootLayout/>,
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;
}
}},
// 뒷주소를 new를 칠경우 어느 주소로 가게될까?
// 결과는 고정 엘리멘트가 역동적 세그먼트를 우선순위에서 우위에 있다.
{ path : ':eventId', element : <EventDetailPage/>},
{ path : 'new', element : <NewEventPage />},
{ path : ':eventId/edit', element: <EditEventPage/>}
'events' 경로에 loader에 대한 데이터를 ‘events’과 동일한 수준이거나 하위 수준에 있는 컴포넌트들은 사용이 가능하지만,
보다 높은 수준에서 정의된 컴포넌트들은 사용이 불가능하다.
ex ( 보다 높은 수준이 루트 컴포넌트 path: '/', element : )
loader를 추가하여 컴포넌트를 개선해준것은 맞지만 App.js 파일이 다음과 같이 더 커졌다고 주장할 수 있다. 또 더 많은 라우터에 더 많은 loader를 추가된다면 감당할 수 없는 App.js 의 코드가 될것이다.
App.js
const router = createBrowserRouter([
{
path: '/', element : <RootLayout/>,
children : [
{ index : true, element: <HomePage /> },
{ path : 'events',
element: <EventRootLayout/>,
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;
}
}},
{ path : ':eventId', element : <EventDetailPage/>},
{ path : 'new', element : <NewEventPage />},
{ path : ':eventId/edit', element: <EditEventPage/>}
],
},
],
},
]);
그래서 EventsPage를 위한 데이터 가져오기 로직이 App.js 파일이 아니라 EventPage에 속한다고 주장 할 수도 있다. 모두 옳은 주장이다. 일반적인 패턴 혹은 권장사항은 실제로 그 loader 코드를 필요로 하는 컴포넌트 파일에 넣는것이다.
정확히 말하면 페이지 컴포넌트 파일에 loader를 넣어야할것이다.
App.js 파일에서 loader 함수를 쓰되 페이지 컴포넌트 파일에서 import하여 함수의 포인터만을 가리키게끔 하는 방식을 사용하자.
Events.js
import { useLoaderData } from 'react-router-dom';
import EventsList from '../components/EventsList';
function EventsPage() {
const data = useLoaderData();
return (
<>
<EventsList events={data} />
</>
);
}
export default EventsPage;
// 컴포넌트에 함수를 서술하여 App.js 에서는 해당 함수의 포인터를 가리키게끔
export async function loader() {
const response = await fetch('http://localhost:8080/events');
if (!response.ok) {
} else {
const resData = await response.json();
return resData.events;
}
}
App.js
// ... 생략
//함수의 포인터를 가리킨다!
import EventsPage,{loader as eventsLoader} from "./pages/Events";
const router = createBrowserRouter([
{
path: '/', element : <RootLayout/>,
children : [
{ index : true, element: <HomePage /> },
{ path : 'events',
element: <EventRootLayout/>,
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;
컴포넌트 파일과 App.js 두 파일 모두 가독성 유지보수면에서 사용하기 용이하게 개선이 되었다.
loader()의 함수가 정확히 언제 실행되는가?
어떤 페이지에 대한 loader는 우리가 그 페이지로 이동하기 시작할때 호출이된다.
가령 상위 라우터 /event 에 컴포넌트가 있고 해당 라우터에 loader 프로퍼티를 정의했다고 하자.
해당 라우터에 대한 자식 라우터들로 event/eventID, /event/new , /event/eventId/edit 이 있다고 해보자.
이 /event에 관련된 컴포넌트에 어떤곳이던지 이동한다면 lodaer()함수가 호출이되어 데이터를 가져오고 가져온 데이터를 이용하여 렌더링이된다.
/event 라우트에 관련한 페이지로 이동 => loader()함수 호출후 데이터 가져옴 => 페이지 컴포넌트 렌더링
위 과정에서 알 수 있듯이 리액트 라우터는 데이터를 가져올때까지, 즉 loader가 작업을 완료할 때까지 대기하고 가져온 데이터로 페이지를 렌더링한다.
물론 단점도 존재한다. 대기 과정 속에서 사용자는 아무 일도 일어나지 않는것처럼 볼 수 있기 때문인데 이런 점도 리액트 라우터에서 제공하는 다양한 도구를 활용하여 개선할 수 있다.
loader 프로퍼티에서 사용되는 함수에는 모든 종류의 데이터를 리턴 할 수 있다.
그런데 fetch 함수를 사용할때 응답객체를 곧바로 리턴하는것도 가능하다.
원래는 loader() 함수 내에서 응답데이터에서 events를 추출하는 코드를 작성하고 데이터를 리턴했지만, fetch함수를 사용하고 곧바로 응답객체를 리턴하는것이다.
리턴받은 컴포넌트는 곧바로 응답객체의 events 키를 추출하여 사용할 수 있다.
자세한건 다음의 코드를 보자.
Events.js
// 가장 가까운 loader 데이터에 액세스하기 위해 실행할 수 있는 특수 훅 "useLoaderData"
// 가장 가까운 데이터란 loader 함수가 리턴한 resData를 말한다.
import { useLoaderData } from 'react-router-dom';
import EventsList from '../components/EventsList';
function EventsPage() {
//userLoaderData()를 호출하여 data 라는 상수 이름으로 받기
const data = useLoaderData();
// 응답객체의 events 키를 추출한다
const events = data.events;
return (
<>
<EventsList events={events} />
</>
);
}
export default EventsPage;
// 해당 리액트 라우터의 loader 프로퍼티의 함수는
// 컴포넌트가 렌더링되기 이전에 실행된다. 데이터를 미리가져옴
export async function loader() {
// loader() 함수에서는 브라우저에서 내장된 fetch 함수로 백엔드에 도달하는 방식을 많이 사용
// 이 fetch 함수는 실제로 Response로 해결되는 Promise를 리턴함.
// 따라서 fetch 함수로 응답 객체를 받아서 곧바로 loader 함수에서 리턴할 수 있는것.
// loader 안에서 응답데이터에서 events를 추출하는것보다 코드를 줄일수있다.
// loader 함수에서 리턴한 응답객체는 사용되는 컴포넌트에서 events키를 추출하여 사용하면된다.
const response = await fetch('http://localhost:8080/events');
if (!response.ok) {
// 올바르지 않은 응답 사례
// ...
} else {
return response;
}
}
주의 : loader() 함수의 코드는 서버가 아니라 브라우저에서 실행되는것이다. 백엔드코드처럼 보일수 있기 때문에 유의하자.
또한 브라우저에서 실행되는것이기 때문에 useState같은 리액트코드는 사용불가능하다.
첫번째 방법 : 단지 오류가 있다는 걸 표시하는 데이터를 리턴
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/이벤율튜ㅠ률뮤륜');
if (!response.ok) {
// 응답객체가 옳지않을때 isError 프로퍼티와 전달메시지 데이터를 감싸 전달한다.
return { isError: true, message: 'fetch 실패'};
} else {
return response;
}
}
두번째 방법(추천) : throw + 커스텀 오류컴포넌트 + errorElement 이용하기
fetch를 사용한 loader 함수에 throw문법을 사용하여 오류를 던질수있다.
Events.js
(...생략)
export async function loader() {
const response = await fetch('http://localhost:8080/events');
if (!response.ok) {
// 만약에 반응객체가 옳지않다면 오류 던지기
throw { message : 'fetch 실패'};
} else {
return response;
}
}
만약 오류가 던져진다면 리액트 라우터는 가장 근접한 오류 요소를 렌더링한다!
App.js
(...생략)
const router = createBrowserRouter([
{
path: '/', element : <RootLayout/>,
children : [
// 에러가 던져진다면(throw) 해당 프로퍼티가 렌더링될것이다
errorElement : <ErrorPage/>,
{ index : true, element: <HomePage /> },
{ path : 'events',
element: <EventRootLayout/>,
children : [
{ index:true,element: <EventsPage/>, loader : eventsLoader },
// 만약에 이곳에도 errorElment를 서술하고 해당관련 라우터가 오류를 발생시킨다면
// root의 errorElement가 트리거되지않고 가장 인접한 이곳의 errorElement가 트리거된다.
{ path : ':eventId', element : <EventDetailPage/>},
{ path : 'new', element : <NewEventPage />},
{ path : ':eventId/edit', element: <EditEventPage/>}
],
},
],
},
]);
Error.js ( 오류가던져지면 렌더링할 오류 컴포넌트 )
function ErrorPage() {
return <h1> 에러가 발생 했습니다! </h1>;
}
export default ErrorPage;
기본적인 404오류 (지정되지 않은 경로를 입력할경우) 뿐만아니라 fetch가 실패하여서 나오는 커스텀 오류등을 만들어서 구분하여 출력하고싶을경우는 어떻게 해야할까?
1.오류를 던질때 생성자 함수를 던져야한다. 던질때 JSON.stringfy로 정의하자.
Events.js
export async function loader() {
const response = await fetch('http://localhost:8080/edsvents');
if (!response.ok) {
throw new Response(JSON.stringify({message: 'fetch 실패'}), { status: 500,
});
} else {
return response;
}
}
2.Error 컴포넌트에서 useRouterError훅을 사용하여 적절히 동적출력을 하면 된다.
import { useRouteError } from "react-router-dom";
import PageContent from "../components/PageContent";
function ErrorPage() {
const error = useRouteError();
let title = '에러가 발생했습니다!';
let message = '에러';
if (error.status === 404) {
title = '찾을 수 없음.';
message = '리소스 또는 페이지를 찾을수 없습니다.';
}
if (error.status === 500) {
// json형식을 다시 객체로 바꾸는것을 잊지말자.
message = JSON.parse(error.data).message;
}
return (
<>
<PageContent title={title}>
<p>{message}</p>
</PageContent>
</>
);
}
export default ErrorPage;
Response는 보통 브라우저 환경에서 네트워크 요청에 대한 응답을 나타내는데 사용한다. JSON의 형식으로 일반적인 응답을 나타낸다.
JSON은 JavaScript Object Notation의 약자로 사람이 읽고 쓰기 쉽고, 기계가 분석하고 생성하기도 쉽다. 그래서 웹 서버와의 통신이나 데이터 저장에 널리사용 된다.
JSON.parse()는 JSON 문자열의 구문을 분석하고, 그 결과에서 JavaScript 값이나 객체를 생성한다.
코드 개선하기 (with JSON)
throw new Response(JSON.stringify({message: 'fetch 실패'}), { status: 500,});
해당 코드를 개선해보자. 이렇게 서술해도 상관은 없지만 좀더 짧고 간단명료하게도 가능하다. 리액트 라우터에서는 작은 헬퍼를 제공하는데 json 함수이다.
import json from 'react-router-dom'으로 import 해준뒤 사용하자.
Events.js
throw json(
{ message : 'fetch 실패'},
{
status : 500,
}
);
이렇게 하면 실제 사용할 코드에는 파싱을 안해도된다!
리액트 라우터가 알아서 파싱해주기 때문에 우리는 단지 곧바로 사용해도 된다.
Error.js
message = error.data.message;
21.11 : 동적라우터와 loader()~
21.xx : 연기해야할 데이터를 제어하는 방법
이부분은 정리하는것을 보류. 강의에 있는것으로 정리하면 조금 난잡해질거같음. 나중에 개인 프로젝트할때 예시를 가져와서 정리하는것이 좀더 나을 것같다. 일단 내용을 이해만 해두자.