[React] 리엑트 라우터 loader

SuamKang·2023년 8월 14일
0

React

목록 보기
27/34
post-thumbnail

우선 react-router-dom은 우리가 SPA인 리엑트 웹 애플리케이션을 서버로부터 다중으로 많은 html페이지를 불러오지 않고도 인터렉티브하게 사용자에게 여러 페이지를 보여줄 수 있도록 도와주는 라이브러리이다.



✔️ loader() 사용하기


EventsPage.js

function EventsPage() {
  const [eventsList, setEventsList] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchData = async () => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch("http://localhost:8080/events");

      if (!response.ok) {
        throw new Error("Fetching is failed!");
      }

      const data = await response.json();

      setEventsList(data.events);
    } catch (error) {
      setError("http network is Unstable!");
    }
    setIsLoading(false);
  };

  useEffect(() => {
    fetchData();
  }, []);

  if (isLoading) return <p>Loading...</p>;

  if (error) return <p>{error}</p>;

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

export default EventsPage;

위 코드처럼 어느 한페이지에서 비동기로 http요청을 백엔드 api로 해야하는 로직이 항상 반복해야하는 상황이 생기기 마련이다.

이는 일종의 보일러 플레이트 코드라는 점을 파악 할 수 있다.


물론 커스텀 훅을 생성해 공통적으로 공유할 컴포넌트를 아웃소싱하여 적용가능하지만, react-router-dom이 제공하는 또다른 방법도 있다.

바로 loader()이다.

❗️ react-router-dom 버전 6이상 부터는 이 기능을 사용가능하다.

그럼 위 http요청 보일러 플레이트 코드를 어떻게 하면 react-router-dom에서 효과적으로 줄이고 통신 할 수 있을까?

우선 해당 요청은 현재 EventPage 라는 페이지 경로에 도달한 경우에만 전송될것이다.
이는 오직 해당 페이지에 도달한 경우에만 즉각적으로 그 요청을 백엔드로 전송하기 시작한다.

즉, http전송이 되기 전에 페이지를 구성하는 컴포넌트 전체가 렌더링 되어야 한다는걸 의미하기도 한다.


간단한 앱이면 모르겠다만, 복잡해질수록 실제로 요청전에 모든 컴포넌트들을 렌더링하고 평가하는건 성능에 그다지 좋은 영향을 주지 못할 것임은 분명하다.

그렇기 때문에 내가 해당 페이지를 접속하자마자 react-router-dom이 데이터 패칭을 동시에 혹은 렌더링하기 전에 하기 시작하면 더 좋을것 같다는 생각이 들었다.

1.데이터 패칭 -> 2.컴포넌트 렌더링 (만약 불러오는 게 느리다면 로딩 상태 폴백을 이용)

그럼 이제 loader라는 프로퍼티를 라우터 정의한 부분에서 사용해 보도록 하자.


✔️ loader() 적용하기


App.js

import { createBrowserRouter, RouterProvider } from "react-router-dom";

import RootPage from "./pages/RootPage";
import EventsRootPage from "./pages/EventsRootPage";

import HomePage from "./pages/HomePage";
import EventsPage from "./pages/EventsPage";
import EventDetailPage from "./pages/EventDetailPage";
import NewEventPage from "./pages/NewEventPage";
import EditEventPage from "./pages/EditEventPage";

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootPage />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "events",
        element: <EventsRootPage />,
        children: [
          {
            index: true,
            element: <EventsPage />,
        	loader: async () => {
              const response = await fetch("http://localhost:8080/events");

              if (!response.ok) {
                throw new Error("Fetching is failed!");
              }

              const data = await response.json();
              return data.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는 함수를 값으로 취하는 프로퍼티이며 일반함수, 오류함수 모두 취급한다.

해당 경로를 방문하기 직전에(jsx코드가 렌더링되기 전에) react-router-dom은 항상 이 loader함수를 실행한다.

그럼 loader함수 안에 데이터를 로딩하고 가져올 수 있다.

loader함수를 정의할 때, react-router-dom은 함수에서 리턴되는 모든 값(데이터)를 자동으로 취하고, 렌더링 되고 있는 페이지 혹은 그 데이터를 필요로하는 다른 모든 컴포넌트에 제공한다는 것이다.


그럼 그 데이터에는 어떻게 접근해야할까?

✔️ loader 데이터 접근하기


우선 사용하려는 컴포넌트로 간다.

EventsPage.js

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

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

function EventsPage() {
  const events = useLoaderData(); // loader가 리턴한 데이터

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

export default EventsPage;

기존에 연결했던 모든 상태와 로직을 지우고, useLoaderData를 import해온다.
-> 이건 가장 가까운() loader데이터에 접근하기 위해 실행할 수 있는 훅이다.

현재 설정한 loader함수는 async 함수이기 때문에 promise를 반환할것이며, 리엑트 라우터는 자동으로 그 promise로 부터 리졸빙된 데이터를 받게 된다.
즉, promise를 리턴하는지 아닌지 신경안써도 된다.

이렇게 사용하면 컴포넌트는 가벼운 '린컴포넌트'하게 만들 수 있다.



✔️ loader 데이터의 다양한 활용법


물론 오류처리를 해야하지만 그걸 살펴보기 이전에 useLoaderData훅을 사용할 수 있는 다른 곳들을 살펴보자.

EventsPage.js

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

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

function EventsPage() {
  // const events = useLoaderData(); 

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

export default EventsPage;

EventsPage에서 렌더링 하고있는 EventsList컴포넌트 자체에서도 사용 가능하다.

EventsList.js

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

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

function EventsList() {
  const events = useLoaderData();
  return (
    <div className={classes.events}>
      <h1>All Events</h1>
      <ul className={classes.list}>
        {events.map((event) => (
          <li key={event.id} className={classes.item}>
            <a href="...">
              <img src={event.image} alt={event.title} />
              <div className={classes.content}>
                <h2>{event.title}</h2>
                <time>{event.date}</time>
              </div>
            </a>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default EventsList;

이렇게 사용하면 정상적으로 동작한다.

하지만, 여기서 loader데이터를 못받는 곳이 있다.

바로 더 높은 수준 즉, 부모라우터부터는 사용 할 수 없다.

현재 아키텍처 상 RootPage가 루트 레이아웃 컴포넌트 페이지로 설정되어있기때문에 이는 상위로 지정된 라우터이다.

RootPage에서 한번 사용해보자.

RootPage.js


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

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

function RootPage() {
  const events = useLoaderData();
  console.log(events);
  return (
    <>
      <MainNavigation />
      <main>
        <Outlet />
      </main>
    </>
  );
}

export default RootPage;

이렇게 만일 해당 페이지에서 로드하고 콘솔을 살펴보면 undefined가 반환되는걸 체크 할 수 있다.

이렇게 되는 이유는 이보다 낮은 수준 즉 하위라우터에 정의된 loader데이터를 받으려고 하기 때문이다. 불가능하다.

즉, loader를 추가한 라우터와 같은 수준에있거나 더 낮은 수준에 있는 컴포넌트에서만 사용가능하다.




✔️ loader를 저장해야하는 위치는?


만약 실제로 App.js파일이 더 커졌다고 가정해 봤을때 더 많은 라우터에 loader를 추가 하게 될테고 해당 파일은 너무 많은 작업을 하게 된다.

리엑트 라우터에서 loader를 사용할때 권장되는 사항은 실제로 해당 loader로직이 필요한 컴포넌트 파일에 넣는것이다.

그럼 난 EventsPage에서 사용할 것이니 여기에 loader함수를 정의해서 export할 수 있다.


EventsPage.js

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

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

function EventsPage() {
  const events = useLoaderData(); // loader가 리턴한 데이터

  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) {
    throw new Error("Fetching is failed!");
  }

  const data = await response.json();
  return data.events;
};

이렇게 작성하고 다시 기존 라우터를 정의했던 App.js파일을 수정해 보도록 하자.

App.js

import { createBrowserRouter, RouterProvider } from "react-router-dom";

import RootPage from "./pages/RootPage";
import EventsRootPage from "./pages/EventsRootPage";

import HomePage from "./pages/HomePage";
import EventsPage, {loader as eventsLoader} from "./pages/EventsPage";
import EventDetailPage from "./pages/EventDetailPage";
import NewEventPage from "./pages/NewEventPage";
import EditEventPage from "./pages/EditEventPage";



const router = createBrowserRouter([
  {
    path: "/",
    element: <RootPage />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "events",
        element: <EventsRootPage />,
        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는 정확히 언제 실행되는지 살펴볼 필요도 있다.

앞에서도 언급했지만, 어떤 페이지 경로에 도달할때부터 호출된다.(렌더링 다음이 아니다!)

하지만 만약 백엔드 API자체적으로 느린 속도로 응답이 오게된다면, 프론트단에서 경로를 전환하자마자 데이터를 가져올 때까지 리엑트 라우터는 대기하고 완료가 되면 렌더링을 하게 된다.

장점 : 해당 페이지에 로드시 확실한 데이터를 받아 볼 수 있다.
단점 : 만약 서버쪽 지연이 있게 되면 사용자 입장에서 경험이 좋지 못할 수 있다.(time to view가 길어짐)

그럼 이걸 개선할 방법이 있을까?


리엑트 라우터는 사용자 경험을 개선하기 위한 다양한 도구들을 제공한다.

그 도구들을 하나씩 살펴보자.

1. 현재 네비게이션 상태(navigation.state)를 ui에 반영하기


리액트 라우터에서 제공하는 훅을 통해 현재 경로 전환 상태를 확인할 수 있다. 즉, 전환이 시작되었는지 아직 데이터가 프론트단으로 도착하지 않은건지, 이미 온건지에 대한 상태를 파악하는것이다.

import { Outlet, useNavigation } from "react-router-dom";

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



function RootPage() {

  const navigation = useNavigation();

  const isLoading = navigation.state === 'loading'

  return (
    <>
      <MainNavigation />
      <main>
        {isLoading && <p>Loading...</p>}
        <Outlet />
      </main>
    </>
  );
}

export default RootPage;

가장 최상위 루트 라우터 컴포넌트에 가서 useNavigation 훅을 import 한다.

이 훅을 통해 현재 전환 진행중인지 데이터를 로딩하는중인지 아님 아예 진행이 되지 않은건지 파악 할 수 있다.

useNavigation훅을 호출해 객체형태의 navigation을 얻게 되면 그 안에 몇가지 프로퍼티가 존재한다.

state : idle, loading, submitting 중에 하나이며 라우트 전환상태 여부에 따라 달라진다.

위 코드처럼 표시하면 사용자는 현재 로딩중이다는걸 파악 할 수 있게된다.




✔️ loader에서 응답 리턴하는 또다른 방법


loader로부터 받아온 응답은 어떤 타입이든 리턴할 수 있다.

즉, 응답객체를 리턴 할 수 있는건데, 브라우저에서 내장된 Response() 생성자 함수를 인스턴스화 해서 새로운 응답 객체를 생성할 수 있다.
-> 이는 최신 브라우저 기능으로 자신만의 응답을 구축 할 수 있게 해준다.

여기서 유의할 점은 loader코드는 서버에서 실행되고 있지 않는다는 점이다. -> 모두 브라우저에서 실행된다.(프론트 단에서 응답을 처리하는것이다.)

그럼에도 응답을 생성할 수 있는 이유는 브라우저에서 생성자와 응답객체를 지원해주고 있기 때문이다.

Response의 생성자의 첫번째 인자로 어떤 데이터도 받을 수 있고, 두번째 인자로 추가적인 객체를 이용해 상태코드 같은 프로퍼티 또한 설정 가능하다.
const res = new Response('any data', { status: 201});

loader에서 이러한 응답을 리턴할 때마다 react-router-dom은 useLoaderData를 사용할 때 자동으로 해당 응답에서 데이터를 추출한다.
사실 그냥 response.json()으로 받은 응답 객체로 생성도 가능하다.

하지만 위 기능이 존재하는 이유는 loader함수에서 브라우저에 내장된 fetch api로 백엔드에 도달하는 방식을 상당히 많이 적용하고 있고, 해당 fetch api는 실제로 Response로 리졸빙 되는 promise를 리턴하고 있다.

더군다나 리엑트 라우터는 이런 응답 객체들을 지원하고 자동으로 데이터를 추출해주기 때문에 흔히 받는 response 즉, 응답객체를 취해서 loader에서 바로 리턴할 수 있는것이다.
(쉽게말해, 수작업으로 데이터를 추출할 필요가 없다는 것!)

const data = await response.json();
return data.evnets;

👇

import { useLoaderData } 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) {
    throw new Error("Fetching is failed!");
  }

  return response;
}

이렇게하면 적용하는 컴포넌트에서 useLoaderData가 response의 일부인 데이터를 자동으로 전달해준다.
이점은 loader로직을 줄일 수 있고, 브라우저에 내장된 응답 객체에 대한 지원을 활용할 수 있게 되는것이다!


📍 loader에서 사용가능한 코드의 종류


loader안에 있는 로직은 서버가 아닌 브라우저에서 실행된다고 했다.
이 말은 더 넓은 시야로 보면 loader함수 안에서 어떤 브라우저 API도 사용할 수 있다는 의미이다.

예를들면 로컬스토리지나 쿠키에 접근할 수 도 있다.

단, 주의할 점은 loader함수 내에서는 useState와 같은 리엑트 훅은 사용할 수 없다.
왜냐하면 이러한 훅들은 리엑트 컴포넌트에서만 적용가능하기 때문이다.

리엑트 컴포넌트 !== loader함수

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

1개의 댓글

comment-user-thumbnail
2023년 8월 14일

좋은 글 감사합니다. 자주 올게요 :)

답글 달기