개발을 하다보면 웹 애플리케이션은 점점 더 복잡해지는데,
더욱 빠르고 사용자 경험이 좋은 웹이 요구되어서 개발에 대해 고민을 많이 하게 됩니다.
React를 개발하다보면 번들 크기가 커지기도하고, 성능이 좋지 않은 문제들이 쉽게 발생하는데,
SSR(Server Side Rendering)과 SSG(Static Site Generation)는 이러한 문제를 해결하기 위해 등장했지만, Hydration 비용과 복잡한 상태 관리, 워터폴 네트워크 요청 등의 문제를 완전히 해결하지는 못했습니다.
RSC(React Server Comonents)가 등장하면서 이러한 문제를 해결할 수 있다고 하는데, 함께 알아가 봅시다!!
클라이언트 사이드 렌더링은 자바스크립트의 다운로드와 파싱이 완료되면
즉각 리액트가 작동해서 모든 DOM을 불러오고,비어있는 <div id="root">에 저장합니다.
이 작업은 시간이 걸리는데 이때 사용자에게 텅 빈 흰색 화면만 보인다는 문제가 있습니다.
그런데 기능을 추가하면 JS 번들의 크기가 증가하면서 작업시간도 똑같이 비례해서 증가하게 됩니다.
이런 문제를 개선하기 위해서 서버 사이드 렌더링이 등장하게 되었습니다.
빈 HTML을 전송하는 것이 아니라,
직접 애플리케이션을 렌더링한 후 HTML을 생성해서 사용자는 완전한 형식의 HTML을 받을 수 있습니다.
서버가 초기 HTML을 생성하기 때문에 JS 번들을 다운로드 파싱하는 동안에도 사용자에게는 텅 빈 화면이 아니라 어느 정도 콘텐츠가 노출됩니다.
서버의 React가 중단한 부분을 클라이언트 측의 React가 이어받아 DOM을 적용하고 상호작용성을 추가하는 것을 바로 서버 사이드 렌더링이라고 합니다.


정적 사이트 생성(SSG, Static Site Generation)
React는 컴파일을 통해 JSX를 일반 자바스크립트로 변환하고 모듈을 번들링해야 합니다.
이때 SSG는 동일한 프로세스를 태우되, 모든 경로의 HTML을 미리 렌더링하는 서버 사이드 렌더링의 하위 변형입니다.
SSR은 서버가 React의 renderToString() 메서드를 사용해 컴포넌트를 HTML로 변환한뒤 브라우저로 전송합니다.
따라서 초기 로딩 속도를 개선하고 SEO 친화적이라는 장점이 있지만,
Hydration 과정에서의 성능 비용과 번들 크기 문제는 여전히 존재합니다.
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">
<div class="App">
<h1>Hello World!</h1>
</div>
</div>
</body>
</html>
SSR은 위와 같은 HTML을 브라우저에 제공하여 초기 로딩 화면을 즉시 보여줄 수 있지만,
브라우저는 Hydration을 통해 이 HTML을 다시 React의 가상 DOM으로 변환해야 합니다.
이 과정에서 React의 상태와 이벤트를 복원하는 작업이 수행됩니다.
SSR은 HTML을 미리 렌더링한 후 클라이언트에서 Hydration을 통해 상호작용을 활성화하는 반면
RSC는 이미 렌더링된 React 요소를 서버에서 생성하여 이를 JSON 형태로 브라우저에 전달합니다.
따라서 Hydration이 필요없다는 장점을 가지고 있습니다.
Hydration이 필요가 없다고요?
RSC는 서버에서만 실행되므로 클라이언트에서 Hydration 과정이 필요없습니다.
Hydration 과정에서 React는 브라우저에서 상태를 생성(useState)하고 이벤트 핸들러를 연결(useEffect)하는데, RSC는 서버에서 HTML과 데이터구조(JSON)을 완성해서 클라이언트로 전달하므로 필요가 없는데요,
따라서useState와 같은 상태 관리를 할 필요도,useEffect와 같은 부수 효과를 처리할 필요도 없습니다!!
RSC는 상태관리나 이벤트 핸들링을 지원하지 않기 때문에,데이터는 부모 컴포넌트나 props를 통해 전달합니다.
RSC의 동작 과정은 다음과 같습니다.
➡️RSC는 HTML과 JSON 결과물을 생성해 전달하는 역할만 수행합니다!!
RSC는 성능을 향상 시키고, 보안도 강화시키며, SEO에도 친화적입니다.
클라이언트로 전송되는 JS 번들이 줄어들기 때문에 초기 로딩시간이 단축되고,
클라이언트와 서버의 작업이 명확히 분리되어 리소스 낭비도 줄일 수 있습니다.
RSC는 서버에서만 실행되기 때문에 클라이언트로 민감한 데이터(API 키)를 노출하지 않습니다.
SSR처럼 서버에서 HTML을 제공하기 때문에 검색엔진 최적화에도 유리합니다.
React의 특징은 상위 컴포넌트에서 하위 컴포넌트로 단방향으로 데이터가 흐른다는 것입니다.
상위 컴포넌트에서 하위 컴포넌트로 전달하면서 데이터가 업데이트되면 React가 전체 트리를 검사해 변경된 컴포넌트와 그 하위 컴포넌트만 다시 렌더링합니다.
이때 RSC는 단방향 데이터 흐름을 서버로 확장합니다.
서버에서 데이터가 변경되었을 때, React는 클라이언트 컴포넌트를 포함한 전체 트리를 검사하고 필요한 컴포넌트만 업데이트하는데,
클라이언트와 서버 간 상태 동기화나 추가적인 로직없이 데이터 일관성을 유지하는데 매우 효과적입니다.
useEffect와 상태(state)를 사용했기 때문에 로직이 복잡했습니다.useEffect와 상태가 늘어나며 코드 가독성과 유지보수가 어렵다는 단점이 있었습니다.또한, 클라이언트에서 데이터를 가져오는 과정에서 워터폴 문제로 네트워크가 지연되었습니다.
1. 클라이언트가 서버에 요청을 보내고
2. 서버는 응답을 준비해서 클라이언트로 반환하고
3. 클라이언트는 받은 데이터를 다시 사용해 추가 요청을 보내야 했습니다...🥲
import { useState, useEffect } from "react";
export function Profile() {
const [profile, setProfile] = useState(null); // 상태 관리
useEffect(() => { // 클라이언트에서 데이터 페칭
fetch("/api/profile")
.then((res) => res.json())
.then((data) => setProfile(data));
}, []);
return <div>{profile?.name || "Loading..."}</div>; // 로딩 상태 관리
}
import { useState, useEffect } from "react";
export function Feed() {
const [feed, setFeed] = useState([]); // 상태로 피드 데이터 관리
useEffect(() => { // 클라이언트에서 데이터 페칭
fetch("/api/feed")
.then((res) => res.json())
.then((data) => setFeed(data));
}, []);
return (
<div>
{feed.map((post) => (
<div key={post.id}>{post.content}</div>
))}
</div>
);
}
서버에서 데이터를 fetching하고 HTML까지 생성해서 클라이언트로 전달하기 때문에,
클라이언트에서 로딩 상태나 API 호출 로직이 필요하지 않습니다!!
export async function Profile() {
const profileData = await fetch("/api/profile");
return <div>{profileData.name}</div>;
}
export async function Feed() {
const feedData = await fetch("/api/feed");
return feedData.map(post => <div>{post.content}</div>);
}
import { useState } from "react";
export function PaymentButton() {
const [status, setStatus] = useState("Pending");
const handlePayment = async () => { // 클라이언트에서 결제 로직 실행
const paymentResponse = await stripeAPI.createPaymentIntent();
setStatus(paymentResponse.status); // 상태 업데이트
};
return <button onClick={handlePayment}>{status}</button>;
}
Stripe와 같은 결제 API를 통합하려면 과거에는 복잡한 로직과 설정이 필요했지만,export async function PaymentButton() {
const paymentResponse = await stripeAPI.createPaymentIntent();
return <button>{paymentResponse.status}</button>;
}
기존 번들 크기 문제
클라이언트 컴포넌트는 의존성 라이브러리나 데이터 처리 로직을 클라이언트로 함께 전달해야 하므로 번들크기가 커질 수 밖에 없었습니다.
RSC를 활용한 해결
클라이언트로 전달되는 출력물만 클라이언트로 전송하기 때문에
RSC의 의존성은 클라이언트에 포함되지 않으므로 번들 크기에 영향을 미치지 않습니다.
export async function MarkdownParser({ content }) {
const parsedContent = markdownLibrary.parse(content);
return <div dangerouslySetInnerHTML={{ __html: parsedContent }} />;
}
RSC는 기존의 문제를 효과적으로 해결해서 로딩 시간을 줄이고 재사용성을 높이기 때문에
대규모 프로젝트에서 특히 유용하게 사용할 수 있습니다.
https://yozm.wishket.com/magazine/detail/2271/
https://servercomponents.dev/