React Router v7 Framework - Route Module

FeelsBotMan·2025년 2월 20일
0

React Router v7

목록 보기
3/4
post-thumbnail

Route Module

routes.ts에서 지정하는 파일(예: "./team.tsx")을 Route Module이라고 한다.

route("teams/:teamId", "./team.tsx"),
//                        ^^^^^^^^ Route Module

Route Module이란?

React Router의 핵심 구조로, 각 라우트의 동작을 정의하는 모듈 파일
React 컴포넌트 외에도 데이터 로딩, 액션 처리, 오류 처리 등을 수행

Route Module은 단순한 페이지 컴포넌트가 아니라, 다음과 같은 기능을 포함할 수 있다.

기능설명
코드 스플리팅필요한 페이지 코드만 로드
데이터 로딩 (loader)서버에서 데이터를 미리 불러오기
액션 (action)폼 제출 등의 이벤트 처리
재검증 (revalidation)데이터를 최신 상태로 유지
에러 경계 (ErrorBoundary)해당 라우트에서 발생한 오류 처리

Component (default)

React Router의 Route Module에서 export default로 정의한 컴포넌트는 해당 경로가 매칭될 때 렌더링되는 컴포넌트이다.

// app/routes/my-route.tsx
export default function MyRouteComponent() {
  return (
    <div>
      <h1>Look ma!</h1>
      <p>I'm still using React Router after like 10 years.</p>
    </div>
  );
}
  • export default로 내보내면 React Router가 해당 경로에서 이 컴포넌트를 자동으로 렌더링한다.

📌 컴포넌트에 전달되는 Props

라우트가 렌더링될 때, React Router는 몇 가지 props를 자동으로 전달한다.

이 props는 Route.ComponentProps 타입을 따른다.

Props설명
loaderData이 라우트 모듈의 loader() 함수가 반환한 데이터
actionData이 라우트 모듈의 action() 함수가 반환한 데이터
paramsURL에 포함된 동적 라우트 매개변수
matches현재 URL과 일치하는 모든 경로 정보 배열

이 props를 활용하면 별도의 훅 (useLoaderData, useParams 등) 없이도 데이터를 사용할 수 있다.

Props를 활용한 예제

1️⃣ 라우트 설정

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

export default [
  route("users/:userId", "./user.tsx"), // userId를 포함한 동적 라우트
] satisfies RouteConfig;

2️⃣ Route Module (user.tsx)

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

export default function MyRouteComponent({
  loaderData,
  actionData,
  params,
  matches,
}: Route.ComponentProps) {
  return (
    <div>
      <h1>Welcome to My Route with Props!</h1>
      <p>Loader Data: {JSON.stringify(loaderData)}</p>
      <p>Action Data: {JSON.stringify(actionData)}</p>
      <p>Route Parameters: {JSON.stringify(params)}</p>
      <p>Matched Routes: {JSON.stringify(matches)}</p>
    </div>
  );
}
  • params.userId를 통해 users/:userId 경로에서 userId 값을 가져올 수 있다.
  • loaderData, actionData는 해당 라우트에서 데이터를 미리 불러올 때 활용된다.

loader

loader()는 라우트 컴포넌트가 렌더링되기 전에 데이터를 제공하는 함수이다.
서버에서 실행되거나 빌드 시점(pre-rendering) 에만 호출된다.

📌 loader의 주요 특징

특징설명
서버에서 실행loader()서버에서만 실행되므로 클라이언트에서는 직접 호출되지 않음
비동기 지원API 요청 등을 수행할 수 있도록 async 함수로 정의
데이터 미리 로드컴포넌트가 렌더링되기 전에 데이터가 제공됨
자동 전달loader()의 반환 값이 loaderData props로 자동 전달
export async function loader() {
  return { message: "Hello, world!" };
}

export default function MyRoute({ loaderData }) {
  return <h1>{loaderData.message}</h1>;
}
  • loader()가 { message: "Hello, world!" } 데이터를 반환
  • MyRoute 컴포넌트에서 loaderData.message를 받아 화면에 출력

참고 https://api.reactrouter.com/v7/interfaces/react_router.LoaderFunctionArgs


clientLoader

clientLoader()브라우저에서만 실행되는 데이터 로딩 함수이다.
serverLoader()의 데이터를 활용할 수도 있고, 독립적으로 동작할 수도 있다.

📌 기본 개념

특징설명
클라이언트에서 실행clientLoader()는 오직 브라우저에서 실행됨
서버 데이터 활용 가능기존 serverLoader()의 데이터를 활용할 수 있음
클라이언트 전용 데이터 로딩브라우저에서만 필요한 데이터를 가져오는 데 사용됨
Hydration 지원서버에서 렌더링된 데이터를 클라이언트에서 보강 가능
export async function clientLoader({ serverLoader }) {
  // 서버에서 데이터를 가져옴
  const serverData = await serverLoader();

  // 클라이언트에서 추가 데이터 가져오기
  const clientData = getDataFromClient();

  // 최종 데이터를 반환
  return { ...serverData, clientData };
}
  • serverLoader()를 실행하여 서버 데이터를 가져옴
  • getDataFromClient()를 호출하여 클라이언트에서 추가 데이터를 로드
  • 두 데이터를 합쳐서 반환

Hydration을 활성화하는 경우

export async function clientLoader() {
  // 클라이언트에서 실행할 데이터 로딩 로직
  const data = await fetch("/api/client-data").then((res) => res.json());
  return data;
}

// Hydration 활성화
clientLoader.hydrate = true as const;
  • clientLoader.hydrate = true as const;를 설정하면 서버에서 제공된 데이터를 클라이언트에서 보강 가능
  • TypeScript에서 true as const를 사용하면 타입이 boolean이 아닌 true로 고정됨
  • React Router는 이를 활용해 loaderData의 타입을 자동으로 유추

언제 clientLoader를 사용할까?

  1. 클라이언트에서만 필요한 데이터를 불러올 때
    • 예: 사용자 인터랙션 후 특정 데이터를 요청하는 경우
  2. 서버 데이터와 클라이언트 데이터를 조합할 때
    • 예: serverLoader()에서 받은 데이터에 추가적인 브라우저 정보를 결합
  3. Hydration을 활용할 때
    • 서버에서 렌더링된 데이터를 클라이언트에서 보강하고 싶을 때

📌 clientLoader vs loader 차이점

비교 항목loader()clientLoader()
실행 위치서버에서 실행브라우저에서 실행
데이터 로딩 시점페이지 요청 시클라이언트 렌더링 후
서버 데이터 사용직접 반환serverLoader()를 호출하여 사용 가능
Hydration 지원기본적으로 서버에서 데이터 전달clientLoader.hydrate = true 설정 시 지원
사용 목적SSR, 초기 데이터 제공클라이언트에서 추가 데이터 요청

참고 https://api.reactrouter.com/v7/types/react_router.ClientLoaderFunctionArgs


action

action()서버에서 데이터를 변경하는 함수이다.
loader()와 함께 사용하면 데이터를 자동으로 갱신한다.
<Form>, useFetcher(), useSubmit()과 함께 사용 가능하다

1️⃣ 기본적인 loader()

export async function loader() {
  const items = await fakeDb.getItems();
  return { items };
}
  • fakeDb.getItems()를 통해 서버에서 데이터를 가져옴
  • 이 데이터는 loaderData.items컴포넌트에서 접근 가능

2️⃣ 컴포넌트에서 loaderData 사용

export default function Items({ loaderData }) {
  return (
    <div>
      <List items={loaderData.items} />
      <Form method="post" navigate={false} action="/list">
        <input type="text" name="title" />
        <button type="submit">Create Todo</button>
      </Form>
    </div>
  );
}
  • List items={loaderData.items}loaderData에서 받은 데이터를 리스트로 표시
  • <Form> 사용
    • method="post"POST 요청을 보냄
    • action="/list"/list 경로의 action()이 호출됨
    • navigate={false}페이지 새로고침 없이 데이터만 갱신

3️⃣ 서버에서 데이터 변경 (action())

export async function action({ request }) {
  const data = await request.formData();
  const todo = await fakeDb.addItem({
    title: data.get("title"),
  });
  return { ok: true };
}
  • request.formData()를 사용해 폼 데이터 가져오기
  • fakeDb.addItem()을 호출해 새로운 할 일 추가
  • { ok: true } 반환 (필수는 아니지만, 성공 여부를 전달 가능)
  • 자동으로 loader()가 다시 실행되며 데이터가 갱신됨

언제 action()을 사용할까?

  1. 서버에서 데이터를 추가, 수정, 삭제할 때
    • 예: 할 일 목록에 새로운 항목 추가 (POST), 항목 수정 (PUT), 삭제 (DELETE)
  2. 폼 제출 후 자동으로 리스트를 갱신하고 싶을 때
    • action() 실행 후 React Router가 자동으로 loader()를 다시 실행
  3. 페이지 새로고침 없이 서버 데이터를 갱신할 때
    • <Form navigate={false}>SPA 경험 유지

📌 action()과 loader()의 관계

기능loader()action()
실행 위치서버서버
역할데이터 조회데이터 변경 (POST, PUT, DELETE)
호출 방식페이지 로드 시 실행<Form>, useSubmit(), useFetcher() 호출 시 실행
자동 갱신X실행 후 loader()가 자동 재실행

clientAction

clientAction()은 클라이언트에서 실행되는 action()이다.

✔ 캐시를 무효화하거나 UI 상태를 변경할 때 유용
✔ 필요하면 serverAction()을 호출해 서버에서도 데이터를 변경 가능
✔ 서버 요청 없이 데이터를 변경할 때 효과적

export async function clientAction({ serverAction }) {
  fakeInvalidateClientSideCache(); // 캐시 초기화
  const data = await serverAction(); // 서버 데이터 다시 요청
  return data;
}
  • 클라이언트 캐시를 무효화 (fakeInvalidateClientSideCache())
  • 필요하면 serverAction()을 호출해 서버에서도 데이터 변경 가능
  • 데이터를 반환하면 클라이언트에서 활용할 수 있음

참고 https://api.reactrouter.com/v7/types/react_router.ClientActionFunctionArgs


ErrorBoundary

ErrorBoundary()를 정의하면 해당 라우트에서 발생한 오류를 잡아 사용자에게 표시한다.

HTTP 응답 에러(404, 500)와 일반적인 JavaScript 에러를 구분하여 처리 가능
예상하지 못한 에러도 대비하여 안전한 UI 제공

import {
  isRouteErrorResponse,
  useRouteError,
} from "react-router";

export function ErrorBoundary() {
  const error = useRouteError();
  • useRouteError() → 현재 라우트에서 발생한 에러 정보를 가져오는 React Router 훅
  • error 변수에 라우트에서 발생한 에러 정보가 담김

1️⃣ HTTP 응답 에러 처리

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>
          {error.status} {error.statusText}
        </h1>
        <p>{error.data}</p>
      </div>
    );
  }
  • isRouteErrorResponse(error) → 에러가 HTTP 응답 관련 에러인지 확인 (404, 500 등)
  • error.statuserror.statusText를 이용해 HTTP 상태 코드와 메시지 표시
  • error.data를 표시해 서버에서 전달된 추가 에러 메시지를 출력

예제:

throw new Response("Not Found", { status: 404 });

이런 응답이 발생하면, 에러 화면은 아래와 같이 표시됨:

404 Not Found

2️⃣ 일반적인 JavaScript 에러 처리

  else if (error instanceof Error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
        <p>The stack trace is:</p>
        <pre>{error.stack}</pre>
      </div>
    );
  }
  • 에러가 일반적인 JavaScript Error 객체인지 확인
  • error.message → 에러 메시지 표시
  • error.stack → 에러가 발생한 위치(콜스택) 표시

예제:

throw new Error("Something went wrong!");

이런 에러가 발생하면 화면에는 아래와 같이 표시됨:

Error
Something went wrong!
The stack trace is:
...

3️⃣ 예상하지 못한 에러 처리

  else {
    return <h1>Unknown Error</h1>;
  }
  • 위 두 가지 조건을 만족하지 않는 경우 (null, undefined, 기타 예외적인 값)
  • "Unknown Error" 메시지를 표시

HydrateFallback

HydrateFallback초기 페이지 로드 시, clientLoader가 데이터를 불러오는 동안 임시로 보여줄 UI를 정의한다.

초기 로딩 지연clientLoader가 데이터를 가져오는 동안 로딩 화면을 표시
클라이언트 전용 → 서버 렌더링 없이 클라이언트에서만 작동
로딩 UX 개선 → 사용자에게 빈 화면 대신 로딩 메시지나 스피너를 보여줄 수 있음

1️⃣ clientLoader – 클라이언트 데이터 로딩

export async function clientLoader() {
  const data = await fakeLoadLocalGameData();
  return data;
}
  • clientLoader가 데이터를 비동기적으로 가져옴
  • 이 과정에서 컴포넌트가 바로 렌더링되지 않음 → 데이터 로딩이 끝난 후에야 렌더링됨

2️⃣ HydrateFallback – 로딩 중 임시 화면

export function HydrateFallback() {
  return <p>Loading Game...</p>;
}
  • clientLoader가 데이터 로딩 중일 때 이 컴포넌트가 대신 렌더링됨
  • 사용자에게 "Loading Game..." 메시지를 보여줌
  • 로딩 스피너, 애니메이션 등으로 대체 가능

3️⃣ Component – 데이터 로딩 후 렌더링

export default function Component({ loaderData }) {
  return <Game data={loaderData} />;
}
  • clientLoader가 데이터를 모두 가져오면, Component가 렌더링됨
  • loaderData를 받아서 실제 게임 컴포넌트에 데이터를 넘김

headers

headers 함수는 서버 렌더링(SSR) 시 클라이언트에 전달할 HTTP 헤더를 설정한다.

SEO 최적화, 보안 설정, 캐싱 등 다양한 목적에 사용
✔ 클라이언트가 아닌 서버에서만 호출
✔ 특정 라우트에 커스텀 헤더를 추가 가능

export function headers() {
  return {
    "X-Stretchy-Pants": "its for fun",
    "Cache-Control": "max-age=300, s-maxage=3600",
  };
}

1️⃣ X-Stretchy-Pants: 커스텀 헤더

  • "X-"로 시작하는 커스텀 헤더
  • 브라우저에 직접적인 영향은 없지만, 디버깅이나 로그 추적에 사용 가능

2️⃣ Cache-Control: 캐싱 전략

  • max-age=300 → 클라이언트 브라우저가 5분(300초) 동안 캐시
  • s-maxage=3600 → CDN 또는 프록시 서버가 1시간(3600초) 동안 캐시
  • 캐싱을 통해 서버 부하 감소페이지 로딩 속도 향상
목적사용 예시
🔒 보안 강화Strict-Transport-Security, Content-Security-Policy
🚀 캐싱 최적화Cache-Control, ETag
🔍 SEO 개선X-Robots-Tag, Link (rel="canonical")
📊 로그/디버깅커스텀 헤더 (X-Custom-Header)

handle

handle객체는 라우트에 커스텀 데이터를 추가할 때 사용한다. 이를 통해 라우트에 부가 정보를 넣어 다양한 기능을 구현할 수 있다.

✔ 메타데이터, 브레드크럼, 권한 정보 등 저장 가능
useMatches 훅을 통해 모든 라우트의 handle 정보 접근 가능
✔ 단순히 데이터 전달용으로 사용됨 (렌더링에는 직접 관여하지 않음)

export const handle = {
  its: "all yours",
};
  • 이 라우트를 사용할 때 useMatches 훅으로 접근 가능
  • 결과: { its: "all yours" }

links 함수는 HTML <head><link> 태그를 추가하기 위한 용도로 사용된다. 이 함수가 반환한 링크들은 전역적으로 적용되며, 주로 아이콘, 스타일시트, 폰트, 프리로드 등을 설정할 때 사용한다.

✔ 파비콘(favicon), 외부 CSS, 자원 프리로드 설정 가능
✔ 모든 라우트의 링크들이 자동으로 통합되어 <head>에 적용
<Links /> 컴포넌트를 통해 <head>에 삽입

export function links() {
  return [
    {
      rel: "icon", // 브라우저 탭의 아이콘 설정
      href: "/favicon.png",
      type: "image/png",
    },
    {
      rel: "stylesheet", // 외부 CSS 파일 불러오기
      href: "https://example.com/some/styles.css",
    },
    {
      rel: "preload", // 이미지 미리 불러오기
      href: "/images/banner.jpg",
      as: "image",
    },
  ];
}

결과적으로 생성되는 <head> 태그

<head>
  <link rel="icon" href="/favicon.png" type="image/png" />
  <link rel="stylesheet" href="https://example.com/some/styles.css" />
  <link rel="preload" href="/images/banner.jpg" as="image" />
</head>

<Links /> 컴포넌트로 적용

import { Links } from "react-router";

export default function Root() {
  return (
    <html>
      <head>
        <Links /> {/* 모든 라우트의 links를 적용 */}
      </head>
      <body>
        <h1>My App</h1>
      </body>
    </html>
  );
}

활용 예시

용도코드 예시
🎨 파비콘 설정{ rel: "icon", href: "/favicon.ico" }
📄 외부 폰트 불러오기{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Roboto" }
이미지 프리로드{ rel: "preload", href: "/hero.jpg", as: "image" }
🛠️ PWA 설정{ rel: "manifest", href: "/site.webmanifest" }

meta

meta 함수는 HTML <head>에 메타 태그를 추가하기 위한 용도로 사용된다. 주로 SEO 최적화, SNS 공유 설정(Open Graph), 문서 정보 제공 등에 활용된다.

<title> 태그 설정
meta 태그로 SEO 및 SNS 미리보기 설정
✔ 모든 라우트의 메타 태그가 통합되어 <head>에 삽입됨

export function meta() {
  return [
    { title: "Very cool app" }, // <title> 태그 설정
    {
      property: "og:title",     // Open Graph: SNS 공유 시 제목
      content: "Very cool app",
    },
    {
      name: "description",      // 검색 엔진 및 미리보기에 표시될 설명
      content: "This app is the best",
    },
  ];
}

결과적으로 생성되는 <head> 태그

<head>
  <title>Very cool app</title>
  <meta property="og:title" content="Very cool app" />
  <meta name="description" content="This app is the best" />
</head>

<Meta /> 컴포넌트로 적용

import { Meta } from "react-router";

export default function Root() {
  return (
    <html>
      <head>
        <Meta /> {/* 모든 라우트의 meta를 적용 */}
      </head>
      <body>
        <h1>My App</h1>
      </body>
    </html>
  );
}

활용 예시

용도코드 예시
🏷️ 페이지 제목{ title: "My Awesome App" }
🖼️ Open Graph 이미지{ property: "og:image", content: "https://example.com/image.png" }
💬 페이지 설명{ name: "description", content: "A cool web app for everyone." }
🐦 Twitter 카드 설정{ name: "twitter:card", content: "summary_large_image" }
📱 반응형 뷰포트 설정{ name: "viewport", content: "width=device-width, initial-scale=1" }

참고 https://api.reactrouter.com/v7/interfaces/react_router.MetaArgs


shouldRevalidate

shouldRevalidate 함수는 라우트의 데이터가 다시 로드되어야 하는지를 결정한다. 기본적으로, 모든 라우트는 액션 후에 자동으로 데이터가 재검증(revalidate) 된다. 하지만, 데이터에 영향이 없는 경우 재검증을 건너뛸 수 있다.

✔ 성능 최적화: 필요할 때만 데이터 다시 가져오기
✔ 불필요한 API 호출 방지로 서버 비용 절감
✔ 복잡한 폼이나 대용량 데이터 페이지에 유용

import type { ShouldRevalidateFunctionArgs } from "react-router";

export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) {
  return true; // true면 데이터를 다시 가져오고, false면 재검증 X
}

특정 액션에 대해 재검증 방지

export function shouldRevalidate({ formAction }) {
  // "/profile/update"에서 발생한 액션이면 재검증 방지
  return formAction !== "/profile/update";
}

ShouldRevalidateFunctionArgs 주요 값

속성설명
currentUrl현재 요청된 URL
nextUrl다음으로 이동할 URL
formMethod폼 요청 시 사용된 HTTP 메서드 (POST, PUT 등)
formAction폼이 전송된 경로
formData폼에서 전송된 데이터
defaultShouldRevalidate기본적으로 재검증해야 하는지 여부

💡 언제 사용하면 좋을까?

상황재검증 필요 여부이유
🖊️ 데이터 수정✅ 필요UI 업데이트를 위해 새 데이터 필요
💬 댓글 작성 시 목록 변경 없음❌ 불필요댓글 목록을 다시 불러올 필요 없음
🔄 필터 변경 시 새로운 데이터 필요✅ 필요필터링에 맞는 데이터 다시 가져와야 함
토글 버튼 클릭(상태값만 변경)❌ 불필요서버 데이터가 변경되지 않음

profile
안드로이드 페페

0개의 댓글