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
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
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
},
}),
],
})
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
기본적으로 routesDirectory은 routes으로 설정됩니다. Next.js App Router 규칙과의 일관성을 유지하려면 대신 app으로 설정할 수 있습니다.
4. 루트 레이아웃 조정
TanStack Start는 Remix와 유사한 라우팅 방식을 사용하며, 중첩 구조와 토큰을 사용하는 특수 기능을 지원하기 위해 일부 사항이 변경되었습니다. 자세한 내용은 라우팅 개념 가이드를 참조하세요.
layout.tsx 대신 src/app 디렉터리에 __root.tsx이라는 파일을 생성합니다. 이 파일은 애플리케이션의 루트 레이아웃 역할을 합니다.
src/app/layout.tsxtosrc/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.tsxtosrc/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.js | TanStack Start |
|---|---|---|
| 루트 레이아웃 | src/app/layout.tsx | src/app/__root.tsx |
/ (홈 페이지) | src/app/page.tsx | src/app/index.tsx |
/posts (정적 라우트) | src/app/posts/page.tsx | src/app/posts.tsx |
/posts/[slug] (동적) | src/app/posts/[slug]/page.tsx | src/app/posts/$slug.tsx |
/posts/[...slug] (포괄 라우트) | src/app/posts/[...slug]/page.tsx | src/app/posts/$.tsx |
/api/endpoint (API 라우트) | src/app/api/endpoint/route.ts | src/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>
)
}