React Router v7에서 URL의 로케일 매개변수를 검증하는 방법

지원되지 않는 로케일 코드를 우아하게 처리하기

문제

로케일 식별자가 URL 구조의 일부가 되면 임의의 값을 포함할 수 있는 사용자 입력으로 변환됩니다. 사용자가 주소 표시줄에 /xx/about, /gibberish/contact 또는 기타 유효하지 않은 로케일 코드를 수동으로 입력할 수 있습니다. 검증 없이는 애플리케이션이 이러한 유효하지 않은 입력을 처리하는 방법을 결정해야 합니다. 유효하지 않은 로케일을 그대로 진행하도록 허용하면 i18n 라이브러리가 존재하지 않는 로케일 데이터를 로드하려고 시도할 때 번역 누락, 형식 오류 또는 런타임 오류가 발생할 수 있습니다. 사용자에게 알리지 않고 기본 로케일로 자동 전환하면 어떤 언어를 보고 있는지에 대한 혼란이 발생합니다. 오류 경계나 빈 페이지를 표시하면 사용자가 명확한 진행 경로 없이 고립됩니다.

로케일 검증은 컴포넌트가 렌더링되기 전과 번역 데이터가 로드되기 전인 요청 수명 주기 초기에 수행되어야 하므로 문제가 더욱 복잡해집니다. 검증이 너무 늦게 발생하면 애플리케이션이 이미 로케일별 리소스를 가져오려고 시도했거나 유효하지 않은 구성으로 i18n 프로바이더를 초기화하여 리소스를 낭비하고 잠재적으로 연쇄 장애를 일으킬 수 있습니다.

해결 방법

페이지가 렌더링되기 전에 라우트 로더에서 로케일 매개변수를 검증합니다. React Router의 로더는 컴포넌트가 마운트되기 전에 실행되므로 요청된 로케일이 애플리케이션의 지원 언어 목록에 존재하는지 확인하기에 이상적인 위치입니다. 로케일이 유효하면 요청이 정상적으로 진행되도록 허용합니다. 로케일이 유효하지 않으면 즉시 사용자를 안전한 대체 경로로 리디렉션합니다. 유효한 기본 로케일이 포함된 동일한 경로 또는 문제를 설명하는 전용 404 페이지로 리디렉션할 수 있습니다.

이 접근 방식은 유효하지 않은 로케일이 컴포넌트와 i18n 프로바이더에 도달하는 것을 방지합니다. 로더에서 리디렉션 응답을 반환함으로써 React Router의 내장 네비게이션 시스템을 활용하여 오류를 우아하게 처리합니다. 리디렉션은 SSR 중에는 서버에서, 네비게이션 중에는 클라이언트에서 발생하여 렌더링 전략 전반에 걸쳐 일관된 동작을 보장합니다. 사용자는 URL 변경을 통해 즉각적인 피드백을 받으며, 애플리케이션은 존재하지 않는 로케일에 대한 리소스 로드 시도를 방지합니다.

단계

1. 지원되는 로케일 정의

애플리케이션이 지원하는 유효한 로케일 코드 목록을 생성합니다. 이 목록은 검증을 위한 신뢰할 수 있는 소스 역할을 합니다.

export const SUPPORTED_LOCALES = ["en", "es", "fr", "de", "ja"] as const;

export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];

export function isValidLocale(locale: string): locale is SupportedLocale {
  return SUPPORTED_LOCALES.includes(locale as SupportedLocale);
}

이 헬퍼 함수는 타입 안전 검증을 제공하며 로더 및 애플리케이션의 다른 부분에서 재사용할 수 있습니다.

2. 로케일 접두사 라우트 로더에 검증 추가

로케일 접두사 페이지의 라우트 모듈에서 로케일 파라미터를 확인하고 유효하지 않은 경우 리디렉션하는 로더를 내보냅니다.

import type { Route } from "./+types/page";
import { redirect } from "react-router";
import { isValidLocale } from "~/i18n/locales";

export async function loader({ params }: Route.LoaderArgs) {
  const { locale } = params;

  if (!locale || !isValidLocale(locale)) {
    return redirect("/en/not-found");
  }

  return { locale };
}

export default function Page({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <h1>Content in {loaderData.locale}</h1>
    </div>
  );
}

로더는 URL에서 로케일을 추출하고 검증한 후, 검증에 실패하면 안전한 폴백으로 리디렉션합니다. 로케일이 유효한 경우 컴포넌트가 사용할 수 있는 데이터를 반환합니다.

3. 로케일 파라미터로 라우트 구성

routes.ts 파일에서 로케일을 동적 세그먼트로 포함하는 라우트를 정의합니다.

import { type RouteConfig, route } from "@react-router/dev/routes";

export default [
  route(":locale/about", "./routes/about.tsx"),
  route(":locale/contact", "./routes/contact.tsx"),
  route(":locale/not-found", "./routes/not-found.tsx"),
] satisfies RouteConfig;

:locale 매개변수가 있는 각 라우트는 컴포넌트가 렌더링되기 전에 검증이 수행되는 로더를 호출합니다.

4. 유효하지 않은 로케일을 위한 not-found 페이지 생성

로케일을 찾을 수 없음을 설명하고 네비게이션 옵션을 제공하는 전용 페이지를 구축합니다.

import { Link } from "react-router";
import { SUPPORTED_LOCALES } from "~/i18n/locales";

export default function NotFound() {
  return (
    <div>
      <h1>Language Not Found</h1>
      <p>The requested language is not supported.</p>
      <nav>
        <p>Choose a language:</p>
        <ul>
          {SUPPORTED_LOCALES.map((locale) => (
            <li key={locale}>
              <Link to={`/${locale}`}>{locale.toUpperCase()}</Link>
            </li>
          ))}
        </ul>
      </nav>
    </div>
  );
}

이 페이지는 명확한 피드백과 실행 가능한 다음 단계를 제공하여 사용자가 애플리케이션을 떠나지 않고 오류에서 복구할 수 있도록 돕습니다.

5. 완전히 유효하지 않은 경로에 대한 catch-all 라우트 추가

정의된 라우트 패턴과 일치하지 않는 URL의 경우, 라우트 구성 끝에 splat 라우트를 추가하세요.

import { type RouteConfig, route } from "@react-router/dev/routes";

export default [
  route(":locale/about", "./routes/about.tsx"),
  route(":locale/contact", "./routes/contact.tsx"),
  route(":locale/not-found", "./routes/not-found.tsx"),
  route("*", "./routes/catch-all.tsx"),
] satisfies RouteConfig;

splat 라우트는 이전 라우트와 일치하지 않는 모든 경로와 매칭되어, 완전히 잘못된 URL을 유효하지 않은 로케일 코드와 별도로 처리할 수 있게 합니다.

6. 선택적으로 not-found 페이지 대신 기본 로케일로 리디렉션

오류를 표시하는 대신 유효하지 않은 로케일을 자동으로 수정하려면, 기본 로케일을 사용하여 동일한 경로로 리디렉션하세요.

import type { Route } from "./+types/page";
import { redirect } from "react-router";
import { isValidLocale } from "~/i18n/locales";

const DEFAULT_LOCALE = "en";

export async function loader({ params, request }: Route.LoaderArgs) {
  const { locale } = params;

  if (!locale || !isValidLocale(locale)) {
    const url = new URL(request.url);
    const newPath = url.pathname.replace(/^\/[^/]+/, `/${DEFAULT_LOCALE}`);
    return redirect(newPath);
  }

  return { locale };
}

이 접근 방식은 유효하지 않은 로케일 세그먼트만 교체하면서 나머지 URL 경로를 보존하여, 로케일만 문제일 때 더 원활한 사용자 경험을 제공합니다.