Recode Log

  • tech-blog
  • apps
  • device
  • features

Copyright © [WebKBS]. All rights reserved.

·이용 안내
  • tech-blog
  • apps
  • device
  • features
  1. Home
  2. tech-blog
  3. Tanstack Router로 Next.js 폴더 구조 그대로 사용하기 (page.tsx, layout.tsx, app 폴더)

Tanstack Router로 Next.js 폴더 구조 그대로 사용하기 (page.tsx, layout.tsx, app 폴더)

Tanstack Router의 indexToken, routeToken, routeFileIgnorePattern 옵션으로 Next.js App Router처럼 폴더마다 page.tsx와 layout.tsx를 두는 방법과 routes 폴더를 app 폴더로 바꾸는 방법을 정리합니다.

  • @tanstack/react-router 1.170.40
  • @tanstack/router-plugin 1.168.41
  • vite 8.3.1
2026년 10월 1일

소개

최근 Next.js로 만든 화면을 React(Vite) + Tanstack Router로 옮기는 작업을 했습니다.

Tanstack Router도 File-based Routing이라 금방 옮길 줄 알았는데, 막상 파일을 옮기다 보니 계속 손이 멈췄습니다.

Next.js에서는 폴더가 주소이고 그 안의 page.tsx가 화면, layout.tsx가 감싸는 틀입니다.

Tanstack Router는 기본 규칙이 조금 다릅니다. 같은 /about 화면을 about.tsx로도, about/index.tsx로도 만들 수 있고 레이아웃은 about/route.tsx로 만듭니다.

규칙 자체가 어려운 것은 아닌데, Next.js에 익숙하다 보니 파일을 만들 때마다 "여기는 index였나, route였나" 하고 다시 확인하게 됐습니다.

그래서 Tanstack Router 설정을 바꿔서 Next.js와 똑같이 page.tsx, layout.tsx로 라우트를 만드는 방법을 정리했습니다.

결론부터 말하면 Vite 플러그인 옵션 몇 줄이면 됩니다.

이 글은 Tanstack Router의 File-based Routing 기본 사용법을 안다는 전제로 작성했습니다. 처음이라면 이전 글을 먼저 읽어주세요./tech-blog/react/tanstack-router

목차

  • 먼저 결론부터
  • 기본 규칙과 Next.js 규칙의 차이
  • indexToken, routeToken 설정하기
  • page와 layout 외의 파일은 라우트에서 빼기
  • loading.tsx 처럼 옆에 두고 쓰기
  • 라우트 그룹과 동적 경로
  • 루트 레이아웃은 __root.tsx 그대로
  • routes 폴더를 app 폴더로 바꾸기
  • 기존 프로젝트를 옮길 때
  • 마치며

먼저 결론부터

vite.config.ts의 tanstackRouter 플러그인에 옵션 세 개를 추가합니다.

vite.config.ts
import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
 
export default defineConfig({
  plugins: [
    // react() 보다 앞에 둡니다.
    tanstackRouter({
      target: "react",
      autoCodeSplitting: true,
      indexToken: "page",
      routeToken: "layout",
      // page, layout, __root 외의 파일은 라우트로 읽지 않습니다.
      routeFileIgnorePattern: String.raw`^(?!(?:page|layout|__root)\.tsx$).+\.tsx?$`,
    }),
    react(),
  ],
});

이렇게 설정하면 폴더 구조가 다음과 같아집니다.

설정 후 폴더 구조

src
routes
__root.tsx# Next.js의 app/layout.tsx
page.tsx# /
about
layout.tsx# /about 하위를 감싸는 틀
page.tsx# /about
news
page.tsx# /about/news
posts
$postId
page.tsx# /posts/1
main.tsx
routeTree.gen.ts# 자동 생성 파일

Next.js와 비교하면 다음과 같습니다.

Next.js (App Router)Tanstack Router 기본이 글의 설정
app/layout.tsxroutes/__root.tsxroutes/__root.tsx (그대로)
app/page.tsxroutes/index.tsxroutes/page.tsx
app/about/page.tsxroutes/about.tsx 또는 about/index.tsxroutes/about/page.tsx
app/about/layout.tsxroutes/about/route.tsxroutes/about/layout.tsx
app/posts/[postId]/page.tsxroutes/posts/$postId.tsxroutes/posts/$postId/page.tsx
app/(marketing)/pricing/page.tsxroutes/(marketing)/pricing.tsxroutes/(marketing)/pricing/page.tsx
app/about/loading.tsx라우트 옵션 pendingComponentloading.tsx + pendingComponent

__root.tsx와 동적 경로의 $ 정도만 다르고 나머지는 Next.js와 같은 모양으로 쓸 수 있습니다.

하나씩 살펴보겠습니다.

기본 규칙과 Next.js 규칙의 차이

먼저 Tanstack Router의 기본 규칙부터 다시 보겠습니다.

Tanstack Router 기본 규칙

routes
__root.tsx
index.tsx# /
contact.tsx# /contact (파일 이름이 경로)
about
route.tsx# /about 하위를 감싸는 레이아웃
index.tsx# /about
news.tsx# /about/news

Tanstack Router에서는 파일 이름에 특별한 의미가 있는 단어가 두 개 있습니다.

  • index - 그 폴더 경로의 화면입니다. Next.js의 page.tsx 역할입니다.
  • route - 그 폴더 하위를 감싸는 라우트입니다. <Outlet />을 두면 Next.js의 layout.tsx 역할을 합니다.

그리고 이 두 단어를 각각 index token, route token이라고 부릅니다.

다행히 이 두 단어는 설정으로 바꿀 수 있습니다.

index 대신 page, route 대신 layout으로 바꾸면 Next.js와 같은 이름이 됩니다.

indexToken, routeToken 설정하기

vite.config.ts에 indexToken과 routeToken을 추가합니다.

vite.config.ts
tanstackRouter({
  target: "react",
  autoCodeSplitting: true,
  indexToken: "page",
  routeToken: "layout",
}),
indexToken과 routeToken은 같은 값을 쓸 수 없습니다. 같은 값을 넣으면 설정 단계에서 에러가 납니다.

이제 about 폴더에 page.tsx와 layout.tsx를 만들어 보겠습니다.

빈 파일을 만들면 이전 글에서 본 것처럼 코드가 자동으로 생성됩니다.

src/routes/about/page.tsx
import { createFileRoute } from "@tanstack/react-router";
 
export const Route = createFileRoute("/about/")({
  component: RouteComponent,
});
 
function RouteComponent() {
  return <div>Hello "/about/"!</div>;
}
src/routes/about/layout.tsx
import { createFileRoute } from "@tanstack/react-router";
 
export const Route = createFileRoute("/about")({
  component: RouteComponent,
});
 
function RouteComponent() {
  return <div>Hello "/about"!</div>;
}

경로를 잘 보면 page.tsx는 "/about/", layout.tsx는 "/about"입니다.

page.tsx는 폴더의 기본 화면(index)이라서 뒤에 /가 하나 더 붙습니다. 이전 글에서 about/index.tsx의 경로가 /about/였던 것과 같습니다.

layout.tsx에는 Outlet을 직접 넣어야 합니다

자동으로 만들어진 layout.tsx에는 <Outlet />이 없습니다.

이대로 두면 /about으로 접속했을 때 page.tsx가 아니라 Hello "/about"!만 보입니다.

Next.js의 layout.tsx가 children을 받아서 그리는 것처럼, Tanstack Router에서는 <Outlet />이 그 자리입니다.

src/routes/about/layout.tsx
import { Outlet, createFileRoute } from "@tanstack/react-router";
 
const AboutLayout = () => {
  return (
    <section>
      <h2>About Layout</h2>
      {/* Next.js layout.tsx의 children 자리 */}
      <Outlet />
    </section>
  );
};
 
export const Route = createFileRoute("/about")({
  component: AboutLayout,
});
layout.tsx를 새로 만들면 반드시 <Outlet />을 추가해 주세요. 빠뜨리면 하위의 page.tsx가 화면에 나오지 않습니다.

이제 /about과 /about/news 모두 About Layout 아래에 각 page.tsx가 그려집니다.

Link의 to에는 / 를 붙이지 않습니다

라우트 파일의 경로는 "/about/"이지만 실제 주소와 Link의 to는 그대로 /about입니다.

<Link to="/about">About</Link> // 정상
<Link to="/about/">About</Link> // 타입 에러

to="/about/"로 적으면 타입 에러가 납니다.

error TS2820: Type '"/about/"' is not assignable to type '"." | ".." | "/" | "/about" | "/about/news" | ...

헷갈릴 수 있는데, 끝의 /는 라우트 파일 안에서만 붙는다고 기억하면 됩니다.

page와 layout 외의 파일은 라우트에서 빼기

여기까지만 해도 이름은 Next.js와 같아졌습니다.

그런데 Next.js와 다른 점이 하나 더 있습니다.

Next.js는 app 폴더 안에서 page.tsx, layout.tsx 같은 정해진 이름만 라우트로 읽습니다. 그래서 같은 폴더에 다른 파일을 같이 둬도 주소가 생기지 않습니다.

Tanstack Router는 반대입니다. routes 폴더 안의 파일은 이름과 상관없이 전부 라우트 후보입니다.

예를 들어 about 폴더에 빈 파일 empty-thing.tsx를 만들면 다음 코드가 자동으로 채워집니다.

src/routes/about/empty-thing.tsx
import { createFileRoute } from "@tanstack/react-router";
 
export const Route = createFileRoute("/about/empty-thing")({
  component: RouteComponent,
});
 
function RouteComponent() {
  return <div>Hello "/about/empty-thing"!</div>;
}

의도하지 않았는데 /about/empty-thing 주소가 생겨버립니다.

내용이 있는 파일이라면 라우트로 만들지는 않지만 빌드할 때마다 경고가 나옵니다.

Warning: Route file "src/routes/about/loading.tsx" does not export a Route. This file will not be included in the route tree.

그래서 routeFileIgnorePattern 옵션으로 page, layout, __root 외의 파일은 아예 읽지 않게 했습니다.

vite.config.ts
tanstackRouter({
  target: "react",
  autoCodeSplitting: true,
  indexToken: "page",
  routeToken: "layout",
  routeFileIgnorePattern: String.raw`^(?!(?:page|layout|__root)\.tsx$).+\.tsx?$`,
}),

정규식이 조금 복잡해 보이는데 뜻은 단순합니다.

  • .+\.tsx?$ - .ts, .tsx 파일 중에서
  • (?!(?:page|layout|__root)\.tsx$) - 이름이 page.tsx, layout.tsx, __root.tsx가 아닌 것은 무시합니다.
String.raw를 사용하면 정규식의 역슬래시(\)를 두 번씩 쓰지 않아도 됩니다.

폴더 이름은 .tsx로 끝나지 않으니 그대로 읽힙니다. about, news 같은 폴더는 지금처럼 경로가 됩니다.

이제 about 폴더에 빈 파일을 만들어도 코드가 자동으로 생성되지 않고, 빌드 경고도 사라집니다.

덤으로 about.tsx, $postId.tsx처럼 파일 이름으로 만드는 라우트도 더 이상 읽지 않습니다. 화면은 항상 폴더/page.tsx로만 만들게 되니 Next.js와 규칙이 완전히 같아집니다.

패턴 대신 - 접두사를 써도 됩니다

정규식을 쓰고 싶지 않다면 Tanstack Router에 원래 있는 방법도 있습니다.

파일이나 폴더 이름 앞에 -를 붙이면 라우트에서 빠집니다.

- 접두사로 제외하기

routes
about
-components# 라우트에서 빠짐
about-header.tsx
layout.tsx
page.tsx

다만 이 방법은 파일을 만들 때마다 -를 붙여야 하고, 깜빡하면 위처럼 라우트가 생깁니다.

저는 규칙을 설정 한 곳에 두는 routeFileIgnorePattern 쪽이 더 편했습니다.

Next.js의 private folder처럼 _components 폴더를 만들어도 패턴 덕분에 안의 파일은 무시됩니다. 하지만 Tanstack Router에서 _ 접두사는 pathless 레이아웃이라는 뜻이 있어서, 저는 헷갈리지 않게 컴포넌트를 routes 밖(components, features 폴더)에 두고 있습니다.

loading.tsx 처럼 옆에 두고 쓰기

routeFileIgnorePattern을 설정하면 좋은 점이 하나 더 있습니다.

Next.js의 loading.tsx처럼 화면에 딸린 파일을 page.tsx 옆에 둘 수 있습니다.

loading.tsx 함께 두기

routes
about
layout.tsx
loading.tsx# 라우트가 아님 (패턴으로 무시)
page.tsx
src/routes/about/loading.tsx
const Loading = () => {
  return <p>loading...</p>;
};
 
export default Loading;

Tanstack Router에는 loading.tsx 같은 파일 규칙이 없습니다. 대신 라우트 옵션의 pendingComponent에 직접 연결해 줍니다.

src/routes/about/page.tsx
import { createFileRoute } from "@tanstack/react-router";
import Loading from "./loading";
 
export const Route = createFileRoute("/about/")({
  loader: async () => {
    // 데이터 불러오기
  },
  pendingComponent: Loading,
  component: AboutPage,
});
 
const AboutPage = () => {
  return <h1>About</h1>;
};

loader가 끝나기 전까지 loading...이 보이고, 끝나면 About 화면으로 바뀝니다.

pendingComponent는 loader가 pendingMs(기본 1초)보다 오래 걸릴 때 나타납니다. 바로 보이게 하려면 pendingMs: 0 을 함께 넣어주세요.

/about/loading 주소로 접속해 보면 Not Found가 나옵니다. loading.tsx는 라우트가 아니라 그냥 컴포넌트 파일이기 때문입니다.

같은 방법으로 Next.js의 error.tsx, not-found.tsx도 각각 errorComponent, notFoundComponent 옵션에 연결해서 쓸 수 있습니다.

라우트 그룹과 동적 경로

라우트 그룹

Next.js의 라우트 그룹 (폴더이름)은 Tanstack Router에서도 똑같이 동작합니다.

라우트 그룹

routes
(marketing)# 주소에 포함되지 않음
pricing
page.tsx# /pricing

괄호로 감싼 폴더 이름은 주소에서 빠지기 때문에 /pricing으로 접속하면 됩니다.

동적 경로

동적 경로는 Next.js와 다릅니다.

Next.js는 [postId]처럼 대괄호를 쓰고, Tanstack Router는 $postId처럼 $를 씁니다. 이 부분은 설정으로 바꿀 수 없습니다.

동적 경로

routes
posts
page.tsx# /posts
$postId# Next.js의 [postId]
page.tsx# /posts/1
src/routes/posts/$postId/page.tsx
import { createFileRoute } from "@tanstack/react-router";
 
const PostPage = () => {
  const { postId } = Route.useParams();
 
  return <h1>post {postId}</h1>;
};
 
export const Route = createFileRoute("/posts/$postId/")({
  component: PostPage,
});

$ 대신 폴더로 만든다는 점만 빼면 이전 글의 동적 라우팅과 같습니다.

`Tanstack Router 동적(dynamic) 라우팅 사용법 및 가이드`

루트 레이아웃은 __root.tsx 그대로

여기까지 하고 나니 __root.tsx도 layout.tsx로 바꾸고 싶어졌습니다.

Next.js는 app/layout.tsx가 최상위 레이아웃이니까요.

routes 폴더 바로 아래에 layout.tsx를 두면 될 것 같지만, 그러면 다음 에러가 납니다.

Error: Invalid route path "" was found. Root routes must be defined via __root.tsx (createRootRoute), not createFileRoute('') or a route file that resolves to an empty path.
Conflicting files:
 src/routes/layout.tsx

virtual file routes(virtualRouteConfig)로 루트 파일 이름을 layout.tsx로 지정하는 방법도 시도해 봤는데 결과는 같았습니다.

에러 메시지 그대로 루트는 __root.tsx로만 정의할 수 있습니다.

이 부분은 Tanstack Router가 정한 이름이라 그대로 두고, __root.tsx가 Next.js의 app/layout.tsx 자리라고 생각하면 됩니다.

src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
 
const RootLayout = () => {
  return (
    <>
      <Header />
      <main>
        <Outlet />
      </main>
    </>
  );
};
 
export const Route = createRootRoute({
  component: RootLayout,
});

routes 폴더를 app 폴더로 바꾸기

폴더 이름까지 Next.js처럼 app으로 바꾸고 싶다면 routesDirectory 옵션을 추가합니다.

vite.config.ts
tanstackRouter({
  target: "react",
  autoCodeSplitting: true,
  routesDirectory: "./src/app",
  indexToken: "page",
  routeToken: "layout",
  routeFileIgnorePattern: String.raw`^(?!(?:page|layout|__root)\.tsx$).+\.tsx?$`,
}),

routesDirectory의 기본값은 ./src/routes입니다. 이 값만 바꾸면 src/app 폴더를 읽습니다.

app 폴더 구조

src
app# routes 대신 app
__root.tsx
page.tsx
about
layout.tsx
page.tsx
posts
$postId
page.tsx
components
features
main.tsx
routeTree.gen.ts# 위치는 그대로

routeTree.gen.ts는 generatedRouteTree 옵션으로 위치가 따로 정해져 있어서 src/routeTree.gen.ts에 그대로 만들어집니다. 안의 import 경로만 ./app/...으로 바뀝니다.

src/routeTree.gen.ts
import { Route as rootRouteImport } from "./app/__root";
import { Route as PageRouteImport } from "./app/page";
import { Route as AboutLayoutRouteImport } from "./app/about/layout";
// ...

main.tsx에서는 ./routeTree.gen을 그대로 가져오기 때문에 따로 고칠 곳이 없습니다.

dev 서버를 켜 둔 채로 폴더를 먼저 옮기면, 예전 routeTree.gen.ts가 ./routes/__root 를 찾지 못해 잠깐 에러가 납니다. vite.config.ts를 저장하면 다시 만들어지지만, 설정을 먼저 바꾸고 폴더를 옮기는 편이 덜 번거롭습니다.

routes와 app 중 무엇을 쓸까

저도 처음에는 폴더 이름까지 app으로 바꿨다가 결국 routes로 되돌렸습니다.

Tanstack Router 문서와 예제가 전부 routes 기준이라, 문서를 보면서 작업할 때 routes가 덜 헷갈렸습니다.

Next.js처럼 느껴지게 만드는 것은 폴더 이름보다 안의 규칙(page.tsx, layout.tsx)이라고 생각합니다. 폴더 이름은 팀이 편한 쪽을 고르면 됩니다.

기존 프로젝트를 옮길 때

이미 about.tsx처럼 파일 이름으로 라우트를 만든 프로젝트라면 파일만 옮기면 됩니다.

옮기기 전 → 후

routes (전)
__root.tsx
index.tsx
about.tsx
settings
profile.tsx
routes (후)
__root.tsx
page.tsx
about
page.tsx
settings
profile
page.tsx

createFileRoute("/about")의 경로를 "/about/"로 직접 고치지 않아도 됩니다.

dev 서버나 빌드를 한 번 돌리면 Tanstack Router가 파일 위치에 맞게 경로를 자동으로 고쳐 줍니다.

src/routes/about/page.tsx
// 옮긴 직후
export const Route = createFileRoute("/about")({ ... });
 
// 빌드 후 자동으로 바뀜
export const Route = createFileRoute("/about/")({ ... });

주소와 Link의 to는 바뀌지 않으니 화면 쪽 코드는 손댈 필요가 없습니다.

경로 에러가 계속 남아 있다면 이전 글처럼 routeTree.gen.ts를 삭제하고 다시 실행해 보세요.

마치며

정리하면 Tanstack Router를 Next.js 폴더 구조처럼 쓰는 방법은 다음과 같습니다.

  1. indexToken: "page", routeToken: "layout"으로 파일 이름을 맞춥니다.
  2. routeFileIgnorePattern으로 page, layout, __root 외의 파일은 라우트에서 뺍니다.
  3. 폴더 이름까지 바꾸고 싶다면 routesDirectory: "./src/app"을 추가합니다.

__root.tsx와 동적 경로의 $만 Tanstack Router 방식으로 남고 나머지는 Next.js와 같은 모양이 됩니다.

Next.js와 React 프로젝트를 같이 다루다 보면 프로젝트마다 라우트 규칙이 달라서 은근히 피곤했는데, 이렇게 맞춰 두니 어느 쪽을 열어도 화면 파일을 바로 찾을 수 있어서 편했습니다.

참고

`File-Based Routing API Reference - Tanstack Router Docs` `File Naming Conventions - Tanstack Router Docs`

다음글

macOS에서 Chrome 표시 언어를 영어로 변경하는 방법


관련 태그

  • react
  • tanstack-router
  • router
  • Next.js