Skip to content

✍️ 필사 모드: React Server Components와 Next.js App Router 완전 정복 — RSC 프로토콜, Server Actions, PPR, Streaming (2025)

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

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

2020년 12월 21일, Dan Abramov와 Lauren Tan이 "React Server Components" 발표 영상을 올렸다. 당시 대부분의 개발자는 "또 새로운 걸 배워야 하나?"라고 반응했다. 2025년이 된 지금, RSC는 Next.js 14/15의 기본값이고, Remix, TanStack Start, RedwoodJS가 모두 RSC를 품으려 한다. 이것은 React 역사상 가장 큰 패러다임 변화다.

그러나 RSC는 복잡하다. "왜 'use server'랑 'use client'가 필요한가?" "이게 SSR과 뭐가 다른가?" "언제 어디서 뭐가 렌더링되는가?" 이 글은 그 모든 질문에 답하는 지도다.


1. React 렌더링의 역사 — 4단계

1단계 — CSR (2013-2015)

create-react-app 시절의 모델:

  1. 서버가 빈 HTML + JS 번들 전송
  2. 브라우저가 JS 실행 → 컴포넌트 렌더링 → DOM 생성

단점:

  • First Paint 느림 — JS 로드 + 실행
  • SEO 부족 — 초기 HTML에 콘텐츠 없음
  • 네트워크 워터폴 — 데이터 페치 → 렌더 → 자식 페치

2단계 — SSR (2017+)

Next.js 등이 ReactDOMServer.renderToString으로 서버에서 HTML 생성:

  1. 서버가 렌더링된 HTML 전송 (FCP 빠름)
  2. 브라우저가 hydrate — 이벤트 리스너 붙이기

단점:

  • 서버 렌더는 번들 크기에 영향 없음 (클라이언트 JS 여전)
  • Hydration TTI가 오히려 더 느림 (HTML + JS 둘 다)
  • getServerSideProps 워터폴

3단계 — SSG / ISR (2019+)

빌드 시 또는 주기적으로 HTML 생성:

  • 정적 사이트의 속도 + React의 상호작용
  • Vercel, Netlify의 전성기

그러나 동적 콘텐츠에는 부족.

4단계 — RSC (2020 발표, 2022 상용화)

서버에서만 렌더링되는 컴포넌트. 클라이언트에 JS가 없다.

// 완전히 서버에서만 실행됨, 번들에 안 들어감
async function ProductList() {
  const products = await db.query('...')  // DB 직접 접근!
  return (
    <ul>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </ul>
  )
}

혁명의 본질: 서버 로직(DB 질의, 파일 IO)이 React 컴포넌트 안에 자연스럽게 들어간다. API 레이어 없이.


2. Server Component vs Client Component

세 종류의 컴포넌트

타입실행 위치번들 포함사용법
Server Component서버에서만X'use server' 불필요, 기본값 (App Router)
Client Component서버+클라이언트O'use client' 명시
Shared Component양쪽조건부지시어 없음

"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>
}

이 파일과 여기서 import하는 모든 것이 클라이언트 번들에 들어간다. "Client Boundary"를 만드는 마커.

"use server" 지시어

// actions.ts
'use server'

export async function createPost(formData: FormData) {
  await db.posts.insert({ ... })
}

// Component.tsx
<form action={createPost}>...</form>

Server Action — 클라이언트에서 호출하지만 서버에서 실행되는 함수. 내부적으로는 RPC 엔드포인트로 변환.

경계(boundary)의 규칙

  1. Server → Server: 자유 (그냥 함수 호출)
  2. Server → Client: children prop 또는 serializable props로 전달
  3. Client → Client: 일반 React
  4. Client → Server: Server Action으로만

흔한 오해 — "서버 컴포넌트는 SSR이다"

아니다. SSR은 "서버에서도 한 번 렌더링"하는 것. RSC는 "서버에서만 렌더링"하는 것. 클라이언트 번들에 아예 없다.

  • SSR Client Component: 서버에서 HTML 생성 + 클라이언트에서 hydrate
  • Server Component: 서버에서만 실행, JS는 클라이언트에 보내지지 않음

3. RSC Flight Protocol — 직렬화의 기술

왜 새 프로토콜이 필요한가

RSC가 생성하는 건 HTML이 아니다. 컴포넌트 트리의 직렬화된 표현이다.

이유:

  • 클라이언트가 이후 네비게이션 시 새 서버 컴포넌트를 받아 기존 트리와 병합해야
  • "이 위치에 이 Client Component + props를 붙여라"를 표현해야

Flight Format — 스트리밍 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}]}]}]
  • $ 접두어로 특수 값(컴포넌트, Suspense, 참조) 표현
  • $L3는 "Client Component 3번을 lazy load" 지시
  • 스트리밍으로 전송 — 나중에 resolve되는 부분은 늦게

장점

  • 증분 렌더링 — 각 청크가 준비되는 대로 전송
  • 번들 분할 — Client Component 참조만 있으므로 필요한 청크만 로드
  • hydration 최적화 — 이미 렌더된 부분은 re-render 안 함

벤치마크

같은 페이지 SSR vs RSC:

  • TTFB: 비슷 (둘 다 서버 렌더)
  • JS 번들: RSC가 30-70% 작음 (서버 코드 제외)
  • TTI: RSC가 빠름 (hydrate할 컴포넌트 적음)

4. Next.js App Router — 컨벤션의 언어

파일 시스템 기반 라우팅

app/
  layout.tsx          # 루트 레이아웃
  page.tsx            # 홈
  blog/
    layout.tsx        # 블로그 레이아웃
    page.tsx          # 블로그 목록
    [slug]/
      page.tsx        # 개별 글
      loading.tsx     # 로딩 UI
      error.tsx       # 에러 UI
    @sidebar/         # 병렬 라우팅
      default.tsx
      page.tsx

특수 파일 규약

파일역할
page.tsxURL에 매칭되는 컴포넌트
layout.tsx공유 레이아웃 (리렌더 안됨)
template.tsxlayout과 유사하지만 매번 리렌더
loading.tsxSuspense fallback 자동 래핑
error.tsxError Boundary 자동 래핑
not-found.tsx404 페이지
default.tsx병렬 라우팅의 기본값
route.tsAPI 엔드포인트

Layout Nesting — 진짜 장점

app/layout.tsx        (웹사이트 프레임)
  └─ app/blog/layout.tsx (블로그 프레임)
      └─ app/blog/[slug]/page.tsx (글 본문)

페이지 간 이동 시 layout은 리렌더 안됨. 사이드바 state가 유지됨. Pages Router에서는 불가능했던 일.

병렬 라우팅 — @slot

app/
  layout.tsx
  @team/
    page.tsx
  @analytics/
    page.tsx
export default function Layout({ children, team, analytics }) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  )
}

한 화면에 여러 독립 라우트. 대시보드에 이상적.

Intercepting Routes — (..)

사진 클릭 시 모달로 열되, URL은 /photo/123으로. 새로고침하면 전체 페이지로. Instagram 같은 UX.


5. 데이터 페칭의 혁명

전통 방식 (Pages Router)

// 페이지별로만 데이터 페칭 가능
export async function getServerSideProps() {
  const data = await fetch('...')
  return { props: { data } }
}

컴포넌트 깊은 곳에서 데이터 필요하면? Prop drilling 또는 전역 상태.

App Router — 어디서든 async 가능

// 깊이 중첩된 서버 컴포넌트
async function UserProfile({ userId }) {
  const user = await db.users.find(userId)
  return <div>{user.name}</div>
}

컴포넌트 트리 어디서든 await. React가 병렬로 실행.

자동 요청 중복 제거 (Dedup)

같은 렌더에서 같은 fetch 여러 번 → 실제로는 한 번만.

// 두 컴포넌트가 같은 URL fetch
async function A() {
  const user = await fetch('/api/user/1').then(r => r.json())
  return ...
}
async function B() {
  const user = await fetch('/api/user/1').then(r => r.json())  // 캐시됨
  return ...
}

React 18의 cache() API로도 가능.

네트워크 워터폴 방지

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

순차는 금물. 독립 데이터는 항상 병렬.


6. Server Actions — fetch의 종말?

기본 사용

// actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  await db.posts.insert({ title })
  revalidatePath('/blog')
}

// page.tsx
import { createPost } from './actions'

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Create</button>
    </form>
  )
}

JavaScript가 비활성화되어도 동작 (form이 서버로 POST). Progressive Enhancement!

useActionState — 에러와 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 — 즉각 피드백

'use client'

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

async function send(formData) {
  addOptimistic({ text: formData.get('text') })
  await sendMessage(formData)
}

사용자 관점에서는 즉시 반영, 실제 저장은 뒤에서. Twitter/WhatsApp UX.

Server Actions vs API Routes

  • API Route: REST 엔드포인트, 외부에서도 접근
  • Server Action: 내부 컴포넌트에서만, 자동 보안 토큰, CSRF 방어

내부 뮤테이션은 Server Action, 외부 API는 Route Handler.


7. 캐싱의 4계층 — 가장 헷갈리는 부분

Next.js의 캐시 모델은 유명할 정도로 복잡하다.

1. Request Memoization (요청 중복 제거)

  • 같은 렌더 내에서 같은 fetch = 1번
  • React 제공, Next.js 확장

2. Data Cache

  • fetch()가 기본으로 영속 캐시 (Vercel 환경)
  • { cache: 'no-store' } 또는 { next: { revalidate: 60 } }로 제어
  • 2024년 Next.js 15부터 기본이 no-store로 변경 — 혼란 회피

3. Full Route Cache

  • 정적 라우트의 HTML + RSC payload 전체 캐시
  • 빌드 시 또는 ISR로 생성

4. Router Cache (클라이언트)

  • 브라우저에서 방문한 라우트의 RSC payload 메모리 캐시
  • 뒤로가기/페이지 전환 즉시
  • 수동 무효화: router.refresh()

무효화

  • revalidatePath('/blog') — 특정 경로
  • revalidateTag('posts') — 태그 단위
  • 재방문해도 stale-while-revalidate 동작

2024-2025 간소화 시도

Next.js 15의 "stable cache semantics":

  • fetch 기본 캐시 → opt-in으로 변경 (export const fetchCache = 'default-cache')
  • dynamicIO flag — 데이터 페치를 명시적으로 정적/동적 표시
  • 'use cache' 지시어 (실험적) — 함수 단위 캐시 선언

8. Streaming과 Suspense

스트리밍의 개념

서버가 HTML/RSC를 완성된 후 한 번에 보내는 게 아니라, 준비되는 대로 청크로 보냄.

자동 Suspense

// page.tsx
export default function Page() {
  return (
    <>
      <Header />
      <SlowComponent />  // DB 느림
      <Footer />
    </>
  )
}

loading.tsx가 있으면 자동으로:

<Suspense fallback={<Loading />}>
  <Page />
</Suspense>

→ Header는 즉시 렌더, SlowComponent 기다리는 동안 fallback 표시, 준비되면 교체.

수동 Suspense 경계

export default function Page() {
  return (
    <>
      <Header />
      <Suspense fallback={<ProductSkeleton />}>
        <SlowProducts />
      </Suspense>
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />
      </Suspense>
      <Footer />
    </>
  )
}

두 개가 독립적으로 로드. 빨리 끝나는 게 먼저 나옴.

스트리밍 HTML의 세부

  • <html><head>... 먼저 전송
  • 각 Suspense boundary가 resolved되면 <script>로 해당 HTML 주입
  • hydration도 순차적(Selective Hydration)

TTFB vs FMP 트레이드오프

  • 전통 SSR: 모두 기다린 후 전송 (TTFB 느림, FMP도 늦음)
  • 스트리밍: 즉시 시작 (TTFB 빠름, FMP 빠름, 일부는 나중)

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

문제

  • 전부 정적: 빠름, but 동적 데이터 못 씀
  • 전부 동적: 느림, 캐시 못 씀
  • 현실: 일부는 정적, 일부는 동적

해결 — 하나의 페이지에 혼합

export const experimental_ppr = true

export default function Page() {
  return (
    <>
      <Header />  {/* 정적 */}
      <Hero />    {/* 정적 */}
      <Suspense fallback={<Skeleton />}>
        <DynamicCart />  {/* 동적, 매 요청마다 */}
      </Suspense>
    </>
  )
}

작동 원리

  1. 빌드 시 정적 부분을 미리 렌더 → shell
  2. 요청 시 동적 부분만 렌더 → streaming으로 결합
  3. 결과: 정적 속도 + 동적 유연성

현재 (2025)

  • Next.js 15에서 experimental
  • Vercel에서 production 사용 중
  • 2026년 안정화 예상

10. React Server Components vs SolidStart vs Qwik

SolidStart

  • Solid.js 기반 (React보다 빠름, reactive 원시 타입)
  • "islands" 모델 + RSC-like 서버 함수
  • Vite 기반, 덜 복잡

Qwik City

  • Resumability — hydration 없음
  • HTML에 상태를 직렬화, 인터랙션 시 해당 코드만 다운로드
  • 초기 JS: 1KB 미만 가능
  • "instant app" 철학

TanStack Start

  • Tanner Linsley(React Query) 주도
  • Vite 기반, TypeScript-first
  • 2025년 beta, RSC 지원 계획

Remix (이제 React Router v7)

  • "Use the Platform" — 웹 표준 사랑
  • 2024년 말 React Router v7로 통합
  • RSC 지원 2025년 도입

선택 기준

조건추천
팀이 React 익숙, 대규모Next.js (RSC)
성능 극한, 작은 앱Qwik
Solid.js 선호SolidStart
타입 안전성 최우선TanStack Start
Use the Platform 철학React Router v7

11. 실제 마이그레이션 — Pages → App Router

점진적 이행

같은 Next.js 프로젝트에 pages/app/을 공존 가능.

app/        # 새 라우트 App Router
  about/page.tsx
pages/      # 기존 라우트 유지
  index.tsx
  api/...

이행 순서

  1. 새 기능부터 app/
  2. 기존 API routes는 그대로 (route.ts로 옮기든 말든)
  3. 페이지별로 점진 이행 (큰 페이지부터 or 간단한 것부터)
  4. _app.tsxapp/layout.tsx로 통합

함정

  • CSS 충돌 — Pages Router의 styles/globals.css 이중 로드
  • middleware — 공유되지만 일부 API 차이
  • Image 컴포넌트next/image는 동일
  • useRouternext/navigation으로 변경 필수

12. 흔한 실수 TOP 10

  1. 모든 곳에 'use client' — RSC 이점 사라짐
  2. Server Component에 useState — 안 됨, TypeScript가 막아줌
  3. Client Component에서 DB 직접 접근 — 보안 사고, 자동 차단
  4. window를 Server Component에서 — 존재 안 함
  5. Large fetch를 매 요청마다 — 캐시 전략 필수
  6. 중첩 Suspense 없음 — 전체가 같이 로딩
  7. 네트워크 워터폴await 순차
  8. Server Action에서 큰 객체 전달 — 직렬화 비용
  9. revalidatePath 누락 — mutation 후 UI 안 바뀜
  10. 클라이언트 상태를 Server Component에 의존 — 불가능

13. App Router 체크리스트

  • 기본은 Server Component'use client'는 필요할 때만
  • layout의 무리한 재사용 — 리렌더 안 됨 확인
  • Suspense boundary — 느린 부분 독립 격리
  • loading.tsx — 페이지별 로딩 UI
  • error.tsx — 페이지별 에러 경계
  • 병렬 데이터 페칭Promise.all 활용
  • Server Action 검증 — zod로 입력 검증
  • revalidate 전략 — path/tag 기반
  • Dynamic/Static 명시export const dynamic = 'force-static'
  • TypeScript strict — 타입 유지
  • Turbopack devnext dev --turbo
  • bundle analysis@next/bundle-analyzer

마치며 — "서버는 다시 중요해졌다"

RSC는 단순한 최적화가 아니다. "프론트엔드와 백엔드의 경계를 컴포넌트 레벨까지 끌어내린" 패러다임 변화다. 10년 전 "SPA가 미래"라고 외쳤던 업계가, 이제는 "서버가 다시 중요하다"고 말한다. 그러나 이것은 과거로의 회귀가 아니라 새로운 합성이다 — 서버의 DB 접근 편의성 + 클라이언트의 상호작용성.

React 팀이 말한 "Use the Platform" 철학은 웹 표준(form, HTML 스트리밍, progressive enhancement)을 React에 녹여넣은 것이다. 2030년의 React는 지금과 많이 다를 것이다. 하지만 그 방향은 이미 보인다: 작은 클라이언트 번들, 빠른 첫 화면, 서버에서 해결할 수 있는 것은 서버에서, 복잡성은 프레임워크로.


다음 글 예고 — TypeScript 타입 시스템의 깊이 — 2026년의 표준

RSC가 런타임의 혁명이었다면, TypeScript는 지난 10년간의 언어 혁명이다. 다음 글에서는:

  • TypeScript의 철학 — Gradual typing의 성공 공식
  • 구조적 타이핑 — nominal과의 대비
  • Generic의 깊이 — Conditional, Mapped, Template Literal Types
  • satisfies vs as — 2022년의 작은 혁명
  • 타입 수준 프로그래밍 — Type Challenges의 세계
  • 모듈 해석의 복잡성 — NodeNext, ESM/CJS interop
  • tsc의 한계와 대안 — SWC, esbuild, Bun
  • TypeScript Go 포팅 (2025) — Anders Hejlsberg의 10배 빠른 컴파일러
  • Zod, ArkType, Valibot — 런타임 검증의 진화
  • Effect-TS — 함수형 타입 시스템의 프런티어
  • tRPC와 end-to-end 타입 안전성

개발자 도구의 가장 중요한 혁명을 정리하는 여정.


"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 (React core, RSC architect)

현재 단락 (1/331)

2020년 12월 21일, Dan Abramov와 Lauren Tan이 "React Server Components" 발표 영상을 올렸다. 당시 대부분의 개발자는 "또 새로운 걸 ...

작성 글자: 0원문 글자: 10,215작성 단락: 0/331