반응형
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

짧은코딩

React Server Components 독후감 본문

개발 독후감

React Server Components 독후감

5_hyun 2025. 11. 25. 23:04
반응형

RSC(React Server Components)

https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md

 

rfcs/text/0188-server-components.md at main · reactjs/rfcs

RFCs for changes to React. Contribute to reactjs/rfcs development by creating an account on GitHub.

github.com

RSC가 탄생하게 된 문서를 읽어보자.

RSC가 필요했던 이유

CSR은 UX와 성능 사이에서 트래이드오프가 항상 존재했다.

  • 컴포넌트가 렌더링 되고 데이터를 요청하는 패턴(useEffect)가 반복되면, 데이터 로딩이 지연된다
  • npm 라이브러리를 계속 설치하다보면 번들 사이즈가 커지게된다.
  • 성능을 위해 데이터를 최상위에서 한번에 가져오면, 하위 컴포넌트의 데이터 의존성이 상위 컴포넌트에 강하게 결합되어 유지보수가 힘들어진다.

해결책

"컴포넌트 트리의 일부는 서버에서 실행하고, 일부는 브라우저에서 실행하자"가 해결책이었다.

  • Server Component: 데이터 로딩, DB 접근, 무거운 연산 담당(브라우저로 코드 전송 X)
  • Client Component: 클릭, 입력, 상태 관리 같은 인터랙션 담당(브라우저로 코드 전송 O)

RSC 동작 원리

  1. 직렬화와 제약 사항
    • 직렬화는 문자열 데이터 변환이 가능한 데이터를 의미한다.
    • 따라서 JSON, JSX Element는 가능하다.
    • 하지만 함수, 클래스 인스턴스, DOM API는 불가능하다.
  2. HTML이 아닌 '데이터 스트림' 전송 
  • RSC는 HTML을 주고 받지 않는다. HTML을 주고 받는 것은 SSR이다
  • RSC는 JSON과 유사한 데이터 스트림을 보낸다.
// 개념적 예시 (실제 프로토콜은 더 복잡함)
{
  "id": "root",
  "children": [
    { "type": "div", "children": "서버에서 렌더링된 텍스트" },
    { "type": "ClientComponent", "props": { "data": "..." }, "path": "chunk.js" }
  ]
}

 

3. 상태 유지

  • 데이터 스트림 방식으로 인해 얻는 장점이다.
  • HTML을 받아오는 것이 아니기 때문에 새로운 데이터를 서버에서 받아와도 전체를 수정하지 않고, 수정이 필요한 컴포넌트만 머지해서 클라이언트 컴포넌트가 갖고 있던(input 포커스, 스크롤 위치, 입력 중인 텍스트 등)은 유지된다.

예시

-1번 API 없이 DB 조회

// app/users/page.tsx (기본적으로 Server Component)
import { db } from '@/lib/db'; // Prisma나 ORM 가정

// 1. 컴포넌트 자체가 async 함수입니다.
export default async function UsersPage() {
  // 2. 별도의 API 호출 없이 바로 DB를 찌릅니다.
  // 이 코드는 서버에서만 돌기 때문에 DB URL 등이 브라우저에 노출되지 않습니다.
  const users = await db.user.findMany({
    where: { isActive: true },
    orderBy: { createdAt: 'desc' }
  });

  return (
    <main className="p-10">
      <h1 className="text-2xl font-bold mb-4">활성 유저 목록 ({users.length}명)</h1>
      
      <ul className="space-y-2">
        {users.map((user) => (
          // 3. 렌더링 된 결과(HTML)만 클라이언트로 전송됩니다.
          <li key={user.id} className="border p-2 rounded">
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
    </main>
  );
}
  • useState, useEffect가 아예 없다
  • 따라서 데이터 로딩 상태 관리도 필요 없다

 

-2번 Client Component로 데이터 전달 

// app/post/[id]/page.tsx
import { db } from '@/lib/db';
import LikeButton from '@/components/LikeButton';

export default async function PostDetail({ params }: { params: { id: string } }) {
  // 1. 서버에서 데이터 로딩
  const post = await db.post.findUnique({ where: { id: params.id } });

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      
      {/* 2. 데이터를 Props로 넘겨줌 (직렬화 가능한 값만!) */}
      {/* 함수(onClick 핸들러 등)는 여기서 만들어서 넘길 수 없음 */}
      <LikeButton initialCount={post.likeCount} postId={post.id} />
    </article>
  );
}
  • LinkButton은 브라우저로 전송
  • 로직과 DB 코드는 서버에 남음 

-3번 스트리밍 방식

// app/dashboard/page.tsx
import { Suspense } from 'react';
import RevenueChart from '@/components/RevenueChart'; // 서버 컴포넌트라고 가정
import UserList from '@/components/UserList';         // 서버 컴포넌트라고 가정

export default function Dashboard() {
  return (
    <main>
      <h1>대시보드</h1>
      
      {/* 1. 사용자 목록은 빠르니까 그냥 기다림 */}
      <div className="mb-8">
        <UserList />
      </div>

      {/* 2. 매출 차트는 3초 걸림 -> 로딩 걸어두고 페이지 먼저 보여줌 */}
      <section className="h-64 bg-gray-50">
        <Suspense fallback={<div className="text-gray-400">차트 분석 중... ⏳</div>}>
          {/* RevenueChart 안에서 async/await로 DB 조회 중 */}
          <RevenueChart /> 
        </Suspense>
      </section>
    </main>
  );
}
  • 무거운 데이터가 전체 페이지 로딩을 막지 않게 하는 패턴
  • RevenueChart가 데이터를 다 가져올 때까지 기다리지 않고, 즉시 fallback을 포함한 HTML을 먼저 내려줌
  • 그리고 데이터 준비가 완료되면 해당 부분만 갈아끼우는 방식
  • 따라서 Suspense, fallback도 RSC라고 볼 수 있다.
반응형
Comments