1. Server Component란 ?
React Server Component(RSC)는 React 18부터 도입된 새로운 개념으로, 오직 서버 측에서만 실행되는 컴포넌트입니다.
즉, 브라우저에서는 이 컴포넌트의 코드가 실행되지도, 전달되지도 않습니다.
Next.js에서는 이 개념을 적극 활용하고 있으며, 특히 App Router 환경에서는 기본적으로 모든 컴포넌트가 서버 컴포넌트로 동작합니다.
2. Server Component의 등장 배경
기존 React에서는 모든 컴포넌트가 JavaScript 번들에 포함되어 브라우저로 전달되고, 이를 통해 Hydration이 이루어졌습니다.
하지만 이 방식에는 비효율적인 부분이 있었습니다.
우리가 만든 컴포넌트 중에는 다음 두 가지가 존재합니다.
- 상호작용이 필요한 컴포넌트 (예: useState, useEffect 등을 사용하는 경우)
- 정적인 컴포넌트 (상호작용 없이 단순히 UI만 출력하는 경우)
기존의 Page Router에서는 이 두 종류를 구분하지 않고 모두 JavaScript 번들에 포함시켰습니다.
그 결과,
- JS 번들의 용량이 커짐
- 번들 로딩 시간이 길어짐
- Hydration에 소요되는 시간이 늘어남
- TTI (Time To Interactive)가 지연됨
해결책: 정적인 컴포넌트의 분리
상호작용이 필요 없는 정적인 컴포넌트들은 JS 번들에서 제외해도 괜찮습니다.
이를 위해 도입된 것이 React Server Component입니다.
3. Server Component와 Client Component의 차이
- 🔴 Server Component: 상호작용이 없어서 서버 측에서만 실행되면 되는 컴포넌트
- 🟢 Client Component: 상호작용이 있어서 하이드레이션이 필요한 컴포넌트
Next 서버는 브라우저로부터 요청을 받으면 먼저 HTML 페이지를 생성하기 위해 모든 컴포넌트를 한 번 실행합니다. 이후 Hydration을 위해 JavaScript 번들을 브라우저에 전달하는 과정에서는, 서버 컴포넌트는 제외되고 클라이언트 컴포넌트만 포함됩니다.
결국 JS 번들에는 클라이언트 컴포넌트만 포함되어 브라우저에 전달되기 때문에, 클라이언트 측에서는 이들 컴포넌트만 다시 한 번 실행되게 됩니다.
- 서버 컴포넌트는 서버 측 렌더링 시 한 번만 실행됩니다.
- 클라이언트 컴포넌트는
- 서버에서 사전 렌더링 시 한 번
- 브라우저에서 Hydration 시 한 번 더
- → 총 두 번 실행됩니다.
그렇기 때문에 Next.js는 가능한 한 서버 컴포넌트를 기본으로 사용하고, 상호작용이 필요한 경우에만 클라이언트 컴포넌트를 사용할 것을 권장합니다. 결과적으로 페이지 내부에 클라이언트 컴포넌트의 개수가 줄어들수록 JS 번들의 용량도 함께 줄어들기 때문입니다.
4. Server Component의 장점ㅈ
- 서버에서만 실행되므로 보안에 민감한 작업도 안전하게 처리 가능
- 데이터 fetching, DB 연동 등 서버 자원을 적극 활용 가능
- JS 번들에서 제외되므로 클라이언트 성능 개선
5. 사용 예시 및 주의사항
📍 useEffect 사용 시 에러
export default function Home() {
useEffect(() => {}); // ❌ 서버 컴포넌트에서 사용 불가
return <div>index page</div>;
}
서버 컴포넌트는 브라우저 환경이 아니기 때문에 useEffect, useState 등 클라이언트 전용 Hook은 사용할 수 없습니다.
에러 메시지에서도 이러한 제약사항이 명시되어 있습니다.
해결 방법) 클라이언트 컴포넌트로 명시
"use client"; // 👈 클라이언트 컴포넌트로 설정
import { useEffect } from "react";
export default function Home() {
useEffect(() => {});
return <div>index page</div>;
}
컴포넌트 상단에 "use client"를 명시하면 해당 컴포넌트는 클라이언트 컴포넌트로 처리됩니다.
따라서 클라이언트 컴포넌트는 사전 렌더링을 위해 서버에서 한 번 실행된 후, 하이드레이션 과정에서 브라우저에서 한 번 더 실행되기 때문에 총 두 번 실행됩니다. 이러한 동작은 위 이미지의 서버 측과 클라이언트 측 콘솔 로그를 통해 확인할 수 있습니다.
6. Server Component 주의사항
⓵ 서버 컴포넌트에서는 브라우저에서 실행될 코드가 포함되면 안된다.
서버 컴포넌트는 오직 서버 측에서만 실행되며, 브라우저에서는 아예 실행되지 않습니다.
따라서 useState, useEffect 등 브라우저 환경에서 작동하는 React Hooks나 이벤트 핸들러는 서버 컴포넌트에서는 사용할 수 없습니다.
또한 브라우저에서 실행되는 기능이 포함된 라이브러리도 서버 컴포넌트에서는 사용할 수 없습니다.
Next.js의 App Router에서는 이러한 제약 때문에 특정 라이브러리를 사용하는 경우 오류가 발생할 수 있습니다.
⓶ 클라이언트 컴포넌트는 클라이언트에서만 실행되지 않는다.
클라이언트 컴포넌트는 Next.js가 서버 측 사전 렌더링 시 먼저 실행한 후, 클라이언트에서 하이드레이션 단계에서도 한 번 더 실행됩니다.
즉, 클라이언트 컴포넌트는 서버와 클라이언트 양쪽 모두에서 한 번씩 실행되는 구조입니다.
⓷ 클라이언트 컴포넌트에서 서버 컴포넌트를 import 할 수 없다.
"use client" 지시자가 선언된 클라이언트 컴포넌트에서는 서버 컴포넌트를 import해서 사용할 수 없습니다.
그 이유는 다음과 같습니다.
서버 컴포넌트는 번들 크기를 줄이기 위해 JS 번들에서 제외되고, 브라우저로 전달되지 않기 때문입니다.
즉, 브라우저에서 존재하지 않는 코드를 import하려 하면 의도치 않은 동작이나 오류가 발생할 수 있습니다.
하지만 실수로 이런 상황이 발생할 수 있기 때문에, Next.js는 런타임 에러 대신 해당 서버 컴포넌트를 클라이언트 컴포넌트로 변환합니다.
// client-component.tsx
"use client";
import ServerComponent from "./server-component";
export default function ClientComponent() {
console.log("Client Component");
return <ServerComponent />;
}
// server-component.tsx
export default function ServerComponent() {
console.log("Server Component");
return <div></div>;
}
위 코드를 실행하면 브라우저 콘솔에 다음과 같이 출력됩니다.
이는 ServerComponent가 클라이언트 컴포넌트로 자동 변환되었기 때문에 발생하는 현상입니다.
하지만 서버 컴포넌트를 불필요하게 클라이언트 컴포넌트로 전환하는 것은 바람직하지 않습니다.
클라이언트 컴포넌트가 많아질수록 JS 번들 용량이 커지고, 하이드레이션 시간도 증가하기 때문입니다.
❓ 정말 어쩔 수 없이 클라이언트 컴포넌트가 서버 컴포넌트를 반드시 자식으로 둬야하는 경우가 된다면 ?
// client-component.tsx
"use client";
import { ReactNode } from "react";
export default function ClientComponent({ children }: { children: ReactNode }) {
console.log("Client Component");
return <div>{children}</div>;
}
children으로 전달하는 방식이 권장됩니다.
// page.tsx
import ClientComponent from "./client-component";
import ServerComponent from "./server-component";
export default function Home() {
return (
<div>
index page
<ClientComponent>
<ServerComponent />
</ClientComponent>
</div>
);
}
이 구조에서는 ServerComponent가 클라이언트 컴포넌트로 변환되지 않기 때문에, 브라우저 콘솔에는 Client Component만 출력됩니다.
⓸ 서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화되지 않는 props는 전달 불가능하다.
직렬화(Serialization)란 객체나 배열 같은 복잡한 구조를 문자열이나 바이트 형태로 변환하는 것을 의미합니다.
예를 들어,
const person = {
name: "shyunu",
age: 28,
};
이 객체는 {"name": "shyunu", "age": 28}로 직렬화될 수 있습니다. 하지만 함수는 직렬화가 불가능합니다.
함수는 클로저, 렉시컬 스코프 등 실행 환경에 종속적인 정보를 담고 있어 문자열로 변환할 수 없기 때문입니다.
서버 컴포넌트와 클라이언트 컴포넌트가 이런 계층 구조를 가지고 있을 때 함수들은 직렬화가 될 수 없어서 props로써 전달될 수 없습니다.
위 이미지와 같이 사전 렌더링 과정에서는 서버 컴포넌트들이 먼저 실행되고 나서 클라이언트 컴포넌트들이 뒤이어 실행이 됩니다.
이때, 서버 컴포넌트를 먼저 실행하게 되면 RSC Payload라는 json과 비슷한 형태의 문자열이 생성됩니다.
RSC Payload ?
Next.js에서 사전 렌더링이 이루어지면, 서버 컴포넌트가 먼저 실행되고 그 결과로 RSC Payload가 생성됩니다.
RSC Payload는 서버 컴포넌트의 렌더링 결과와 함께 클라이언트 컴포넌트에게 전달될 데이터 및 위치 정보를 담고 있는 JSON과 유사한 복잡한 형태입니다.
이 과정에서 클라이언트 컴포넌트에게 전달되는 props는 반드시 직렬화 가능한 형태여야합니다.
함수나 순환 참조가 있는 객체 등은 전달이 불가능합니다.
'📍 프로그래밍 언어 > Next.js' 카테고리의 다른 글
[ Next.js ] 데이터 캐시 (Data Cache) - 개념부터 옵션 이해하기 (0) | 2025.06.17 |
---|---|
[ Next.js ] App Router에서의 데이터 페칭, 어떻게 달라졌을까? (0) | 2025.06.16 |
[ Next.js ] App Router - 구조부터 레이아웃까지 (0) | 2025.06.14 |
[ Next.js ] ISR (Incremental Static Regeneration) - 시간 기반과 주문형 재검증(On-Demand-ISR) (3) | 2025.06.11 |
[ Next.js ] getStaticPaths의 fallback 옵션 설정하기(false/blocking/true) (2) | 2025.06.10 |