
React Router 말고 Tanstack Router요
최근에 사용해봤던 Tanstack Router에 대해서 끄적끄적
Tanstack Router는 라우팅 라이브러리이다. 그렇다면 먼저 라우팅의 정의에 대해서 정확히 알고 있어야겠다..! 검색해보자

라우팅은 위 처럼 어떠한 경로를 선택하는 프로세스를 말한다. 웹 개발 관점에서 또한 라우팅 또한 경로를 결정하는 프로세스를 말한다. 예를 들어 루트 경로/ 에서 Next 버튼을 클릭해서 /next라는 경로로 이동 했을 때, url 경로를 이동하는 프로세스 자체를 라우팅이라고 할 수 있다.
React가 제공하는 API를 이용하면 웹 개발을 직관적이고 간편하게 할수 있다. 공식문서에는 리액트를 아래와 같이 소개하고 있다.

그렇다 리액트는 웹 및 사용자 인터페이스를 위한 라이브러리이다. 리액트에 라우팅 기능은 포함 되어 있지않다... 그렇다면 라우팅 동작을 직접 구현하거나, 잘 만들어진 라이브러리를 사용해야겠다. 다음과 같이 2가지 방안을 생각해볼 수 있겠다.
- 라우팅 동작 직접 구현 (history API + popstate event)
- 라우팅 라이브러리 사용 (react-router, tanstack router...)
라이브러리의 기능만을 잘 활용하는것도 물론 좋다!
하지만 특정 기능을 구현할 때 직접 구현 vs 라이브러리 사용 이라는 선택지에 대한 판단 기준을 갖기 위해서는 로우 레벨에서 해당 기능을 구현하려면 어떤 노력이 필요한지 알고 있는것도 좋은것 같다. 어떤 중요한 순간에서 판단을 내릴 때 더 도움이 될수 있을것이라고 생각한다.
또한 라이브러리는 언제든지 사라지거나 대체될 수 있기때문에 무작정 라이브러리를 사용하는것을 경계해야한다.
그래서 Tanstack Router를 바로 소개할 수도 있지만 굳이 라우팅 동작을 직접 구현하는 방법 부터 소개하고자 한다!
history API
브라우저는 현재 세션 동안 사용자가 방문한 페이지들을 내부적으로 기록하는 세션 히스토리 스택(session history stack)을 관리한다. histroy API는 유저의 이 히스토리 스택을 조작할 수 있는 메서드와 속성들을 제공한다.
이 중
pushState 메서드를 활용하면 새로운 히스토리 기록에 연결할 URL을 지정할 수 있다. 이 때 현재 페이지의 URL은 변경되지만, 페이지 전체가 새로 로드되진 않는다.
popstate event
popstate event는 하나의 document에 기록된 두개의 history entry의 활성상태에 변화가 일어날 때 발생한다. 쉽게 말하면 브라우저 세션 히스토리 스택에 저장된 두 히스토리 기록 사이를 이동할 때 발생한다는 말이다. 그래서 뒤로가기나 앞으로 가기를 눌러 현재 활성화된 history entry가 변경되면 popstate 이벤트가 발생한다.
주의 할 점은 history.pushState(), history.replaceState()로는 popstate event가 발생하지 않는다는 사실이다. 이 메서드들은 "네비게이션 동작"이 아니라 "히스토리 스택의 조작"이기 때문이다.
따라서 popstate event는 뒤로가기 또는 앞으로 가기 동작
history.back() history.forward()을 하고 난 후 실행할 동작을 정의하는데 활용할 수 있겠다.
이를 통해서 라우팅 하는 기능을 간단하게 구현해보면 아래와 같다.
import React, { useState, useEffect } from 'react';
// 각 경로에 대응하는 컴포넌트
function Home() {
return <h1>홈 페이지</h1>;
}
function About() {
return <h1>소개 페이지</h1>;
}
function NotFound() {
return <h1>404 - 페이지를 찾을 수 없습니다.</h1>;
}
// 경로에 따라 렌더링할 컴포넌트를 매핑하는 객체
const routes = {
'/': <Home />,
'/about': <About />,
};
function App() {
const [currentPath, setCurrentPath] = useState(window.location.pathname);
useEffect(() => {
// 브라우저 뒤로가기나 앞으로가기 시 URL 변경 감지
const onPopState = () => {
setCurrentPath(window.location.pathname);
};
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, []);
// 네비게이션 함수: URL 변경 및 상태 업데이트
const navigate = (to) => {
window.history.pushState({}, '', to);
setCurrentPath(to);
};
return (
<div>
<nav>
<button onClick={() => navigate('/')}>홈</button>
<button onClick={() => navigate('/about')}>소개</button>
</nav>
<div>
{routes[currentPath] || <NotFound />}
</div>
</div>
);
}
export default App;
내가 직접 history api나 popstate event를 활용해서 라우팅을 구현하지 않아도, 멋진 라이브러리 개발자 분들께서 라우팅 동작을 잘 구현해서 인터페이스를 제공해주신다. 참 좋다.
대표적으로는 React-router가 있다. 최근에 v7으로 업데이트가 되었다.. v4였나 v5버전 부터 사용했던것 같은데 참 세월이 빠르다..
그리고 다른 라이브러리 중 하나는 Tanstack Router이다. 여러분들이 잘 아는 Tanstack query를 만든 Tanstack 팀에서 만든 라우팅 라이브러리이다. 나는 최근 이 Tanstack Router를 자주 사용한다.
Tanstack Router를 선택한 이유
Tanstack Router는 React-router에 비해서 패키지가 가볍고, type safe한 라우팅을 지원한다. 보일러플레이트 또한 확실히 간결한 느낌을 받았다.
최근 V7부터 React-router도 각 라우트 모듈에 대한 타입 생성(typegen)이 도입되어, loader, action, route params 등의 타입 안전성이 크게 개선되긴했다
나는 개인적으로 Router 상수를 만들고 정의해서 사용하는것보다 파일기반 라우팅이 더 직관적이어서 선호하는 편이다.
내가 가장 좋았던 부분은 바로 이 ⭐️ 파일기반 라우팅을 지원한다는 것이다 하하. 아무튼 아래에서 Tanstack Router를 사용하는 방법에 대해서 알아보자. (vite를 사용했을 때 기준이다)
pnpm add @tanstack/react-router
# or
yarn add @tanstack/react-router
TanStackRouterVite()
Tanstack Router의 vite 플러그인을 사용하자
//vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import path from "path";
// https://vite.dev/config/
export default defineConfig({
plugins: [TanStackRouterVite(), react()],
resolve: {
alias: [{ find: "@", replacement: path.resolve(__dirname, "./") }],
},
});
createRootRoute
createRootRoute로 루트 라우트를 정의할 수 있다. <Outlet/>으로 페이지 컴포넌트를 받아서 사용한다.
// src/routes/__root.tsx
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { css } from "@/styled-system/css";
import "./index.css";
import "@sopt-makers/ui/dist/index.css";
export const Route = createRootRoute({
component: () => (
<>
<div
className={css({
...rootLayoutStyle,
})}
>
<Outlet />
</div>
<TanStackRouterDevtools />
</>
),
});
RouterProvider
라우트 프로바이더를 적용해주자.
기본적으로 Tanstack Router의 파일 기반 라우팅 기능을 사용하게 되면 프로젝트 실행 시 ./routeTree.gen 경로에 routeTree가 생성된다. 이 routeTree에서 Tanstack router는 모든 파일기반 라우팅을 한곳에 모아 라우터를 구성한다. 이 routeTree를 불러와서 router 상수를 만들어 주자. 이후<RouteProvider/>의 router에 적용해주면 된다.
// 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>
<RouterProvider router={router} />
</StrictMode>
);
}
초기 세팅 완성! 확실히 보일러 플레이트가 작고 너무 간결하다.
경로에 해당하는 폴더를 만든 후 그 폴더 내부에 index.tsx를 생성한다.(lazy loading을 적용할 경우 index.lazy.tsx) 그러면 폴더명에 따라 url 경로가 생성된다.
이후 index.tsx 내부에서 createFileRoute를 통해서 Route를 생성하여 export 해주면 라우팅 완료! (lazy loading을 적용할 경우 createLazyFileRoute 사용)
// src/routes/login-error/index.lazy.tsx
import ReLoginSection from "@/src/components/login-error/ReLoginSection";
import { css } from "@/styled-system/css";
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/login-error/")({
component: Index,
});
function Index() {
return (
<div className={css({ ...pageWrapperStyles })}>
<main className={css({ ...mainWrapperStyles })}>
...
</main>
</div>
);
}
Tanstack Router는 각 라우트의 loader 함수가 반환하는 값을 캐시하여 동일한 라우트를 다시 방문할 때 네트워크 요청 없이 빠르게 데이터를 가져올 수 있다. 페이지에서 간단한 데이터 페칭만 이루어진다면 이 부분을 잘 활용해서 불필요한 네트워크 요청을 줄일 수 있을것이다.
Tanstack Query 처럼 staleTime과 gcTime을 설정할 수 있다.
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: 10_000, // 10초 동안 fresh한 데이터로 취급
gcTime: 1000 * 60 * 5, // 5분 후 캐시 자동 삭제
})
최근 새로운 기술을 사용해보고 직접 장단점을 느껴보는게 필요하다고 생각이든다. Tanstack Router 또한 그러한 맥락에서 사용한것이었지만, 너무 마음에 들어서 다른 분들께도 간단하게 소개드리고자 구구절절 글을 써봤다.. 파이팅
참고자료
https://reactrouter.com/home
https://tanstack.com/router/latest
https://developer.mozilla.org/en-US/docs/Web/API/History_API
https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState