376 lines
8.0 KiB
Markdown
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]]
|