Files
2nd/10_Wiki/Topics/Coding/Frontend_TanStack_Start.md
T
2026-05-10 22:08:15 +09:00

376 lines
8.0 KiB
Markdown

---
id: frontend-tanstack-start
title: TanStack Start — modern fullstack React
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [frontend, fullstack, vibe-coding]
tech_stack: { language: "TS / React", applicable_to: ["Frontend"] }
applied_in: []
aliases: [TanStack Start, TanStack Router, server functions, Remix alternative, fullstack React]
---
# TanStack Start
> Next.js alternative. **TanStack Router (file-based) + Vite + server functions**. Full-stack React, type-safe end-to-end.
## 📖 핵심 개념
- File-based routing (Next 비슷).
- Type-safe route params (search, path).
- Server functions (RPC 식).
- Vite 가 build (Next 보다 simple).
## 💻 코드 패턴
### Setup
```bash
npm create @tanstack/start@latest my-app
cd my-app
npm run dev
```
### File-based route
```
src/routes/
├── __root.tsx # layout
├── index.tsx # /
├── about.tsx # /about
├── posts/
│ ├── index.tsx # /posts
│ └── $id.tsx # /posts/:id
└── _authenticated/ # auth-required
├── dashboard.tsx
```
### Route 정의
```tsx
// src/routes/posts/$id.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/posts/$id')({
component: PostPage,
loader: ({ params }) => fetchPost(params.id),
});
function PostPage() {
const post = Route.useLoaderData();
const params = Route.useParams();
return <h1>{post.title}</h1>;
}
```
→ Type-safe params (string `id`).
### Search params (type-safe)
```tsx
import { z } from 'zod';
export const Route = createFileRoute('/products')({
validateSearch: z.object({
page: z.number().default(1),
sort: z.enum(['asc', 'desc']).default('asc'),
}),
loader: ({ deps }) => fetchProducts(deps),
loaderDeps: ({ search }) => search,
component: Products,
});
function Products() {
const { page, sort } = Route.useSearch();
return <div>Page {page}</div>;
}
```
→ URL `?page=1&sort=asc` 가 type-safe.
### Server function (RPC)
```tsx
// src/routes/posts/$id.tsx
import { createServerFn } from '@tanstack/start';
export const getPost = createServerFn('GET', async (id: string) => {
return await db.posts.findUnique({ where: { id } });
});
// Client 또는 server 가 호출
const post = await getPost('abc');
```
`getPost` 가 server-only — client bundle 안 들어감.
### Mutation (server function)
```tsx
export const createPost = createServerFn('POST', async (data: PostInput) => {
// Server-only
const session = useSession();
if (!session) throw new Error('unauthorized');
return db.posts.create({ data });
});
// Client
async function handleSubmit(data: PostInput) {
const post = await createPost(data);
}
```
### Loader + suspense
```tsx
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const [user, stats] = await Promise.all([
fetchUser(),
fetchStats(),
]);
return { user, stats };
},
pendingComponent: () => <Spinner />,
errorComponent: ({ error }) => <Error error={error} />,
});
```
→ Suspense / error boundary 가 declarative.
### Defer (streaming)
```tsx
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const user = await fetchUser(); // 빠름 — wait
const slow = fetchSlow(); // promise — defer
return { user, slow };
},
});
function Dashboard() {
const { user, slow } = Route.useLoaderData();
return (
<>
<h1>{user.name}</h1>
<Suspense fallback={<Spinner />}>
<SlowComponent slowPromise={slow} />
</Suspense>
</>
);
}
```
→ User 빠른 first paint, slow 가 streaming.
### Layout (nested)
```tsx
// src/routes/__root.tsx
import { Outlet } from '@tanstack/react-router';
export const Route = createRootRoute({
component: () => (
<>
<Header />
<Outlet />
<Footer />
</>
),
});
// src/routes/posts/__layout.tsx
export const Route = createFileRoute('/posts')({
component: () => (
<div className="posts-layout">
<Sidebar />
<Outlet />
</div>
),
});
```
### Route protection
```tsx
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ context }) => {
if (!context.user) throw redirect({ to: '/login' });
},
});
// src/routes/_authenticated/dashboard.tsx
// → user 있어야만 access.
```
### Link (type-safe)
```tsx
import { Link } from '@tanstack/react-router';
<Link
to="/posts/$id"
params={{ id: '123' }}
search={{ tab: 'comments' }}
>
View
</Link>
// ❌ Compile error — wrong route or param type.
```
### Devtools
```tsx
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
<TanStackRouterDevtools />
// → Route tree, search, params 시각화.
```
### vs Next.js
```
Next:
- App Router (RSC + Server Action)
- 큰 ecosystem
- Vercel 친화
TanStack Start:
- 100% type-safe
- File 또는 code-based route
- Vite (작은, 빠름)
- 작은 ecosystem (newer)
```
→ Type-safe + Vite = TanStack.
크고 / RSC heavy / Vercel = Next.
### vs Remix
```
Remix:
- Loader / Action 가 file 별
- Web standards 친화 (Form, Request)
TanStack:
- Loader 가 비슷
- Server function 가 RPC 식
- Type-safety 강함
```
### vs SvelteKit / Nuxt
```
SvelteKit: Svelte 친화.
Nuxt: Vue 친화.
TanStack Start: React 친화 + 가장 type-safe.
```
### Middleware
```tsx
export const Route = createFileRoute('/admin')({
beforeLoad: async ({ location }) => {
const session = await getSession();
if (!session?.isAdmin) {
throw redirect({ to: '/login', search: { redirect: location.href } });
}
},
});
```
### Server function + form
```tsx
const createPostFn = createServerFn('POST', async (data: FormData) => {
const title = data.get('title') as string;
return db.posts.create({ data: { title } });
});
<form action={createPostFn}>
<input name="title" />
<button>Submit</button>
</form>
```
→ Native form action 식 (Remix 비슷).
### Deploy
```bash
# Vercel
npx vercel deploy
# Cloudflare
npx wrangler deploy
# Bun / Node server
node .output/server/index.mjs
```
### Use case
```
- 작은-중간 React app
- Type-safety priority
- Vite 친화 (Next 보다 빠른 dev)
- Internal tool
- Solo / 작은 팀
```
### vs Vite + React Router (no SSR)
```
Vite + React Router: 모든 거 client.
TanStack Start: SSR + server function.
→ SEO / 빠른 first paint = Start.
```
### TanStack Query 통합
```tsx
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
const userQuery = (id: string) => queryOptions({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
export const Route = createFileRoute('/users/$id')({
loader: ({ params, context }) => context.queryClient.ensureQueryData(userQuery(params.id)),
});
function User() {
const { id } = Route.useParams();
const { data } = useSuspenseQuery(userQuery(id));
return <h1>{data.name}</h1>;
}
```
→ Query + Router 가 best mate.
### Stage / status
```
2026-05: Start 가 still beta.
TanStack Router: stable (1.0+).
Server function: stable.
→ Production OK 가 small / medium.
큰 = Next 가 mature.
```
## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| New React + type-safety | TanStack Start |
| 큰 enterprise | Next.js |
| Static / blog | Astro |
| 작은 SPA | Vite + React Router |
| Internal admin | TanStack Start |
| Server-heavy | Next.js / Remix |
| Edge | TanStack / Astro |
## ❌ 안티패턴
- **Manual route 등록 + file-based 둘 다**: confused.
- **Server function 안 client logic**: bundle 폭발.
- **Loader 가 큰**: defer 사용.
- **Search param schema 없음**: 깨짐.
- **TanStack Query 없이 client cache**: refetch 폭발.
- **모든 거 SSR**: client island 도 OK.
## 🤖 LLM 활용 힌트
- TanStack Router 가 가장 type-safe.
- Server function = RPC 식 (Remix 와 비슷).
- TanStack Query 와 deep integration.
- Vite + Start = simple stack.
## 🔗 관련 문서
- [[React_TanStack_Router_Patterns]]
- [[React_TanStack_Query_Advanced]]
- [[React_Server_Components]]