React Router 사용 규칙
기존 React(Vite/CSR) 프로젝트에 React Router 8.x(7.x 호환) Data Mode(createBrowserRouter + RouterProvider)를 도입하거나, 이미 react-router가 구성된 프로젝트에 라우트/레이아웃/내비게이션/Loader 등 부분 기능을 추가하는 스킬.
이 스킬은 Data Mode 기준이다. <BrowserRouter><Routes><Route>로 구성되는 Declarative Mode 또는 Remix 기반 Framework Mode(@react-router/dev)가 필요하면 이 스킬은 적합하지 않다.
필수 실행 체크리스트 (MANDATORY)
스킬 시작 즉시, 아래 항목을 TodoWrite에 1:1로 등록한 뒤 순서대로 진행한다. 건너뛰기 금지.
- 프로젝트 상태 감지 (1단계)
- 작업 모드 결정: 초기 도입 vs 기능 추가 (2단계)
react-router설치 [미설치 시] (3단계)- 기본 라우터 골격 생성 [라우터 파일 없을 때] (4단계)
- 사용자가 추가로 요청한 기능을 기능 가이드 섹션에서 찾아 적용 (5단계)
- 최종 검증: 1단계 감지 표를 다시 돌며 누락된 자동 적용 항목이 있으면 재실행하고, lint와 build를 통과시킨다 (6단계)
각 항목은 조건 충족 시 "skipped"로 완료 처리하되, 조건 판단 근거(파일/패키지 존재 여부)를 명시한 뒤 넘어간다.
동작 흐름
1단계: 프로젝트 상태 감지
| 확인 대상 | 감지 방법 |
|---|---|
| React 프로젝트 | package.json의 dependencies에 react 존재 |
| React 버전 | package.json의 react 버전이 19.2.7 이상인지 (미만이면 v8 설치 불가, v7 사용) |
| TypeScript | tsconfig.json 또는 tsconfig.app.json 존재 |
| react-router 설치 | package.json의 dependencies에 react-router 존재 |
react-router-dom 사용 여부 |
package.json의 dependencies 또는 src/ 소스에 from 'react-router-dom' 임포트 존재 (v6/v7 react-router-dom -> react-router 마이그레이션 감지용) |
| 라우터 파일 | src/routes/index.tsx 존재 여부 |
| 레이아웃 파일 | src/routes/layouts/DefaultLayout.tsx 존재 여부 |
| 헤더 컴포넌트 | src/components/TheHeader.tsx 존재 여부 |
main.tsx 렌더 구조 |
src/main.tsx의 createRoot(...).render(...) 자식이 (a) 단순 <App />인지, (b) 이미 <Router />/<RouterProvider>가 연결되어 있는지, (c) 다른 Provider/Wrapper(QueryClientProvider, ThemeProvider, ErrorBoundary, i18n 등)가 감싸고 있는지 |
@/* 경로 별칭 |
tsconfig 또는 vite.config에 @/* alias 존재 (이 스킬의 예제는 @/ 임포트 사용) |
| 패키지 매니저 | pnpm-lock.yaml -> pnpm, yarn.lock -> yarn, bun.lockb 또는 bun.lock -> bun, package-lock.json 또는 lock 파일 없음 -> npm |
2단계: 작업 모드 결정
초기 도입 모드: react-router 미설치 그리고 react-router-dom 미사용 그리고 src/routes/index.tsx 없음:
- 3단계로
react-router를 설치한다 - 4단계로 기본 라우터 골격(
src/routes/index.tsx,src/routes/layouts/DefaultLayout.tsx,src/components/TheHeader.tsx,src/routes/pages/Home.tsx/About.tsx)을 자동 생성하고src/main.tsx의 렌더 호출을 surgical하게 라우터 연결로 바꾼다(자세한 규칙은 4단계src/main.tsx절 참고) - 사용자가 추가로 요청한 기능만 5단계에서 적용한다
기능 추가 모드: react-router 이미 설치되어 있거나 라우터 파일이 이미 존재:
- 3, 4단계는 skipped로 처리한다 (조건 미충족)
- 5단계에서 사용자 요청에 해당하는 기능 가이드만 골라 적용한다
마이그레이션 모드: 1단계 감지에서 react-router-dom 사용이 확인됨:
- 사용자에게
react-router-dom->react-router마이그레이션을 진행할지 명시적으로 확인한다. 동의 없이 임의로 임포트를 바꾸지 않는다. - 동의가 있으면, 모든
from 'react-router-dom'임포트를from 'react-router'로 일괄 변경하고react-router-dom을 제거({pm} remove react-router-dom)한 뒤 3단계로react-router를 설치한다. 그 외 기존 라우트 구조는 그대로 둔다. - 마이그레이션 후 사용자가 새로 요청한 기능만 5단계에서 적용한다.
이미 존재하는 파일은 덮어쓰지 않는다. 기존 파일은 사용자가 명시적으로 변경을 요청한 부분만 최소한으로 수정한다.
3단계: react-router 설치 [조건: 미설치 시]
v7부터
react-router-dom은 사용하지 않고, v8에서는 패키지 자체가 제거됐다. 항상react-router만 설치한다. v8은 React 19.2.7 이상, Node.js 22.22 이상이 필요하다. 1단계에서 감지한 React 버전에 따라 분기한다.
React 19.2.7 이상 (v8):
{pm} add react-router
React 19.2.7 미만 (v7 유지):
{pm} add react-router@7
{pm}은 프로젝트의 lock 파일로 판별한 패키지 매니저로 대체한다.pnpm-lock.yaml->pnpm,yarn.lock->yarn,bun.lockb또는bun.lock->bun,package-lock.json또는 lock 파일 없음 ->npm.{pmx}는 해당 패키지 매니저의 실행 명령으로 대체한다.npm->npx,pnpm->pnpm dlx,yarn->yarn dlx,bun->bunx.
4단계: 기본 라우터 골격 생성 [조건: 라우터 파일 없을 때]
다음 폴더/파일 구조를 생성한다. 이미 존재하는 파일은 건드리지 않는다.
src/
├─components/
│ └─TheHeader.tsx
├─routes/
│ ├─layouts/
│ │ └─DefaultLayout.tsx
│ ├─pages/
│ │ ├─About.tsx
│ │ └─Home.tsx
│ └─index.tsx
└─main.tsx
임포트 규칙: 상대 경로는 같은 폴더 안의 파일을 가져올 때만 쓴다. 다른 폴더의 파일은 @/ 별칭으로 가져온다.
별칭이 없으면
react-vite-scaffold스킬의 경로 별칭 단계를 먼저 적용한다. 사용자가 거부하면 상대 경로로 바꾼다.
각 파일의 초기 내용은 다음과 같다.
// src/routes/pages/Home.tsx
export default function Home() {
return <h1>Home</h1>
}
// src/routes/pages/About.tsx
export default function About() {
return <h1>About</h1>
}
src/components/TheHeader.tsx: <Link>/<NavLink>를 쓰면 페이지 이동 시 전체가 다시 로드되지 않고 필요한 부분만 업데이트된다.
// src/components/TheHeader.tsx
import { NavLink } from 'react-router'
const navigations = [
{ to: '/', label: 'Home' },
{ to: '/about', label: 'About' }
]
export default function TheHeader() {
return (
<header>
<nav>
{navigations.map(nav => (
<NavLink
key={nav.to}
to={nav.to}>
{nav.label}
</NavLink>
))}
</nav>
</header>
)
}
src/routes/layouts/DefaultLayout.tsx: <Outlet> 자리에 자식 라우트가 렌더링된다. <ScrollRestoration>은 페이지 이동 시 스크롤 위치를 자동으로 처리한다.
// src/routes/layouts/DefaultLayout.tsx
import { Outlet, ScrollRestoration } from 'react-router'
import TheHeader from '@/components/TheHeader'
export default function DefaultLayout() {
return (
<>
<TheHeader />
<Outlet />
<ScrollRestoration />
</>
)
}
src/routes/index.tsx: 경로(path) 없이 element만 지정한 최상위 라우트의 children에 페이지 라우트를 둔다. 그러면 자식 라우트가 렌더링될 때 부모 <DefaultLayout />도 같이 렌더링된다.
// src/routes/index.tsx
import { createBrowserRouter, RouterProvider } from 'react-router'
import DefaultLayout from '@/routes/layouts/DefaultLayout'
import Home from '@/routes/pages/Home'
import About from '@/routes/pages/About'
const router = createBrowserRouter([
{
element: <DefaultLayout />,
children: [
{
path: '/',
element: <Home />
},
{
path: '/about',
element: <About />
}
]
}
])
export default function Router() {
return <RouterProvider router={router} />
}
src/main.tsx: 항상 이미 존재하는 파일이므로 전체를 덮어쓰지 않는다. 기존 wrapper(<StrictMode>, 다른 Provider)는 유지하고 <Router />만 끼워 넣는다. QueryClientProvider가 있으면 그 안에 둔다. 1단계의 main.tsx 렌더 구조 감지 결과에 따라 다음과 같이 처리한다.
- (a) 자식이 단순
<App />인 경우 (Vite 기본 템플릿):App임포트를 제거하고Router임포트를 추가한 뒤 렌더 자리의<App />만<Router />로 교체한다.<StrictMode>등 기존 wrapper는 그대로 유지한다.App.tsx가 더 이상 어디에서도 사용되지 않으면 사용자에게 삭제 여부를 확인한 뒤 제거한다. - (b) 이미
<Router />/<RouterProvider>가 연결되어 있는 경우:main.tsx는 건드리지 않는다. - (c) 다른 Provider/Wrapper(
QueryClientProvider, ThemeProvider, ErrorBoundary, i18n 등)가<App />을 감싸고 있는 경우: wrapper는 모두 유지하고 가장 안쪽의<App />만<Router />로 교체한다.<Router />를 wrapper 바깥으로 빼는 것은 사용자가 명시적으로 요청한 경우에만 한다.
원하는 결과 예시 (가장 단순한 (a) 케이스. 기존 ./index.css 임포트가 있으면 그대로 둔다):
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import Router from '@/routes'
import '@/index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<Router />
</StrictMode>
)
5단계: 사용자 요청에 따른 기능 적용
사용자가 명시적으로 추가 기능을 요청한 경우에만 기능 가이드 섹션에서 해당 항목을 찾아 적용한다. 요청이 없으면 기본 골격만 두고 종료한다.
요청과 기능의 매핑 예:
| 사용자 요청 예시 | 적용할 기능 |
|---|---|
| "/movies/:movieId 같은 동적 페이지 만들어 줘" | 동적 세그먼트 |
| "검색 결과를 모달로 띄우고 싶어" / "중첩 라우트로 처리" | 중첩 라우팅 |
| "404 페이지 만들어 줘" | 찾을 수 없는 페이지 |
| "로그인한 사용자만 접근하게 해 줘" / "Protected Route" | 보호된 경로 |
| "초기 로딩 줄이게 코드 스플리팅" / "lazy 적용" | 페이지 지연 로딩 |
| "페이지 바뀔 때 페이드 효과" | 페이지 전환 애니메이션 |
| "Vercel/Netlify/Firebase 배포 시 새로고침에서 404" | 배포 설정 |
| "NavLink 활성 스타일 / end / caseSensitive" | NavLink 활용 |
| "프로그래밍 방식으로 페이지 이동" | useNavigate / Navigate / redirect |
| "Link에 state 넘기기 / replace / 스크롤 유지" | Link 활용 |
| "Declarative/Framework 모드 차이" | 모드 비교 |
여러 기능을 함께 적용했을 때 src/routes/index.tsx가 어떤 모습이어야 하는지는 누적 적용 완성 예시를 기준으로 한다.
6단계: 최종 검증 (MANDATORY)
모든 단계 수행 후, 1단계의 감지 표를 다시 한 번 스캔해 다음을 확인한다.
- 초기 도입 모드였던 경우:
package.json에react-router존재,src/routes/index.tsx,src/routes/layouts/DefaultLayout.tsx,src/components/TheHeader.tsx,src/routes/pages/Home.tsx/About.tsx존재,src/main.tsx가<Router />를 렌더링하고 기존의 wrapper(<StrictMode>등)는 보존됨 - 마이그레이션 모드였던 경우:
package.json에서react-router-dom이 제거되고react-router가 추가됨,src/의 어느 파일에도from 'react-router-dom'임포트가 남아 있지 않음 - 모든 모드 공통: 사용자가 명시적으로 요청한 기능별 가이드의 결과 파일(예:
src/routes/loaders/requiresAuth.ts)이 모두 존재하고, 라우트 트리에 올바르게 연결됨 (404 라우트는 마지막,/signin은 보호 라우트 앞) - 생성한 파일의 임포트가 임포트 규칙(같은 폴더만 상대 경로, 그 외
@/)을 따름 - 사용자가 별도로 요청하지 않은 영역의 기존 파일은 수정되지 않음 (특히 기존
main.tsx의 커스텀 Provider/Wrapper가 보존됨) -
{pm} run lint통과 -
{pm} run build통과 (TypeScript 검사 포함)
누락 항목이 있으면 해당 단계로 돌아가 즉시 보완한다. 검증 통과 전에는 작업 종료 금지.
기능 가이드
사용자가 명시적으로 요청한 기능만 골라 적용한다. 각 항목의 변경 사항은 누적되도록 설계되어 있으므로, 이미 다른 항목이 적용된 상태에서 추가로 적용해도 충돌하지 않는다.
모드 비교
React Router는 선언적(Declarative), 데이터(Data), 프레임워크(Framework)의 3가지 모드를 제공하며 기능이 누적적으로 확장된다. 이 스킬은 Data 모드를 기준으로 한다.
- Declarative 모드:
<BrowserRouter><Routes><Route>기반. 가장 기본적인 API. 단순한 SPA에 적합. - Data 모드:
createBrowserRouter+RouterProvider. Loader/Action/Fetcher 등 데이터 기능 추가. 좀 더 복잡한 CSR 프로젝트에 적합. - Framework 모드: Remix와 통합. SSR, Type-Safe href 등 추가 기능. 풀 스택 프로젝트에 적합.
@react-router/dev가 필요하므로 이 스킬의 범위 밖.
자세한 모드별 기능 비교는 React Router 공식 문서의 API & Mode availability table을 참고한다.
레이아웃과 ScrollRestoration
기본 골격(4단계)이 이미 <DefaultLayout>과 <ScrollRestoration>을 포함한다. 추가 레이아웃이 필요한 경우(예: 인증 후 영역 전용 레이아웃)는 src/routes/layouts/<이름>Layout.tsx 파일에 export default function <이름>Layout() 컴포넌트를 만들고, 라우트 트리에서 해당 영역의 부모 라우트 element로 둔다.
<ScrollRestoration>은 최상위 레이아웃에 한 번만 추가한다. 페이지 이동 시 스크롤 위치를 복원하거나 새 페이지의 스크롤을 최상단으로 이동시킨다.
그 밖의 기능
아래 기능은 references/로 분리돼 있다. 5단계 매핑 표에서 해당 항목이 필요할 때만 그 파일을 읽는다.
| 파일 | 다루는 내용 |
|---|---|
| references/navigation.md | Link 활용, NavLink 활성 스타일, useNavigate / Navigate / redirect |
| references/routing-patterns.md | 동적 세그먼트, 중첩 라우팅, 찾을 수 없는 페이지(404), 누적 적용 완성 예시 |
| references/protected-routes.md | 보호된 경로 (loader + redirect) |
| references/lazy-loading.md | 페이지 지연 로딩 (dynamic 헬퍼: lazy + Suspense + ErrorBoundary) |
| references/animation-and-deploy.md | 페이지 전환 애니메이션, SPA 호스팅 리라이트 설정 |
주의사항
- 프로젝트에
CLAUDE.md나.claude/rules/react-router.md가 있으면 그 내용이 이 스킬보다 우선한다. 기존 코드가 있으면 파일 위치, 이름, 선언 형식을 먼저 확인하고 같은 스타일로 만든다. - 이미 존재하는 설정/소스 파일은 덮어쓰지 않는다. 사용자가 명시적으로 요청한 부분만 수정한다.
- 상대 경로는 같은 폴더 안의 파일을 가져올 때만 쓴다. 다른 폴더의 파일은
@/별칭으로 가져온다. - Declarative Mode(
<BrowserRouter>) 또는 Framework Mode(@react-router/dev) 기반 코드를 작성해 달라는 요청은 이 스킬의 범위가 아니다. 사용자에게 모드 선택을 한 번 더 확인한 뒤, 필요하면 이 스킬을 사용하지 않는다는 사실을 알린다. - React Router v7부터
react-router-dom은 사용하지 않고 v8에서는 패키지가 제거됐다. 1단계에서react-router-dom사용이 감지되면 2단계의 마이그레이션 모드로 동작한다(사용자 동의 없이 임의로 임포트를 바꾸지 않는다). RouterProvider는react-router에서 가져온다.navigate/submit에flushSync: true옵션을 쓸 때만react-router/dom의RouterProvider가 필요하다.- Loader 함수는 페이지 컴포넌트 렌더링 전에 실행되므로, 그 안에서 동기적으로 무거운 작업을 수행하지 않는다. 외부 요청은
await로 처리하되 사용자 경험을 해치지 않는 최소한의 작업으로 제한한다. <ScrollRestoration>은 라우터 트리에 한 번만 둔다. 여러 레이아웃에 중복으로 두면 동작이 예측 불가능해진다.dynamic헬퍼 적용 시 라우트 정의의element가 아니라Component속성을 사용해야 한다.element는 React 엘리먼트(<Foo />)를,Component는 컴포넌트 타입(Foo)을 받는다.
함께 보는 스킬
| 필요한 것 | 스킬 |
|---|---|
| 프로젝트 기반 설정 (Tailwind, 경로 별칭, ESLint + Prettier) | react-vite-scaffold |
| 서버 데이터 fetching / 캐싱 | tanstack-react-query-use |
| 전역 상태 (스토어) | zustand-use |
| Next.js App Router로 전환 | react-vite-to-next-migration |
| 성능, 접근성, SEO 측정 | lighthouse |