[Nextjs] Server Component (서버 컴포넌트)

김채운·2024년 1월 12일
4

Next.js

목록 보기
4/4

서버 컴포넌트는 애플리케이션의 서버 부분에서 렌더링되는 컴포넌트이다.

React에서는 2020년 12월에 React Server Component를 소개했다. 즉, RSC는 react팀이 설계한 새로운 어플리케이션 아키텍처이고 React 18v 이후에 이것을 nextjs에서도 사용할 수 있도록 업데이트한 것이다.

그래서 Next13이전의 버전에서는 페이지 단위로 렌더링 되어서 한 가지 렌더링 방식으로만 작동했다면, 이제는 한 페이지 안에서 서버 컴포넌트와 클라이언트 컴포넌트를 같이 사용해 데이터 특성에 따라 다양한 방식으로 렌더링 할 수 있게 컴포넌트 단위로 렌더링 방식을 규정하여서 효율적으로 웹페이지를 구성할 수 있게 했다.

React 18부터 Server Component가 도입되었고, Next.js 13은 이를 완벽하게 지원한다.
이에 따라 Next.js 13 부터는 '서버 컴포넌트', '클라이언트 컴포넌트' 이 두가지 형식으로 컴포넌트를 만들 수 있다.

➡️ Server Component?

React Server Components는 UI를 서버에서 렌더링하고 필요한 경우 선택적으로 캐시할 수 있다. 이는 웹 애플리케이션의 성능 및 사용자 경험을 향상시키는 데 도움이 된다.
Next.js에서는 렌더링 작업이 경로 세그먼트에 따라 더 나눠져 스트리밍 및 부분 렌더링을 가능하게 하며, 세 가지 서버 렌더링 전략이 있다.

정적 렌더링(Static Rendering)

  • 전에 렌더링된 정적 페이지를 제공하며, 이는 속도와 성능 면에서 이점을 제공한다.

동적 렌더링(Dynamic Rendering)

  • 요청 시에 동적으로 페이지를 렌더링하여 사용자에게 최신 콘텐츠를 제공한다.

스트리밍(Streaming)

  • 렌더링이 진행되는 동안 서버가 데이터를 조금씩 클라이언트로 전송하여 페이지를 조금씩 표시할 수 있게 한다.

🔖 Server Component 특징

  • Nextjs 프로젝트를 생성하면 생기는 /app디렉토리 내에 있는 컴포넌트들은 기본적으로 다 서버 컴포넌트가 된다.
  • 이 서버 컴포넌트는 서버에서 동작하기 때문에 서버 컴포넌트 내에서는 브라우저 환경의 코드나 제공해주는 API는 사용할 수 없다. ex) Event Listener(onClick같은..), React Hooks(useState, useEffect ...) 대신 Node환경에서 제공해주는 Node API를 사용할 수 있다.
  • Server Component는 Client Compoent 포함이 가능하다(반대는 불가)

🔖 Server Component 장점

Data Fetching (데이터 가져오기)

  • RSC는 그 자체가 서버에서 렌더링 되므로, 데이터를 가져오는 작업도 서버에서 수행된다. 그렇기 때문에 data자원에 더 가까워져 렌더링에 필요한 data를 가져오는데 걸리는 시간과 client가 요청해야 하는 횟수를 줄일 수 있다.(네트워크 오버헤드를 최소화할 수 있다)

Security (보안)

  • 서버 컴포넌트를 통해 서버에 민감한 데이터 및 로직을 유지할 수 있다. 예를 들어, 토큰이나 API 키와 같은 정보를 클라이언트에 노출하지 않고 서버에서 안전하게 처리할 수 있다.

Caching (캐싱)

  • 서버에서 렌더링하면 결과를 캐시하고 후속 요청 및 사용자 간에 재사용할 수 있다. 이는 각 요청마다 수행되는 렌더링 및 데이터 가져오기 양을 줄이므로 성능을 향상시키고 비용을 절감할 수 있다.

Bundle Sizes (번들 크기)

  • 서버 컴포넌트를 사용하면 클라이언트 자바스크립트 번들 크기에 영향을 미치지 않도록 서버에 큰 종속성을 유지할 수 있다 이 말뜻은, 웹 애플리케이션의 클라리언트 자바스크립트 번들은 애플리케이션의 모든 코드를 포함하는 파일이다. 이 번들이 크면 사용자가 앱을 로딩하는 데 많은 시간이 걸릴 수 있다. 하지만 서버 컴포넌트를 사용하면 클라이언트에 필요한 모든 코드를 포함하는 대신, 일부 코드는 서버에서 유지된다. (특히 서버에서 처리해야 하는 복잡한 로직이나 데이터를 가져오는 작업 등이 이에 해당함) 그래서 서버에서 유지되는 코드는 클라이언트에게 필요하지 않을 경우, 클라이언트는 해당 코드를 다운로드, 구문 분석, 실행할 필요가 없게 되는데 이는 서버 컴포넌트를 사용함으로서 클라이언트가 로딩하는 데 필요한 자바스크립트 코드의 양을 줄일 수 있는 장점이 된다. 이런 장점은 느린 인터넷이나 성능이 낮은 기기를 사용하는 사용자에게 이점이 되고, Next의 TTI(Time To Interactive) 개선에 크게 기여할 수 있다. Next에서는 SSR을 사용한다고 하더라도 초기 로딩속도에 이점이 있을 뿐 CSR과 동일한 사이즈의 자바스크립트 번들을 다운받아야 하기 때문에 TTI는 여전히 CSR대비 큰 이점이 없었기 때문이다. 하지만 RSC를 도입하면서 다운받아야 하는 번들 사이즈가 줄어들어 TTI개선에도 큰 도움이 됐다.

Initial Page Load and First Contentful Paint (초기 페이지 로드 및 FCP)

  • 서버에서는 HTML을 생성하여 사용자가 페이지를 즉시 볼 수 있도록 한다. 클라이언트가 페이지를 렌더링하는 데 필요한 JavaScript를 다운로드, 구문 분석 및 실행하기를 기다릴 필요가 없다.

Streaming (스트리밍)

  • 서버 컴포넌트를 사용하면 렌더링 작업을 청크로 나누어 클라이언트로 스트리밍할 수 있다. 이를 통해 사용자는 페이지 전체가 서버에서 렌더링될 때까지 기다릴 필요 없이 일부분을 먼저 볼 수 있다. 이 뜻은, 스크린의 모든 화면정보를 수신할 떄까지 기다릴 필요 없이, 클라이언트는 먼저 수신된 데이터부터 반영해서 화면에 띄워줄 수 있게 된다. 그래서 모든 데이터를 기다릴 필요 없이 먼저 보여줄 수 있는 부분을 로드한 뒤 data fetch가 완료되면 그 결과가 즉각적으로 반영된다.

🔖 Server Component 렌더링 방식

페이지 안에서 어떤 특징을 가지고 있냐에 따라서 '서버 컴포넌트'로 만들 수도 있고, 사용자와의 interaction이 필요한 부분은 '클라이언트 컴포넌트'론 만들 수도 있다. 이런 특징을 이해하고 특징에 맞게 렌더링 방식을 적용하기 위해서는 서버 컴포넌트가 어떻게 동작하는지 이해할 필요가 있다.

1. 요청 및 렌더링 작업 분할

  • 사용자가 페이지를 요청하면 서버에서는 해당 페이지를 렌더링 하기 위해 컴포넌트 트리를 root부터 실행한다. 이때에 nextjs에서는 컴포넌트들을 확인하며 정적으로 만들어줄 부분을 찾아 뼈대를 만든다.
    그리고 전체 페이지가 여러 덩어리(chunk)로 나뉘어 처리가 되는데 이 덩어리가 나뉘어져 실행되는 렌더링 작업은 두 가지 기준으로 나누어진다. route segments와 Suspense Boundaries로 구분된다. 이 말의 뜻은,

✔️ 개별 라우트 세그먼트

  • 페이지 URL의 route 경로를 세그먼트로 나누어 처리한다. 각 세그먼트는 해당 세그먼트에 대응되는 렌더링 작업을 담당한다. 이것은 라우트 경로를 따라 페이지의 일부를 서버에서 미리 렌더링하고 클라이언트로 전송하여 초기 페이지 로드 성능을 최적화하는 데 사용된다.

✔️ Suspense Boundaries(서스펜스 경계)

  • Suspense는 React에서 비동기 작업을 처리하는 패턴 중 하나이며, 서스펜스 경계는 이러한 비동기 작업이 발생하는 영역을 가리킨다. 서버 컴포넌트는 데이터를 비동기적으로 가져오는 경우가 많은데, 이때 서스펜스 경계를 통해 비동기 작업이 일어나는 위치를 정의하고 관리할 수 있다. Suspense는 데이터가 불러와질 때까지 대체 콘텐츠를 보여주는 처리를 지원한다.

이러한 렌더링 작업의 분할은 성능 최적화 페이지의 부분적인 렌더링을 가능하게 하며, 특히 대규모 애플리케이션에서 초기 로딩 속도를 향상시키는 데 도움이 된다. 페이지를 세그먼트로 나누고, 스스펜스 경계를 정의하여 비동기 작업을 관리함으로써 사용자에게 빠르게 페이지를 표시해 보일 수 있다.

❓ Suspense

Suspense는 React에서 비동기적인 작업의 상태를 관리하고, 그 동안 대체 컨텐츠를 보여줄 수 있는 기능을 제공하는 리액트의 컴포넌트이다. 주로 데이터를 불러오거나 코드를 비동기적으로 처리할 때 사용된다.

기본적으로 Suspense는 두 가지 주요 목적을 가지고 있는데,

비동기 작업의 대기 및 처리

  • 데이터 fetching, 코드 스플리팅(Code Splitting)과 같은 비동기 작업이 발생할 때 Suspense는 해당 작업의 완료를 기다리는 역할을 한다. 이때, Suspense로 감싼 컴포넌트 내에서 비동기 작업이 완료되기를 기다리며 다른 컴포넌트나 대체 콘텐츠를 보여줄 수 있다.

대체 콘텐츠 표시

  • Suspense로 감싼 컴포넌트 내에서 비동기 작업이 완료될 때까지 보여줄 대체 콘텐츠를 지정할 수 있다. 이는 사용자에게 로딩 중임을 알리거나, 에러가 발생했을 때 대처하는 등의 상황에서 유용하게 활용된다.

아래의 코드에서 Suspense로 감싼 Profile 컴포넌트는 데이터 fetching이 완료될 때까지 로딩 중인 상태를 보여준다.

import { Suspense } from 'react';

const Profile = React.lazy(() => import('./Profile'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Profile />
      </Suspense>
    </div>
  );
}

Profile 컴포넌트는 React.lazy를 사용하여 비동기적으로 로딩되며, Suspense 컴포넌트로 감싸져 있다. fallback prop은 Suspense가 비동기 작업을 기다리는 동안 로딩 중에 표시할 컴포넌트를 지정한다.

Suspense는 주로 React 코드 스플리팅과 함께 사용되며, 리액트의 동적 임포트나 데이터 fetching 시에 유용하게 활용된다.

2. 서버에서 RSC Payload로 렌더링

  • React는 서버에서 서버 컴포넌트를 렌더링하고, 이를 React Server Component Payload(RSC Payload)라는 특수한 데이터 형식으로 변환한다. RSC Payload에는 서버에서 렌더링된 결과물과 클라이언트에서 필요한 정보가 포함되어 있다. Next.js는 서버에서 생성된 RSC Payload와 클라이언트 컴포넌트 자바스크립트 명령을 사용하여 최종적으로 HTML을 렌더링한다. 클라이언트에서 필요한 자바스크립트 코드는 이 단계에서 함께 전달되어 클라이언트에서 동작한다.

✔️ React Server Component Payload (RSC Payload)?

  • RSC Payload는 렌더링된 React 서버 컴포넌트 트리의 압축된 이진 표현이다.
    RSC Payload에는 다음 내용이 포함되어 있다.
  • 서버 컴포넌트의 렌더링 결과.
  • 클라이언트 컴포넌트가 렌더링되어야 하는 위치에 대한 placeholder및 해당 JavaScript파일에 대한 참조.
  • 서버 컴포넌트에서 클라이언트 컴포넌트로 전달되는 어떤 props.

초기 데이터 전달

  • 서버 컴포넌트에서 미리 렌더링되어 클라이언트로 전송되는 데이터 중에는 클라이언트에서 초기 렌더링 시에 필요한 데이터가 포함된다. 이 데이터는 RSC Payload 내의 일부로 전달되어, 클라이언트에서는 초기 렌더링 시에 이 데이터를 사용하여 페이지를 구성할 수 있다.

클라이언트 컴포넌트의 동작 제어

  • RSC Payload에는 클라이언트 컴포넌트의 동작을 제어하기 위한 정보도 포함될 수 있다. 예를 들어, 서버 컴포넌트에서 전달된 정보를 바탕으로 클라이언트에서 특정 이벤트가 발생했을 때 어떤 동작을 수행할지에 대한 설정 등이 여기에 포함될 수 있다. (이벤트 핸들링, 초기 상태 설정)

서버 컴포넌트에서의 설정

// 서버 컴포넌트 예시
const ServerComponent = () => {
  // 클라이언트에서 이벤트 발생 시 동작할 함수
  const handleClientEvent = () => {
    // 클라이언트에서 발생한 이벤트에 대한 처리
    console.log('Client event handled on the server!');
  };

  return (
    <div onClick={handleClientEvent}>
      Click me on the server!
    </div>
  );
};

서버 컴포넌트에서 클라이언트로 전달되는 RSC Payload

{
  "type": "RSC_PAYLOAD",
  "data": {
    "initialState": {
      // 클라이언트 컴포넌트의 초기 상태 설정
      "counter": 0
    },
    "eventHandlers": {
      // 클라이언트에서 발생한 이벤트에 대한 핸들러 설정
      "onClick": "handleClientEvent"
    }
  }
}

클라이언트에서의 설정

// 클라이언트에서 이벤트 핸들링 및 동작 설정
const ClientComponent = ({ initialState, eventHandlers }) => {
  // 클라이언트에서 이벤트 핸들러 정의
  const handleClientEvent = () => {
    // 클라이언트에서 발생한 이벤트에 대한 처리
    console.log('Client event handled on the client!');
  };

  // 클라이언트에서 이벤트 핸들러 등록
  const handleClick = () => {
    if (eventHandlers && eventHandlers.onClick) {
      // 서버에서 전달된 이벤트 핸들러 실행
      window[eventHandlers.onClick]();
    }
  };

  return (
    <div onClick={handleClick}>
      Click me on the client! Counter: {initialState.counter}
    </div>
  );
};

이 예시에서는 서버 컴포넌트에서 클라이언트로 전달되는 RSC Payload에 초기 상태와 클라이언트 이벤트 핸들러의 설정이 포함되어 있다. 클라이언트에서는 받아온 정보를 기반으로 초기 상태를 설정하고, 클라이언트 이벤트가 발생했을 때 서버에서 전달된 핸들러를 실행하는 방식으로 동작한다. 이를 통해 서버와 클라이언트 간의 동작을 효과적으로 제어할 수 있다.

3. 클라이언트에서의 처리

  • 클라이언트에서는 받아온 HTML과 자바스크립트를 이용하여 초기 페이지를 렌더링한다. 후에 클라이언트에서는 RSC Payload를 사용하여 클라이언트와 서버 컴포넌트 트리를 조정하고 DOM을 업데이트한다. 그리고 자바스크립트 명령을 사용하여 클라이언트 컴포넌트를 hydration하는데 이때, 서버 컴포넌트와 클라이언트 컴포넌트 간의 조율이 이루어지고, 초기 렌더링에서 받아온 데이터를 활용하여 동적인 상태 및 이벤트 핸들러 등을 설정해서 애플리케이션을 상호작용 가능하게 만든다.
    즉, 사용자의 입력이나 이벤트에 대응하여 동적으로 업데이트되고, 클라이언트 측에서의 상태 변화 등이 반영될 수 있게 된다.

이 과정에서 주목해야 할 점은 서버 컴포넌트의 일부가 서버에서 미리 렌더링되고, 클라이언트에 전달되는 동시에 클라이언트에서 필요한 자바스크립트 코드도 함께 전달된다는 것이다. 이를 통해 초기 로딩 성능을 향상시키면서 클라이언트에서는 필요할 때 동적으로 업데이트가 가능한 형태의 페이지를 제공할 수 있다.

➡️ Server Component & Client Component

Next에서의 컴포넌트는 기본적으로 모두 Server Component로 만들어진다. 여기서 서버 컴포넌트와 클라이언트의 개념이 ssr과 csr이 아니다. 그렇기 때문에 클라이언트 컴포넌트라고 클라이언트에서만 렌더링 되는 것이 아니다. 클라이언트 컴포넌트이지만 미리 골격을 만들어놓을 수 있다면 서버에서 미리 렌더링해 정적인 html을 만들고 interaction이 필요한 부분을 클라이언트 단에서 JS코드와 컴포넌트를 실행하는데 필요한 react component 관련한 코드들을 통해 사용자와 상호작용하게 해주는 것이다. 예를들면 사용자가 클릭할 수 있는 button이 있다면 이 button의 형태는 미리 서버에서 렌더링 해줄 수 있는 부분이다. 그럼 그 모양은 미리 서버에서 만들어주고 클라이언트에 보내주면 클라이언트에서 이벤트 핸들링을 처리해주면 된다. 그렇기 때문에 사용자의 click을 처리한다던가 무언가 브라우저에서 실행되어야 하는 코드들이 보내져 브라우저측에서 hydration을 통해 브라우저에서 이벤트를 처리할 수 있다.

Client Component 사용이 필요할 때

Client Component를 사용할 때

  • interacive한 동작이나 Event Listener를 사용할 때
  • React Hook(useState, useEffect...)의 사용이 필요할 때
  • Browser API 사용이 필요할 때 (web storage...)
  • Search, Button 컴포넌트러첨 user interaction이 발생하는 영역
  • change, click 이벤트 처리나 useState등의 React Hook이 필요한 곳

Server Component를 사용할 때

  • 데이터를 가져올 때 (Fetch Data)
  • 백엔드 리소스에 직접 접근할 때
  • 서버에 민감한 정보를 보관해야 할 때 (access token, API keys,...)
  • 큰 종속성을 서버에서 유지할 때
    (서버 컴포넌트를 사용함으로써 클라이언트에 전달되는 데이터와 컴포넌트를 렌더링하는 데 필요한 코드 중에서, 특히 크기가 큰 종속성(dependencies)을 서버에 유지할 수 있습니다. 이는 클라이언트 측에서는 불필요한 큰 파일을 다운로드하거나 처리하지 않아도 되게 하여 초기 페이지 로딩 속도를 향상시킬 수 있다.)
  • 클라이언트 측의 Javascript를 줄일 때
    (서버 컴포넌트를 사용하면 클라이언트 측에서 필요한 JavaScript 코드를 최소화할 수 있다. 서버에서 렌더링된 결과물을 클라이언트로 전송하면, 클라이언트는 초기 렌더링에 필요한 최소한의 JavaScript 코드만 다운로드하고 실행하면 된다. 이로써 초기 페이지 로딩 속도가 향상되고 사용자가 빠르게 콘텐츠를 볼 수 있게 된다.)
  • Navbar, Sidebar, Footer, Main 컴포넌트
  • 컨텐츠 및 단순 링크(a태그)등을 담는 컨테이너
  • user interaction이 없거나 불필요할 때

출처

0개의 댓글