React Router v7 완벽 가이드
React Router v7이 왔다
React Router v7은 단순한 업데이트가 아니다.
Remix와 React Router가 합쳐지면서, 라우팅 라이브러리에서 풀스택 프레임워크로 진화했다.
무엇이 달라졌나?
Remix + React Router = React Router v7
기존에 Remix를 사용하던 팀은 React Router v7으로 마이그레이션하면 된다.
React Router를 사용하던 팀은 Remix의 강력한 기능들을 사용할 수 있게 되었다.
핵심 변화
- Remix의 data loading 패턴 통합
- 서버 사이드 렌더링(SSR) 내장
- 파일 기반 라우팅 지원
- 더 나은 타입 안전성
설치하기
새 프로젝트
npx create-react-router@latest my-app
cd my-app
npm run dev기존 프로젝트 업그레이드
npm install react-router@7프로젝트 구조
React Router v7은 파일 기반 라우팅을 지원한다.
app/
├── routes/
│ ├── _index.tsx # / (홈)
│ ├── about.tsx # /about
│ ├── blog._index.tsx # /blog
│ ├── blog.$slug.tsx # /blog/:slug
│ └── blog.tsx # /blog 레이아웃
├── root.tsx # 루트 레이아웃
└── entry.client.tsx # 클라이언트 엔트리라우트 파일 명명 규칙
| 파일명 | URL | 설명 |
|---|---|---|
_index.tsx |
/ |
인덱스 라우트 |
about.tsx |
/about |
기본 라우트 |
blog.$slug.tsx |
/blog/:slug |
동적 파라미터 |
blog_.new.tsx |
/blog/new |
레이아웃 무시 |
($lang).about.tsx |
/about, /ko/about |
선택적 파라미터 |
Data Loading - loader
v7의 가장 강력한 기능 중 하나다.
컴포넌트가 렌더링되기 전에 데이터를 미리 로드한다.
import { useLoaderData } from 'react-router';
export async function loader({ params }) {
const post = await getPost(params.slug);
if (!post) {
throw new Response('Not Found', { status: 404 });
}
return { post };
}
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}loader의 장점
- 워터폴 제거: 부모-자식 라우트의 데이터를 병렬로 로드
- SSR 지원: 서버에서 데이터를 미리 로드
- 타입 안전성:
typeof loader로 타입 자동 추론
Data Mutation - action
폼 제출이나 데이터 변경을 처리한다.
import { Form, useActionData, redirect } from 'react-router';
export async function action({ request }) {
const formData = await request.formData();
const title = formData.get('title');
const content = formData.get('content');
const errors = {};
if (!title) errors.title = '제목을 입력하세요';
if (!content) errors.content = '내용을 입력하세요';
if (Object.keys(errors).length > 0) {
return { errors };
}
await createPost({ title, content });
return redirect('/blog');
}
export default function NewPost() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<input name="title" />
{actionData?.errors?.title && (
<span>{actionData.errors.title}</span>
)}
<textarea name="content" />
{actionData?.errors?.content && (
<span>{actionData.errors.content}</span>
)}
<button type="submit">작성하기</button>
</Form>
);
}중첩 라우팅 (Nested Routes)
레이아웃을 공유하는 중첩 라우팅이 더욱 직관적이다.
routes/blog.tsx (레이아웃)
import { Outlet, NavLink } from 'react-router';
export default function BlogLayout() {
return (
<div className="blog-layout">
<nav>
<NavLink to="/blog">전체 글</NavLink>
<NavLink to="/blog/new">새 글 작성</NavLink>
</nav>
<main>
<Outlet />
</main>
</div>
);
}routes/blog._index.tsx (목록)
export async function loader() {
const posts = await getPosts();
return { posts };
}
export default function BlogIndex() {
const { posts } = useLoaderData<typeof loader>();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link to={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
);
}Pending UI 처리
데이터 로딩 중 상태를 쉽게 처리할 수 있다.
useNavigation
import { useNavigation } from 'react-router';
export default function SubmitButton() {
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';
return (
<button disabled={isSubmitting}>
{isSubmitting ? '저장 중...' : '저장'}
</button>
);
}낙관적 UI (Optimistic UI)
import { useFetcher } from 'react-router';
export default function LikeButton({ postId, likes }) {
const fetcher = useFetcher();
// 낙관적 업데이트: 서버 응답 전에 UI 먼저 변경
const optimisticLikes = fetcher.formData
? likes + 1
: likes;
return (
<fetcher.Form method="post" action={`/posts/${postId}/like`}>
<button>❤️ {optimisticLikes}</button>
</fetcher.Form>
);
}에러 처리
라우트별로 에러 바운더리를 설정할 수 있다.
import { useRouteError, isRouteErrorResponse } from 'react-router';
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return (
<div>
<h1>오류가 발생했습니다</h1>
<p>{error.message}</p>
</div>
);
}타입 안전성 강화
v7은 TypeScript 지원이 크게 향상되었다.
import type { Route } from './+types/blog.$slug';
export async function loader({ params }: Route.LoaderArgs) {
// params.slug 타입이 자동으로 추론됨
const post = await getPost(params.slug);
return { post };
}
export default function BlogPost({ loaderData }: Route.ComponentProps) {
// loaderData 타입이 자동으로 추론됨
return <h1>{loaderData.post.title}</h1>;
}SPA vs SSR 모드
React Router v7은 두 가지 모드를 지원한다.
SPA 모드 (클라이언트만)
// react-router.config.ts
export default {
ssr: false
};기존 React Router처럼 클라이언트 사이드에서만 동작한다.
SSR 모드 (서버 + 클라이언트)
// react-router.config.ts
export default {
ssr: true
};Remix처럼 서버에서 렌더링하고 클라이언트에서 하이드레이션한다.
v6에서 v7로 마이그레이션
1. 패키지 업데이트
npm install react-router@72. import 경로 변경
// Before (v6)
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// After (v7)
import { BrowserRouter, Routes, Route } from 'react-router';3. 점진적 마이그레이션
기존 코드는 그대로 동작한다.
새로운 기능(loader, action)은 필요할 때 점진적으로 도입하면 된다.
React Router v7 vs Next.js
| 기능 | React Router v7 | Next.js |
|---|---|---|
| 라우팅 | 파일 기반 / 설정 기반 | 파일 기반 |
| SSR | 선택적 | 기본 |
| Data Loading | loader/action | Server Components |
| 번들러 | Vite | Webpack/Turbopack |
| 학습 곡선 | React 경험 있으면 쉬움 | 새로운 개념 필요 |
| 유연성 | 높음 | 중간 |
마무리
React Router v7은 단순한 라우팅 라이브러리를 넘어 풀스택 프레임워크로 진화했다.
Remix의 강력한 data loading 패턴과 React Router의 유연함이 합쳐졌다.
기존 React Router 사용자라면 점진적으로 새 기능을 도입할 수 있고,
새 프로젝트라면 처음부터 loader/action 패턴을 활용해보자.
B
u
y
M
e
A
C
o
f
f
e
e
☕
️