React Server 구성 요소 – 정신 모델 전환

작성자

카테고리:

← 피드로
DEV Community · DimiDan · 2026-07-20 개발(SW)

React Server Components are not a performance trick. They are not a simpler way to do SSR. They represent a genuine renegotiation of where computation lives in a React application — and if you approach them as an incremental upgrade, you will architect yourself into corners that are painful to escape.

After working through RSC adoption in production Next.js applications, I want to lay out what actually changes at the architectural level: data fetching patterns, bundle composition, state management boundaries, and how teams need to think about component ownership.

How Shift did happened

Before RSC, the React component tree was a client-side concept. Server rendering was a serialization step — you ran the same component code on the server, emitted HTML, then React rehydrated it on the client. The component tree was fundamentally one thing that happened to run in two places.

RSC breaks this. The component tree now has two genuinely distinct zones with different capabilities, different execution environments, and a one-way data boundary between them. Server Components run exclusively on the server. They can access databases, filesystems, and secrets directly. They never ship to the browser. Client Components are the subset of your tree that runs in the browser — they receive serialized props from the server boundary and manage their own state and effects.

The key constraint is that this boundary is not symmetric. You can compose Client Components inside Server Components by passing them as props (including children). You cannot import a Client Component into a Server Component and expect it to remain a Client Component — it gets treated as a Server Component unless explicitly marked with 'use client'. And you cannot import a Server Component into a Client Component at all.

This asymmetry has real architectural consequences.

Data Fetching: Colocation at Scale

The most immediate change is to data fetching strategy. In a pre-RSC Next.js app, data fetching was pushed to page boundaries — getServerSideProps, getStaticProps, or client-side fetches with TanStack Query. Components deep in the tree couldn’t fetch their own data without either prop-drilling from a page or reaching for a client-side solution.

RSC removes that constraint. A deeply nested Server Component can fetch exactly the data it needs:

// app/dashboard/RecentActivity.tsx
// No 'use client' directive — this is a Server Component

import { getRecentActivity } from '@/lib/db/activity';

interface Props {
  userId: string;
}

export async function RecentActivity({ userId }: Props) {
  const activity = await getRecentActivity(userId);

  return (
    <ul>
      {activity.map((item) => (
        <li key={item.id}>
          <span>{item.label}</span>
          <time dateTime={item.timestamp}>{formatRelative(item.timestamp)}</time>
        </li>
      ))}
    </ul>
  );
}

Enter fullscreen mode Exit fullscreen mode

This is genuinely useful. Components declare their own data dependencies. There is no prop-drilling waterfall through parent components that don’t need the data. React can parallelize sibling server fetches.

But colocation at this level has an architectural cost: it distributes data access logic across the component tree. In large codebases, this creates pressure toward duplicated query logic, inconsistent caching behavior, and data layer concerns bleeding into rendering concerns. The discipline that kept data fetching at page or feature boundaries was a forcing function for clean separation.

The practical answer is a service or repository layer that Server Components call into — not raw database queries inside JSX files. This is not a new idea, but RSC makes it newly necessary to enforce.

Bundle Optimization Is Now Structural

The bundle benefits of RSC are real but understated. It is not just that dependencies don’t ship to the client. The server/client boundary is the module graph split point.

Consider a rich-text editor scenario. You have a document viewer that renders stored content, and a document editor that allows mutation. Pre-RSC, both likely live in the client bundle because rendering document content requires the same parser/renderer used for editing. With RSC, the viewer becomes a Server Component that runs the parser on the server and emits HTML. The editor, which genuinely needs client interactivity, is a Client Component. The parser library ships to zero browsers for the view-only case.

This pattern — server for read paths, client boundary for write and interactive paths — maps cleanly onto a lot of B2B SaaS product structures. Dashboards, reports, document feeds: mostly reads. Editors, forms, collaborative tools: require client boundaries.

What changes architecturally is that bundle composition is now a first-class design decision, not a post-hoc optimization. When you create a component, you are deciding which execution environment it belongs to. That decision needs to be visible in your component design conventions, code review checklists, and module organization.

State Management Boundaries

State management under RSC requires explicit rethinking, not just adaptation.

The instinct to reach for Zustand or Redux for shared state runs into a hard constraint: stores initialize and live in the Client Component subtree. Server Components cannot access them. This is correct behavior — but it means you need a clear model for what kind of state lives where.

URL state — search params, filters, pagination — becomes a first-class citizen. Server Components can read URL parameters directly and use them to parameterize data fetches. This moves a lot of state that previously lived in client stores back to the URL, which improves shareability and reduces client-side complexity.

// app/invoices/page.tsx
import { InvoiceList } from './InvoiceList';

interface SearchParams {
  status?: 'paid' | 'pending' | 'overdue';
  page?: string;
}

export default async function InvoicesPage({
  searchParams,
}: {
  searchParams: SearchParams;
}) {
  const status = searchParams.status ?? 'pending';
  const page = Number(searchParams.page ?? '1');

  return <InvoiceList status={status} page={page} />;
}

Enter fullscreen mode Exit fullscreen mode

Server-owned state — user session, feature flags, tenant configuration — can be fetched once in a root Server Component and passed down as props, or accessed directly in any Server Component that needs it, without a context provider.

Client state remains appropriate for: ephemeral UI state (hover, focus, open/closed), optimistic updates, real-time collaboration state (Yjs documents, presence), and anything that responds to user input before a server round-trip.

The architectural mistake to avoid is treating the client state layer as a cache for server data. TanStack Query handles this well, but under RSC the primary data loading path moves to the server. Client-side query caches are better scoped to mutations, optimistic updates, and cases where you genuinely need background refetching without a navigation event.

The Client Boundary as an API Contract

Every 'use client' directive is an interface declaration. The props a Client Component accepts are serializable data flowing from the server domain into the client domain. This is a meaningful constraint: you cannot pass functions (unless they are Server Actions), class instances, or non-serializable objects across this boundary.

Architecturally, this forces clarity. When you design a Client Component, you are defining what the server needs to provide — the same problem as API design. What is the contract, and who owns the schema?

For complex components, this pushes toward explicit prop types that mirror your data model, rather than passing rich objects and picking fields inside the component:

// Clear boundary contract — serializable, explicit
interface DocumentCardProps {
  id: string;
  title: string;
  status: 'draft' | 'published' | 'archived';
  lastEditedAt: string; // ISO string, not Date — must be serializable
  collaboratorCount: number;
}

'use client';

export function DocumentCard({
  id,
  title,
  status,
  lastEditedAt,
  collaboratorCount,
}: DocumentCardProps) {
  // interactive behavior, local state, event handlers
}

Enter fullscreen mode Exit fullscreen mode

This is good discipline regardless of RSC. But RSC turns violations into runtime errors rather than code smell.

Team and Collaboration Implications

At the team level, RSC introduces a new axis of component ownership: server versus client. In organizations where backend and frontend responsibilities overlap — product teams owning full features end-to-end — this is largely positive. Engineers can write data access directly without an API contract mediation step.

In organizations with stronger frontend/backend splits, it creates friction. Server Components that access databases or internal services blur the ownership model. You need explicit team agreements about what Server Components are allowed to call and where the service boundary sits.

RFC-driven engineering practices help here. Before RSC adoption, it is worth writing down:

  • Which data sources Server Components may access directly versus through an API layer
  • Conventions for when client boundaries should be introduced (interactive features, real-time state, third-party SDKs)
  • How Server Actions fit into your mutation story alongside existing API routes
  • Testing strategy — Server Components require different test infrastructure than Client Components

What This Is Not

RSC does not eliminate the need for a client-side data fetching library. TanStack Query remains the right tool for mutation state, optimistic updates, polling, and any data that needs to stay fresh without a navigation event.

RSC does not make SSR obsolete. Streaming SSR and RSC are complementary. Suspense boundaries work across both.

RSC does not simplify everything. The dual execution model adds cognitive overhead. Developers need to reason about which environment their code runs in, what is available there, and where the serialization boundary sits. That cost is real and needs to be weighed against the benefits for your specific application.

Where It Lands

RSC is the most significant change to React’s execution model since hooks. It draws a clear line through the component tree and assigns different capabilities to each side. The teams that will benefit most are those building data-heavy, read-path-dominant applications — dashboards, document tools, B2B reporting surfaces — where the separation between “fetch and render” and “interact and mutate” maps cleanly to the server/client split.

The architectural work is not in learning the API. It is in auditing your existing component and data fetching patterns, defining clear conventions for boundary placement, and communicating those conventions to your team before the codebase drifts into inconsistency.

Done deliberately, RSC moves meaningful computation server-side in a way that is maintainable at scale. Done reactively, it produces a confusing mix of fetching strategies, inconsistent boundary placement, and components that are harder to reason about than what you had before.

The paradigm is sound. The architectural investment is on you.

원문에서 계속 ↗

추출 본문 · 출처: dev.to · https://dev.to/dimidan/rsc-the-mental-model-shift-5efg

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다