카테고리 없음

React vol.17 (React Server Components)

곽빵 2023. 10. 2. 22:24

개요

예전부터 이해가 어려웠던 RSC에 대하여 인프런의 한 입 크기로 잘라먹는 Next.js 강의를 보고 어느정도 RSC가 무엇이며 어떻게 활용해야 하는지에 대해 알게 되었으므로 글로써 남기고자 한다.

 

RSC의 등장배경

User가 Nextjs로 만든 웹 어플리케이션에 접근할 때 SSR이 어떻게 이루어지며 Hydration이 언제 발생하는지에 대한 시퀀스 다이어그램이다.

(여기서 Hydration이란 SSR로 만들어진 정적 HTML에 클라이언트 사이드의 Javascript가 다시 활성화되는 과정을 말한다. 즉, 서버에서 생선된 정적 HTML이 브라우저에 로드된 후, 해당 HTML에 Javascript를 삽입시켜 동적인 인터랙티브가 가능하게 만드는 과정이다.)

 

여기서 RSC의 등장배경이 되는 문제를 찾아볼 수 있다. 위의 ⑤는 하이드레이션을 위해 JS Bundle을 다운로드 하는데 JS Bundle 파일을 보면 하이드레이션이 필요한 컴포넌트뿐만 아니라 필요없는 컴포넌트들도 포함이 되어 있기 때문에 JS Bundle 파일의 사이즈가 커져서 화면의 상호작용(ex. 버튼 클릭)이 가능해 질때까지의 시간이 더욱 걸리게 된다. 이를 해결하기 위해 나온게 리액트의 서버 컴포넌트이다.

 

RSC(React Server Component)란?

서버에서만 렌더링되고, 클라이언트로는 직렬화된 UI 결과만 전달되는 컴포넌트이다.

  • 서버 전용이므로 브라우저 API나 useState, useEffect같은 훅을 사용할 수 없다.
  • 오로지 서버에서만 실행되므로 데이터베이스 요청, 파일 시스템 접근, 비동기 로직등을 직접 실행해도 문제없다.
  • Zero-bundle-size 컴포넌트
    • 최종 결과물은 문자열 혹은 RSC Payload(직렬화된 데이터)로 클라이언트에게 전달 -> 클라이언트에서는 별도의 JS 번들 파일 없이 UI를 그릴수 있다.

RSC를 어떻게 활용할까?

1. Server Component와 Client Components의 구분과 용도

  • Server Component
    • 파일 상단에 "use server" 지시자가 없더라도, Next.js 13+ App Router에서는 디폴트가 서버 컴포넌트다.
    • 브라우저 API, useState, useEffect, 이벤트 핸들링 등은 불가.
    • 데이터베이스나 서버 자원을 사용하는 로직을 자유롭게 구현한다.
    • 컴포넌트 함수 자체에 async await를 넣어서 비동기 함수로 사용할 수 있는데 이는 서버 컴포넌트 Node.js 런타임 환경에서 실행이 되기 때문에 비동기 로직을 처리한 뒤 결과를 jsx의 형태로 리턴할 수 있다.
  • Client Component
    • Client라고 적었다고 오로지 Client(Browser)에서만 실행되는 컴포넌트가 아니고 서버에서도 실행이 된다. Client Component라는 명칭이 새롭게 생겼을 뿐이지 그냥 Page Route일때 사용되었던 모든 컴포넌트가 Client Component이다.
    • 파일 상단에 "use client" 지시자를 선언해야 한다.
    • DOM, 브라우저 API, Hooks(useState, useEffect) 사용 가능.
    • 컴포넌트 함수 자체에 async await를 사용할 수 없다. 클라이언트(브라우저)에서는 React는 즉시 JSX를 필요로하므로 (렌더링은 동기적으로 실행되어야 한다.) 함수 전체를 비동기로 만들 수 없다.
    • 이 컴포넌트는 빌드 시 클라이언트용 번들을 생성하므로, 번들 사이즈 최적화를 위해 꼭 필요한 로직만 Client Component로 만든다.

2. App Router(Next.js 13+)에서 서버, 클라이언트 컴포넌트의 예시

// app/page.tsx (Server Component by default)
export default async function Page() {
  const data = await getDataFromDB(); // 서버 전용 로직 가능
  return (
    <div>
      <h1>Server Component Example</h1>
      <MyClientComponent />
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

// app/MyClientComponent.tsx
"use client"; // 클라이언트 전용
import { useState } from 'react';

export default function MyClientComponent() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c+1)}>
      Count is {count}
    </button>
  );
}
  • MyClientComponent는 브라우저 상호작용이 필요하므로 "use client"를 선언한다.
  • Page는 디폴트로 Server Component이므로 DB 접근 등을 직접 해도 된다.

3. 주의점

  • 서버 컴포넌트 내부에서 클라이언트 컴포넌트를 호출 가능하지만 반대의 경우(클라이언트 컴포넌트 안에서 서버 컴포넌트 호출)는 서버 컴포넌트가 자동적으로 클라이언트 컴포넌트로 변경된다. 하지만 이하와 같이 서버 컴포넌트를 children으로 전달하게 되면 서버 컴포넌트를 유지한채로 렌더링된다.
// app/page.tsx
import ClientComponent from './ClientComponent';
import ServerComponent from './ServerComponent';

export default function Home() {
  return (
    <div className={styles.page}>
      인덱스 페이지
      <ClientComponent>
        <ServerComponent />
      </ClientComponent>
    </div>
  );
}
"use client";

export default function ClientComponent({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <h2>Client Component</h2>
      {children}
    </div>
  );
}
export default function ServerComponent() {
  return <p>Server Component</p>;
}
  • 상태/이벤트 로직 분리
    • 서버 로직(데이터 패칭, 보안 로직) vs. 클라이언트 로직(상호작용, 브라우저 API)을 분리하는 것이 좋다.
  • 문법 제약
    • Server Component 안에서는 브라우저와 관련된 API(window, document, localStorage 등)를 사용할 수 없다.
    • useState나 useEffect 등 Hook도 사용할 수 없다.
  • 서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화 되지 않는 Props는 전달 불가하다.
    • 사전 렌더링 과정중에 서버 컴포넌트들만 따로 실행되는 단계가 있는데 이때 서버 컴포넌트의 모든 내용을 직렬화해서 RSC Payload라는 형태로 보관하게 된다. 이때 클라이언트 컴포넌트에게 전달되는 Props 또한 직렬화를 하는데 이 Props중에 함수와 같은 직렬화가 불가능한(외부 컨텍스트나 실행환경에 영향을 받아서 불가능)것이 포함되어 있다면 런타임 에러가 발생한다.
  • 초기 접속(SSR로 동작)의 경우에는 서버 컴포넌트가 제외되서 작아진 JS 번들 파일만을 전달 하지만 초기 접속 이후(CSR로 동작)의 경우에는 JS 번들 파일(클라이언트 컴포넌트) + RSC Payload(서버 컴포넌트)를 같이 전달한다. 

생각되어지는 장단점

  • 번들 사이즈 감소 & 초기 로드 성능 개선
    • 불필요한 로직이 클라이언트에 전달되지 않아, JS 번들이 작아진다.
    • 이는 사용자 체감 성능(FCP)이 개선
  • 보안성
    • 민감한 비즈니스 로직, DB Queries, API Keys 등을 서버 컴포넌트에서만 처리하면, 클라이언트로 노출되지 않아 안전하다.
  • UI/로직 분할
    • “서버에서 처리할” vs. “클라이언트에서 처리할” 컴포넌트를 명시적으로 구분해, 코드 가독성과 책임 범위가 명확해진다.
  • 개발자 경험
    • SSR, SSG, CSR, ISR 등 복잡했던 ‘데이터 패칭 로직’을 Server/Client 컴포넌트로 나누어 직관적으로 관리 가능.
    • 하지만 초기에는 “서버/클라이언트 컴포넌트가 뭘 가능/불가능 한지”를 익히는 학습 곡선이 존재.

결론

머리 아프다.

 

 

참고한 문서들

 

Rendering: Server Components | Next.js

Learn how you can use React Server Components to render parts of your application on the server.

nextjs.org

 

Understanding React Server Components - Vercel

React Server Components are changing the fundamental paradigms of React. Learn how Next.js handles the complexities and improves the performance of your applications.

vercel.com

 

Next) 서버 컴포넌트(React Server Component)에 대한 고찰

이번에 회사에서 신규 웹 프로젝트를 진행하기로 결정했는데, 정말 뜬금 없게도 앱 개발자인 내가 이 프로젝트를 리드하게 되었다. 사실 억지로 떠맡게 된 것은 아니고, 새로운 웹 기술 스택을

velog.io