Skip to content

✍️ 필사 모드: React Server Components & Next.js App Router — RSC Protocol, Server Actions, PPR, Streaming (2025)

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

"The future of React is not about rendering faster. It's about rendering less." — Dan Abramov (RSC introduction, Dec 2020)

In December 2020, Dan Abramov and Lauren Tan published the "React Server Components" talk. Most developers reacted with "do I have to learn another thing?" Now, in 2025, RSC is the default in Next.js 14/15, and Remix, TanStack Start, and RedwoodJS are all moving to embrace it. This is the biggest paradigm shift in React's history.

But RSC is complex. Why 'use server' and 'use client'? How does it differ from SSR? When and where does anything render? This post is the map.


1. Four eras of React rendering

Era 1 — CSR (2013–2015)

The create-react-app model: server sends an empty HTML shell + JS bundle, browser executes JS and builds the DOM. Slow first paint, weak SEO, network waterfalls.

Era 2 — SSR (2017+)

Next.js and friends use ReactDOMServer.renderToString to produce HTML on the server, then the browser hydrates. FCP is fast — but the client bundle is unchanged, TTI often gets worse, and getServerSideProps waterfalls plagued the ecosystem.

Era 3 — SSG / ISR (2019+)

Pre-rendered HTML at build time or at intervals. Vercel and Netlify's glory days. Weak on dynamic content.

Era 4 — RSC (announced 2020, shipped 2022)

Components that render only on the server. No JS ships to the client.

// Runs only on the server, never in the bundle
async function ProductList() {
  const products = await db.query('...')  // direct DB access!
  return (
    <ul>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </ul>
  )
}

The revolution: server logic (DB queries, file IO) lives inside React components — no API layer in between.


2. Server vs Client Components

KindRunsIn bundleMarker
Server ComponentServer onlyNoDefault (App Router)
Client ComponentServer + ClientYes'use client'
SharedEitherConditionalNo directive

"use client"

'use client'
import { useState } from 'react'
export default function Counter() {
  const [n, setN] = useState(0)
  return <button onClick={() => setN(n+1)}>{n}</button>
}

This file and everything it imports enters the client bundle. It draws the "Client Boundary."

"use server"

// actions.ts
'use server'
export async function createPost(formData: FormData) {
  await db.posts.insert({ ... })
}
// <form action={createPost}>

Server Action — invoked from the client, executed on the server. Internally compiled to an RPC endpoint.

Boundary rules

  1. Server → Server: free function call.
  2. Server → Client: via children or serializable props.
  3. Client → Client: normal React.
  4. Client → Server: only via Server Action.

Common misconception — "Server Components are SSR"

No. SSR means "rendered once on the server as HTML and hydrated." RSC means "rendered only on the server; no JS is sent." Two different mechanisms — they can combine.


3. The RSC Flight Protocol

RSC produces not HTML but a serialized representation of a component tree — so the client can later fetch a new subtree on navigation and merge.

Flight Format — streaming JSON

1:"$Sreact.suspense"
2:{"children":["$","h1",null,{"children":"Hello"}]}
3:I[{"id":"Counter","chunks":["..."]}]
0:["$","$1",null,{"children":["$","div",null,{"children":["$","$L3",null,{"initial":0}]}]}]
  • $ prefix denotes special values (components, Suspense, refs).
  • $L3 means "lazy-load Client Component 3."
  • Streamed incrementally — later-resolving chunks arrive later.

Benefits

  • Incremental rendering — chunks ship as they're ready.
  • Bundle splitting — only Client Component chunks that are actually used.
  • Hydration optimization — already-rendered parts don't re-render.

Benchmarks

Same page SSR vs RSC: TTFB similar, JS bundle 30–70% smaller, TTI faster (fewer components to hydrate).


4. App Router — the convention language

File-system routing

app/
  layout.tsx
  page.tsx
  blog/
    layout.tsx
    page.tsx
    [slug]/
      page.tsx
      loading.tsx
      error.tsx
    @sidebar/
      default.tsx
      page.tsx

Special files

  • page.tsx — URL-matched component.
  • layout.tsx — shared, does not re-render across nested navigation.
  • template.tsx — re-renders every time.
  • loading.tsx — auto-wraps with Suspense.
  • error.tsx — auto-wraps with Error Boundary.
  • not-found.tsx — 404.
  • default.tsx — default for parallel routes.
  • route.ts — API endpoint.

Layout nesting — the real win

Navigating between pages does not re-render the layout — sidebar state persists. Impossible with the Pages Router.

Parallel routes (@slot)

Multiple independent routes in one screen — ideal for dashboards.

Intercepting routes ((..))

Click a photo → opens as modal, URL becomes /photo/123, reload shows full page. Instagram-style UX.


5. The data-fetching revolution

Old way (Pages Router)

getServerSideProps only at the page level → prop drilling or global state for deep components.

App Router — async anywhere

async function UserProfile({ userId }) {
  const user = await db.users.find(userId)
  return <div>{user.name}</div>
}

await anywhere in the tree; React runs them in parallel.

Automatic dedup

The same fetch URL invoked twice in one render hits the source once. React 18 cache() too.

Avoid waterfalls

async function Page() {
  const [user, posts] = await Promise.all([getUser(), getPosts()])
  return <UI user={user} posts={posts} />
}

Independent data → always parallel.


6. Server Actions — the end of fetch?

Basic use

'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
  await db.posts.insert({ title: formData.get('title') })
  revalidatePath('/blog')
}
// <form action={createPost}><input name="title" /><button>Create</button></form>

Works even with JavaScript disabled — the form just POSTs to the server. Progressive enhancement.

useActionState — error + pending

'use client'
const [state, action, isPending] = useActionState(createPost, null)
<form action={action}>
  {state?.error && <p>{state.error}</p>}
  <button disabled={isPending}>Submit</button>
</form>

useOptimistic — instant feedback

const [optimisticMsgs, addOptimistic] = useOptimistic(messages,
  (state, newMsg) => [...state, { ...newMsg, sending: true }])

Twitter/WhatsApp-style UX: reflect immediately, save in the background.

Server Actions vs API Routes

  • API Route: REST endpoint, externally callable.
  • Server Action: internal only, automatic CSRF/token protection.

Internal mutations → Server Action. External APIs → Route Handler.


7. Four cache layers — the confusing part

  1. Request Memoization — same fetch in one render = one call. React-level.
  2. Data Cachefetch() is cached on disk/edge. Control via { cache: 'no-store' } or { next: { revalidate: 60 } }. Next.js 15 changed the default to no-store.
  3. Full Route Cache — entire HTML + RSC payload for static routes. Built at build or via ISR.
  4. Router Cache (client) — in-memory RSC payload for visited routes. Back button is instant. Invalidate with router.refresh().

Invalidation

  • revalidatePath('/blog') — path-level.
  • revalidateTag('posts') — tag-level.
  • Stale-while-revalidate under the hood.

2024–2025 simplification

Next.js 15 "stable cache semantics": fetch cache is opt-in, dynamicIO flag for explicit static/dynamic, experimental 'use cache' directive for function-level caching.


8. Streaming and Suspense

Servers don't wait to finish everything — they stream chunks as they become ready.

Automatic Suspense via loading.tsx

export default function Page() {
  return (
    <>
      <Header />
      <SlowComponent />
      <Footer />
    </>
  )
}

With loading.tsx, this is auto-wrapped in <Suspense fallback={<Loading />}>.

Manual Suspense boundaries

<Suspense fallback={<ProductSkeleton />}><SlowProducts /></Suspense>
<Suspense fallback={<ReviewsSkeleton />}><Reviews /></Suspense>

Independent boundaries resolve independently — the faster one arrives first.

How streaming HTML works

  • <html><head>... ships immediately.
  • Each Suspense boundary injects its HTML via <script> on resolution.
  • Hydration is selective.

TTFB vs FMP

Traditional SSR: wait for everything, slow TTFB and FMP. Streaming: immediate TTFB, fast FMP, some parts later.


9. PPR — Partial Prerendering (Next.js 15)

The problem

Fully static is fast but rigid. Fully dynamic is flexible but slow. Real pages are some static, some dynamic.

Solution — mix in one page

export const experimental_ppr = true
export default function Page() {
  return (
    <>
      <Header />  {/* static */}
      <Hero />    {/* static */}
      <Suspense fallback={<Skeleton />}>
        <DynamicCart />  {/* dynamic per request */}
      </Suspense>
    </>
  )
}

How it works

  1. Build-time: pre-render the static shell.
  2. Request-time: render only dynamic parts, stream them in.
  3. Result: static speed + dynamic flexibility.

Status

Experimental in Next.js 15, running in Vercel production, expected to stabilize in 2026.


10. RSC vs SolidStart vs Qwik vs friends

SolidStart

Solid.js-based (faster than React, reactive primitives). Islands + RSC-like server functions. Vite-based, simpler.

Qwik City

Resumability — no hydration. State is serialized into HTML; code downloads only on interaction. Initial JS can be under 1KB.

TanStack Start

Tanner Linsley (React Query). Vite, TypeScript-first. Beta in 2025, RSC planned.

Remix (now React Router v7)

"Use the Platform." Merged into React Router v7 in late 2024. RSC support arriving in 2025.

Choice matrix

Team / goalPick
React-native team, large appNext.js (RSC)
Extreme performance, small appQwik
Prefer Solid.jsSolidStart
Type-safety-firstTanStack Start
"Use the Platform"React Router v7

11. Migrating Pages → App Router

Coexist

Same Next.js project can have both app/ and pages/.

Order

  1. New features in app/.
  2. Keep API routes until it's convenient.
  3. Migrate page-by-page.
  4. Fold _app.tsx into app/layout.tsx.

Pitfalls

  • CSS — styles/globals.css loaded twice.
  • Middleware — shared but some API drift.
  • <Image> — identical.
  • useRouternext/navigation (breaking).

12. Ten common mistakes

  1. 'use client' everywhere — RSC benefits evaporate.
  2. useState in a Server Component — TypeScript catches it.
  3. DB access in a Client Component — security incident; framework blocks it.
  4. window in a Server Component — doesn't exist.
  5. Huge fetch on every request — use cache strategy.
  6. No nested Suspense — the whole page waits.
  7. Sequential await — network waterfall.
  8. Large objects through Server Actions — serialization cost.
  9. Missing revalidatePath — UI stale after mutation.
  10. Client state expected inside Server Component — impossible.

13. App Router checklist

  • Default to Server Component'use client' only when needed.
  • Layouts don't re-render — verify shared state.
  • Suspense boundaries — isolate slow parts.
  • loading.tsx per route.
  • error.tsx per route.
  • Parallel fetchingPromise.all.
  • Server Action validation — zod.
  • Revalidation strategy — path/tag.
  • Static/dynamic explicitexport const dynamic = 'force-static'.
  • TypeScript strict.
  • Turbopack devnext dev --turbo.
  • Bundle analysis@next/bundle-analyzer.

Closing — "the server matters again"

RSC isn't an optimization. It is a paradigm shift that pushes the frontend/backend boundary down to the component level. Ten years ago the industry shouted "SPAs are the future." Now we say "the server matters again." This is not nostalgia — it's a new synthesis: server's DB access convenience + client's interactivity.

React's "Use the Platform" mantra is about folding web standards (forms, HTML streaming, progressive enhancement) back into React. The 2030 React will look different — but the direction is already visible: small client bundles, fast first paint, solve server problems on the server, push complexity into the framework.


"With Server Components, you don't have to choose between 'it's a rich app' and 'it's fast'. You get both." — Sebastian Markbåge

현재 단락 (1/193)

In December 2020, Dan Abramov and Lauren Tan published the "React Server Components" talk. Most deve...

작성 글자: 0원문 글자: 10,526작성 단락: 0/193