https://github.com/reactwg/server-components/discussions/5
RSC From Scratch. Part 1: Server Components · reactwg server-components · Discussion #5
RSC From Scratch. Part 1: Server Components In this technical deep dive, we'll implement a very simplified version of React Server Components (RSC) from scratch. This deep dive will be published in...
github.com
구현하기 전
역할 분리를 위해서 다음과 같이 두개의 서버로 구현했습니다.
실제로 두 서버는 nextjs에서 하나의 프로세스로 통합되어 있습니다.
nextjs 다른 방식으로 보완
- Edge Runtime 설정으로 특정 라우트를 엣지에서 실행 가능
- ISR, 캐싱 등으로 RSC 결과를 캐시해서 DB 왕복을 줄임
- Vercel 같은 플랫폼에서 서버리스 함수 단위로 배치 위치를 조절
문서에서는 다음과 같이 서버를 나누면 이런 장점이 있을 수 있다고 합니다.
RSC 서버 — 데이터센터에 배치
- DB, API 등 데이터 접근이 필요
- 데이터센터 가까이 있어야 DB 쿼리 지연시간이 낮음
- 컴포넌트를 실행하고 JSX 트리를 만드는 무거운 연산 담당
SSR 서버 — 엣지(유저 가까이)에 배치
- RSC에서 받은 JSX를 HTML로 변환하거나 패스스루하는 가벼운 작업
- DB 접근이 필요 없음
- 유저와 가까울수록 HTML 응답이 빨리 도착
유저 (서울) ←── 가까움 ──→ SSR 서버 (엣지, 도쿄)
│
│ 내부 네트워크
▼
RSC 서버 (데이터센터, 미국)
│
▼
DB


다음 레포를 통해 확인 가능합니다.
https://github.com/sins051301/build-self-nextjs
GitHub - sins051301/build-self-nextjs
Contribute to sins051301/build-self-nextjs development by creating an account on GitHub.
github.com
초기 랜더링

### 1. 첫 페이지 로드 (SSR)
```
브라우저가 http://localhost:8080/ 요청
│
▼
SSR 서버 (ssr.js)
│
│ fetchFromRSC() → http://localhost:9090/ 요청
▼
RSC 서버 (rsc.js)
│
│ renderJSXToClientJSX(<Router />)
│ → 서버 컴포넌트(Router, BlogLayout 등) 실행
│ → 클라이언트 컴포넌트(Counter)는 "$C:Counter"로 마킹
│
│ JSON.stringify(clientJSX, stringifyJSX)
│ → Symbol(react.transitional.element) → "$RE"
│ → "$C:Counter" → "$$C:Counter" ($ 이스케이프)
│
▼ JSX JSON 응답
SSR 서버
│
│ renderJSXToHTML(clientJSX) → HTML 문자열 생성
│ + <script>window.__INITIAL_CLIENT_JSX_STRING__ = ...</script> 삽입
│
▼ HTML 응답
브라우저
│
│ 1. HTML 화면 표시 (빠른 첫 화면)
│ 2. client.bundle.js 로드
│ 3. getInitialClientJSX()로 초기 JSX 파싱
│ → "$RE" → Symbol 복원
│ → "$$C:Counter" → clientComponentMap["Counter"] (실제 함수)
│ 4. hydrateRoot(document, clientJSX) → React가 DOM에 연결
│ 5. Counter 컴포넌트의 useState, onClick 활성화
▼
인터랙티브한 페이지 완성
```
ssr 서버의 요청
// RSC 서버에서 JSX JSON을 가져옴
async function fetchFromRSC(url) {
const rscUrl = new URL(url.pathname, RSC_URL);
const response = await fetch(rscUrl);
if (!response.ok) {
const err = new Error(`RSC server responded with ${response.status}`);
err.statusCode = response.status;
throw err;
}
return await response.text();
}
아래는 데이터 예시
export function BlogLayout({ children }) {
const author = "Jae Doe";
return (
<html>
<head>
<title>My blog</title>
</head>
<body>
<nav>
<a href="/">Home</a>
<hr />
<input />
<Counter />
<hr />
</nav>
<main>
{children}
</main>
<Footer author={author} />
<script src="/client.js"></script>
</body>
</html>
);
}
rsc 서버의 직렬화
- renderJSXToClientJSX는 서버 컴포넌트를 실행해서 JSX 객체 트리를 만듭니다.
- 이 함수는 jsx객체를 $$typeof: jsx.$$typeof (Symbol.for("react.transitional.element"))으로 반환한다.
- type: "$C:" + componentName 클라이언트 컴포넌트의 경우 다음과 같이 표시하고 진행한다.
이때 $C:는 이 학습용 프로젝트에서 임의로 정한 접두사입니다. React 내부에서는 $L, $ 같은 다른 마커를 사용합니다.
//구조적으로 이 함수는 와 유사 renderJSXToHTML하지만, HTML 대신 객체를 순회하고 반환합니다.
//renderJSXToClientJSX: 이후 페이지 이동 시 CSR용 → JSX를 객체 트리로 변환해서 React root.render()에 전달
import { clientComponentMap } from "./clientComponentMap.js";
export async function renderJSXToClientJSX(jsx) {
if (jsx == null || typeof jsx === "boolean" || typeof jsx === "string" || typeof jsx === "number") {
// 원시 값은 그대로 반환 (클라이언트에서 React가 처리)
return jsx;
} else if (Array.isArray(jsx)) {
return await Promise.all(jsx.map((child) => renderJSXToClientJSX(child)));
} else if (typeof jsx === "object") {
if (jsx.$$typeof === Symbol.for("react.transitional.element")) {
const props = jsx.props ?? {};
if (typeof jsx.type === "function") {
const Component = jsx.type;
const componentName = Component.name;
// 클라이언트 컴포넌트: 실행하지 않고 참조로 남김
// 예: <Counter /> → { type: "$C:Counter", props: {...} }
// 클라이언트에서 이 참조를 실제 컴포넌트로 교체하여 렌더링 - 클라이언트 컴포넌트 맵
if (componentName in clientComponentMap) {
const resolvedProps = {};
for (const [propName, value] of Object.entries(props)) {
resolvedProps[propName] = await renderJSXToClientJSX(value);
}
return {
$$typeof: jsx.$$typeof,
type: "$C:" + componentName,
props: resolvedProps,
ref: jsx.ref,
};
}
// 서버 컴포넌트: 실행하여 해석
const returnedJsx = await Component(props);
return await renderJSXToClientJSX(returnedJsx);
}
// HTML 엘리먼트는 JSX 객체 형태로 유지하되, children만 재귀 처리
const resolvedProps = {};
for (const [propName, value] of Object.entries(props)) {
resolvedProps[propName] = await renderJSXToClientJSX(value);
}
return {
$$typeof: jsx.$$typeof,
type: jsx.type,
props: resolvedProps,
ref: jsx.ref,
};
} else {
// $$typeof가 없는 일반 객체 - 내부에 JSX가 있을 수 있으므로 재귀 처리
// 예: BlogLayout의 props 객체 { children: <Main />, author: "Jae Doe" }
// → children에 JSX 컴포넌트가 있으므로 해석 필요
// → { children: { $$typeof: ..., type: "main", ... }, author: "Jae Doe" }
return Object.fromEntries(
await Promise.all(
Object.entries(jsx).map(async ([propName, value]) => [
propName,
await renderJSXToClientJSX(value),
])
)
);
}
} else {
throw new Error(`Not implemented: unsupported JSX type "${typeof jsx}".`);
}
}
2. JSON.stringify(clientJSX, stringifyJSX)가 그 트리를 직렬화합니다.
이때 Symbol은 JSON으로 변환이 안되기 때문에 직렬화가 필요합니다.
JSON.stringify(Symbol.for("react.transitional.element")) // undefined (사라짐)
//React가 JSX 객체의 식별자로 일부러 Symbol을 선택했다는 뜻입니다.
//Symbol은 JSON.stringify에서 자동으로 사라지니까,
//외부에서 악의적인 JSON을 보내더라도$$typeof가 Symbol이 아닌 이상 React가 JSX로 인식하지 않습니다.
//핵심은 Symbol.for('react.element')`<Symbol>`과 같은 심볼 값이 JSON 직렬화 과정에서 제거됩니다.
그래서 "$RE" 문자열로 대체하고, 클라이언트에서 다시 Symbol로 복원합니다.
직렬화 함수
function stringifyJSX(key, value) {
if (value === Symbol.for("react.transitional.element")) {
return "$RE";
} else if (typeof value === "string" && value.startsWith("$")) {
return "$" + value;
} else {
return value;
}
}
직렬화 이전 모습
{
$$typeof: Symbol.for("react.transitional.element"),
type: "html",
props: {
children: [
{
$$typeof: Symbol.for("react.transitional.element"),
type: "head",
props: {
children: {
$$typeof: Symbol.for("react.transitional.element"),
type: "title",
props: { children: "My blog" },
ref: null
}
},
ref: null
},
{
$$typeof: Symbol.for("react.transitional.element"),
type: "body",
props: {
children: [
{
$$typeof: Symbol.for("react.transitional.element"),
type: "nav",
props: {
children: [
// ... 생략 ...
{
$$typeof: Symbol.for("react.transitional.element"),
type: "$C:Counter", // 아직 $ 하나
props: {},
ref: null
},
]
}
},
]
}
}
]
}
}
직렬화 시킨 모습
{
"$$typeof": "$RE", // Symbol → "$RE"
"type": "html",
"props": {
"children": [
{
"$$typeof": "$RE",
"type": "head",
"props": {
"children": {
"$$typeof": "$RE",
"type": "title",
"props": { "children": "My blog" },
"ref": null
}
},
"ref": null
},
{
"$$typeof": "$RE",
"type": "body",
"props": {
"children": [
{
"$$typeof": "$RE",
"type": "nav",
"props": {
"children": [
// ... 생략 ...
{
"$$typeof": "$RE",
"type": "$$C:Counter", // 클라이언트 컴포넌트 마킹
"props": {},
"ref": null
},
]
}
},
]
}
}
]
}
}
- 리액트 심볼 타입의 경우 $RE로 직렬화를 진행합니다.
- 클라이언트 컴포넌트의 경우 $$C:Counter로 $를 하나 더 붙여 진행합니다.
다음과 같은 경우 때문에 $를 하나 더붙여 일반 문자열인지, 클라이언트 컴포넌트 참조인지를 구분 가능하게끔 만드는 것입니다.
{ "type": "$C:Counter" } // 클라이언트 컴포넌트 참조
{ "children": "$C:Counter" } // 누군가 실제로 "$C:Counter"라는 텍스트를 쓴 경우
ssr서버의 역직렬화
다음과 같이 역직렬화 과정을 거치게 됩니다. 먼저 ssr랜더링부터 보겠습니다.
- parseJSX — JSON 문자열 → JSX 객체 복원 (역직렬화)
- renderJSXToHTML — 복원된 JSX 객체 → HTML 문자열 생성
역직렬화 함수
const clientJSX = JSON.parse(clientJSXString, parseJSX);
// RSC 서버에서 받은 JSX JSON을 파싱할 때 Symbol 복원
function parseJSX(key, value) {
if (value === "$RE") {
return Symbol.for("react.transitional.element");
} else if (typeof value === "string" && value.startsWith("$$")) {
return value.slice(1);
} else {
return value;
}
}
{
$$typeof: Symbol(react.transitional.element),
type: 'html',
props: {
children: [
{
$$typeof: Symbol(react.transitional.element),
type: 'head',
props: {
children: {
$$typeof: Symbol(react.transitional.element), -> 타입 심볼형태로 역직렬화
type: 'title',
props: { children: 'My blog' }, // ← 문자열 그대로
ref: null
}
},
ref: null
},
{
$$typeof: Symbol(react.transitional.element),
type: 'body',
props: {
children: [
{
$$typeof: Symbol(react.transitional.element),
type: 'nav',
props: {
children: [
// ... a, hr, input 생략 ...
{
$$typeof: Symbol(react.transitional.element),
type: '$C:Counter', // 참조 클라이언트 컴포넌트 마킹
props: {},
ref: null
},
// ...
]
}
},
// ... main, footer, script 생략 ...
]
}
}
]
}
}
다음의 함수를 통해 jsx객체를 복원한 것을 html 문자열로 바꾸게 됩니다.
크게 가져갈 부분은 3개 입니다.
- 재귀적으로 html문자열로 바뀌게 된다.
- 클라이언트 컴포넌트의 경우 빈 문자열을 통해 위치를 비워둔다.
- escpeHtml함수를 통해 xss공격을 막고 있다.
"빈 문자열로 비우면 Counter가 화면에 아예 안 보이는 거 아닌가?" → hydration 전까지는 비어있습니다.
따라서 콘솔 보면 하이드레이션 에러가 발생합니다.

escapeHtml('<script>alert("xss")</script>')
// 결과: "<script>alert("xss")</script>"
//renderJSXToHTML: 첫 페이지 로드 시 SSR용 → JSX를 HTML 문자열로 변환
import escapeHtml from "escape-html";
export async function renderJSXToHTML(jsx) {
try {
if (jsx == null || typeof jsx === "boolean") {
return "";
} else if (typeof jsx === "string" || typeof jsx === "number") {
return escapeHtml(String(jsx));
} else if (Array.isArray(jsx)) {
return (await Promise.all(jsx.map((child) => renderJSXToHTML(child)))).join("");
} else if (typeof jsx === "object") {
const $typeof = jsx.$$typeof;
const isReactElement = $typeof === Symbol.for("react.transitional.element");
if (isReactElement) {
const props = jsx.props ?? {};
const FRAGMENT = Symbol.for("react.fragment");
if (jsx.type === FRAGMENT) {
return await renderJSXToHTML(props.children);
}
// type이 함수인 경우 = 커스텀 컴포넌트 (서버 컴포넌트)
// 예: <BlogLayout children={...}> → type은 BlogLayout 함수
// → BlogLayout({ children: ... })를 호출하면 <html><body>...</body></html> JSX를 반환
// → 반환된 JSX를 다시 renderJSXToHTML로 재귀 처리하여 HTML 문자열로 변환
// "div", "html" 같은 문자열 type은 아래에서 바로 HTML 태그로 렌더링하지만,
// 함수 type은 실행해야 실제 렌더링할 JSX를 알 수 있음
if (typeof jsx.type === "function") {
const Component = jsx.type;
const props = jsx.props;
const returnedJsx = await Component(props);
return await renderJSXToHTML(returnedJsx);
}
// 클라이언트 컴포넌트 참조: 서버에서는 렌더링할 수 없으므로
// placeholder를 삽입하고 클라이언트에서 hydration 시 실제 컴포넌트로 교체
if (typeof jsx.type === "string" && jsx.type.startsWith("$C:")) {
return "";
}
let html = "<" + jsx.type;
for (const propName in props) {
if (
Object.prototype.hasOwnProperty.call(props, propName) &&
propName !== "children"
) {
html += " ";
html += propName;
html += "=";
html += escapeHtml(String(props[propName]));
}
}
html += ">";
html += await renderJSXToHTML(props.children);
html += "</" + jsx.type + ">";
return html;
}
} else {
const err = new Error(`Not implemented: unsupported JSX type "${typeof jsx}".`);
err.statusCode = 500;
throw err;
}
} catch (e) {
if (!e.statusCode) {
e.statusCode = 500;
}
throw e;
}
}
브라우저단 js bundle 요청을 통한 csr
아까 ssr 랜더링 시에 위치를 비워 놓았음으로 이제 하이드레이션을 통한 csr이 일어나야 합니다.
먼저 역직렬화가 된 뒤에 하이드레이션이 일어나게 됩니다.
const root = hydrateRoot(document, getInitialClientJSX());
//getInitialClientJSX의 경우 jsx트리를 제공하여 클라이언트 컴포넌트가 하이드레이트 가능하게 만듭니다.
function getInitialClientJSX() {
// 서버가 HTML에 심어둔 초기 JSX JSON 문자열을 파싱하여 hydration에 사용
const raw = window.__INITIAL_CLIENT_JSX_STRING__;
const clientJSX = JSON.parse(raw, parseJSX);
return clientJSX;
}
- 역직렬화를 한다.
function parseJSX(key, value) {
if (value === "$RE") {
//아까 본 $RE 가 리액트 엘리먼트 타입으로 바뀜
return Symbol.for("react.transitional.element");
} else if (typeof value === "string" && value.startsWith("$$C:")) {
// 클라이언트 컴포넌트 참조: "$$C:Counter" (서버에서 $이스케이프됨) → 실제 Counter 컴포넌트로 교체
const componentName = value.slice(4);
return clientComponentMap[componentName]; //클라이언트 컴포넌트 맵
} else if (typeof value === "string" && value.startsWith("$$")) {
// This is a string starting with $. Remove the extra $ added by the server.
return value.slice(1);
} else {
return value;
}
}
2. 클라이언트 컴포넌트 맵을 거친다.
// 클라이언트 컴포넌트 맵
// 서버: 이 맵에 등록된 컴포넌트는 실행하지 않고 참조("$C:이름")
// 클라이언트: 참조를 받으면 이 맵에서 실제 컴포넌트를 찾아 렌더링
//클라이언트: parseJSX가 "$C:Counter"를 만나면 실제 Counter 함수로 교체
//React가 렌더링: 교체된 Counter 함수를 브라우저에서 실행 → useState, onClick 동작
import { Counter } from "./Counter.js";
// 클라이언트 컴포넌트를 추가하려면 여기에 등록
export const clientComponentMap = {
Counter,
};
3.html을 하이드레이션 한다.
React가 hydrateRoot를 실행할 때 이 객체를 만나면 Counter 함수를 호출해서 그 위치에 렌더링합니다.
hydrateRoot: React DOM이 제공하는 함수로, 이미 존재하는 HTML에 React를 연결합니다.
// "$$C:Counter" → clientComponentMap["Counter"] → 실제 Counter 함수
{
$$typeof: Symbol(react.transitional.element),
type: Counter, // ← 문자열이 아니라 실제 함수
props: {},
ref: null
}
페이지 이동 시

이때의 차이점은 SSR 서버는 RSC 서버의 JSON을 그대로 전달만 합니다.
### 2. 클라이언트 사이드 네비게이션 (CSR)
```
유저가 링크 클릭 (예: /hello-world)
│
│ e.preventDefault() → 브라우저 기본 새로고침 방지
│ history.pushState() → URL만 변경
│
▼
client.js의 navigate() 함수
│
│ fetch("/hello-world?jsx") → SSR 서버에 JSX 요청
▼
SSR 서버 (ssr.js)
│
│ ?jsx 파라미터 감지 → HTML 대신 JSON으로 응답
│ fetchFromRSC() → RSC 서버에서 JSX JSON 가져옴
│
▼ JSX JSON 패스스루
client.js
│
│ JSON.parse(response, parseJSX)
│ → Symbol 복원 + 클라이언트 컴포넌트 교체
│
│ root.render(clientJSX)
│ → React가 기존 DOM과 비교(diffing)
│ → 바뀐 부분만 업데이트
│ → <input> 값 등 기존 상태 유지
▼
새로고침 없이 페이지 전환 완료
```
아래를 보면 초기 랜더링 시에는 client.js를 통해 번들링을 가져옵니다.
이동 시에는 파일이름?jsx로 이동하고 요청하게 됩니다.
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === "/client.js") {
// 정적 파일 서빙 (번들된 클라이언트 JS)
await sendScript(res, "./client.bundle.js");
} else if (url.searchParams.has("jsx")) {
// 클라이언트 사이드 네비게이션용: RSC 서버에서 받은 JSX JSON을 그대로 패스스루
url.searchParams.delete("jsx");
const clientJSXString = await fetchFromRSC(url);
res.setHeader("Content-Type", "application/json");
res.end(clientJSXString);
}
client.js를 통해 다음의 navigate가 라우팅을 통해 클라이언트 랜더링을 하게끔 합니다.
async function navigate(pathname) {
currentPathname = pathname;
const clientJSX = await fetchClientJSX(pathname);
if (pathname === currentPathname) {
root.render(clientJSX);
}
}
window.addEventListener("click", (e) => {
// Only listen to link clicks.
if (e.target.tagName !== "A") {
return;
}
// Ignore "open in a new tab".
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
// Ignore external URLs.
const href = e.target.getAttribute("href");
if (!href.startsWith("/")) {
return;
}
// Prevent the browser from reloading the page but update the URL.
e.preventDefault();
window.history.pushState(null, null, href);
// Call our custom logic.
navigate(href);
}, true);
window.addEventListener("popstate", () => {
// When the user presses Back/Forward, call our custom logic too.
navigate(window.location.pathname);
});
클라이언트의 역직렬화
이동을 하고 rsc에서 직렬화된 json 응답을 받은 후 다음과 같이 클라이언트에서 역직렬화 함수로 역직렬화를 하게 됩니다.
ssr서버는 응답을 받은 뒤 그대로 브라우저로 넘기게 됩니다.
async function fetchClientJSX(pathname) {
const response = await fetch(pathname + "?jsx");
const clientJSXString = await response.text();
const clientJSX = JSON.parse(clientJSXString, parseJSX);
return clientJSX;
}
브라우저는 응답을 받아 아까 봤던 역직렬화 함수를 실행하게 됩니다.
function parseJSX(key, value) {
if (value === "$RE") {
//아까 본 $RE 가 리액트 엘리먼트 타입으로 바뀜
return Symbol.for("react.transitional.element");
} else if (typeof value === "string" && value.startsWith("$$C:")) {
// 클라이언트 컴포넌트 참조: "$$C:Counter" (서버에서 $이스케이프됨) → 실제 Counter 컴포넌트로 교체
const componentName = value.slice(4);
return clientComponentMap[componentName]; //클라이언트 컴포넌트 맵
} else if (typeof value === "string" && value.startsWith("$$")) {
// This is a string starting with $. Remove the extra $ added by the server.
return value.slice(1);
} else {
return value;
}
}
다음과 같이 alert를 띄어보면 어떤식으로 이제 응답을 받는지 볼 수 있습니다.
async function navigate(pathname) {
currentPathname = pathname;
const clientJSX = await fetchClientJSX(pathname);
if (pathname === currentPathname) {
alert(JSON.stringify(clientJSX, null, 2));
root.render(clientJSX);
}
}
이동시에 응답 받는 형태
{
"type": "html",
"props": {
"children": [
{
"type": "head",
"props": {
"children": {
"type": "title",
"props": { ... }
}
},
"ref": null
},
{
"type": "body",
"props": {
"children": [
{
"type": "nav",
"props": { ... }
},
...
]
},
"ref": null
}
]
}
}
root.render(clientJSX)가 새 JSX 트리를 받으면 React가 기존 DOM과 비교(diffing)해서 바뀐 부분만 업데이트합니다.
root.render: 리액트 내부 구현함수로 React DOM의 reconciler(재조정기)가 새 JSX 트리와 현재 DOM을 비교해서 최소한의 변경만 적용합니다.
async function navigate(pathname) {
currentPathname = pathname;
const clientJSX = await fetchClientJSX(pathname);
if (pathname === currentPathname) {
root.render(clientJSX);
}
}
결론
nextjs는 rsc서버의 직렬화와 ssr 서버 또는 클라이언트의 역직렬화의 반복이라고 볼 수 있습니다.
서버와 브라우저는 서로 다른 환경이라 JSX 객체를 직접 주고받을 수 없기 때문입니다. Symbol은 네트워크로 전송이 안 되고, 함수도 JSON으로 보낼 수 없습니다. 그래서 직렬화로 변환하고, 받는 쪽에서 역직렬화로 복원하는 과정이 필수입니다
stringifyJsx - parseJsx
| 단계 | 누가 | 뭘하나 |
| 직렬화 | RSC 서버 | JSX 객체 → JSON 문자열 |
| 역직렬화 | SSR 서버 | JSON → JSX 객체 → HTML로 렌더링 (첫 페이지) |
| 역직렬화 | 클라이언트 | JSON → JSX 객체 → hydrateRoot()로 DOM 연결 (첫 페이지) |
| 역직렬화 | 클라이언트 | JSON → JSX 객체 → root.render()로 DOM 업데이트 (네비게이션) |
미구현된 것들..
- JSON 대신 RSC Payload라는 스트리밍 형식 사용 (줄 단위로 점진적 전송)
- clientComponentMap을 수동 등록하는 대신 "use client" 지시어로 자동 처리
- 번들링, 코드 스플리팅, 캐싱 등 최적화가 추가됨
방식은 유사하되 다음과 같이 rsc 페이로드를 통해 스트리밍이 가능합니다.
// 우리 프로젝트: JSON 한 번에 전송
{"$$typeof":"$RE","type":"html","props":{"children":[...]}}
rsc페이로드
Transfer-Encoding: chunked입니다. HTTP 응답의 전체 크기를 미리 알려주지 않고, 준비된 청크 단위로 계속 보내다가 끝나면 종료 신호를 보내는 방식입니다.
// RSC Payload: 줄 단위로 스트리밍
0:["$","html",null,{"children":[["$","head",null,...],["$","body",null,...]]}]
1:["$","div",null,{"children":"Hello World"}]
2:["$","$Lcounter",null,{"count":0}]'코딩 정보 > NextJs' 카테고리의 다른 글
| next/dynamic 세부 동작을 통해 모듈 설계하기 (0) | 2026.05.10 |
|---|---|
| nextjs에서 토큰 관리하기 (0) | 2026.03.09 |
| [개념부터 NextJs] 페이지 라우터와 앱라우터 차이 (0) | 2025.12.03 |
| 서버 컴포넌트에서 쿼리 파라미터 프롭스 드릴링 해결하기 (0) | 2025.10.13 |
| nextjs 배포 하기 전 개념부터 (0) | 2025.10.06 |