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

8.0 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
frontend-tanstack-start TanStack Start — modern fullstack React Coding draft B conceptual 2026-05-09 2026-05-09
frontend
fullstack
vibe-coding
language applicable_to
TS / React
Frontend
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

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 정의

// 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)

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)

// 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)

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

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)

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)

// 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

// 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.
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

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

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

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

# 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 통합

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.

🔗 관련 문서