[React] 리엑트 라우터 loader 2

SuamKang·2023년 8월 14일
0

React

목록 보기
28/34
post-thumbnail

react-router-dom에서 제공하는 아주 중요한 기능인 loader의 전반적인 개념과 기능은 리엑트 라우터 loader에서 살펴보았다.

이제 응답에대한 오류 처리와 더불어 조금 더 동적으로 라우터를 사용하는 방법을 알아보자.




✔️ 커스텀 오류를 이용한 오류 처리


기존 useEffect를 통해 수작업으로 오류를 처리하곤 했다.
이젠 loader를 사용해 처리하는 방법을 살펴보자

export async function loader() {
  const response = await fetch("http://localhost:8080/events");

  if (!response.ok) {
    throw { message: "Could not fetch events"}
  }

  return response;

데이터를 컴포넌트에서 리턴하는 대신 loader로직 안에서 throw를 써서 오류를 낼 수 있다.

오류 생성자를 만들어 낼 수도있지만, 그냥 오류 객체를 리턴해도 좋다.
이렇게 던져주면 리엑트 라우터는 가장 근접한 오류 element를 렌더링 해준다.

라우터를 정의내릴때 가능한 프로퍼티중 errorElement를 지정해 오류 페이지를 리턴해주는 기능이 있었다.(지원하지 않는 경로에 대해)

하지만 이 errorElement는 단지 유효하지 않은 경로에대한 폴백 페이지를 표시하기 위해서만 있는것이 아니다.
-> 이는 loader를 포함한 어떤 라우트 관련 코드에 오류가 발생할 때마다 화면에 표시해주는 역할도 갖고 있는것이다.


우선 루트 라우터에 errorElement를 구성할 페이지 하나를 생성해 주자.

Error.js

import PageContent from "../components/PageContent";

function ErrorPage() {
  return (
    <>
      <PageContent title="오류 발생!!">
        <p>무언가 잘못 되었어요!</p>
      </PageContent>
    </>
  );
}

export default ErrorPage;

App.js

import ErrorPage from "./pages/Error";
...

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootPage />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      ...

이렇게 설정하면 ErrorPage는 정의한 라우트 어디경로에서나 어떤 종류의 오류라도 발생하게 되면 그때마다 표시 될것이다.

children으로 중첩된 구조가 얼마나 깊든 루트라우터에서 지정한 ErrorElement가 있다면 전부 영향을 미친다는 것이다.


하지만, 오류의 종류와 원인은 너무 다양하고 상황마다 적절한 오류 메시지를 표시해줄 필요도 분명 존재한다.

예를들어 404상태 오류가 발생했을 수 도 있고, loader에 응답으로 전달한 오류와도 구분이 되어야한다.

기본적인 오류메시지를 내가 정의한 메세지로 대체해보자.


export async function loader() {
  const response = await fetch("http://localhost:8080/events22");

  if (!response.ok) {
    // 브라우저에서 json형태로 응답을 보내주는 형태이다.
    throw new Response(JSON.stringify({ message: "Could not fetch events!" }), {
      status: 404,
    });
  }
  return response;
}

오류들을 구분하기 위해 객체를 throw하는 대신 Response생성자를 이용해서 Response를 throw했다.

그리고 그 Response에 약간의 데이터를 넣었다.

첫번째 인자로 응답객체를 전달하는 JSON파일을 넣어주고, 두번째 인자로 status같은 오류 상태로도 설정할 수 있다.

이렇게 설정한 이유는 errorElement로 지정되어 렌더링 되는 컴포넌트 안에서 오류로써 내보내지는 데이터를 실제로 내가 직접 컨트롤 할 수 있기 때문이다.

이때 react-router-dom은 useRouteError라는 훅을 제공한다.
이 훅은 적용할 컴포넌트에서 에러를 호출하기 위해 사용되며 반환되는 에러의 종류는 내가 던진 오류객체의 형태에따라 다르다.

import { useRouteError } from "react-router-dom";

import PageContent from "../components/PageContent";

function ErrorPage() {
  const error = useRouteError();
 // error에는 status필드가 포함되어있다.
  return (
    <>
      <PageContent title="오류 발생!!">
        <p>무언가 잘못 되었어요!</p>
      </PageContent>
    </>
  );
}

export default ErrorPage;

위 코드처럼 Response생성자를 통한 에러를 throw 하면 error객체는 당연히 status 필드를 포함하게 된다.

따라서 내가 오류를 컨트롤 하기 위해 일반적인 객체를 리턴하는게 아닌 Response를 생성하여 throw할 수 있다는 것을 알 수 있었다.


그리고 이렇게 내가 직접 구성한 오류객체는 컴포넌트에서 오류를 처리하는데 유연하게 구축하는 장점이 분명이 있다.

그 이유는 현재 ErrorPage에서 title과 message prop이 기본값으로 설정 되어 있지만, 내가 설정한 오류필드를 통해서 상황에 적절한 오류로 대체할 수 있기 때문이다.

import { useRouteError } from "react-router-dom";

import PageContent from "../components/PageContent";

function ErrorPage() {
  const error = useRouteError();

  // default
  let title = "오류 발생!!";
  let message = "Could not fetch events!";

  if (error.status === 500) {
    // 당연히 다시 객체로 사용하기 위해서 parsing해줘야 한다!
    message = JSON.parse(error.data).message;
  }

  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;

여기서 error.data는 생성했던 오류 Response에 포함된 데이터에 접근할 수 있게 해준다.

그럼 내가 loader에서 정해놓은 status에 따라서 다른 message에 접근할 수 있는것이다!

위 코드에서 처럼 500상태의 에러일땐 응답쪽에서 무언가 잘못 주었다고 판단할 수 있도록 했고, 404상태의 에러일땐 잘못된 url경로를 통해 접근했을때 나오게 설정했다.


이를 통해 loader에서 오류 처리를 진행할때 Response를 내가 직접 컨트롤 해서 throw하고, 루트 라우터에 추가된 errorElement 컴포넌트를 호출해서 오류를 보여주고 있다.

리엑트 라우터에 내장된 가능들을 이런식으로 오류처리를 활용할 수 도 있는걸 알게 되었다.


📍 json() 유틸리티 함수


하지만 오류를 Response로 직접 생성하는것도 한편으로 번거롭기도 하다.

그래서 리엑트 라우터는 헬퍼 유틸리티 함수를 제공한다.

import { useLoaderData, json } from "react-router-dom";

import EventsList from "../components/EventsList";

function EventsPage() {
  const data = useLoaderData();
  const events = data.events;

  return (
    <>
      <h1>This is EventsPage!</h1>
      <EventsList events={events} />
    </>
  );
}

export default EventsPage;


export async function loader() {
  const response = await fetch("http://localhost:8080/events");

  if (!response.ok) {
    // return { isError: true, message: "Could not fetch events!"}
    // throw new Response(JSON.stringify({ message: "Could not fetch events!" }), {
    //   status: 500,
    // });
    return json({ message: "Could not fetch events!" }, { status: 500 });
  }

  return response;
}

json은 리엑트 라우터가 제공하고 json형식의 데이터가 포함된 Response객체를 생성하는 함수인 것이다.

사용하는 방법은 그냥 간단하게 인자로 객체 그대로 넣어주면된다.

import { useRouteError } from "react-router-dom";

import PageContent from "../components/PageContent";
import MainNavigation from "../components/MainNavigation";

function ErrorPage() {
  const error = useRouteError();

  let title = "오류 발생!!";
  let message = "Could not fetch events!";

  if (error.status === 500) {
    message = error.data.message; // JSON.parse안해도 됨
  }

  if (error.status === 404) {
    title = "Not Found!";
    message = "Could not find resource or page.";
  }

  return (
    <>
      <MainNavigation />
      <PageContent title={title}>
        <p>{message}</p>
      </PageContent>
    </>
  );
}

export default ErrorPage;

그렇게 하면 수동으로 JSON형식을 파싱할 필요도 없게 되는것이다. 훨씬 단순해진것을 볼 수 있다!

이렇게 리엑트 라우터에서 제공하는 loader함수로 로직을 생성하여 응답을 통한 에러 처리 헨들링을 살펴보았다.



✔️ 동적 라우터 loader로 연결하기


이제 EventsPage에서 loader로 부터 받아와 나열된 이벤트 리스트들은 디테일 페이지들이고 결국 이를 로딩해줘야 한다.

구조
EventsPage(EventList컴포넌트 반환) >>> EventDetailPage(EventItem컴포넌트 반환)

EventsPage.js

import { useLoaderData, json } from "react-router-dom";

import EventsList from "../components/EventsList";

function EventsPage() {
  const data = useLoaderData();
  const events = data.events;

  return (
    <>
      <h1>This is EventsPage!</h1>
      <EventsList events={events} />
    </>
  );
}

export default EventsPage;

export async function loader() {
  const response = await fetch("http://localhost:8080/events");

  if (!response.ok) {
    return json({ message: "Could not fetch events!" }, { status: 500 });
  }

  return response;
}

일단 먼저 현재 유효하게 만들어진 EventsList컴포넌트에선 Link를 걸어주고 해당 디테일 페이지 경로도 알맞게 지정해주어야한다.

EventsList.js

import { Link } from "react-router-dom";

import classes from "./EventsList.module.css";

function EventsList({ events }) {
  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;

현재 events라우트는 상대경로로 지정했기 때문에 각 디테일로 이어지는 건 eventId로 연결되어 매핑되는 events중 각각의 데이터의 id로 링크해주면 되겠다.

EventItem.js

import classes from "./EventItem.module.css";


function EventItem({ event }) {
  function startDeleteHandler() {
    // ...
  }

  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}>
        <a href="edit">Edit</a>
        <button onClick={startDeleteHandler}>Delete</button>
      </menu>
    </article>
  );
}

export default EventItem;

EventDetailPage.js

import EventItem from "../components/EventItem";

function EventDetailPage() {

  return (
    <>
      <EventItem event={data.event} />
    </>
  );
}

export default EventDetailPage;

이곳에 EventItem컴포넌트를 렌더링 시켜준다.
-> 그리고 event prop을 설정해 evnetDetail 데이터를 넣어준다.
-> 즉, EventDetailPage에 렌더링할 EventItem컴포넌트에 props로 전달할 실제 event 데이터를 무엇을 통해 전달하게 할지 정하는 수순인것!

나는 useEffect훅을 이용해 일반적으로 데이터 패칭을 하는 방법 말고, 또다른 loader함수를 추가해 줄것이다.

import { useParams, json, useLoaderData } from "react-router-dom";
import EventItem from "../components/EventItem";

function EventDetailPage() {
  const data = useLoaderData(); // data = response객체

  return (
    <>
      {/* 백엔드에서 eventId 엔드포인트에 대한 응답줄때 객체 전체에서 event 프로퍼티에 응답 데이터를 포함시키고 있다. */}
      <EventItem event={data.event} />
    </>
  );
}

export default EventDetailPage;

export async function loader({ request, params }) {
  const id = params.eventId;

  const response = await fetch(`http://localhost:8080/events/${id}`);

  if (!response.ok) {
    throw json(
      { message: "Could not fetch details for selected event." },
      { status: 500 }
    );
  }

  return response;
}

loader함수 내에서 요청 로직을 구성할때 이제 해당 event각각에 해당하는 동적인 세그먼트 페이지로 설정하기 위해 eventId가 필요한 상황이 발생한다.

이때 useParams훅을 사용하고 싶지만, loader안에서는 사용할 수 없다.

대신 loader의 파라미터로 전달해준다. 이는 리엑트라우터가 실제로 loader를 실행할 때,
객체하나를 전달하는데 그 객체 중 하나는 요청객체를 담고 있는 requset프로퍼티 이며 다른하나는 모든 라우터파리미터가 담긴 params프로퍼티이다.
function loader({request, params}) {}

전달받은 requset객체를 통해 url에 접근할 수도 있지만, 내가 알고싶은건 해당 events의 어떤 경로의 상세 페이지인지 알고싶은 params이다.

fetch호출을 통해 전달 받는 response객체는 오류에 대한 부분도 체트 해야하기 때문에 바로 리턴하지않고 내장된 json 함수를 통한 오류 헨들링을 구성해줬다.

❗️주의 : 물론 이렇게 loader를 생성해 주었다면 라우터 정의 부분에서 꼭 해당 loader를 등록해주어야하는 것을 잊으면 안된다!



이렇게 해서 백엔드 데이터베이스에 저장되어있는 이벤트 데이터를 각각 요청 주소에 맞게 데이터 객체를 받아 적절하게 화면에 렌더링이 되는 결과를 볼 수 있게 되었다.



✔️ 다른 경로에서 데이터에 접근하는 방법


본격적인 사용자의 이벤트를 받고 데이터를 서버에 전송하기 전에,
마지막으로 다뤄바야할 부분이 있다!

EventItem.js

import { Link } from "react-router-dom";

import classes from "./EventItem.module.css";

function EventItem({ event }) {
  function startDeleteHandler() {
    // ...
  }

  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;

이 컴포넌트는 사용자가 해당 detailpage 경로로 갔을때 수정할 수 있는 기능(Edit)이 담긴 컴포넌트이기 때문에 수정할 수 있는 페이지인 EditEventPage로 라우터 이동을 위한 Link를 걸어줄 필요가 있다.


그다음엔 Link가 된 EditEventPage안에서 수정을 해야하기 때문에 수정이 가능한 양식이 들어있는 EventFrom컴포넌트를 추가해준다.

EditEventPage.js

import EventForm from "../components/EventForm";

function EditEventPage() {
  return <EventForm />;
}

export default EditEventPage;

이 페이지는 말그대로 수정할수 있는 페이지이기 때문에 미리 그전 데이터가 담겨있을 것이다.

그렇다면 생각해야한다.

어떻게하면 기존의 detailpage에 있던 데이터가 그대로 옮겨져 이 수정페이지에도 채워질 수 있을까??


처음 라우터들을 정의할 때 상세페이지와 수정페이지는 각각 다른 라우터로 정의했으니 각각 다른 loader가 필요하다고 느껴지지만, 다행이 반복해서 작성할 필요가 없이 사용할 방법이 있다.


우선 App.js안 router정의 부분에 path가 ":eventId"인 라우터를 하나 추가한다.
이 라우터가 다른점은 연결되는 element는 없을것이고 대신 children을 추가하여 방금 추가한 라우터를 부모라우터로 가지고 있는 형태로 상세페이지와 수정페이지를 설정한다.

App.js


const router = createBrowserRouter([
  {
    path: "/",
    element: <RootPage />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "events",
        element: <EventsRootPage />,
        children: [
          {
            index: true,
            element: <EventsPage />,
            loader: eventsLoader,
          },
          // 이부분이 변경됨.
          { 
            path: ":eventId",
            loader: eventDetailLoader,
            children: [
              {
                index: true,
                element: <EventDetailPage />,
              },
              {
                path: "edit",
                element: <EditEventPage />,
              },
            ]
          },
          {
            path: "new",
            element: <NewEventPage />,
          },
        ],
      },
    ],
  },
]);

그리고 나서 path가 ":eventId"인 라우터안에는 기존에 EventDetailPage에서만 사용하던 loader를 공유하기위해 끌어 올려 적용시켜준다.

이렇게 해서 중첩된 라우터 기능을 단지 래퍼 레이아웃 컴포넌트만이 아니라 loader를 공유하기 위해서 사용하기위해 쓸 수도 있다는것을 알았다.

=> 이렇게 하면 부모를 제외한 동일한 수준 혹은 더 하위 수준의 컴포넌트에있는 loader데이터를 useLoaderData 훅을 사용해서 접근할 수 있다는걸 배웠기 때문이다.

그렇다면 이제 수정페이지에서 useLoaderData훅을 사용할 수 있다!


EditEventPage.js

import { useLoaderData } from "react-router-dom";

import EventForm from "../components/EventForm";

function EditEventPage() {

  const data = useLoaderData();
  const event = data.event;
  const method = data.method;

  return <EventForm event={event} method={method}/>;
}

export default EditEventPage;

그리고 나서 받아온 수정해야할 상태가 담긴 데이터를 EventForm컴포넌트 안에서 각각 인풋값에 기본값(defaultValue)으로 설정해주면 된다.


❗️ 하지만 오류가 나오는데 이는 useLoaderData의 접근이 잘못되었다고 나왔다.

현재 수정페이지에서 useLoaderData로 접근하는 데이터는 내가 라우터에서 정의한 경로가 ":eventId"에 해당하는 라우터이며, 사실상 상세페이지와 수정페이지를 하위로 가지고 있는 형태임을 확인할 수 있다.

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootPage />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "events",
        element: <EventsRootPage />,
        children: [
          {
            index: true,
            element: <EventsPage />,
            loader: eventsLoader,
          },
          {
            path: ":eventId",
            loader: eventDetailLoader,
            children: [
              {
                index: true,
                element: <EventDetailPage />,
              },
              {
                path: "edit",
                element: <EditEventPage />,
              },
            ],
          },
          {
            path: "new",
            element: <NewEventPage />,
          },
        ],
      },
    ],
  },
]);

원래는 상위수준의 라우터에 있는 loader는 일반적으로 접근할 수 없지만, 그럼에도 불구하고 상위 수준에서 정의된 loader를 사용해야만 한다면,
즉 부모라우터의 데이터를 사용하려면, 특수한 id프로퍼티가 필요하다.

이 id는 부모라우터에 정의하고 해당 id를 통해 접근할 수 있다.
여기서 중요한점은 보통은 useLoaderData훅을 사용했지만, 이 경우엔 다른 훅을 사용해야한다.

바로 useRouteLoaderData 훅이다.

...
          {
            path: ":eventId",
            id: "event-detail", // 이 부분이다!
            loader: eventDetailLoader,
            children: [
              {
                index: true,
                element: <EventDetailPage />,
              },
              {
                path: "edit",
                element: <EditEventPage />,
              },
            ],
          },

...

그리고 다시 이 loader함수를 사용할 수정페이지(EditEventPage)와 상세페이지(EventDetailPage)에 가서 useRouteLoaderData훅을 사용하며, 인자가 필요한데 이는 아까 라우트 정의시 설정한 id를 넣어주면 된다.


EventDetailPage.js

import { useRouteLoaderData } from "react-router-dom";
import EventItem from "../components/EventItem";

function EventDetailPage() {
  const data = useRouteLoaderData("event-detail"); // data = response객체
  const event = data.event;

  return (
    <>
      <EventItem event={event} />
    </>
  );
}

export default EventDetailPage;

EditEventPage.js

import { useRouteLoaderData } from "react-router-dom";

import EventForm from "../components/EventForm";

function EditEventPage() {
  const data = useRouteLoaderData("event-detail");
  const event = data.event;
  const method = data.method;

  return <EventForm event={event} method={method} />;
}

export default EditEventPage;

이렇게 하고 보면 정상적으로 이전 데이터가 로딩되는걸 확인 할 수 있다.

이런식으로 id를 통해 loader가 없는 라우터에서 더 높은 상위 라우터에 있는 loader에 접근할 수 있다.
대신 이땐 useLoaderData가 아닌 useRouteLoaderData훅을 써야한다는걸 명심하자!

profile
마라토너같은 개발자가 되어보자

0개의 댓글