최근에 루키톤을 하고있다! 같이 하는 제롬이 리액트 라우터 돔을 많이 싫어해서... TanStack Router를 도입했다.
React Router를 사용하다가 TanStack Router를 사용해보면서 겪은 경험과 장점들을 정리해봤다.
TanStack Router이란? 🤔
TanStack Router는 React Router v7이라고도 불린다.
파일 시스템 기반 라우팅과 타입 안전성이 특징인 라우터 라이브러리이다.
Next의 파일 기반 라우터를 좋아했던 사람들이라면 이것 역시 좋아할 듯
주요 특징
1. 파일 시스템 기반 라우팅
2. 완벽한 타입스크립트 지원
3. 자동 코드 분할
4. 성능 최적화
시작하기 👶
먼저 필요한 패키지를 설치해준다.
pnpm add @tanstack/react-router
설치 후에 vite.config.ts에 설정을 해준다.
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react-swc'
import {TanStackRouterVite} from '@tanstack/router-plugin/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
TanStackRouterVite(),
],
})
그리고 나서 기본적인 세팅은 이렇게 한다.
// src/router.ts
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
export const router = createRouter({
routeTree,
defaultPreload: 'intent' // 호버시 프리로드
});
main.tsx는 다음과 같이 설정한다.
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
// Create a new router instance
const router = createRouter({ routeTree })
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// Render the app
const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
<StrictMode>
<YDSWrapper>
<RouterProvider router={router} />
</YDSWrapper>
</StrictMode>,
)
}
createRouter에 라우터 트리를 전달해야 한다. 파일 기반 라우팅을 사용했다면 일반적으로 생성된 라우트 트리 파일은 src/routeTree.gen.ts에 생성된다.
Router Type Safety
TanStack Router provides amazing support for TypeScript, even for things you wouldn't expect like bare imports straight from the library!
To make this possible, you must register your router's types using TypeScripts'
Declaration Merging feature. This is done by extending the
Register interface on @tanstack/react-router with a router property that has the type of your router instance:
공식 문서를 따라서 아래와 같이 타입을 정의하고 라우터를 등록하면 앱 내 라우팅 관련한 모든 것에 대해 타입 안전성을 확보할 수 있다고 한다.
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
파일 구조 📁
TanStack Router의 가장 큰 특징은 파일 시스템 기반 라우팅이다. Next.js처럼 직관적인 구조를 가지고 있다는게 가장 큰 장점이다.
src/routes/
├── index.tsx // '/' 경로
├── about.tsx // '/about' 경로
├── users/
│ ├── index.tsx // '/users' 경로
│ └── [id].tsx // '/users/:id' 경로
└── -components/ // 라우팅에서 제외 (대시로 시작)
기본적으로 설정된 routeFileIngorePrefix가 - (대시) 이기 때문에 -로 시작하는 파일이나 디렉토리는 라우팅에 고려되지 않는다.
__root.tsx | - 루트 라우트의 파일 이름은 __root.tsx로 정의된다. - routesDirectory의 루트에 파일이 위치해야한다. |
. 구분자 | - . 문자를 사용해 중첩 라우트를 표현한다. - blog.post라면 blog의 하위로 post가 생성됨을 의미한다. |
$ 토큰 | - URL 경로명에서 값을 추출하여 라우트 파라미터로 사용한다. |
_ 접두사 | - 레이아웃 라우트로 간주된다. (자체적 렌더링시 사용) - 하위 자식 라우트들의 URL일치 여부에는 관여하지 않는다. |
_ 접미사 | - 부모 라우트 아래 중첩되지 않도록 제외된다. |
index 토큰 | - 부모 경로와 URL이 정확히 일치할 때, 부모 경로를 매치한다. |
.route.tsx 파일 | - route 접미사를 사용하여 디렉터리 경로에 라우트 파일을 생성한다. - blog.post.route.tsx 또는 blog/post/route.tsx는 /blog/post 라우트의 파일로 사용될 수 있다. |
.lazy.tsx 파일 | - 라우트의 컴포넌트를 코드 분할 할 수 있다. |
- tsr.config.json를 만들어서 수정할 수도 있다. 대시가 아닌 다른 걸로!
- routeFilePrefix와 routeFileIgnorePrefix 옵션을 사용하여 파일 기반 라우팅 설정 시 특정 접두사로 시작하는 파일과 디렉터리만 포함하거나 제외할 수 있다.
실제 사용 예시 💻
1. 기본 라우트 설정
// routes/index.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({
component: () => {
return <div>홈페이지입니다!</div>
}
});
Lazy loading을 적용시키고 싶으면 createLazyFileRoute를 써주면 된다.
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/about')({
component: About,
})
function About() {
return <div className="p-2">Hello from About!</div>
}
이렇게!
2. 동적 라우트
// routes/users/[id].tsx
interface UserRoute {
params: {
id: string;
}
}
export const Route = createFileRoute('/users/$id')({
loader: async ({ params }) => {
const user = await fetchUser(params.id);
return { user };
},
component: UserPage
});
function UserPage() {
const { user } = useLoaderData();
return <div>{user.name}님의 페이지입니다.</div>;
}
3. 인증 라우트
// routes/-auth.tsx
export const Route = createFileRoute('/-auth')({
beforeLoad: async () => {
const isAuthenticated = await checkAuth();
if (!isAuthenticated) {
throw redirect({
to: '/login',
search: {
redirect: window.location.pathname
}
});
}
},
component: AuthLayout
});
TanStack Router에서 Not Found 처리 방식
Not Found 오류란?
TanStack Router에서는 Not Found 오류가 두 가지 주요 상황에서 발생한다.
1. 경로가 일치하지 않는 경우 (Non-matching Route Paths)
- 사용자가 잘못된 URL을 입력했거나, 정의되지 않은 경로에 접근했을 때 발생.
2. 리소스가 누락된 경우 (Missing Resources)
- 특정 데이터를 찾을 수 없는 상황 (예: `id=1`인 게시물이 없는 경우).
Deprecated된 `NotFoundRoute`
이전에는 `NotFoundRoute` API를 사용해 Not Found 상태를 처리했다. 근데 이건 deprecated되었다고 한다. 다른 거 쓰자!
대신 아래 두 가지 방식으로 Not Found 상태를 처리한다.
1. NotFoundComponent API
2. `notFound()` 함수 사용
1) NotFoundComponent API
`NotFoundComponent`는 라우트 정의에서 각 라우트나 전체 앱 수준에서 사용될 컴포넌트를 설정한다.
이 컴포넌트는 경로가 일치하지 않을 때 렌더링된다!
NotFoundComponent API 설정 방식
1. 라우트 별 NotFoundComponent 설정
- 각 라우트에서 `notFoundComponent`를 정의하여 Not Found 상태를 처리할 수 있다.
2. 전역적으로 defaultNotFoundComponent 설정
- 전역 설정을 통해 모든 Not Found 상태를 하나의 컴포넌트로 처리한다. 이게 공용 fallback 컴포넌트 만드는 느낌
1. 라우트 별 NotFoundComponent 설정
import { createFileRoute } from '@tanstack/react-router';
export const LearningRoute = createFileRoute('/learning')({
component: () => <div>학습 관리 페이지</div>,
notFoundComponent: () => <div>LearningRoute - NOT FOUND ERROR</div>, // 경로가 일치하지 않는 경우 처리
});
export const Route = createRootRoute({
component: () => (
<div>
<Link to='/'>Home</Link>
<Link to='/untact'>Untact</Link>
<Outlet />
</div>
),
notFoundComponent: () => <div>RootRoute - NOT FOUND ERROR</div>, // 루트 라우트에서 처리
});
2. 전역적으로 defaultNotFoundComponent 설정
import { createRouter } from '@tanstack/react-router';
const router = createRouter({
routeTree,
defaultNotFoundComponent: () => (
<div>
<p>Not Found! 잘못된 경로입니다.</p>
<Link to="/">홈으로 이동</Link>
</div>
),
});
2)`notFound()` 함수 사용
`notFound()` 함수는 비동기 데이터 로딩 중 누락된 리소스를 처리할 때 사용된다.
라우트와 연결된 데이터를 불러오는 `loader`에서 조건에 따라 `notFound()`를 호출한다.
`notFound()` 함수 동작 방식
- `notFound()`는 리디렉션과 유사하게 동작한다.
- 오류를 처리할 라우트를 지정하거나, 기본적으로 가까운 부모 라우트가 처리한다.
`notFound()` 함수 예제
1. 기본적으로 `notFound()` 호출
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId); // 데이터 로드
if (!post) throw notFound(); // 데이터가 없으면 notFound() 호출
return { post }; // 데이터가 있으면 반환
},
});
2. `routeId`를 지정하여 특정 라우트에서 처리
export const Route = createFileRoute('/_layout/a')({
loader: async () => {
throw notFound({ routeId: '/_layout' }); // Layout 라우트에서 오류 처리
},
notFoundComponent: () => <p>_layout/a에서 처리되지 않았습니다</p>, // 무시됨
});
3. `rootRouteId`로 루트 라우트에서 처리
import { rootRouteId } from '@tanstack/react-router';
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId);
if (!post) throw notFound({ routeId: rootRouteId }); // 루트에서 오류 처리
return { post };
},
});
Not Found 처리 모드
TanStack Router는 Not Found 상태를 처리하는 두 가지 모드를 제공한다.
1. fuzzy (기본값)
- 가장 가까운 부모 라우트가 오류를 처리한다.
2. root
- 루트 라우트가 오류를 처리한다.
Not Found 처리 모드 설정 방식
`createRouter`에서 `notFoundMode`를 설정한다.
const router = createRouter({
routeTree,
notFoundMode: 'fuzzy', // 기본값
});
근데 이미 기본으로 설정되어있는 듯 하다! 설정하지 않은 경로를 들어가면 Not Found가 자동으로 뜬다. 정말 next 같군
6) 실제 적용 시 주의점
1. 라우트 계층구조
- 라우트 계층구조에 따라 Not Found 처리가 달라지므로, 적절한 부모 라우트를 지정해야 한다.
2. 전역 처리 vs 로컬 처리
- 앱의 복잡도와 사용자의 경험을 고려하여 전역 처리(defaultNotFoundComponent)와 로컬 처리(notFoundComponent)를 병행해야 한다.
3. 비동기 오류 처리
- 데이터 로딩 중 발생하는 오류는 반드시 `notFound()`를 사용해야 한다.
실제로 좋았던 점들 👍
1. 타입 안전성이 미쳤어요
- 라우트 파라미터 자동완성
- 잘못된 경로 컴파일타임에 catch 이게 가장 좋은 듯 하다! ts의 장점이 사전 에러 차단인데 라우터까지 더해지니까 강력한 느낌
2. 개발 생산성이 올라갔어요
- 파일 구조가 곧 라우트 구조 -> 근데 next의 맛을 아직 모르겠어서... ㅎ
3. 성능도 좋아요
- 자동 코드 분할
- 프리로딩 지원 -> 이런건 좋은듯 lazy 쓰기 쉽다!
- 효율적인 캐싱
주의할 점들 ⚠️
1. 러닝커브가 있다
- 기존 React Router와는 다른 패턴
- 타입스크립트 필수
2. 아직 발전 중인 라이브러리
- 문서화가 완벽하지 않음
- 커뮤니티가 작음
'React' 카테고리의 다른 글
React에서 WebSocket 채팅 구현하기 / SockJS 사용하기 (0) | 2025.01.07 |
---|---|
React에서 웹과 앱 환경에 따라 하단바 다르게 표시하기 (0) | 2025.01.07 |
[React/리액트] 스크롤 감지 버튼 구현하기 / 스크롤 이벤트와 버튼 스타일 동적 변경 (4) | 2024.10.13 |
[React/리액트] 상태 관리 추천 / Zustand 사용법 (0) | 2024.08.03 |
[React/TS] 주소 검색 기능 구현하기 / 주소를 위도 경도로 변환하기 / 카카오맵 API 타입 정의 (0) | 2024.07.30 |