최근 Next.js 14를 사용해 프로젝트를 진행하고 있습니다. 처음 써보는 Next.js에 이것저것 공부할 게 많았는데 그 중에서도 SSR과 서버 컴포넌트라는 개념이 새롭게 다가왔습니다. 왜 서버 컴포넌트를 쓰는지 이것은 왜 나왔는지 SSR과 RSC의 차이는 무엇인지 궁금증 투성이었습니다. 근본적으로 RSC의 등장 이유와 기존의 어떤 점이 불편해 나온 것인지 탐색해 본 과정을 정리해 보았습니다.
기존의 클라이언트 측 렌더링은 클라이언트가 요청을 하면 서버는 브라우저(클라이언트)에 단일 HTML 페이지를 보냅니다.
HTML 페이지에는 JS 파일에 대한 참조인 간단한 div 태그만 포함되어 있고, JS 파일에는 React 라이브러리 자체와 애플리케이션 코드를 포함하여 애플리케이션 실행에 필요한 모든 것이 포함됩니다. 이후 HTML 파일이 구문 분석되면 JS 코드 다운로드가 진행되며 컴퓨터에서 추가적인 HTML 생성 후 이를 루트 div 요소 아래의 DOM에 삽입하면 브라우저에서 사용자 인터페이스를 볼 수 있게 됩니다.

이런 CSR방식은 클라이언트에서 콘텐츠를 렌더링 하기 위해 JS 를 사용하여 높은 상화 작용성과 반응성을 제공하지만, 초기 로드 시간이 걸린다는 단점이 존재합니다.

위 과정에서 저는 "샌드위치"라는 비유를 들어보려고 합니다.
CSR방식은 마치 샌드위치를 주문했는데 빈접시만을 주고 사용자가 필요한 샌드위치의 속재료들을 요청하여 그 재료를 하나씩 받아서 쌓는 과정이에요.

다시 한번 샌드위치 비유를 사용해보자면 만약 빈 접시가 아닌 이미 완성된 샌드위치를 전달해준다면 어떨까요?
이걸 다르게 표현해보자면,대부분이 모두 완성된 HTML을 서버에서 내려준다면 클라이언트는 그것을 보여주기만 하면 되지 않을까요?
CSR의 단점을 극복하기 위해 Next.js와 같은 최신 React 프레임워크는 서버라는 측면으로 접근하였습니다.
위 접근 방식은 콘텐츠가 사용자에게 전달되는 방식을 근본적으로 달리합니다.
페이지를 구성하기 위해 클라이언트 측 JS에 의존하는 빈 HTML 파일을 보내는 것이 아닌 서버는 완전히 구성된 HTML 문서는 브라우저로 전송됩니다. 위와 같은 방식은 HTML이 서버에서 생성되었기에 브라우저는 HTML을 빠르게 구문 분석하고 표시할 수 있어 초기 페이지 로드 시간이 단축됩니다.
[추가]
SSR과 함께SSG(정적 생성)개념도 존재합니다.
SSG(정적 생성): 빌드 시점에 html 생성되어 각 요청에서 재사용 되는 것
SSR(서버 측 렌더링): 요청마다 html 생성
해당 글에서는 SSR의 개념만 다루려 합니다.

위와 같은 서버에서 내용이 차있는 HTML 파일을 내려주는 방식인 서버 측 접근 방식은 CSR과 관련된 문제를 보완합니다.
그러나 SSR방식을 사용하여 서버에서 HTML을 생성할 때, 내용이 차있는 HTML을 위해서는 “데이터 패칭” 과정이 존재합니다.
이는 getServerSideProps 를 통하여 서버에서 데이터 패칭이 가능합니다.
[추가]
SSR뿐만 아닌SSG방식에서도 데이터 패칭이 필요합니다. 두 방식의 간단한 차이는 아래와 같습니다.
getStaticProps(SSG): 빌드 시점에 한번만 실행 ⇒ 모두에게 동일하게 보이거나 변화가 별로 없는 요소, 요청 시점에 데이터를 가져올 필요가 없거나 데이터와 사전 렌더링된 HTML을 캐시 할 경우
getServerSideProps(SSR): 모든 요청에서 실행 ⇒ 따라서 개인화된 데이터나 요청 시에만 알 수 잇는 정보에 의존하는 페이지 등등
SSR은 getServerSideProps를 활용하여 데이터를 서버에서 패치 해옵니다. 이후 가져온 데이터를 활용하여 서버에서 HTML을 그려 클라이언트에게 전달합니다.
그러나 여기서 추가적으로 살펴봐야 하는 요소가 있습니다.
getServerSideProps)getServerSideProps)]여기서도 다시한번 샌드위치로 비교를 해볼게요
거의 온전한 HTML 파일 즉 샌드위치를 미리 다 만들어서 주려면 그에 맞는 재료를 미리 알고 가지고 있어야겠죠.
그렇다면 그 재료는 미리 어떻게 가져올 수 있는 것일까요?
SSR방식에서 데이터 패칭을 진행 시 gerServerSideProps를 사용하여 함수를 page 최상단에서 실행하고 함수를 통해 불러온 데이터를 실질적으로 필요한 page 컴포넌트에 props로 넘겨 사용해야 합니다.
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next'
type Repo = {
name: string
stargazers_count: number
}
export const getServerSideProps = (async () => {
// Fetch data from external API
const res = await fetch('https://api.github.com/repos/vercel/next.js')
const repo: Repo = await res.json()
// Pass data to the page via props
return { props: { repo } }
}) satisfies GetServerSideProps<{ repo: Repo }>
export default function Page({
repo,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<main>
<p>{repo.stargazers_count}</p>
</main>
)
}
이는 구조는 data를 하위 page에서 사용하려면 props를 통해 넘겨주어야만 하는 props drilling이 불가피한 상황입니다.
CSR방식과 다르지 않은 JS 번들 사이즈]여기서 부터는 조금 더 컴퓨터처럼 생각을 해볼게요.
자 우리는 처음부터 완성된 형태의 샌드위치를 전달받았으나, 이 샌드위치는 “맛”이라는 요소가 들어가지 않았어요. 모양은 샌드위치인데 맛은 아무것도 나지 않아요. 샌드위치가 빠르게 배송 되었지만 무맛인 상태인거죠.
실질적으로 샌드위치를 맛있게 먹으려면 우리는 맛이라는 요소를 첨부하는 필요할 것이에요.
이것이 하이드레이션 과정이에요
SSR 방식을 통해서 CSR 방식보다 빠른 첫 화면을 보여줄 수 있게 되었습니다. (초기 HTML을 서버에서 생성해서 클라이언트에게 전달하기에) 그러나 여기서 중요한 부분은 hydration(수화)라는 파트입니다.
hydration(수화) 란 SSR을 통해 HTML 파일을 빠르게 받아오고 이와 병렬적으로 js 번들도 함께 받아와 사전에 받아온 HTML 파일과 병합하는 과정을 말합니다. hydration 과정은 거치며 껍데기만 있던 HTML 파일이 사용자와 상호작용이 가능한 살아있는 웹으로 변화하는 과정입니다.
여기서 살펴봐야 할 부분은 초기 로딩 속도에는 이점이 있지만, 실제적으로 페이지와 사용자가 상호작용하기 위해서는 CSR과 동일한 사이즈의 JS 번들을 모두 다운로드해야 한다는 점입니다.
이런 과정은 사용자가 페이지를 볼 수 있는 시점은 빨라졌지만, TTI(Time To Interactive)에서는 CSR과 동일한 시간이 걸림으로 유의미한 성과가 나타나지 않습니다.
위에서 CSR, SSR을 살펴보았습니다.
CSR의 단점을 극복하려 SSR과 같이 서버에서 HTML을 미리 생성하지만, 그럼에도 JS 번들 사이즈 및 TTI의 측면에서는 아쉬움이 존재하였습니다.
따라서 리액트 팀은 React Server Component(RSC)라는 새로운 아키텍처로 클라이언트 컴포넌트 + 서버 컴포넌트로 둘로 구분하는 이중 구성 요소 모델을 도입하였습니다.
서버 컴포넌트와 클라이언트를 왜 나누었을까요? 나눈 기준은 무엇이었을까요?
두 컴포넌트의 분류로 생기는 장점에 대해서 이야기해 보려 합니다.
이전에 작성했던 RSC관련 포스트를 참고해주셔도 좋을 것 같습니다!
RSC가 뭐지요,,?

두 컴포넌트의 주요 차이점은 렌더링 위치보다는 기능에 있습니다.
각 컴포넌트가 어떤 기능을 제공하고 있는지 그 기능은 어떤 장점을 가지고 오는지 이야기하고자 합니다.
(RSC개념을 학습하실 때도 서버/클라이언트라는 워딩보다는 해당 요소의 기능에 초점을 두시는 게 좋습니다.)
보통의 클라이언트 컴포넌트의 경우 기존에 알고 컴포넌트입니다.
등이 가능하며 사용자와 풍부하고 빠른 상호작용이 가능합니다.
그러나 애플리케이션의 사이즈가 늘어남에 따라 많은 JS 청크를 다운로드해야 하고 이는 사용자가 느끼기에 느린 페이지라는 인식이 생기게 될 것입니다.
따라서 이런 무거운 클라이언트의 짐을 덜어주고자 서버 에서 할 수 있는 것은 서버에서 미리 해석하여 클라이언트에게 전달하자는 개념을 적용하게 된 것이 서버 컴포넌트의 등장의 기본입니다.



위 그림은 RSC와 관련된 블로그를 보면 매우 자주 보았을 그림입니다.
간단하게 설명하자면, 노란색 노드는 서버 컴포넌트를, 파란색 노드는 클라이언트 컴포넌트를 의미합니다. 사용자는 해당 페이지를 렌더하기 위해서는 서버로 요청을 보냅니다. 이때 서버는 컴포넌트 트리를 Root부터 실행하며 직렬화된 JSON 형태로 재구성합니다.
직렬화는 특정 개체를 다른 컴퓨터 환경으로 전송하고 재구성할 수 있게 형태를 변환시키는 과정입니다. 그러나 이 직렬화 과정을 모든 컴포넌트에서 진행하는 것이 아닙니다. 서버 컴포넌트는 직렬화 과정을 거치지만, 클라이언트 컴포넌트는 해석하지 않고, “여기는 클라이언트 컴포넌트가 있는 자리입니다”라는 의미로 placeholder 를 배치합니다.
이렇게 직렬화된 결과물을 클라이언트가 전달받습니다. 그 과정에서 함께 다운로드 한 JS 번들을 참조하여 클라이언트 컴포넌트의 자리(placeholder)의 자리를 렌더링 하여 빈 공간을 채웁니다.
그런데 여기까지 이해하고 보면 RSC와 SSR의 차이점은 뭐지?라는 생각이 들곤 합니다.
두 방법을 이용해도 결과론적으로는 컴포넌트가 서버에서 해석되어서 HTML형태로 클라이언트로 전달되는거 아닌가?
그러나 여기서 생각하고 넘어가면 좋은 것은 RSC와 SSR 두 방식의 역할과 목적에 대해서 이해하면 이해하기 쉽습니다.
우리는 개발을 할 때 일반적으로 jsx로 구성된 컴포넌트가 브라우저가 이해할 수 있는 js로 변환하여 서버에서 빌드 되는 과정을 거칩니다. 위 과정 속에서 SSR은 거의 온전한 html을 만들어 클라이언트로 전달하고, JS 번들도 전할하여 이후 hydration 과정을 거치며 웹을 렌더링합니다. RSC는 서버에서 빌드 시 컴포넌트 tree를 RSC 페이로드로 직렬화 하는 과정을 거치고 RSC Payload를 클라이언트로 전달합니다. 이 전달 받은 RSC payload의 서버 컴포넌트 부분이 이미 HTML 태그로 직렬화되어있는 것입니다.
RSC를 사용하며 아래와 같은 이점이 있습니다.
SSR을 사용했을 때는 데이터를 사전에 가져오기 위해 getServerSideProps 를 사용하여 함수를 page 최상단에서 호출하고 props를 전달하는 방식으로 data를 전달했습니다. 그러나 RSC는 컴포넌트 자체가 서버에서 렌더링 되기 때문에 함수 호출과 props전달 과정이 필요없어집니다. 이는 컴포넌트 내부에서 데이터를 패치할 수 있게 되어 좀더 간편하게 데이터에 접근할 수 있게 됩니다.
RSC는 서버 측에서 컴포넌트에 대한 해석 즉 RSC 페이로드로 직렬화 하는 과정을 진행하기에 서버 컴포넌트의 JS 번들이 클라이언트로 전달되지 않습니다. 이런 특징은 번들 사이즈를 획기적으로 줄일 수 있습니다. 또한 번들 사이즈 감소로 TTI(Time To Interactive)를 크게 개선할 수 있습니다. 이는 SSR의 단점 중 하나인 초기 로딩 속도는 개선되었지만, CSR와 동일한 양의 JS 번들 다운으로 인해 TTI은 CSR 대비 큰 메리트가 없다는 점을 크게 보완할 수 있게 되었습니다.
추가적으로 RSC를 사용하면 자동 code splitting, 점진적 렌더링 등이 가능합니다.
SSR과 RSC는 상반되어 둘 중 하나를 선택하는 개념이 아닌 서로를 보완하는 개념입니다.
RSC는 서버 구성 요소와 클라이언트 구성 요소를 분리함으로써 번들 크기를 줄이고 성능을 최적화하며, 초기 로딩 시간을 단축하는 등 기존 방식의 한계를 극복하고자 합니다. 또한 서버 측에서 직접 데이터를 처리하고 보안을 강화하는 등 여러 가지 장점을 제공하여 애플리케이션의 효율성을 크게 향상시킬 수 있습니다.
SSR과 RSC를 적절하게 섞어 쓰며 더 나은 성능을 제공할 수 있습니다.
글을 정리하고 작성하며 렌더링이 어떻게 이루어지는지 보다는 기존 방식의 특징과 단점들을 이야기해 보고 그것을 극복하기 위한 방법에 대해서 작성해 보았습니다. 또한 RSC의 개념인 서버 구성 요소와 클라이언트 구성 요소의 특징에 대해서 이야기해 보았습니다. 어떤 상황에서 서버 요소를 사용하고 클라이언트 요소를 사용하는지 조금은 감이 잡히는 순간이 되었기를 바랍니다.
기존의 클라이언트 사이드 렌더링(CSR)과 서버 사이드 렌더링(SSR)의 동작 방식과 한계를 살펴보고, 이러한 한계를 해결하기 위해 React Server Components(RSC)가 등장하게 된 배경을 정리했습니다. 특히 CSR의 초기 로드 지연 문제와 SEO 비효율성, SSR의 수화(hydration)와 관련된 성능 문제를 RSC가 어떻게 해결하는지에 중점을 두었습니다.
이 글을 통해 RSC의 도입 배경과 핵심 개념을 이해하고, 언제 서버 구성 요소와 클라이언트 구성 요소를 사용할지에 대한 감을 조금이라도 잡을 수 있기를 바랍니다. RSC는 단순히 성능을 향상시키는 기술을 넘어, 개발자가 클라이언트와 서버 환경의 장점을 모두 활용할 수 있는 중요한 도구입니다. 앞으로 프로젝트에 어떻게 활용할 수 있을지 고민해보면 좋겠습니다.