--- id: backend-server-components-pattern title: Server Components / Server Actions / TanStack Start category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [backend, server-components, fullstack, vibe-coding] tech_stack: { language: "TS / React", applicable_to: ["Backend", "Frontend"] } applied_in: [] aliases: [RSC, server actions, TanStack Start, fullstack TS, server functions, isomorphic] --- # Server Components / Server Functions > Frontend / backend 경계가 흐려짐. **RSC (React Server Components), Next App Router, TanStack Start, Remix, Astro**. Server function = REST endpoint 의 typed alternative. ## 📖 핵심 개념 - RSC: 서버 render, 0 client JS. - Server function: 'use server' — type-safe RPC. - 'use client': 인터랙션 component. - Streaming: Suspense + 점진 hydration. ## 💻 코드 패턴 ### Next.js App Router (RSC) ```tsx // app/users/page.tsx — server component (default) async function UsersPage() { const users = await db.user.findMany(); return (

Users

); } // app/users/UserList.tsx — server function UserList({ users }: { users: User[] }) { return ( ); } // app/users/UserSearch.tsx — client 'use client'; import { useState } from 'react'; export function UserSearch() { const [q, setQ] = useState(''); return setQ(e.target.value)} />; } ``` → Server component 가 default. 인터랙션만 'use client'. ### Server Action (mutation) ```tsx // app/users/actions.ts 'use server'; import { db } from '@/db'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; const CreateUser = z.object({ email: z.string().email(), name: z.string().min(1), }); export async function createUser(formData: FormData) { const data = CreateUser.parse(Object.fromEntries(formData)); await db.user.create({ data }); revalidatePath('/users'); } ``` ```tsx // Form import { createUser } from './actions';
``` → JS 없어도 form submit OK + 활성 시 SPA-like. ### useActionState + useFormStatus ```tsx 'use client'; import { useActionState } from 'react'; import { useFormStatus } from 'react-dom'; function SubmitButton() { const { pending } = useFormStatus(); return ; } function Form() { const [state, formAction] = useActionState(createUser, { error: null }); return (
{state.error &&

{state.error}

} ); } ``` ### TanStack Start (modern) ```ts // routes/users.tsx import { createFileRoute } from '@tanstack/react-router'; import { createServerFn } from '@tanstack/start'; const fetchUsers = createServerFn('GET', async () => { return db.user.findMany(); }); export const Route = createFileRoute('/users')({ loader: () => fetchUsers(), component: UsersPage, }); function UsersPage() { const users = Route.useLoaderData(); return ; } ``` → Type-safe server function — RPC. ### Mutation (TanStack Start) ```ts const createUser = createServerFn('POST', async (input: { email: string; name: string }) => { return db.user.create({ data: input }); }); // Component async function handleSubmit(formData: FormData) { await createUser({ email: formData.get('email') as string, name: formData.get('name') as string, }); } ``` ### Remix ```tsx // app/routes/users.tsx import { json, type LoaderFunctionArgs, type ActionFunctionArgs } from '@remix-run/node'; import { useLoaderData, Form } from '@remix-run/react'; export async function loader() { return json(await db.user.findMany()); } export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); await db.user.create({ data: { email: formData.get('email') as string, name: formData.get('name') as string, }, }); return json({ ok: true }); } export default function Users() { const users = useLoaderData(); return ( <>
...
    {users.map(u =>
  • {u.email}
  • )}
); } ``` ### Streaming (Suspense) ```tsx import { Suspense } from 'react'; async function SlowPanel() { const data = await fetch('https://slow-api.com').then(r => r.json()); return
{data}
; } export default function Page() { return ( <>

Title

{/* 즉시 보임 */} }> {/* 도착 시 stream */} ); } ``` → Fast TTFB + 점진 reveal. ### Cache (Next 15) ```ts 'use cache'; async function getUsers() { 'use cache'; return db.user.findMany(); } // 또는 fetch 의 cache option fetch(url, { next: { revalidate: 60, tags: ['users'] } }); // Invalidate revalidateTag('users'); revalidatePath('/users'); ``` ### 'use client' boundary ```tsx // Server component import { ClientCounter } from './counter'; // imports client async function Page() { const data = await fetchData(); return (

{data.title}

); } // Client component 'use client'; import { useState } from 'react'; export function ClientCounter({ initial }: { initial: number }) { const [count, setCount] = useState(initial); return ; } ``` → Server data → client component prop. Serializable 만. ### Server function pitfalls ``` 1. Public endpoint — Auth 매번 검사 필요. 2. Input validate (Zod / Valibot). 3. Rate limit. 4. Error handling — exception → user-facing message. 5. Logging — PII 제외. ``` ```ts 'use server'; export async function deletePost(postId: string) { const user = await getUser(); if (!user) throw new Error('Unauthorized'); const post = await db.post.findUnique({ where: { id: postId } }); if (!post) throw new Error('Not found'); if (post.userId !== user.id && !user.isAdmin) { throw new Error('Forbidden'); } await db.post.delete({ where: { id: postId } }); revalidatePath('/posts'); } ``` ### vs REST API ``` REST: + Standard + Multi-client (web / mobile / 3rd party) + Cache 표준 (HTTP) - Type drift (server / client) Server functions: + Type-safe end-to-end + Less boilerplate + Co-located with UI - Single-app (web only) - Cache 어려움 (POST) - 다른 client (mobile) X ``` → Web only / fullstack TS = server functions. Multi-client / public API = REST. ### tRPC (related) ```ts // Server const appRouter = router({ users: { list: publicProcedure.query(() => db.user.findMany()), create: publicProcedure .input(z.object({ email: z.string().email() })) .mutation(({ input }) => db.user.create({ data: input })), }, }); // Client const trpc = createTRPCReact(); const users = trpc.users.list.useQuery(); trpc.users.create.useMutation(); ``` → Type-safe + framework agnostic. ### Caching strategy ``` Static (build-time): Generate at build → CDN. ISR (incremental): Revalidate every N seconds. SSR (per-request): Always fresh. Client-only: No server. Server actions: Mutation → revalidate. ``` ### Hydration ``` 1. Server render HTML 2. Client receives HTML (visible immediately) 3. Client downloads JS 4. React hydrates (event listeners attach) → JS 가 작아야 빠른 hydration. ``` ### Streaming SSR ``` Old: Server 가 모든 거 render → send. New: HTML 가 stream — first paint 빠름 + Suspense 가 점진. ``` ### Server-only (security) ```ts import 'server-only'; export const apiKey = process.env.API_KEY!; // Client component 가 import 시도 = build error. ``` → Secret 누설 방지. ### Astro (SSG / SSR / RSC-like) ```astro --- // Server only — build / request time const users = await db.user.findMany(); ---

Users

{users.map(u =>
  • {u.email}
  • )} ``` → Static-first + 작은 JS. ### Phoenix LiveView (Elixir) ```elixir # Server 가 HTML diff push defmodule MyAppWeb.UserLive do use MyAppWeb, :live_view def mount(_params, _session, socket) do {:ok, assign(socket, users: list_users())} end def handle_event("search", %{"q" => q}, socket) do {:noreply, assign(socket, users: search_users(q))} end end ``` → Server-driven + WebSocket. ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | Next.js + fullstack TS | App Router + Server Actions | | Type-safe + flexibility | TanStack Start | | Old Remix users | Remix | | Mostly static + 작은 island | Astro | | Multi-client | REST + tRPC | | Real-time / chat | LiveView / Hotwire | ## ❌ 안티패턴 - **Server action auth 무 검사**: public endpoint. - **Input validate 없음**: 위험. - **모두 'use client'**: bundle 폭발. - **Server-only secret 누설**: import 'server-only'. - **Server / client component 혼동**: build error. - **Cache 안 — 매 request DB**: latency. - **Rate limit 없음**: DoS. ## 🤖 LLM 활용 힌트 - Server component default + 'use client' 작게. - Server action = form action. - Validate (Zod) + auth + rate limit. - Streaming + Suspense = TTFB 빠름. ## 🔗 관련 문서 - [[React_RSC_Server_Actions_Deep]] - [[React_TanStack_Router_Patterns]] - [[Backend_Hono_Modern]]