서버 컴포넌트(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 등의 리액트 hookschildren prop을 활용하여 Nextjs가 서버 컴포넌트의 결과물만 클라이언트 컴포넌트 내부에 끼워넣게 할 수 있다.직렬화(Serialization)
객체, 배열, 클래스 등의 복잡한 데이터 구조의 데이터를 네트워크 상으로 전송하기 위해 문자열, 바이트의 단순한 형태로 변환하는 것이다. 자바스크립트의 함수는 직렬화될 수 없는 특징을 지닌다.
Rendering work: Route segment와 Suspense boundary를 기준으로 chunk 단위로 split한다. 페이지를 구성하는 모든 컴포넌트(서버+클라이언트 컴포넌트)들을 실행해서 완성된 HTML 페이지를 생성한다.
자세히는 서버 컴포넌들만 따로 먼저 실행하는 과정이 존재하고, 그 결과물로 RSC Payload가 생성된다. 이 RSC Payload와 클라이언트 컴포넌트용 JavaScript Instructions(자바스크립트 명령어 뭉치)의 일부(ex. useState의 초깃값 등)를 조합하여 HTML을 만든다.
React 서버 컴포넌트의 순수한 데이터(결과물)로서, 서버 컴포넌트를 직렬화한 결과를 나타낸다.
App Router에서는 페이지가 Static과 Dynamic으로 나뉜다.
먼저 서버 컴포넌트만 존재하는 경우를 살펴보자. 이 경우에는
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>
<Link href={"/search"}>SEARCH</Link>
<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 (이정환)