주어진 답안 (모범 답안)
Next.js에서 서버 컴포넌트와 클라이언트 컴포넌트는 각각 다른 용도로 사용되며, 성능과 사용자 경험에 중요한 영향을 미칩니다.
서버 컴포넌트는 서버에서 렌더링되며, 브라우저로 전송되기 전에 HTML이 미리 생성됩니다.
따라서 초기 로딩 속도가 빠르고, 검색 엔진 최적화에 유리합니다.
서버에서 데이터를 가져오고 렌더링할 수 있으며, 불필요한 자바스크립트 번들을 줄여 성능 최적화가 가능합니다.
예를 들어,getServerSideProps를 활용하여 데이터를 가져와 서버에서 미리 렌더링하는 방식으로 사용됩니다.클라이언트 컴포넌트는 브라우저에서 실행되며, 사용자의 상호작용이 필요한 UI에 적합합니다.
예를 들어, 버튼 클릭, 폼 입력, 모달 팝업 등의 동적인 기능을 구현할 때 사용됩니다.
데이터를 가져올 때useEffect훅을 사용하여 클라이언트 측에서 비동기 요청을 수행합니다.
클라이언트에서 실행되므로 서버 컴포넌트보다 초기 로딩 속도가 느릴 수 있지만, 사용자 경험을 풍부하게 만들 수 있습니다.마지막으로 정리하자면, 서버 컴포넌트는 성능 최적화와 SEO에 강점을 가지며, 클라이언트 컴포넌트는 사용자와의 상호작용이 많은 경우에 유용합니다.
따라서, 프로젝트의 요구사항에 따라 적절히 혼합하여 사용하는 것이 중요합니다.
🪄 RSC(React Server Components)란?
- React 컴포넌트를 서버에서 렌더링하고, 그 결과만 클라이언트로 전송하는 기술
- React가 클라이언트(브라우저)에서만 동작하던 기존 방식과 달리, 서버에서 React 컴포넌트를 먼저 실행하고, 그 결과 HTML + 구조 데이터(JSON 형태)만 브라우저로 보내는 개념
- 브라우저에 불필요한 JS를 보내지 않고 더 빠르고 안전한 렌더링을 가능하게 함
🪄 RSC의 동작 구조
RSC는 단순히 SSR(Server-Side Rendering)보다 더 정교하게 작동함
SSR은 완성된 HTML만 보내지만, RSC는 HTML + 컴포넌트 트리 구조(RSC Payload)를 함께 전송함
- 서버에서 React 컴포넌트를 실행함
fetch(), DB 접근 등 서버 전용 로직 처리 가능- React가 RSC Payload(JSON 비슷한 데이터)를 생성함
- 렌더링된 UI 구조와 props 등의 메타데이터 포함
- 클라이언트는 이 Payload를 해석해 React 트리를 복원함(hydrate)
- 브라우저에서는 JS 로직 없이 HTML을 즉시 표시
- 필요한 부분만 클라이언트 컴포넌트로 하이드레이션됨
➡️ RSC는 SSR + CSR의 장점을 결합한 방식
🪄 기존 방식과의 비교
구분 CSR(클라이언트 렌더링) SSR(서버 사이드 렌더링) RSC(서버 컴포넌트) 실행 위치 브라우저 서버 서버+클라이언트 혼합 초기 로딩 느림 (JS 다운로드 필요) 빠름 매우 빠름 번들 크기 큼 중간 가장 작음 보안 낮음 (API 키 노출 위험) 높음 매우 높음 상태 관리 클라이언트 서버 서버/클라이언트 분리 상호작용 처리 클라이언트 클라이언트 클라이언트만 담당 🪄 RSC가 보안이 높다고 평가받는 이유
- 민감 정보가 브라우저에 가지 않음
- DB 쿼리, API 키, 인증 토큰 등은 서버에서만 실행되고, 그 결과(HTML or 데이터 조각)만 클라이언트로 전달됨
- 따라서 사용자에게 내부 로직이 절대 노출되지 않음
- 브라우저에 JS 번들이 거의 없음
- 클라이언트로 전달되는 건 HTML 결과뿐이며, 로직이 담긴 JS 파일은 없음
- 따라서 리버스 엔지니어링(코드 분석)을 통해 내부 로직을 유출하기 어려움
- DB 직접 접근 가능 (서버 환경)
- RSC는 서버에서 실행되므로 DB에 직접 쿼리할 수 있음
- 기존 CSR 환경에서는
fetch('/api/users')처럼 중간 API 서버를 둬야 했지만, RSC는 서버 내부에서 곧바로 Prisma, MongoDB, PostgreSQL 등 접근 가능- 데이터 전송 최소화
- 클라이언트로는 오직 렌더링 결과만 전송함
- JSON/HTML 형태로 직렬화되어 전달되므로, 내부 비즈니스 로직이 포함되지 않음
- 공격자가 네트워크 트래픽을 가로채더라도 UI 구조 외의 정보는 얻기 어려움
- 서버-클라이언트 경계가 명확
"use client"지시어로 "이 부분만 브라우저에서 실행된다"가 명확히 구분됨- 실수로 서버 로직을 클라이언트에 넣는 일이 거의 없음
- 즉, 코드 레벨에서 보안 경계가 문법적으로 보장됨
🪄 RSC Payload란?
RSC가 단순히 HTML만 반환하지 않는 이유는, React 트리의 구조 정보도 함께 전달하기 때문임
- 이 데이터가 바로 RSC Payload
- 일종의 React 컴포넌트 트리 스냅샷 역할을 함
- 클라이언트는 이 데이터를 해석해 하이드레이션을 최소화함
즉, RSC는 단순 SSR이 아니라 React 단위로 렌더링 정보를 직렬화(serialized)해 전송하는 구조임
🪄 RSC와 SSR의 차이점
구분 SSR RSC 반환 결과 완성된 HTML HTML+React Payload 렌더 단위 전체 페이지 (새 요청마다 전면 재계산) 컴포넌트 단위 (부분 갱신 가능) 데이터 전달 브라우저에서 재요청 필요 서버 렌더링 시 함께 포함 Suspense/Streaming 제한적 완벽 지원 React 특성 반영 제한적 완전 React-native 구조
주요 특징
| 구분 | 설명 |
|---|---|
| 실행 위치 | 서버에서만 실행됨 |
| JavaScript 번들 | 브라우저로 전송되지 않음 |
| React 훅 | useState, useEffect 등 클라이언트 훅 사용 불가 |
| 데이터 접근 | 직접 DB 쿼리, API 호출 가능 |
| 보안 | 민감한 데이터 노출 위험 없음 (서버 내부에서 처리됨) |
| SEO | SSR 결과가 HTML로 바로 반환되어 SEO에 유리 |
| 예시 | layout.tsx, page.tsx, 서버 데이터 처리용 컴포넌트 |
"use client" 지시어가 파일 최상단에 명시되어야 함주요 특징
| 구분 | 설명 |
|---|---|
| 실행 위치 | 브라우저 (클라이언트 측) |
| JavaScript 번들 | 브라우저로 전송되어 실행됨 |
| React 훅 | useState, useEffect, useRef 등 사용 가능 |
| 브라우저 API | window, document, localStorage 접근 가능 |
| 상호작용 | 클릭, 입력, 스크롤 등 사용자 이벤트 처리 가능 |
| 예시 | 버튼, 모달, 폼, 탭, 드롭다운 등 UI 인터랙션 요소 |
| 항목 | 서버 컴포넌트 | 클라이언트 컴포넌트 |
|---|---|---|
| 실행 위치 | 서버 | 브라우저 |
| 선언 방식 | 기본 | "use client" 필요 |
상태 관리 (useState 등) | ❌ 불가 | ⭕ 가능 |
이벤트 핸들러 (onClick 등) | ❌ 불가 | ⭕ 가능 |
| 브라우저 API 접근 | ❌ 불가 | ⭕ 가능 |
| 보안 | 높음 | 상대적으로 낮음 |
| 번들 크기 | 작음 | 커질 수 있음 |
| 데이터 fetch | 서버에서 직접 처리 | API 호출만 가능 |
| 사용 예시 | 페이지, 데이터 표시 | 폼, 버튼, 인터랙션 UI |
// app/posts/page.tsx (Server Component)
export default async function Page() {
const res = await fetch("https://api.example.com/posts", { cache: "no-store" });
const posts = await res.json();
return (
<main>
<h1>게시글 목록</h1>
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
</main>
);
}
💻 파일 위치와 역할
app/posts/page.tsx파일은 Next.js의 App Router 구조에서/posts경로를 담당하는 페이지- 즉, 사용자가
/postsURL로 접속하면 이 컴포넌트가 실행됨- App Router에서는 모든
page.tsx파일이 기본적으로 서버 컴포넌트임
💻
export default async function Page()
async function은 서버에서 데이터를 비동기(fetch)로 가져올 수 있음을 의미함- 서버 컴포넌트는 await/async를 자유롭게 사용할 수 있음
- 클라이언트 컴포넌트는
useEffect안에서만 fetch 가능하지만, 서버 컴포넌트는 함수 본문 안에서 바로await fetch()가 가능함
💻
fetch("https://api.example.com/posts", { cache: "no-store" })
- 작동 방식
- Next.js의
fetch()는 서버 환경에서 실행됨 (서버 컴포넌트의 경우)- 브라우저 네트워크 요청이 아닌 Node.js 서버의 내부 요청임
{ cache: "no-store" }옵션
이 옵션은 Next.js의 데이터 캐싱 정책을 제어함
옵션 설명 "force-cache"기본값, 결과를 캐시에 저장하고 재사용 "no-store"캐시하지 않고 매 요청마다 새로운 데이터 요청 "revalidate"ISR(Incremental Static Regeneration)용 설정
💻
const posts = await res.json();
- 서버에서 받은 응답(
Response객체)을 JSON으로 변환함posts변수에는 게시글 목록 데이터 배열이 저장됨
💻 JSX 렌더링 부분
- 서버에서 데이터를 다 받아온 후, JSX를 HTML로 서버에서 미리 렌더링(SSR)함
- 이 결과가 HTML 형태로 브라우저에 전송됨
- 즉, 브라우저는 JS 실행 없이 완성된 HTML을 바로 렌더링함
"use client";
import { useState } from "react";
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button
className={`p-2 rounded ${liked ? "bg-red-500" : "bg-gray-200"}`}
onClick={() => setLiked(!liked)}>
{liked ? "❤️" : "♡"}
</button>
);
}
위 LikeButton은 보통 서버 컴포넌트 내부에 포함되어 사용됨
// app/posts/page.tsx (Server Component)
import LikeButton from "./LikeButton";
export default async function Page() {
const posts = await getPosts();
return (
<main>
{posts.map((p) => (
<div key={p.id}>
<h2>{p.title}</h2>
<LikeButton /> {/* 여기는 브라우저에서만 실행됨 */}
</div>
))}
</main>
);
}
Page는 서버에서 렌더링 (HTML 생성)LikeButton만 클라이언트에서 작동 (이벤트 핸들링)➡️ 즉, 하이브리드 렌더링이 이루어짐
| 상황 | 선택할 컴포넌트 |
|---|---|
| 데이터 조회 및 렌더링 | 서버 컴포넌트 |
사용자 입력 처리 (onClick, onChange 등) | 클라이언트 컴포넌트 |
| 정적 콘텐츠 (소개글, 문서 등) | 서버 컴포넌트 |
| 다크 모드 토글, 검색창, 모달 | 클라이언트 컴포넌트 |
| 복잡한 API 연동, SEO 고려 페이지 | 서버 컴포넌트 중심 + 필요한 부분만 클라이언트화 |