routes.ts
에서 지정하는 파일(예: "./team.tsx")을 Route Module이라고 한다.
route("teams/:teamId", "./team.tsx"),
// ^^^^^^^^ Route Module
Route Module이란?
React Router의 핵심 구조로, 각 라우트의 동작을 정의하는 모듈 파일
React 컴포넌트 외에도 데이터 로딩, 액션 처리, 오류 처리 등을 수행
Route Module은 단순한 페이지 컴포넌트가 아니라, 다음과 같은 기능을 포함할 수 있다.
기능 | 설명 |
---|---|
코드 스플리팅 | 필요한 페이지 코드만 로드 |
데이터 로딩 (loader ) | 서버에서 데이터를 미리 불러오기 |
액션 (action ) | 폼 제출 등의 이벤트 처리 |
재검증 (revalidation ) | 데이터를 최신 상태로 유지 |
에러 경계 (ErrorBoundary ) | 해당 라우트에서 발생한 오류 처리 |
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() 함수가 반환한 데이터 |
params | URL에 포함된 동적 라우트 매개변수 |
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()
는 라우트 컴포넌트가 렌더링되기 전에 데이터를 제공하는 함수이다.
서버에서 실행되거나 빌드 시점(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>;
}
참고 https://api.reactrouter.com/v7/interfaces/react_router.LoaderFunctionArgs
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;
를 설정하면 서버에서 제공된 데이터를 클라이언트에서 보강 가능true as const
를 사용하면 타입이 boolean
이 아닌 true
로 고정됨언제 clientLoader를 사용할까?
serverLoader()
에서 받은 데이터에 추가적인 브라우저 정보를 결합📌 clientLoader vs loader 차이점
비교 항목 | loader() | clientLoader() |
---|---|---|
실행 위치 | 서버에서 실행 | 브라우저에서 실행 |
데이터 로딩 시점 | 페이지 요청 시 | 클라이언트 렌더링 후 |
서버 데이터 사용 | 직접 반환 | serverLoader() 를 호출하여 사용 가능 |
Hydration 지원 | 기본적으로 서버에서 데이터 전달 | clientLoader.hydrate = true 설정 시 지원 |
사용 목적 | SSR, 초기 데이터 제공 | 클라이언트에서 추가 데이터 요청 |
참고 https://api.reactrouter.com/v7/types/react_router.ClientLoaderFunctionArgs
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()
을 사용할까?
POST
), 항목 수정 (PUT
), 삭제 (DELETE
)action()
실행 후 React Router가 자동으로 loader()
를 다시 실행<Form navigate={false}>
→ SPA 경험 유지📌 action()과 loader()의 관계
기능 | loader() | action() |
---|---|---|
실행 위치 | 서버 | 서버 |
역할 | 데이터 조회 | 데이터 변경 (POST, PUT, DELETE) |
호출 방식 | 페이지 로드 시 실행 | <Form> , useSubmit() , useFetcher() 호출 시 실행 |
자동 갱신 | X | 실행 후 loader() 가 자동 재실행 |
clientAction()
은 클라이언트에서 실행되는 action()
이다.
✔ 캐시를 무효화하거나 UI 상태를 변경할 때 유용
✔ 필요하면 serverAction()
을 호출해 서버에서도 데이터를 변경 가능
✔ 서버 요청 없이 데이터를 변경할 때 효과적
export async function clientAction({ serverAction }) {
fakeInvalidateClientSideCache(); // 캐시 초기화
const data = await serverAction(); // 서버 데이터 다시 요청
return data;
}
참고 https://api.reactrouter.com/v7/types/react_router.ClientActionFunctionArgs
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.status
와 error.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>
);
}
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
은 초기 페이지 로드 시, 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
가 데이터 로딩 중일 때 이 컴포넌트가 대신 렌더링됨3️⃣ Component
– 데이터 로딩 후 렌더링
export default function Component({ loaderData }) {
return <Game data={loaderData} />;
}
clientLoader
가 데이터를 모두 가져오면, Component
가 렌더링됨loaderData
를 받아서 실제 게임 컴포넌트에 데이터를 넘김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
객체는 라우트에 커스텀 데이터를 추가할 때 사용한다. 이를 통해 라우트에 부가 정보를 넣어 다양한 기능을 구현할 수 있다.
✔ 메타데이터, 브레드크럼, 권한 정보 등 저장 가능
✔ 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
함수는 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
함수는 라우트의 데이터가 다시 로드되어야 하는지를 결정한다. 기본적으로, 모든 라우트는 액션 후에 자동으로 데이터가 재검증(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 업데이트를 위해 새 데이터 필요 |
💬 댓글 작성 시 목록 변경 없음 | ❌ 불필요 | 댓글 목록을 다시 불러올 필요 없음 |
🔄 필터 변경 시 새로운 데이터 필요 | ✅ 필요 | 필터링에 맞는 데이터 다시 가져와야 함 |
✅ 토글 버튼 클릭(상태값만 변경) | ❌ 불필요 | 서버 데이터가 변경되지 않음 |