본문으로 건너뛰기

Next.js에서 마이그레이션

이 가이드에서는 Next.js App Router에서 TanStack Start로 프로젝트를 마이그레이션하는 과정을 단계별로 안내합니다. Next.js의 강력한 기능을 존중하며, 최대한 원활하게 전환할 수 있도록 돕는 것을 목표로 합니다.

단계별 안내(기본)

이 단계별 가이드에서는 Next.js App Router 프로젝트를 TanStack Start로 마이그레이션하는 방법을 개괄적으로 설명합니다. 마이그레이션 과정의 기본 단계를 이해하여 프로젝트의 구체적인 요구 사항에 맞게 적용할 수 있도록 돕는 것이 목표입니다.

사전 요구 사항

시작하기 전에, 이 가이드에서는 프로젝트 구조가 다음과 같다고 가정합니다:

├── next.config.ts
├── package.json
├── postcss.config.mjs
├── public
│ ├── file.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── vercel.svg
│ └── window.svg
├── README.md
├── src
│ └── app
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
└── tsconfig.json

또는 다음 스타터 템플릿을 복제하여 함께 진행할 수 있습니다:

npx gitpick nrjdalal/awesome-templates/tree/main/next.js-apps/next.js-start next.js-start-er

이 구조는 App Router를 사용하는 기본 Next.js 애플리케이션이며, 이를 TanStack Start로 마이그레이션합니다.

1. Next.js 제거

먼저 Next.js를 제거하고 관련 구성 파일을 삭제합니다:

npm uninstall @tailwindcss/postcss next
rm postcss.config.* next.config.*

2. 필수 의존성 설치

TanStack Start는 TanStack Router를 활용하며 빌드 도구로 Vite 또는 Rsbuild를 지원합니다. 아래 Vite 설정에는 배포 플러그인으로 Nitro가 포함됩니다.

Vite

npm i @tanstack/react-router @tanstack/react-start nitro vite @vitejs/plugin-react

Rsbuild

npm i @tanstack/react-router @tanstack/react-start @rsbuild/core @rsbuild/plugin-react

Tailwind CSS의 경우 사용하려는 빌드 도구 통합을 설치합니다:

Vite

npm i -D @tailwindcss/vite tailwindcss

Rsbuild

npm i -D @tailwindcss/postcss tailwindcss

3. 프로젝트 구성 업데이트

필요한 의존성을 설치했으므로, TanStack Start에서 작동하도록 프로젝트 구성 파일을 업데이트합니다.

Vite

{
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"start": "node .output/server/index.mjs"
}
}

Rsbuild

{
"type": "module",
"scripts": {
"dev": "rsbuild dev",
"build": "rsbuild build"
}
}

Vite

vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { nitro } from 'nitro/vite'

export default defineConfig({
server: {
port: 3000,
},
resolve: {
// Enables Vite to resolve imports using path aliases.
tsconfigPaths: true,
},
plugins: [
tailwindcss(),
tanstackStart({
srcDirectory: 'src', // This is the default
router: {
// Specifies the directory TanStack Router uses for your routes.
routesDirectory: 'app', // Defaults to "routes", relative to srcDirectory
},
}),
viteReact(),
nitro(),
],
})

Rsbuild

rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'

export default defineConfig({
server: {
port: 3000,
},
plugins: [
pluginReact(),
tanstackStart({
srcDirectory: 'src', // This is the default
router: {
// Specifies the directory TanStack Router uses for your routes.
routesDirectory: 'app', // Defaults to "routes", relative to srcDirectory
},
}),
],
})
postcss.config.mjs
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}

기본적으로 routesDirectoryroutes으로 설정됩니다. Next.js App Router 규칙과의 일관성을 유지하려면 대신 app으로 설정할 수 있습니다.

4. 루트 레이아웃 조정

TanStack Start는 Remix와 유사한 라우팅 방식을 사용하며, 중첩 구조와 토큰을 사용하는 특수 기능을 지원하기 위해 일부 사항이 변경되었습니다. 자세한 내용은 라우팅 개념 가이드를 참조하세요.

layout.tsx 대신 src/app 디렉터리에 __root.tsx이라는 파일을 생성합니다. 이 파일은 애플리케이션의 루트 레이아웃 역할을 합니다.

  • src/app/layout.tsx to src/app/__root.tsx
- import type { Metadata } from "next" // [!code --]
import {
Outlet,
createRootRoute,
HeadContent,
Scripts,
} from "@tanstack/react-router"
import appCss from "./globals.css?url"

- export const metadata: Metadata = { // [!code --]
- title: "Create Next App", // [!code --]
- description: "Generated by create next app", // [!code --]
- } // [!code --]
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "utf-8" },
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{ title: "TanStack Start Starter" }
],
links: [
{
rel: 'stylesheet',
href: appCss,
},
],
}),
component: RootLayout,
})

- export default function RootLayout({ // [!code --]
- children, // [!code --]
- }: Readonly<{ // [!code --]
- children: React.ReactNode // [!code --]
- }>) { // [!code --]
- return ( // [!code --]
- <html lang="en"> // [!code --]
- <body>{children}</body> // [!code --]
- </html> // [!code --]
- ) // [!code --]
- } // [!code --]
function RootLayout() {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
)
}

5. 홈페이지 조정

page.tsx 대신 / 라우트용 index.tsx 파일을 생성합니다.

  • src/app/page.tsx to src/app/index.tsx
+ import { createFileRoute } from '@tanstack/react-router' // [!code ++]

- export default function Home() { // [!code --]
+ export const Route = createFileRoute('/')({ // [!code ++]
+ component: Home, // [!code ++]
+ }) // [!code ++]

+ function Home() { // [!code ++]
return (
<main className="min-h-dvh w-screen flex items-center justify-center flex-col gap-y-4 p-4">
<img
className="max-w-sm w-full"
src="https://raw.githubusercontent.com/TanStack/tanstack.com/main/public/images/logos/splash-dark.png"
alt="TanStack Logo"
/>
<h1>
<span className="line-through">Next.js</span> TanStack Start
</h1>
<a
className="bg-foreground text-background rounded-full px-4 py-1 hover:opacity-90"
href="https://tanstack.com/start/latest"
target="_blank"
>
Docs
</a>
</main>
)
}

6. 이제 마이그레이션이 완료되었나요?

개발 서버를 실행하려면 먼저 TanStack Start 내에서 TanStack Router의 동작을 정의하는 파일을 생성해야 합니다.

  • src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
})

return router
}

🧠 여기에서 기본 프리로딩 기능부터 캐시 비활성 경과 시간까지 모든 항목을 구성할 수 있습니다.

이 시점에 TypeScript 오류가 표시되어도 걱정하지 마세요. 다음 단계에서 해결됩니다.

7. 마이그레이션 확인

개발 서버를 실행합니다:

npm run dev

그런 다음 http://localhost:3000에 접속합니다. 로고와 문서 링크가 포함된 TanStack Start 시작 페이지가 표시됩니다.

문제가 발생하면 위 단계를 검토하고 파일 이름과 경로가 정확히 일치하는지 확인합니다. 참조 구현은 마이그레이션 후 저장소를 확인합니다.

다음 단계(고급)

이제 Next.js 애플리케이션의 기본 구조를 TanStack Start로 마이그레이션했으므로, 더 고급 기능과 개념을 살펴볼 수 있습니다.

라우팅 개념

라우트 예시Next.jsTanStack Start
루트 레이아웃src/app/layout.tsxsrc/app/__root.tsx
/ (홈 페이지)src/app/page.tsxsrc/app/index.tsx
/posts (정적 라우트)src/app/posts/page.tsxsrc/app/posts.tsx
/posts/[slug] (동적)src/app/posts/[slug]/page.tsxsrc/app/posts/$slug.tsx
/posts/[...slug] (포괄 라우트)src/app/posts/[...slug]/page.tsxsrc/app/posts/$.tsx
/api/endpoint (API 라우트)src/app/api/endpoint/route.tssrc/app/api/endpoint.ts

라우팅 개념에 대해 자세히 알아봅니다.

동적 및 포괄 라우트

TanStack Start에서 동적 라우트 매개변수를 가져오는 방법은 간단합니다.

- export default async function Page({ // [!code --]
- params, // [!code --]
- }: { // [!code --]
- params: Promise<{ slug: string }> // [!code --]
- }) { // [!code --]
+ export const Route = createFileRoute('/app/posts/$slug')({ // [!code ++]
+ component: Page, // [!code ++]
+ }) // [!code ++]

+ function Page() { // [!code ++]
- const { slug } = await params // [!code --]
+ const { slug } = Route.useParams() // [!code ++]
return <div>My Post: {slug}</div>
}

참고: 포괄 라우트(예: src/app/posts/$.tsx)를 만든 경우 const { _splat } = Route.useParams()를 통해 매개변수에 접근할 수 있습니다.

마찬가지로 const { page, filter, sort } = Route.useSearch()를 사용하여 searchParams에 접근할 수 있습니다.

동적 및 포괄 라우트에 대해 자세히 알아봅니다.

- import Link from "next/link" // [!code --]
+ import { Link } from "@tanstack/react-router" // [!code ++]

function Component() {
- return <Link href="/dashboard">Dashboard</Link> // [!code --]
+ return <Link to="/dashboard">Dashboard</Link> // [!code ++]
}

링크에 대해 자세히 알아봅니다.

이미지

Next.js는 최적화된 이미지에 next/image 컴포넌트를 사용합니다. TanStack Start에서는 유사한 기능을 제공하고 거의 그대로 대체할 수 있는 Unpic 패키지를 사용할 수 있습니다.

import Image from 'next/image' // [!code --]
import { Image } from '@unpic/react' // [!code ++]
function Component() {
return (
<Image
src="/path/to/image.jpg"
alt="Description"
width="600" // [!code --]
height="400" // [!code --]
width={600} // [!code ++]
height={400} // [!code ++]
/>
)
}

서버 액션 함수

- 'use server' // [!code --]
+ import { createServerFn } from "@tanstack/react-start" // [!code ++]

- export const create = async () => { // [!code --]
+ export const create = createServerFn().handler(async () => { // [!code ++]
return true
- } // [!code --]
+ }) // [!code ++]

서버 함수에 대해 자세히 알아보세요.

서버 라우트 핸들러

- export async function GET() { // [!code --]
+ export const Route = createFileRoute('/api/hello')({ // [!code ++]
+ server: { // [!code ++]
+ handlers: { // [!code ++]
+ GET: async () => { // [!code ++]
+ return Response.json("Hello, World!")
+ } // [!code ++]
+ } // [!code ++]
+ } // [!code ++]
+ }) // [!code ++]

서버 라우트에 대해 자세히 알아보세요.

글꼴

- import { Inter } from "next/font/google" // [!code --]

- const inter = Inter({ // [!code --]
- subsets: ["latin"], // [!code --]
- display: "swap", // [!code --]
- }) // [!code --]

- export default function Page() { // [!code --]
- return <p className={inter.className}>Font Sans</p> // [!code --]
- } // [!code --]

next/font 대신 Tailwind CSS의 CSS 우선 접근 방식을 사용합니다. 글꼴을 설치합니다(예: Fontsource에서 설치):

npm i -D @fontsource-variable/dm-sans @fontsource-variable/jetbrains-mono

src/app/globals.css에 다음 내용을 추가합니다:

@import 'tailwindcss' source('../');

@import '@fontsource-variable/dm-sans'; /* [!code ++] */
@import '@fontsource-variable/jetbrains-mono'; /* [!code ++] */

@theme inline {
--font-sans: 'DM Sans Variable', sans-serif; /* [!code ++] */
--font-mono: 'JetBrains Mono Variable', monospace; /* [!code ++] */
/* ... */
}

/* ... */

데이터 가져오기

- export default async function Page() { // [!code --]
+ export const Route = createFileRoute('/')({ // [!code ++]
+ component: Page, // [!code ++]
+ loader: async () => { // [!code ++]
+ const res = await fetch('https://api.vercel.app/blog') // [!code ++]
+ return res.json() // [!code ++]
+ }, // [!code ++]
+ }) // [!code ++]

+ function Page() { // [!code ++]
- const data = await fetch('https://api.vercel.app/blog') // [!code --]
- const posts = await data.json() // [!code --]
+ const posts = Route.useLoaderData() // [!code ++]

return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}