---
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 (
{users.map(u => - {u.email}
)}
);
}
// 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 (
);
}
```
### 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 {users.map(u => - {u.email}
)}
;
}
```
→ 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]]