React Router의 <Link> 클릭 시 페이지 이동이 안 되던 현상을 임시로 해결
1. 개요
<Button variant="link" asChild className="text-lg self-center">
<Link to="/products/leaderboards/daily">Explore all products →</Link>
</Button>;
// routes.ts
import {
type RouteConfig,
index,
prefix,
route,
} from "@react-router/dev/routes";
export default [
index("common/pages/home-page.tsx"),
...prefix("products", [
index("features/products/pages/products-page.tsx"),
...prefix("leaderboards", [
index("features/products/pages/leaderboard-page.tsx"),
route(
"/daily/:year/:month/:day",
"features/products/pages/daily-leaderboard-page.tsx"
),
route(
"/:period",
"features/products/pages/leaderboards-redirection-page.tsx"
),
]),
...prefix("categories", [
index("features/products/pages/categories-page.tsx"),
route("/:category", "features/products/pages/category-page.tsx"),
]),
]),
] satisfies RouteConfig;
노마드 코더의 Maker 마스터클래스 강의를 듣던 중 발생한 에러이다. 컴포넌트 중 위와 같은 버튼이 있었는데, 이 버튼을 클릭하면 /products/leaderboards/daily로 이동하게 된다. 그리고 routes.ts에 의해 /features/products/pages/leaderboards-redirection-page.tsx에서 우선 loader 함수를 실행한다.
// leaderboards-redirection-page.tsx
import { data, redirect } from "react-router";
import { DateTime } from "luxon";
import type { Route } from "./+types/leaderboards-redirection-page";
export function loader({ params }: Route.LoaderArgs) {
const { period } = params;
let url: string;
const today = DateTime.now().setZone("Asia/Seoul");
if (period === "daily") {
url = `/products/leaderboards/daily/${today.year}/${today.month}/${today.day}`;
} else if (period === "weekly") {
url = `/products/leaderboards/weekly/${today.year}/${today.weekNumber}`;
} else if (period === "monthly") {
url = `/products/leaderboards/monthly/${today.year}/${today.month}`;
} else if (period === "yearly") {
url = `/products/leaderboards/yearly/${today.year}`;
} else {
return data(null, { status: 404 });
}
console.log({ period, url });
return redirect(url);
}
이어서 period가 daily이기 때문에, "/products/leaderboards/daily/2025/6/1"로 이동(2025년 6월 1일 기준)하게 되고, routes.ts에 의해 features/products/pages/daily-leaderboard-page.tsx를 실행하게 된다.
// daily-leaderboard-page.tsx
import { data, isRouteErrorResponse, Link } from "react-router";
import { DateTime } from "luxon";
import { z } from "zod";
import type { Route } from "./+types/daily-leaderboard-page";
import { Hero } from "~/common/components/hero";
import { ProductCard } from "../components/product-card";
import { Button } from "~/common/components/ui/button";
import ProductPagination from "~/common/components/product-pagination";
const paramsSchema = z.object({
year: z.coerce.number(),
month: z.coerce.number(),
day: z.coerce.number(),
});
export const loader = ({ params }: Route.LoaderArgs) => {
const { success, data: parsedData } = paramsSchema.safeParse(params);
console.log({ success, parsedData, params });
if (!success) {
throw data(
{ error_code: "invalid_praams", message: "Invalid params" },
{ status: 400 }
);
}
const date = DateTime.fromObject(parsedData).setZone("Asia/Seoul");
console.log("date", date);
if (!date.isValid) {
throw data(
{ error_code: "invalid_date", message: "Invalid date" },
{ status: 400 }
);
}
const today = DateTime.now().setZone("Asia/Seoul").startOf("day");
console.log("today", today);
if (date > today) {
throw data(
{ error_code: "future_date", message: "Future date" },
{ status: 400 }
);
}
return { ...parsedData };
};
export default function DailyLeaderboardPage({
loaderData,
}: Route.ComponentProps) {
console.log("loaderData", loaderData);
const urlDate = DateTime.fromObject({
year: loaderData.year,
month: loaderData.month,
day: loaderData.day,
});
// 생략
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (isRouteErrorResponse(error)) {
return (
<div>
{error.data.message} / {error.data.error_code}
</div>
);
}
if (error instanceof Error) {
return <div>{error.message}</div>;
}
return <div>Unknown error</div>;
}

그런데 위 움짤처럼 제대로 이동이 되지 않는 현상이 있었다. 너무 빠르게 /products/leaderboards 로 리다이렉트 되어서 잠깐 보이는 저 에러 문구는 아래와 같았다.

Error loading route module '/app/features/products/pages/daily-leaderboard-page.tsx', reloading page...
2. 원인 (추측)
여러 방법으로 해결해보려고 했는데 계속 실패했다. 그런데 주소창에서 http://localhost:3000/products/leaderboards/daily/2025/6/1를 직접 입력해서 들어가거나, Link를 아래처럼 window.location.href로 변경했을 때는 제대로 이동되는 것을 확인했다.
<button
className="text-lg self-center"
onClick={() => {
window.location.href = "/products/leaderboards/daily";
}}
>
Explore all products →
</button>;
그리고 React Router 공식 문서랑 같이 확인해 보았는데, 이 현상은 아무래도 React Router v7의 redirect 처리 버그 혹은 history stack 관리 문제가 원인일 수도 있다는 거 같다. React Router v7은 서버 우선 아키텍처를 기반으로 동작하며, 클라이언트 사이드에서의 redirect 처리에는 아직 완벽하지 않은 부분이 있는데, 특히 loader 내부의 redirect()는 서버에선 잘 작동하지만 클라이언트에선 히스토리 또는 모듈 로딩 이슈로 인해 무시될 수 있다.
3. 임시 해결
<Button variant="link" asChild className="text-lg self-center">
<Link to="/products/leaderboards/daily" reloadDocument>
Explore all products →
</Link>
</Button>
조금 더 찾아보니 reloadDocument라는 것을 알게 되었다. reloadDocument 속성은 페이지 이동 시, SPA의 클라이언트 라우팅을 우회하고 브라우저의 기본 동작인 전체 페이지 리로드를 수행하도록 강제한다. 즉, 클라이언트에서 JavaScript로 라우트를 전환하는 것이 아니라, 새로고침을 한 것처럼 HTML 전체를 서버에서 다시 요청하게 됩니다. 이로 인해, 모듈 로딩 실패나 클라이언트 라우터의 동적 경로 처리 문제와 관계없이 정상적으로 페이지가 로딩될 수 있습니다.

하지만 이는 임시방편으로 수정한 것이라, 근본적으로는 route 모듈 로딩 문제를 추적해서 해결하는 것이 바람직하다고 생각한다.