Skip to content

Commit 68546f1

Browse files
committed
feat: 전체적인 구조 변경
1 parent ddad3d1 commit 68546f1

File tree

24 files changed

+506
-376
lines changed

24 files changed

+506
-376
lines changed

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,33 @@ We're going to write super simple React Steaming SSR(Server Side Rendering).
1010

1111
Please follow these steps to get started:
1212

13-
1. Clone this repository
13+
### Installation
1414

15-
```bash
16-
git clone https://github.com/mugglim/build-your-own-rsc-framework
17-
cd build-your-own-rsc-framework
18-
```
15+
- Clone this repository
16+
```bash
17+
git clone https://github.com/mugglim/build-your-own-rsc-framework
18+
cd build-your-own-rsc-framework
19+
```
20+
- Install package
21+
```bash
22+
npm install
23+
```
1924

20-
2. Install package
25+
### Run dev server
2126

2227
```bash
23-
npm install
28+
# The dev server will run on https://localhost:3000 by default
29+
npm run dev
2430
```
2531

26-
3. Run dev server
32+
Now you can visit https://localhost:3000 in the browser.
33+
34+
### Run docs dev server
2735

2836
```bash
29-
# The dev server will run on https://localhost:3000 by default
30-
npm run dev
37+
npm run docs:dev
3138
```
3239

33-
4. Now you can visit https://localhost:3000 in the browser.
34-
3540
## Reference
3641

3742
- [React server components from scratch!](https://www.youtube.com/watch?v=MaebEqhZR84)

docs/.vitepress/config.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,27 @@ export default defineConfig({
1010
},
1111
themeConfig: {
1212
siteTitle: "Home",
13+
outline: {
14+
level: "deep"
15+
},
1316
search: {
1417
provider: "local"
1518
},
16-
nav: [{ text: "학습하기", link: "/intro" }],
19+
nav: [
20+
{ text: "소개", link: "/intro" },
21+
{ text: "학습하기", link: "/what-is-streaming-ssr" }
22+
],
1723
sidebar: [
1824
{
1925
text: "가이드",
2026
items: [{ text: "소개", link: "/intro" }]
27+
},
28+
{
29+
text: "학습하기",
30+
items: [
31+
{ text: "Streaming SSR 과정 이해하기", link: "/what-is-streaming-ssr" },
32+
{ text: "Streaming SSR 구현하기", link: "/streaming-ssr" }
33+
]
2134
}
2235
],
2336
socialLinks: [{ icon: "github", link: "https://github.com/mugglim/build-your-own-react-streaming-ssr" }]

docs/index.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ hero:
66
tagline: "밑바닥부터 구현해보기"
77
actions:
88
- theme: brand
9-
text: 학습하기
9+
text: 소개
1010
link: /intro
11-
11+
- theme: brand
12+
text: 학습하기
13+
link: /what-is-streaming-ssr
14+
- theme: alt
15+
text: View on Github
16+
link: https://github.com/mugglim/build-your-own-react-streaming-ssr
1217
features:
1318
- title: 직접 만들어보는 경험
1419
details: 밑바닥부터 구현해보면서 React Streaming SSR의 이해도를 높일 수 있습니다.
15-
- title: Node.js stream
16-
details: Node.js stream을 직접 사용해봅니다.
1720
- title: React Server API
1821
details: Node.js 환경에서 React Server API를 직접 사용해봅니다.
22+
- title: Node.js stream
23+
details: Node.js stream을 직접 사용해봅니다.
1924
---

docs/intro.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,38 @@
11
# 소개
22

3-
WIP.
3+
React Streaming SSR(Server Side Rendering)을 소개합니다.
4+
5+
## 목표
6+
7+
- React streaming SSR 구조를 이해합니다.
8+
- SSR 기반으로 서버에서 컴포넌트를 렌더링 하는 방법을 소개합니다.
9+
10+
## 개발 환경 구성하기
11+
12+
```bash
13+
## 1. Repositry를 clone 합니다.
14+
git clone https://github.com/mugglim/build-your-own-rsc-framework
15+
16+
## 2. 의존성을 설치합니다.
17+
npm install
18+
19+
## 3. 개발 서버을 실행합니다.
20+
## 개발 서버는 기본적으로 https://localhost:3000 로 실행됩니다.
21+
npm run dev
22+
```
23+
24+
## 구현 코드
25+
26+
최종적으로 구현된 코드는 [Github](https://github.com/mugglim/build-your-own-react-streaming-ssr)을 참고해주세요.
27+
28+
## 참고 자료
29+
30+
- [React server components from scratch!](https://www.youtube.com/watch?v=MaebEqhZR84)
31+
- [The Forensics Of React Server Components (RSCs)](https://www.smashingmagazine.com/2024/05/forensics-react-server-components/)
32+
- [[한국어] 리액트 서버 컴포넌트 톺아보기 (번역)](https://roy-jung.github.io/250323-react-server-components/)
33+
- [SSR의 기쁨과 슬픔: 렌더링의 변화의 흐름을 통해 알아보는 SSR과 Streaming SSR | 인프콘2023](https://www.youtube.com/watch?v=hPyyFu3lrEg)
34+
- [Sample Code](https://github.com/rotoshine/infcon2023-sample-streaming-ssr/tree/main)
35+
- [TanStack Start Docs](https://tanstack.com/start/latest/docs/framework/react/overview)
36+
- [react-start](https://github.com/TanStack/router/tree/main/packages/react-start)
37+
- [react-start-client](https://github.com/TanStack/router/tree/main/packages/react-start-client)
38+
- [react-start-server](https://github.com/TanStack/router/tree/main/packages/react-start-server)

docs/public/flow.png

531 KB
Loading
323 KB
Loading
60.3 KB
Loading

docs/streaming-ssr.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Streaming SSR 구현하기
2+
3+
## 목표
4+
5+
- Hono로 Node.js 서버를 구성합니다.
6+
- Streaming SSR 구조를 작성하고 결과를 분석해봅니다.
7+
8+
전체적인 구조는 다음과 같습니다.
9+
10+
<img src="./flow.png">
11+
12+
## 서버 구성하기
13+
14+
Node.js에서 Hono를 사용해 서버를 실행해봅시다.
15+
16+
```ts
17+
/** server.ts */
18+
19+
import { serve } from "@hono/node-server";
20+
import { Hono } from "hono";
21+
22+
const app = new Hono();
23+
24+
// 3000 포트로 서버를 실행합니다.
25+
serve(app, (info) => {
26+
console.log(`Server is running on http://localhost:${info.port}`);
27+
});
28+
29+
app.get("/", (ctx) => {
30+
return ctx.text("Hello World");
31+
});
32+
```
33+
34+
개발 서버를 실행한 후, http://localhost:3000에 접속하면 "Hello World" 문자열이 반환됩니다.
35+
36+
## 컴포넌트 작성하기
37+
38+
서버에서 렌더링 할 컴포넌트를 작성해봅시다. 컴포넌트는 구조는 다음과 같습니다.
39+
40+
- App: 최상위 컴포넌트입니다.
41+
- TodoList: 비동기로 작성된 컴포넌트입니다. 내부의 비동기 작업은 2초 뒤 종료됩니다.
42+
43+
```tsx
44+
/** client.ts */
45+
46+
import React, { Suspense } from "react";
47+
import { getTodoList } from "./lib";
48+
49+
async function TodoList() {
50+
const todoList = await getTodoList();
51+
52+
return (
53+
<ul>
54+
{todoList.map((todoItem) => {
55+
return <li key={todoItem.id}>{todoItem.title}</li>;
56+
})}
57+
</ul>
58+
);
59+
}
60+
61+
function App() {
62+
return (
63+
<div>
64+
<h1>Todo List</h1>
65+
<Suspense fallback={<div>loading...</div>}>
66+
<TodoList />
67+
</Suspense>
68+
</div>
69+
);
70+
}
71+
72+
export default App;
73+
```
74+
75+
App 컴포넌트를 서버에서 렌더링하면 렌더링 결과는 2단계로 나눌 수 있습니다.
76+
77+
- 초기 렌더링: loading... 문구가 표시됩니다.
78+
- 최종 렌더링: TodoList 데이터가 표시됩니다.
79+
80+
### 초기 렌더링
81+
82+
```html
83+
<!DOCTYPE html>
84+
<html>
85+
<body>
86+
<div>
87+
<h1>Todo List</h1>
88+
<div>loading...</div>
89+
</div>
90+
</body>
91+
</html>
92+
```
93+
94+
### 최종 렌더링 (TodoList 비동기 작업 종료 후 시점)
95+
96+
```html
97+
<!DOCTYPE html>
98+
<html>
99+
<body>
100+
<div>
101+
<h1>Todo List</h1>
102+
<div>
103+
<h1>Todo List</h1>
104+
// [!code --]
105+
<div>loading...</div>
106+
// [!code ++]
107+
<ul>
108+
// [!code ++]
109+
<!-- `getTodoList` 기반으로 생성된 li 목록 ... -->
110+
// [!code ++]
111+
<li></li>
112+
// [!code ++]
113+
</ul>
114+
</div>
115+
</div>
116+
</body>
117+
</html>
118+
```
119+
120+
이제 서버에서 React 컴포넌트를 streaming 렌더링하는 방법을 살펴봅시다.
121+
122+
## 서버에서 렌더링하기
123+
124+
React는 서버에서 컴포넌트를 렌더링 하기 위해 `react-dom/server` 기반의 [서버 API](https://ko.react.dev/reference/react-dom/server)를 제공합니다. Node.js를 사용하는 경우 [renderToPipeableStream](https://ko.react.dev/reference/react-dom/server/renderToPipeableStream) API를 사용해야 합니다.
125+
126+
### stream 준비하기
127+
128+
App 컴포넌트를 stream 형태로 렌더링하고, 브라우저로 전송할 준비합니다.
129+
130+
```ts
131+
/** server.ts */
132+
133+
import { PassThrough, Readable } from "stream";
134+
135+
import ReactDomServer from "react-dom/server";
136+
137+
import App from "./client";
138+
139+
router.get("/", async () => {
140+
const element = createElement(App);
141+
142+
const reactAppPassThrough = new PassThrough();
143+
const reactAppStream = Readable.toWeb(reactAppPassThrough);
144+
145+
const { pipe, abort } = ReactDomServer.renderToPipeableStream(element, {
146+
onShellReady() {
147+
pipe(reactAppPassThrough);
148+
},
149+
onError(error) {
150+
reactAppPassThrough.destroy();
151+
abort(error);
152+
}
153+
});
154+
});
155+
```
156+
157+
> [!TIP] onShellReady을 무엇인가요?
158+
>
159+
> - `onShellReady` 함수는 서버에서 [초기 렌더링 결과(Shell)](https://ko.react.dev/reference/react-dom/server/renderToPipeableStream#specifying-what-goes-into-the-shell)가 생성했을 때 호출되는 콜백입니다.
160+
> - `onShellReady` 함수가 호출되기 전 HTML의 기본구조 `<!DOCTYPE html>`, `<html>` 등을 chunk로 전송해야 합니다.
161+
162+
> [!TIP] 왜 PassThrough을 사용하나요?
163+
> stream 을 직접 제어하려고 [PassThrough](https://nodejs.org/api/stream.html#class-streampassthrough) 기반으로 stream을 중계합니다.
164+
165+
### 브라우저에 stream 전달하기
166+
167+
stream 기반의 통신을 위해서 데이터의 디코딩, 인코딩이 필요합니다. 관련 로직을 다음과 같이 작성해봅시다.
168+
169+
```ts
170+
const textDecoder = new TextDecoder();
171+
const textEncoder = new TextEncoder();
172+
173+
// 인코딩 된 데이터를 디코딩 하는 함수
174+
const decodeChunk = (chunk: unknown) => {
175+
if (chunk instanceof Uint8Array) {
176+
return textDecoder.decode(chunk);
177+
}
178+
179+
return String(chunk);
180+
};
181+
```
182+
183+
브라우저에 전달할 stream 생성하여 브라우저에 반환해봅시다. stream이 데이터를 전송하는 시점은 3가지로 분리할 수 있습니다.
184+
185+
1. 브라우저 stream 생성: HTML 시작 태그 chunk를 전송합니다.
186+
2. 중간: React 컴포넌트에서 생성된 chunk를 전송합니다.
187+
3. React stream 종료: HTML 종료 태그 chunk를 전송합니다.
188+
189+
```ts
190+
const header = `<!DOCTYPE html><html><head><title>Hello World</title></head><body><div id="root"></div>`;
191+
const trailer = `</body></html>`;
192+
193+
app.get("/", async () => {
194+
// ...
195+
196+
const onResponseStreamStart = async (controller: ReadableStreamDefaultController<unknown>) => {
197+
// [1] HTML 시작 태그 chunk 전달
198+
// // <!DOCTYPE html><html><head><title>Hello World</title></head><body><div id="root"></div>
199+
controller.enqueue(textEncoder.encode(header));
200+
201+
// [2] React Stream chunk 전달
202+
try {
203+
let chunk;
204+
205+
const reader = reactAppStream.getReader();
206+
207+
// React 스트림 읽기
208+
while (true) {
209+
chunk = await reader.read();
210+
211+
if (chunk.done) {
212+
break;
213+
}
214+
215+
const decodedChunk = decodeChunk(chunk.value);
216+
controller.enqueue(textEncoder.encode(decodedChunk));
217+
}
218+
} catch (error) {
219+
controller.error(error);
220+
} finally {
221+
// [3] HTML 종료 태그 chunk 전달
222+
// </body></html>
223+
controller.enqueue(textEncoder.encode(trailer));
224+
controller.close();
225+
}
226+
};
227+
228+
const responseStream = new ReadableStream({ start: onResponseStreamStart });
229+
230+
return new Response(responseStream, {
231+
headers: {
232+
"Content-type": "text/html"
233+
}
234+
});
235+
```
236+
237+
## 렌더링 결과 분석하기
238+
239+
App 컴포넌트가 Streaming SSR 기반으로 렌더링 된 결과를 살펴봅시다.
240+
241+
### 초기 렌더링
242+
243+
Suspense 영역은 loading... 으로 표기되고, 나머지 영역이 DOM에 반영된 것을 확인할 수 있습니다.
244+
245+
<img src="./streaming-ssr-init-render.png">
246+
247+
### 최종 렌더링
248+
249+
초기 렌더링 후 2초가 뒤 TodoList 목록이 DOM에 표시된 것을 확인할 수 있습니다. 추가로 HTML 응답 결과를 확인해보면, `$RC` 함수를 통해 `B:0` 영역의 요소에 `S:0` 요소로 치환된 코드를 확인해볼 수 있습니다.
250+
251+
<img src="./streaming-ssr-final-render.png">
252+
253+
> [!TIP] B:0, S:0, $RC 태그는 무엇인가요?
254+
>
255+
> - `B:0`, `S:0`, `$RC` React Fizz에서 사용하는 용어입니다.
256+
> - Fizz는 서버에서 React 컴포넌트를 스트리밍 렌더링하는 시스템입니다.
257+
> - Fizz는 초기 HTML과 함께 B:0(Boundary 시작), S:0(Segment 시작), $RC(React Component 렌더링) 같은 명령어를 전송합니다.
258+
> - 브라우저는 스트림을 따라 준비된 컴포넌트를 점진적으로 렌더링하고, 서버에서도 Suspense를 지원합니다.
259+
> - 관련 코드는 https://github.com/facebook/react/tree/main/packages/react-server 에서 확인하실 수 있습니다.

0 commit comments

Comments
 (0)