본문 바로가기
코딩 정보/React

[React][Router] 라우터 활용을 통한 비동기 통신 (상)

by 꽁이꽁설꽁돌 2024. 8. 22.
728x90
반응형

목차

     

    lodaer활용하기

    우리는 페이지를 불러오기 전에 데이터를 먼저 불러오고 그 데이터를 기반으로 페이지를 불러오고 싶은 경우가 있다.

    그럴떄 loader를 이용하면 된다.

     

    import { createBrowserRouter, RouterProvider } from "react-router-dom";
    import HomePage from "../pages/HomePage";
    import Layout from "./components/Layout";
    import EventsPages from "../pages/EventsPage";
    import EventDetailPage from "../pages/EventDetailPage";
    import EventLayout from "../pages/EventLayout";
    import NewEventPage from "../pages/NewEventPage";
    import EditEventPage from "../pages/EditEventPage";
    
    const router = createBrowserRouter([
      {
        path: "/",
        element: <Layout />,
        children: [
          {
            index: true,
            element: <HomePage />,
          },
          {
            path: "/events",
            element: <EventLayout />,
            children: [
              {
                path: "/events",
                element: <EventsPages />,
                loader: async () => {
                  const response = await fetch("http://localhost:8080/events");
    
                  if (!response.ok) {
                  } else {
                    const resData = await response.json();
                    return resData.events;
                  }
                },
              },
              {
                path: "/events/:id",
                element: <EventDetailPage />,
              },
              {
                path: "/events/:id/edit",
                element: <EditEventPage />,
              },
              {
                path: "/events/new",
                element: <NewEventPage />,
              },
            ],
          },
        ],
      },
    ]);
    
    function RouterLecture() {
      return <RouterProvider router={router}></RouterProvider>;
    }
    
    export default RouterLecture;

     

    위와 같이 페이지를 라우터를 통해 이동한다고 하면 다음과 같이 응용할 수 있다.

     

    EventsPages.jsx

    import EventsList from "../src/components/EventsList";
    import { useLoaderData } from "react-router-dom";
    function EventsPages() {
      const events = useLoaderData();
    
      return <EventsList events={events}></EventsList>;
    }
    
    export default EventsPages;

     

    주의해야 하는 점이 있다면 동급이하의 컴포넌트에서만 데이터를 불러올 수 있다는 점이다.

    즉 위의 컴포넌트들 중에서 HomePage, EventLayout, Layout 컴포넌트에서는 데이터를 불러올 수 없다.

     

     

    loader의 불러오는 위치 바꾸기

    다음과 같이 loader를 작성하면 페이지를 모아놓은 컴포넌트가 비대해지게 된다.

    따라서 다음과 같이 해결할 수 있다.

     

    EventsPages.jsx

    import EventsList from "../src/components/EventsList";
    import { useLoaderData } from "react-router-dom";
    function EventsPages() {
      const events = useLoaderData();
    
      return <EventsList events={events}></EventsList>;
    }
    
    export default EventsPages;
    
    
    export async function loader() {
      const response = await fetch("http://localhost:8080/events");
    
      if (!response.ok) {
      } else {
        const resData = await response.json();
        return resData.events;
      }
    }

     

    이런식으로 loader를 불러오는 위치를 변경해서 간결하게 만들 수 있다.

    ...
    import EventsPages, { loader as eventsLoader } from "../pages/EventsPage";
    
    
    ...
    const router = createBrowserRouter([
      {
      ...
            children: [
              {
                path: "/events",
                element: <EventsPages />,
                loader: eventsLoader,
              },
              ...
             
            ],
          },
        ],
      },
    ]);
    
    function RouterLecture() {
      return <RouterProvider router={router}></RouterProvider>;
    }
    
    export default RouterLecture;

     

    위와 같이 코드를 작성하면 백엔드에서 데이터를 가져올때 우리는 컴포넌트에 빈 데이터가 들어오는지에 대해 고려할 필요가 없다. 데이터가 들어올때까지 기다렸다가 컴포넌트가 랜더링되기 때문이다.

     

    그렇다면 우리는 현재 불러오고 있는 상태를 사용자에게 보여주고 싶다면 어떻게 하면 될까?

    바로 useNavigation을 이용하는 것이다.

    import { Outlet, useNavigation } from "react-router-dom";
    import MainNavigation from "./MainNavigation";
    
    function Layout() {
      const navigation = useNavigation();
    
      return (
        <>
          <MainNavigation />
          <main>
            {navigation.state === "loading" && <p>Loading...</p>}
            <Outlet />
          </main>
        </>
      );
    }
    export default Layout;

     

    이렇게 하면 아래와 같이 로딩문구와 Homepage의 문구가 같이 뜨게 되는데 이동하기 전에 

    http://localhost:3000/ 이곳에 머무르기 때문이다.

     

    loader를 통한 에러 핸들링하기

     

    1. 에러 객체 전달하기

    import EventsList from "../src/components/EventsList";
    import { useLoaderData } from "react-router-dom";
    import { useNavigation } from "react-router-dom";
    function EventsPages() {
      const data = useLoaderData();
      const events = data.events;
      if(data.isError){
        return <p>{data.message}</p>
      }
      return <EventsList events={events}></EventsList>;
    }
    
    export default EventsPages;
    
    export async function loader() {
      const response = await fetch("http://localhost:8080/events");
    
      if (!response.ok) {
        return { isError: true, message: "could not fetch events" };
      } else {
        return response;
      }
    }

      

    2. errorElement 페이지 활용하기

    import EventsList from "../src/components/EventsList";
    import { useLoaderData } from "react-router-dom";
    function EventsPages() {
      const data = useLoaderData();
      const events = data.events;
    
      return <EventsList events={events}></EventsList>;
    }
    
    export default EventsPages;
    
    export async function loader() {
      const response = await fetch("http://localhost:8080/eventsa");
    
      if (!response.ok) {
        console.log("error");
        throw new Response(JSON.stringify({ message: "Could not fetch events" }), {
          status: 500,
        });
       
      } else {
        return response;
      }
    }

     

    이런식으로 에러가 발생하면 에러요소가 있는 페이지가 나올때까지

    라우터의 상위 요소로 가서 상위 요소의 에러 페이지를 발생 시킨다.

    import ErrorPage from "../pages/ErrorPage";
    
    const router = createBrowserRouter([
      {
        path: "/",
        element: <Layout />,
        errorElement: <ErrorPage />,
        children: [
          {
            index: true,
            element: <HomePage />,
          },
         {
            path: "/events",
            element: <EventLayout />,
            children: [
              {
                path: "/events",
                element: <EventsPages />,
                loader: eventsLoader,
              },
        ],
        ...
      },
    ]);
    
    function RouterLecture() {
      return <RouterProvider router={router}></RouterProvider>;
    }
    
    export default RouterLecture;

     

    하지만 이렇게 하면 아래와 같이 json형식으로 바꾸고 파싱해야 하는 번거로움이 생긴다.

    import PageContent from "../src/components/PageContent";
    import { useRouteError } from "react-router-dom";
    function ErrorPage() {
      const error = useRouteError();
    
      let title = "An error occurred!";
      let message = "Something went wrong!";
    
      if (error.status === 500) {
        message = JSON.parse(error.data).message;
        console.log(error);
      }
      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;

     

    따라서 아래와 같이 json()을 이용해 바꿀 수 있다.

    주의해야 할 것은 예외를 발생시키고 중단해야 하기 때문에 thorw가 아닌 return을 해주면 오류에 직면한다.

    import EventsList from "../src/components/EventsList";
    import { useLoaderData, json } from "react-router-dom";
    function EventsPages() {
      const data = useLoaderData();
      const events = data.events;
    
      return <EventsList events={events}></EventsList>;
    }
    
    export default EventsPages;
    
    export async function loader() {
      const response = await fetch("http://localhost:8080/eventsa");
    
      if (!response.ok) {
        // console.log("error");
        // throw new Response(JSON.stringify({ message: "Could not fetch events" }), {
        //   status: 500,
        // });
        //throw 대신 return 해주면 오류남
        throw json(
          { message: "Could not fetch events" },
          {
            status: 500,
          }
        );
      } else {
        return response;
      }
    }

     

    동적인 페이지 예외처리하기

    그렇다면 동적인 params를 이용한 페이지는 어떻게 예외처리를 해야할까?

    바로 loader함수 자체에서 제공해주는 params를 이용하는 것이다.

    import EventItem from "../src/components/EventItem";
    import { useParams, json } from "react-router-dom";
    function EventDetailPage() {
      const params = useParams();
      return (
        <>
          <h1>EventDetailPage</h1>
    
          <EventItem></EventItem>
        </>
      );
    }
    
    export default EventDetailPage;
    
    //요청 객체와 parmas를 포함하고 있음
    export async function loader({ request, params }) {
      const id = params.eventId;
      const response = await fetch("http://localhost:8080/events/" + id);
      if (!response.ok) {
        throw json(
          { msessage: "Could not fetch details for selected event." },
          {
            status: 500,
          }
        );
      } else {
        return response;
      }
    }

     

    loader사용 시 주의할 점 (useLoaderData)

    최신 리액트에서는 부모에서의 loader를 자식들이 쓰지 못하기 때문에 하나하나 loader를 지정해주어야 한다.

     

    사용 불가 코드

     {
                path: ":eventId",
                loader: eventDetailLoader,
                children: [
                  {
                    index: true,
                    element: <EventDetailPage />,
                  },
                  {
                    path: "edit",
                    element: <EditEventPage />,
                  },
                ],
              },

     

    올바른 코드

     {
                path: ":eventId",
                children: [
                  {
                    index: true,
                    element: <EventDetailPage />,
                    loader: eventDetailLoader,
                  },
                  {
                    path: "edit",
                    element: <EditEventPage />,
                    loader: eventDetailLoader,
                  },
                ],
              },

     

    useRouteLoaderData

    이렇게 하면 하나하나 써주어야 하는 번거로움이 있는데 이를 위해서 useRouteLoaderData를 써주면 된다.

    이렇게 하면 id를 통해 부모의 loader의 접근이 가능하다.

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

     

    import EventForm from "../src/components/EventForm";
    import { useRouteLoaderData } from "react-router-dom";
    function EditEventPage() {
      const data = useRouteLoaderData("event-detail");
      const event = data.event;
    
      return (
        <>
          <EventForm event={event}></EventForm>
        </>
      );
    }
    export default EditEventPage;

     

    action을 이용해서 데이터 post 요청 해보기

    라우터에서 제공해주는 Form으로 바꾸고 method를 지정해 준다.

    그러면 액션함수에서 request인자로 전달 받을 수 있다.

     

    EventForm.jsx

    import { useNavigate, Form } from "react-router-dom";
    
    import classes from "./EventForm.module.css";
    
    function EventForm({ method, event }) {
      const navigate = useNavigate();
      function cancelHandler() {
        navigate("..");
      }
    
      return (
        <Form method={method} className={classes.form}>
          <p>
            <label htmlFor="title">Title</label>
            <input
              id="title"
              type="text"
              name="title"
              required
              defaultValue={event?.title}
            />
          </p>
          <p>
            <label htmlFor="image">Image</label>
            <input
              id="image"
              type="url"
              name="image"
              defaultValue={event?.image}
              required
            />
          </p>
          <p>
            <label htmlFor="date">Date</label>
            <input
              id="date"
              type="date"
              name="date"
              defaultValue={event?.date}
              required
            />
          </p>
          <p>
            <label htmlFor="description">Description</label>
            <textarea
              id="description"
              name="description"
              rows="5"
              defaultValue={event?.description}
              required
            />
          </p>
          <div className={classes.actions}>
            <button type="button" onClick={cancelHandler}>
              Cancel
            </button>
            <button>Save</button>
          </div>
        </Form>
      );
    }
    
    export default EventForm;

     

    NewEventPage.jsx

    loader와 비슷하게 인자로 받아주고 사용하는 것을 볼 수 있다.

    redirect를 통해 action실행 후 페이지의 이동을 만들었다. 

    import EventForm from "../src/components/EventForm";
    import { json, redirect } from "react-router-dom";
    function NewEventPage() {
      return <EventForm method={"POST"}></EventForm>;
    }
    export default NewEventPage;
    
    export async function action({ request, params }) {
      const data = await request.formData();
      
      //Object.fromEntries()는 이 이터레이터를 일반 객체로 변환합니다.
      //data.entries()는 FormData 객체의 모든 키-값 쌍을 이터레이터로 반환합니다.
      const fd = Object.fromEntries(data.entries());
      //json형식으로 바꾸어준다.
      const jsonFd = JSON.stringify(fd);
    
      const response = await fetch("http://localhost:8080/events", {
        method: "POST",
        body: jsonFd,
        headers: {
          "Content-Type": "application/json",
        },
      });
      if (!response.ok) {
        throw json(
          { message: "Could not save event" },
          {
            status: 505,
          }
        );
      }
      //리다이렉션을 통해 지정한 곳으로 이동한다.
      return redirect("/events");
    }

     

    그렇다면 의문점이 생긴다? 굳이 useNavigate를 통해 이동하면 되는데 redirect를 써야할까?

    일단 비동기 함수이기 때문에 컴포넌트 안이 아니라 useNavigate는 쓰지 못한다.

     

    또한 아래 이유 때문에 쓴다.

    1. 페칭 전 이동 -  데이터를 가져오기 전에 경로를 변경하여 불필요한 리소스 로드를 방지할 수 있다.
    2. 선 처리 리다이렉트 - 리소스를 로드하기 전에 특정 조건을 기반으로 경로를 변경할 수 있다.

     

    loader와 비슷하게 불러와준다.

       import NewEventPage, { action as newEventAction } from "../pages/NewEventPage";
       {
                path: "new",
                element: <NewEventPage />,
                action: newEventAction,
       },

     

    action에서 useSubmit을 이용해 요청 제어하기

       {
    	index: true,
    	element: <EventDetailPage />,
    	action: deleteEventAction,
       },

     

    아래와 같이 하면 method로 delete가 전달이 되면서 action함수에서 request.method로 사용가능할 수 있게 된다.

    삭제이기 때문에 첫 인자는 null로 넣어주었다.

    import classes from "./EventItem.module.css";
    import { Link, useSubmit } from "react-router-dom";
    function EventItem({ event }) {
      const submit = useSubmit();
      function startDeleteHandler() {
        const proceed = window.confirm("Are you sure?");
        if (proceed) {
          //첫번째는 우리가 제출할려는 데이터
          //두번째는 method및 action을 통한 경로 설정
          submit(null,{method:"DELETE"})
        }
      }
    
      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;

     

    이어서..

    너무 내용이 길어져서 아래 링크로 가면 더 있습니다.

    https://be-senior-developer.tistory.com/176

     

    [React][Router] 라우터 활용을 통한 비동기 통신 (하)

    목차 참고전편을 보고 보면 이해에 도움이 됩니다.https://be-senior-developer.tistory.com/175 [React][Router] 라우터 활용을 통한 비동기 통신 (상)목차 lodaer활용하기우리는 페이지를 불러오기 전에 데이터

    be-senior-developer.tistory.com

     

    반응형