Jace Docs

React Router

React Router v7

https://reactrouter.com/

설치

npx create-react-router@latest my-react-router-app
cd my-react-router-app
pnpm install
pnpm run dev

라우터: Routes

1. 라우트 설정 (Configuring Routes)

app/routes.ts에서 URL 패턴과 실행할 파일(Route Module)을 연결합니다.

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

export default [
  // 1. 인덱스 라우트 (루트 페이지)
  index("./home.tsx"),

  // 2. 일반 라우트
  route("about", "./about.tsx"),

  // 3. 레이아웃 라우트 (URL에 영향 없음)
  layout("./auth/layout.tsx", [
    route("login", "./auth/login.tsx"),
    route("register", "./auth/register.tsx"),
  ]),

  // 4. 접두사(Prefix) 사용
  ...prefix("concerts", [
    index("./concerts/home.tsx"),
    route(":city", "./concerts/city.tsx"),
  ]),
] satisfies RouteConfig;

2. 라우트 모듈 (Route Modules)

라우트 파일은 데이터 로딩(loader)과 화면 렌더링(Component)을 담당합니다.

import type { Route } from "./+types/team";

// 데이터 로직: 컴포넌트 렌더링 전 실행
export async function loader({ params }: Route.LoaderArgs) {
  let team = await fetchTeam(params.teamId);
  return { name: team.name };
}

// UI 로직: loader에서 받은 데이터 사용
export default function Component({ loaderData }: Route.ComponentProps) {
  return <h1>Team Name: {loaderData.name}</h1>;
}

3. 중첩 라우팅 (Nested Routes)

부모 라우트 안에 자식 라우트를 배치하며, 부모 컴포넌트의 <Outlet /> 위치에 자식이 렌더링됩니다.

설정 (routes.ts):

route("dashboard", "./dashboard.tsx", [
  index("./dashboard-home.tsx"), // /dashboard
  route("settings", "./settings.tsx"), // /dashboard/settings
])

부모 컴포넌트 (dashboard.tsx):

import { Outlet } from "react-router";

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard Header</h1>
      <Outlet /> {/* 자식 컴포넌트들이 여기에 나타남 */}
    </div>
  );
}

4. 동적 및 선택적 세그먼트 (Dynamic Segments)

URL의 가변적인 부분을 파라미터로 처리합니다.

// 동적 파라미터: /teams/123 -> params.teamId
route("teams/:teamId", "./team.tsx"),

// 선택적 파라미터: /categories 또는 /en/categories 모두 매칭
route(":lang?/categories", "./categories.tsx"),

// 여러 파라미터 조합
route("c/:catId/p/:prodId", "./product.tsx"),

5. Splat (Catch-all) 라우트

*를 사용하여 일치하는 라우트가 없을 때의 처리를 정의합니다.

route("files/*", "./files.tsx"), // /files/any/path/here
route("*", "./catchall.tsx"),    // 404 처리용

파라미터 추출:

export async function loader({ params }: Route.LoaderArgs) {
  const splat = params["*"]; // "any/path/here"
}

6. 컴포넌트 라우트 (Component Routes)

파일 기반 라우팅 시스템 밖에서 단순 UI 전환이 필요할 때 사용합니다.

import { Routes, Route } from "react-router";

function Wizard() {
  return (
    <div>
      <Routes>
        <Route index element={<StepOne />} />
        <Route path="step-2" element={<StepTwo />} />
      </Routes>
    </div>
  );
}

핵심 요약:

  1. routes.ts: 전체 지도를 그립니다.
  2. loader: 필요한 데이터를 미리 준비합니다.
  3. <Outlet />: 중첩된 자식 페이지가 들어갈 자리를 만듭니다.
  4. params: URL에서 필요한 변수를 추출합니다.

데이터 로딩: Loader

1. 데이터 로딩 개요

React Router에서 데이터는 loaderclientLoader를 통해 컴포넌트에 전달됩니다.

  • 직렬화(Serialization): 로더에서 반환된 데이터(문자열, 숫자, Promise, Map, Set, Date 등)는 자동으로 직렬화되어 컴포넌트의 loaderData로 전달됩니다.
  • 타입 안전성: +types 파일을 통해 loaderData의 타입이 자동으로 생성되어 안전하게 사용할 수 있습니다.

2. 클라이언트 데이터 로딩 (clientLoader)

브라우저에서만 데이터를 가져오고 싶을 때 사용합니다. SPA(Single Page App) 방식에 익숙한 패턴입니다.

import type { Route } from "./+types/product";

export async function clientLoader({ params }: Route.ClientLoaderArgs) {
  // 브라우저의 fetch API 사용
  const res = await fetch(`/api/products/${params.pid}`);
  return await res.json();
}

// 데이터를 불러오는 동안 보여줄 UI
export function HydrateFallback() {
  return <div>로딩 중...</div>;
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <h1>{loaderData.name}</h1>
      <p>{loaderData.description}</p>
    </div>
  );
}

3. 서버 데이터 로딩 (loader)

SSR(서버 사이드 렌더링) 환경에서 사용됩니다. 첫 페이지 로드 시 서버에서 실행되며, 이후 클라이언트 탐색 시에도 자동으로 서버를 호출합니다.

import type { Route } from "./+types/product";
import { fakeDb } from "../db";

export async function loader({ params }: Route.LoaderArgs) {
  // 서버 전용 API나 DB 직접 접근 가능 (클라이언트 번들에는 포함되지 않음)
  const product = await fakeDb.getProduct(params.pid);
  return product;
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <h1>{loaderData.name}</h1>
      <p>{loaderData.description}</p>
    </div>
  );
}

4. 정적 데이터 로딩 (Static Data Pre-rendering)

빌드 시점에 데이터를 미리 가져와 HTML을 생성(Pre-render)할 때 사용합니다.

라우트 파일 (app/product.tsx):

export async function loader({ params }: Route.LoaderArgs) {
  // 빌드 시점에 실행됨
  return await getProductFromCSVFile(params.pid);
}

설정 파일 (react-router.config.ts):

import type { Config } from "@react-router/dev/config";

export default {
  async prerender() {
    // 미리 렌더링할 URL 목록을 반환
    let products = await readProductsFromCSVFile();
    return products.map((p) => `/products/${p.id}`);
  },
} satisfies Config;

5. 두 로더 함께 사용하기 (loader + clientLoader)

서버 로더와 클라이언트 로더를 조합하여 하이브리드 방식으로 데이터를 구성할 수 있습니다.

export async function loader({ params }: Route.LoaderArgs) {
  // 초기 서버 데이터
  return { serverMessage: "Hello from Server" };
}

export async function clientLoader({ serverLoader, params }: Route.ClientLoaderArgs) {
  // 1. 서버 로더 실행 결과를 가져옴
  const serverData = await serverLoader();
  // 2. 클라이언트에서 추가 데이터 호출
  const clientRes = await fetch(`/api/products/${params.pid}`);
  const clientData = await clientRes.json();
  
  // 두 데이터 합치기
  return { ...serverData, ...clientData };
}

// 하이드레이션 시점에 clientLoader 강제 실행 설정
clientLoader.hydrate = true as const;

export function HydrateFallback() {
  return <div>초기화 중...</div>;
}

💡 핵심 포인트 요약

  1. loader: 서버 측 작업(DB 접근 등) 및 SSR에 최적화.
  2. clientLoader: 브라우저 전용 API 사용 및 클라이언트 측 탐색 최적화.
  3. HydrateFallback: 클라이언트 로딩 중 사용자 경험(UX) 개선을 위한 필수 요소.
  4. serverLoader(): clientLoader 안에서 서버 데이터를 호출할 때 사용하는 유용한 함수.

액션: Actions

1. 액션(Actions) 개요

액션은 데이터를 변경(Mutation)할 때 사용합니다. 액션이 완료되면 페이지의 모든 loader 데이터가 자동으로 **재검증(Revalidation)**되어, 별도의 코드 작성 없이도 UI와 서버 데이터를 동기화해 줍니다.

  • action: 서버에서만 실행되며, 서버 전용 API나 DB 접근이 가능합니다.
  • clientAction: 브라우저에서 실행되며, 클라이언트 측 로직이나 외부 API 호출에 사용됩니다.

2. 클라이언트 액션 (clientAction)

브라우저에서 실행되며, 서버 액션과 함께 정의된 경우 클라이언트 액션이 우선권을 가집니다.

import type { Route } from "./+types/project";
import { Form } from "react-router";
import { someApi } from "./api";

export async function clientAction({ request }: Route.ClientActionArgs) {
  let formData = await request.formData();
  let title = formData.get("title");
  // 클라이언트 측 API 호출
  let project = await someApi.updateProject({ title });
  return project;
}

export default function Project({ actionData }: Route.ComponentProps) {
  return (
    <div>
      <Form method="post">
        <input type="text" name="title" />
        <button type="submit">수정</button>
      </Form>
      {actionData && <p>{actionData.title} 업데이트 완료!</p>}
    </div>
  );
}

3. 서버 액션 (action)

서버에서만 실행되며 클라이언트 번들에는 포함되지 않습니다. 보안이 중요한 작업에 적합합니다.

import type { Route } from "./+types/project";
import { Form } from "react-router";
import { fakeDb } from "../db";

export async function action({ request }: Route.ActionArgs) {
  let formData = await request.formData();
  let title = formData.get("title") as string;
  // DB 직접 업데이트
  let project = await fakeDb.updateProject({ title });
  return project;
}

export default function Project({ actionData }: Route.ComponentProps) {
  return (
    <Form method="post">
      <input type="text" name="title" />
      <button type="submit">저장</button>
    </Form>
  );
}

4. 액션을 호출하는 방법

A. <Form> 사용 (선언적 방식)

가장 표준적인 방법입니다. 제출 시 브라우저 히스토리에 새 기록이 추가됩니다.

import { Form } from "react-router";

function MyComponent() {
  return (
    <Form action="/projects/123" method="post">
      <button type="submit">프로젝트 삭제</button>
    </Form>
  );
}

B. useSubmit 사용 (명령적 방식)

타이머 종료나 특정 이벤트 발생 시 프로그래밍 방식으로 액션을 실행할 때 사용합니다.

import { useSubmit } from "react-router";

function Quiz() {
  let submit = useSubmit();
  
  // 예: 시간이 초과되었을 때 자동으로 제출
  const onTimeout = () => {
    submit(
      { quizTimedOut: true },
      { action: "/end-quiz", method: "post" }
    );
  };
}

C. Fetcher 사용 (페이지 이동 없는 방식)

현재 페이지의 상태나 히스토리를 변경하지 않고 데이터를 전송하고 싶을 때 사용합니다. (예: 좋아요 버튼, 할 일 체크 등)

import { useFetcher } from "react-router";

function Task() {
  let fetcher = useFetcher();
  let isSaving = fetcher.state !== "idle";

  return (
    <fetcher.Form method="post" action="/update-task/1">
      <button type="submit">{isSaving ? "저장 중..." : "저장"}</button>
    </fetcher.Form>
  );
}

💡 핵심 요약

  1. 자동 업데이트: 액션이 끝나면 loader가 다시 돌아 데이터를 최신화합니다.
  2. Form vs Fetcher: 페이지 이동이 필요하면 Form, 배경에서 처리하려면 Fetcher를 선택하세요.
  3. 데이터 접근: request.formData()를 통해 전송된 데이터를 쉽게 꺼낼 수 있습니다.
  4. 타입 지원: actionData를 통해 액션의 결과값을 타입 안전하게 컴포넌트에서 쓸 수 있습니다.

네비게이션: Navigating

1. 탐색(Navigating) 개요

사용자가 앱 내에서 페이지를 이동하는 방법은 크게 5가지입니다:

  • <NavLink>: 현재 활성 상태(Active)를 표시해야 하는 링크
  • <Link>: 단순 페이지 이동 링크
  • <Form>: 검색어 등을 URL 파라미터로 전달하며 이동
  • redirect: 로더나 액션 내부에서 서버 측 이동
  • useNavigate: 특정 이벤트 발생 시 프로그래밍 방식으로 이동

메뉴바나 탭처럼 "현재 어디에 있는지" 시각적으로 보여줘야 할 때 사용합니다.

import { NavLink } from "react-router";

export function Navbar() {
  return (
    <nav>
      {/* end 속성은 경로가 정확히 일치할 때만 active 클래스를 붙입니다 */}
      <NavLink to="/" end>Home</NavLink>
      <NavLink to="/concerts">Concerts</NavLink>
    </nav>
  );
}

자동 생성되는 CSS 클래스:

  • .active: 현재 경로와 일치할 때
  • .pending: 이동 중(데이터 로딩 중)일 때
  • .transitioning: 뷰 전환 애니메이션이 진행 중일 때

함수형 스타일링:

<NavLink
  to="/messages"
  style={({ isActive }) => ({
    fontWeight: isActive ? "bold" : "normal",
    color: isActive ? "red" : "black",
  })}
>
  Messages
</NavLink>

활성 상태 스타일이 필요 없는 일반적인 텍스트 링크에 사용합니다.

import { Link } from "react-router";

export function Footer() {
  return (
    <p>
      계정이 없으신가요? <Link to="/signup">회원가입</Link>
    </p>
  );
}

4. Form (검색 및 파라미터 이동)

입력값을 URLSearchParams로 변환하여 페이지를 이동시킵니다.

<Form action="/search">
  <input type="text" name="q" placeholder="검색어 입력..." />
  <button type="submit">검색</button>
</Form>
  • 사용자가 "react"를 입력하면 /search?q=react로 이동합니다.

5. redirect (서버 측 이동)

loaderaction 함수 안에서 특정 조건(로그인 여부 등)에 따라 사용자를 다른 페이지로 보낼 때 사용합니다.

import { redirect } from "react-router";

// 로더에서 권한 체크 후 리다이렉트
export async function loader({ request }) {
  const user = await getUser(request);
  if (!user) return redirect("/login");
  return { user };
}

// 액션에서 데이터 생성 후 상세 페이지로 이동
export async function action({ request }) {
  const formData = await request.formData();
  const project = await createProject(formData);
  return redirect(`/projects/${project.id}`);
}

6. useNavigate (프로그래밍 방식 이동)

사용자의 직접적인 클릭 없이 코드로 페이지를 이동시켜야 할 때 사용합니다. (최후의 수단으로 권장)

import { useNavigate } from "react-router";

export function InactivityLogout() {
  let navigate = useNavigate();

  // 예: 10분간 활동이 없으면 자동 로그아웃 페이지로 이동
  onInactivity(() => {
    navigate("/logout");
  });

  return null;
}

💡 핵심 요약

  1. 메뉴/네비게이션: NavLink를 사용하여 현재 위치를 표시하세요.
  2. 일반 본문 링크: Link를 사용하세요.
  3. 검색창: Form의 GET 방식(기본값)을 활용하세요.
  4. 로그인 체크/저장 후 이동: redirect를 사용하세요.
  5. 특수 상황(타이머 등): useNavigate를 활용하세요.

대기 중 UI: Pending UI

1. 대기 중 UI(Pending UI) 개요

사용자가 새로운 경로로 이동하거나 데이터를 제출할 때, UI는 즉각적으로 반응해야 합니다. 로더(Loader)가 데이터를 가져오는 동안이나 액션(Action)이 처리되는 동안 사용자에게 "작업 중"임을 알려주는 상태를 의미합니다.


2. 전역 대기 상태 (Global Pending Navigation)

새로운 URL로 이동할 때 다음 페이지의 로더가 완료될 때까지 기다립니다. 이때 useNavigation 훅을 사용하여 앱 전체의 로딩 상태를 표시할 수 있습니다.

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

export default function Root() {
  const navigation = useNavigation();
  // 현재 이동 중인 위치(location)가 있으면 true
  const isNavigating = Boolean(navigation.location);

  return (
    <html>
      <body>
        {/* 페이지 상단에 전역 스피너 표시 */}
        {isNavigating && <GlobalSpinner />}
        <Outlet />
      </body>
    </html>
  );
}

3. 지역 대기 상태 (Local Pending Navigation)

전체 화면이 아닌, 클릭한 링크 자체에 로딩 상태를 표시할 수 있습니다. NavLinkchildren, className, style 속성에서 isPending 값을 받아 처리합니다.

import { NavLink } from "react-router";

function Navbar() {
  return (
    <nav>
      <NavLink to="/home">
        {({ isPending }) => (
          <span>홈 {isPending && <Spinner />}</span>
        )}
      </NavLink>
      
      <NavLink
        to="/about"
        style={({ isPending }) => ({
          color: isPending ? "gray" : "black",
        })}
      >
        소개
      </NavLink>
    </nav>
  );
}

4. 폼 제출 대기 상태 (Pending Form Submission)

폼을 제출할 때 버튼의 텍스트를 "저장 중..."으로 바꾸는 등의 처리가 필요합니다.

A. Fetcher 사용 (권장)

useFetcher는 페이지 이동 없이 데이터를 주고받으므로 독립적인 상태 관리에 최적화되어 있습니다.

import { useFetcher } from "react-router";

function NewProjectForm() {
  const fetcher = useFetcher();
  // fetcher.state가 "idle"이 아니면 제출 중
  const isSubmitting = fetcher.state !== "idle";

  return (
    <fetcher.Form method="post">
      <input type="text" name="title" />
      <button type="submit">
        {isSubmitting ? "제출 중..." : "제출"}
      </button>
    </fetcher.Form>
  );
}

B. 일반 Form 사용

페이지 이동이 발생하는 일반 폼의 경우 useNavigation을 사용합니다.

const navigation = useNavigation();
const isSubmitting = navigation.formAction === "/projects/new";

5. 낙관적 UI (Optimistic UI)

서버의 응답을 기다리지 않고, 사용자가 입력한 데이터를 바탕으로 성공할 것이라 가정하여 UI를 즉시 업데이트하는 방식입니다. 체감 속도를 극적으로 높여줍니다.

function Task({ task }) {
  const fetcher = useFetcher();

  // 기본값은 서버에서 온 데이터
  let isComplete = task.status === "complete";

  // 만약 현재 폼이 제출 중이라면, 폼에 담긴 데이터를 우선 UI에 반영 (낙관적 업데이트)
  if (fetcher.formData) {
    isComplete = fetcher.formData.get("status") === "complete";
  }

  return (
    <div>
      <span>{task.title} (상태: {isComplete ? "완료" : "진행 중"})</span>
      <fetcher.Form method="post">
        <button name="status" value={isComplete ? "incomplete" : "complete"}>
          {isComplete ? "취소하기" : "완료하기"}
        </button>
      </fetcher.Form>
    </div>
  );
}

💡 핵심 요약

  1. useNavigation: 앱 전체의 이동 상태나 폼 제출 상태를 알 수 있습니다.
  2. NavLink: 개별 메뉴 링크의 로딩 상태를 시각화합니다.
  3. useFetcher: 페이지 전환 없는 데이터 변경 작업의 상태 관리에 유리합니다.
  4. 낙관적 UI: fetcher.formData를 읽어 서버 응답 전에 화면을 미리 바꿔줌으로써 최고의 UX를 제공합니다.

On this page