[Next.js] Server Component, 서버에서만 실행되는 컴포넌트

Woonil·2025년 12월 24일

Next.js

목록 보기
4/8

서버 컴포넌트(Server Component)는 기존의 (클라이언트) 컴포넌트 개념을 서버로 확장한 컴포넌트 개념으로, 서버측에서만 실행되며 브라우저에서는 실행되지 않는다.

리액트에는 상호작용이 있는 컴포넌트와 상호작용이 없는 컴포넌트가 존재하며, 상호작용이 있는 컴포넌트는 hydration 과정이 필요하다. 서버에서는 hydration 이전에 JS 번들을 클라이언트(브라우저)에 보낸다. 이때, Pages Router에서는 상호작용의 여부와 관계없이 모든 컴포넌트들을 번들에 묶어 보냈었다. 하지만 이는 hydration이 불필요한 컴포넌트들까지 포함되어 번들 크기가 커져 hydration이 늦어지고 상호작용을 시작하는 TTI(Time to Interaction)도 늦어지는 비효율이 발생했다. 따라서 상호작용이 없는 컴포넌트는 JS 번들에 포함하지 않도록 할 필요가 생겼고, 서버 컴포넌트와 클라이언트 컴포넌트를 구별하게 된다.

서버 컴포넌트는 서버측에서 사전 렌더링 시 단 한 번 실행되고, 클라이언트 컴포넌트는 사전 렌더링, hydration 총 두 번 실행된다. 따라서 페이지의 대부분을 서버 컴포넌트로 구성하고, 클라이언트 컴포넌트는 꼭 필요한 경우에만 사용할 것이 권장된다.

🤔개념

💧클라이언트 컴포넌트

SSR(pre render) + CSR
Next.js에서 SSR은 모든 컴포넌트에서 발생한다. 이때, 컴포넌트를 클라이언트 측에서 hydrate 되게 만드려면 파일 최상단(import보다 위)에 React "use client" 지시어를 추가해야 한다.

결국, use client 는 서버와 클라이언트 컴포넌트 모듈 간의 경계를 선언하는 데 사용된다. 즉, 파일에 "use client"를 정의하면 하위 컴포넌트를 포함하여 해당 파일로 가져온 다른 모든 모듈을 클라이언트 번들의 일부로 간주하게 된다.

⚠️주의사항

  • 브라우저에서 실행될 코드가 포함되면 안된다.
    • 브라우저 환경에 의존적인 useState, useEffect 등의 리액트 hooks
    • 이벤트 핸들러
    • 브라우저에서 실행되는 기능을 담고 있는 라이브러리
  • 클라이언트 컴포넌트라고 클라이언트에서만 실행되는 것이 아니다: 사전 렌더링을 위해 서버에서 1번, hydration을 위해 브라우저에서 1번 총 2번 실행된다.
  • 클라이언트 컴포넌트에서 서버 컴포넌트를 import 할 수 없다: 만약 import 하더라도 Nextjs는 해당 서버 컴포넌트를 클라이언트 컴포넌트로 변환한다. 즉, 클라이언트 컴포넌트 하위의 컴포넌트들은 모두 클라이언트 컴포넌트 취급하는 것이다. 만약 클라이언트 컴포넌트 하위에 서버 컴포넌트를 위치시키려면 children prop을 활용하여 Nextjs가 서버 컴포넌트의 결과물만 클라이언트 컴포넌트 내부에 끼워넣게 할 수 있다.
  • 서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화되지 않는 props(함수)는 전달 불가하다.

    직렬화(Serialization)
    객체, 배열, 클래스 등의 복잡한 데이터 구조의 데이터를 네트워크 상으로 전송하기 위해 문자열, 바이트의 단순한 형태로 변환하는 것이다. 자바스크립트의 함수는 직렬화될 수 없는 특징을 지닌다.

동작 과정

서버 측 동작

Rendering work: Route segment와 Suspense boundary를 기준으로 chunk 단위로 split한다. 페이지를 구성하는 모든 컴포넌트(서버+클라이언트 컴포넌트)들을 실행해서 완성된 HTML 페이지를 생성한다.

자세히는 서버 컴포넌들만 따로 먼저 실행하는 과정이 존재하고, 그 결과물로 RSC Payload가 생성된다. 이 RSC Payload와 클라이언트 컴포넌트용 JavaScript Instructions(자바스크립트 명령어 뭉치)의 일부(ex. useState의 초깃값 등)를 조합하여 HTML을 만든다.

RSC(React Server Component) Payload

React 서버 컴포넌트의 순수한 데이터(결과물)로서, 서버 컴포넌트를 직렬화한 결과를 나타낸다.

  • 구성요소
    • 서버 컴포넌트의 렌더링된 결과물
    • placeholder(연결된(import한) 클라이언트 컴포넌트의 렌더 위치와 클라이언트 컴포넌트의 JavaScript 파일 위치)
    • 서버 컴포넌트가 클라이언트 컴포넌트에게 전달하는 props
      Next.js는 리액트가 렌더한 RSC Payload와 함께 자바스크립트 명령어 뭉치를 활용하여 HTML을 렌더한다. (서버 내 동작)
  • 클라이언트 측 동작
    1. HTML을 즉시 보여준다. (routes의 프리뷰)
    2. 클라이언트 컴포넌트가 들어갈 빈자리(placeholder)를 메꾸고, 서버 컴포넌트와 조합하는 reconcile(재조정) 단계를 거쳐 리액트 Virtual DOM 트리를 구성한다. 이후 실제 DOM에도 반영한다.
    3. placeholder를 채운 컴포넌트들에 대해 interaction이 가능하도록 JavaScript Instructions(ex. useState의 setState)를 활용하여 hydrate한다.

도움되는 자료
How React server components work: an in-depth guide

Prefetching

App Router에서는 페이지가 Static과 Dynamic으로 나뉜다.

  • Static: 추가적인 데이터 업데이트가 불필요하므로, RSC Payload와 JS번들 모두를 prefetching한다.
  • Dynamic: url 파라미터나 쿼리스트링을 포함한 컴포넌트의 경우 RSC Payload만 prefetching한다. JS번들은 실제 페이지 이동이 일어난 경우에만 불러온다.

😎실습

서버 컴포넌트의 RSC Payload 확인

서버 컴포넌트

먼저 서버 컴포넌트만 존재하는 경우를 살펴보자. 이 경우에는

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable}`}>
        <header>
          <Link href={"/"}>ONEBITE CINEMA</Link>
          &nbsp;
          <Link href={"/search"}>SEARCH</Link>
          &nbsp;
          <Link href={"/movie/1"}>MOVIE 1</Link>
        </header>
        {children}
      </body>
    </html>
  );
}
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q: string }>;
}) {
  const { q } = await searchParams;

  return <div>Search : {q}</div>;
}

서버+클라이언트 컴포넌트

만약, 서버 컴포넌트 하위에 클라이언트 컴포넌트가 존재한다면 네트워크 탭의 응답이 어떻게 달라질까?

"use client";

export default function ClientComponent() {
  return <div>ClientComponent</div>;
}
// movie/[id]/page.tsx
import ClientComponent from "@/components/client-component";

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  return (
    <>
      <div>Movie : {id}</div>
      <ClientComponent />
    </>
  );
}

참고자료
한 입 크기로 잘라먹는 Next.js (이정환)

profile
무엇이든 최선을 다하고자 노력합니다:)

0개의 댓글