[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user