๊ฐ์ธ๊ณผ์ ๋ฅผ ์งํํ๋ ์ค, Layout Shift ๋ฌธ์ ์ ๋ก๊ทธ์ธ ์ํ์ ๋ฐ๋ผ
ํ์ด์ง๋ฅผ ์ด๋์ํค๋ ๋ก์ง์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์๋ ๋ญ๊ฐ ์์๊น?
๋ผ๊ณ ์๊ฐํ๋ค๊ฐ, ๋ฌธ๋ React Router ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ Loader๊ฐ ๋ ์ฌ๋์ต๋๋ค.

๊ฐ ๊ฒฝ๋ก๋ ๋ ๋๋ง๋๊ธฐ ์ ์ ๊ฒฝ๋ก ์์์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๋
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๊ฐ์ง์์ต๋๋ค.
// ์๋ฒ์ ์ก์ธ์ค ํ ํฐ์ ๋ฃ์ด `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("/");
}
};