๐Ÿ•๏ธ React Router Loader ๋„์ž…๊ธฐ

๋ฐ•์›๋นˆยท2024๋…„ 6์›” 13์ผ

๊ฐœ์ธ๊ณผ์ œ๋ฅผ ์ง„ํ–‰ํ•˜๋˜ ์ค‘, Layout Shift ๋ฌธ์ œ์™€ ๋กœ๊ทธ์ธ ์ƒํƒœ์— ๋”ฐ๋ผ
ํŽ˜์ด์ง€๋ฅผ ์ด๋™์‹œํ‚ค๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์—๋Š” ๋ญ๊ฐ€ ์žˆ์„๊นŒ?
๋ผ๊ณ  ์ƒ๊ฐํ•˜๋‹ค๊ฐ€, ๋ฌธ๋“ React Router ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ Loader๊ฐ€ ๋– ์˜ฌ๋ž์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉํ•˜๊ธฐ์ „์— Docs๋ถ€ํ„ฐ ์ฝ์–ด๋ณด์ž!

๊ฐ ๊ฒฝ๋กœ๋Š” ๋ Œ๋”๋ง๋˜๊ธฐ ์ „์— ๊ฒฝ๋กœ ์š”์†Œ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋Š” loader ๊ธฐ๋Šฅ์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋กœ ์ด๋™ํ•  ๊ฒฝ์šฐ, ํ•ด๋‹น ๊ฒฝ๋กœ์˜ loader ๊ธฐ๋Šฅ์ด ๋ณ‘๋ ฌ์ ์œผ๋กœ ์‹คํ–‰๋˜๊ณ ,
useLoaderData ํ›…์„ ํ†ตํ•ด ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

๋ ˆ์ด์•„์›ƒ ์‹œํ”„ํŠธ ๋ฌธ์ œ๋Š” ํ•ด๋‹น ํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋ง ๋œ ํ›„ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ด
์—†๋˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์ง ! ํ•˜๊ณ  ๋‚˜ํƒ€๋‚˜๋Š” ํ˜„์ƒ์ž…๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ React Router ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ loader ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด
๋ Œ๋”๋ง ๋˜๊ธฐ ์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ด ๋ ˆ์ด์•„์›ƒ ์‹œํ”„ํŠธ ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ฒ ์ฃ ?

๋˜ํ•œ ๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋„ loader ๋กœ์ง์—์„œ ํŒ๋‹จํ•ด ๋งŒ์•ฝ ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด
๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋˜๊ณ , ๋กœ๊ทธ์ธ ํ† ํฐ์ด ์•„์ง ์œ ํšจํ•˜๋‹ค๋ฉด ๋กœ๊ทธ์ธํ•˜์˜€์„๋•Œ ๋ณด์ด๋Š” ํŽ˜์ด์ง€(๋งˆ์ดํŽ˜์ด์ง€)๋กœ ์ด๋™์‹œํ‚ค๋Š” ๋กœ์ง๋„ ๋‹ด์•„์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

loader์˜ ๋‹จ์ 

๋งŒ์•ฝ ์ด๋™ํ•œ ํŽ˜์ด์ง€์—์„œ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง์„ loader์— ์ž‘์„ฑํ•  ๊ฒฝ์šฐ,
loader๋Š” ๊ทธ ๋ฐ์ดํ„ฐ๋“ค์ด ์™„๋ฒฝํžˆ ๋ถˆ๋Ÿฌ์™€์ง€๊ธฐ์ „๊นŒ์ง€๋Š” ํ™”๋ฉด์„ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๊ธฐ๋•Œ๋ฌธ์—,
์œ ์ € ์ž…์žฅ์—์„œ๋Š” ์›น์‚ฌ์ดํŠธ๊ฐ€ ๋ฉˆ์ถฐ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ(๋ธ”๋กœํ‚น๋œ ๊ฒƒ์ฒ˜๋Ÿผ) ๋ณด์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

๋”ฐ๋ผ์„œ ๋งŒ์•ฝ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์™€์•ผ๋˜๋Š” ์ƒํ™ฉ์—์„œ ๋ ˆ์ด์•„์›ƒ ์‹œํ”„ํŠธ๊ฐ€ ๊ฑฑ์ •๋œ๋‹ค๋ฉด,
React์˜ Suspense ์ปดํฌ๋„ŒํŠธ์™€ ํ•จ๊ป˜ Skeleton UI ๋ฅผ ์ ์šฉ์‹œํ‚ค๋Š” ํŽธ์ด ๋‚˜์„ ๊ฒƒ ๊ฐ™๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค!


Docs๋„ ์ฝ์—ˆ๊ณ , ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€๋„ ์•Œ์•˜์œผ๋‹ˆ ์ด์ œ๋Š” ์ฝ”๋“œ๋กœ ์˜ฎ๊ฒจ ์ ์šฉ์‹œ์ผœ๋ณผ๊นŒ์š”?

๋กœ๋”๋ฅผ ๋„ฃ์–ด๋ณด์ž!

์ ์šฉ์‹œ์ผœ๋ณด๊ธฐ

// router.jsx
const router = createBrowserRouter([
  {
    path: "/",
    element: <LoginPage />,
  },
  {
    element: <Layout />,
    children: [
      {
        path: "/home/:user",
        loader: async ({ params }) => {
          try {
            const token = sessionStorage.getItem("token");
            const user = await authApi.getUserData(token);
            if (params.user !== user.id) return redirect(`/home/${user.id}`);
            return paymentHistoryApi.getPaymentHistoryById(user.id);
          } catch (err) {
            toast.error("์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.");
            sessionStorage.removeItem("token");
            return redirect("/");
          }
        }
        element: <HomePage />,
      },
      {
        path: "/detail/:user/:itemId",
        element: <DetailPage />,
      },
    ],
  },
]);

๋กœ์ง์— ํ•„์š”ํ•œ ์š”๊ตฌ์‚ฌํ•ญ์€ 2๊ฐ€์ง€์˜€์Šต๋‹ˆ๋‹ค.

  1. ์œ ์ €๊ฐ€ ์„ธ์…˜์— ๊ฐ€์ง€๊ณ ์žˆ๋Š” ์•ก์„ธ์Šค ํ† ํฐ์ด ์œ ํšจํ•˜์ง€์•Š๋”๋ผ๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™
  2. 1๋ฒˆ ๋‹จ๊ณ„๊ฐ€ ํ†ต๊ณผ๋˜์—ˆ๋‹ค๋ฉด ๊ทธ ์œ ์ €์˜ id๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์„œ๋ฒ„์—์„œ ์œ ์ €์˜ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
// ์„œ๋ฒ„์— ์•ก์„ธ์Šค ํ† ํฐ์„ ๋„ฃ์–ด `GET` ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ ,
const token = sessionStorage.getItem("token");
const user = await authApi.getUserData(token);
// ๋งŒ์•ฝ ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š๋‹ค๋ฉด ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•  ๊ฒƒ์ด๋ฏ€๋กœ `catch` ๋ฌธ์—์„œ ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์„ ์‚ญ์ œ,
// ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋Š” ๋กœ์ง์„ ์ž‘์„ฑ
catch (err) {
  toast.error("์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.");
  sessionStorage.removeItem("token");
  return redirect("/");
}
// loader ํ•จ์ˆ˜์˜ ๋งค๊ฐœ๋ณ€์ˆ˜ params์—์„œ user ๋ถ€๋ถ„์„ ๊ฐ€์ ธ์™€,
// ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์œ ์ €์˜ ์•„์ด๋””์™€ url์ƒ์˜ user๊ฐ€ ๋‹ค๋ฅด๋‹ค๋ฉด ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์œ ์ €์˜ ํŽ˜์ด์ง€๋กœ ์ด๋™
if (params.user !== user.id) return redirect(`/home/${user.id}`);
// ์ตœ์ข…์ ์œผ๋กœ ์„œ๋ฒ„์— ์œ ์ €์˜ ๋ฐ์ดํ„ฐ๋“ค์„ ๊ฐ€์ ธ์™€ return ํ•ด์ฃผ๊ธฐ
return paymentHistoryApi.getPaymentHistoryById(user.id);

๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉํ•ด์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” Docs์—์„œ ์•Œ๋ ค์ค€๊ฒƒ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

// HomePage.jsx
const initialHistoryList = useLoaderData();

๊ฐ€๋…์„ฑ ๋†’์—ฌ๋ณด๊ธฐ

๋‹ค๋ฅธ ํŽ˜์ด์ง€์—๋„ ์œ„์™€ ๊ฐ™์ด ๋กœ๋”๋ฅผ ์ ์šฉ์‹œ์ผœ์•ผ๋˜๋Š” ์ƒํ™ฉ์ด์–ด์„œ ๋‹ค๋ฅธ ํŽ˜์ด์ง€์—๋„ ์ ์šฉ์„ ์‹œ์ผœ์คฌ์œผ๋‚˜
๋กœ๋” ๋กœ์ง๋“ค์„ ํŽ˜์ด์ง€๋งˆ๋‹ค ๋‹ด์•„๋‘๋‹ˆ ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์ง€๊ณ  ๊ฐ€๋…์„ฑ์ด ์ข‹์ง€์•Š์•„
๋”ฐ๋กœ loaders.js ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ๋ถ„๋ฆฌ์‹œ์ผœ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

// router.jsx
const router = createBrowserRouter([
  {
    path: "/",
    loader: loginPageLoader,
    element: <LoginPage />,
  },
  {
    element: <Layout />,
    children: [
      {
        path: "/home/:user",
        loader: homePageLoader,
        element: <HomePage />,
      },
      {
        path: "/detail/:user/:itemId",
        loader: detailPageLoader,
        element: <DetailPage />,
      },
    ],
  },
]);

export default router;
// loaders.js
const getUser = async () => {
  const token = sessionStorage.getItem("token");
  const user = await authApi.getUserData(token);
  return user;
};

export const loginPageLoader = async () => {
  try {
    const user = await getUser();
    return redirect(`/home/${user.id}`);
  } catch (err) {
    return null;
  }
};

export const homePageLoader = async ({ params }) => {
  try {
    const user = await getUser();
    if (params.user !== user.id) return redirect(`/home/${user.id}`);
    return paymentHistoryApi.getPaymentHistoryById(user.id);
  } catch (err) {
    toast.error("์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.");
    sessionStorage.removeItem("token");
    return redirect("/");
  }
};

export const detailPageLoader = async ({ params }) => {
  try {
    const user = await getUser();
    if (params.user !== user.id) return redirect(`/home/${user.id}`);
    return paymentHistoryApi.getPaymentHistoryByItemId(params.itemId);
  } catch (err) {
    toast.error("์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.");
    sessionStorage.removeItem("token");
    return redirect("/");
  }
};

0๊ฐœ์˜ ๋Œ“๊ธ€