06
자리는 선언 순으로, 내용은 완료 순으로
2장에서 응답이 세 덩어리로 온다는 것을 확인하고 넘어갔습니다.
| 시점 | 내용 | |
|---|---|---|
| 1 | 0.006초 | loading...과 빈 <template id="B:0"> |
| 2 | 2초 | <div hidden id="S:0"> 안에 담긴 목록 |
| 3 | 2초 | $RC("B:0","S:0") 스크립트 |
이번 장에서는 왜 그런 모양인지 짚어보려고 합니다.
출발점은 HTML의 특징입니다. HTML은 위에서 아래로 한 방향으로만 흐릅니다. 서버가 이미 보낸 부분은 고칠 수 없습니다.
그런데 스트리밍이 해야 하는 일은 이렇습니다.
0.006초 <div>loading...</div> <- 일단 이걸 보낸다
...
2초 이 자리를 실제 목록으로 바꾸고 싶다 <- 그런데 이미 보냈다
이미 브라우저로 나간 것을 어떻게 바꿀 수 있을까요.
HTML로는 불가능하지만 DOM은 수정할 수 있습니다.
hidden을 달아 안 보이게 한다.생각보다 단순했습니다. 뒤에 붙이고, 감추고, 옮기는 것뿐입니다.
0.006초 — 자리를 미리 잡는다
<div id="root">
<h1>Todo List</h1>
<!--$?--><template id="B:0"></template>
<div>loading...</div>
<!--/$-->
</div>
| 요소 | 역할 |
|---|---|
<div>loading...</div> |
fallback. 지금 화면에 보이는 것 |
<template id="B:0"> |
자리 표시자. 나중에 “B:0으로 보내라”고 지시할 때 쓸 이름표 |
<!--$?--> ... <!--/$--> |
영역 경계. 이 구간이 해당 Suspense의 구역 |
2초 — 콘텐츠가 문서 끝에 도착한다
<div hidden id="S:0">
<ul>
<li>Buy groceries</li>
...
</ul>
</div>
hidden이라 화면에 보이지 않습니다. 문서 맨 아래에 도착했습니다.
2초 — 교체 명령
<script>
$RC("B:0", "S:0");
</script>
$RC는 React가 만든 평범한 JS 함수입니다.
자리 표시자와 숨겨진 콘텐츠를 id로 찾아서, 후자를 전자의 자리로 옮깁니다.
DOM 조작 그 이상은 없습니다.
브라우저는 <script>를 만나면 즉시 실행하므로 도착하자마자 교체됩니다.
React 내부 렌더러(Fizz)의 자료구조 이름에서 왔습니다. Request 아래에 SuspenseBoundary가 있고, 그 아래에 Segment가 있는 구조입니다.
B — Boundary. Suspense 영역S — Segment. 그 영역에 채워질 HTML 덩어리<div id="root"><h1>Todo List</h1><!--$?--><template id="B:0"></template><div>loading...</div><!--/$--></div>
자리 표시자(B:0)와 fallback만 먼저 보낸다. 이 시점에 TodoList는 아직 완료되지 않았다.
지금까지 boundary가 하나뿐이라 “2초 뒤 한 번”밖에 보지 못했습니다. 지연 시간이 다른 컴포넌트 세 개를 배치해봤습니다.
선언 순서와 완료 순서를 일부러 어긋나게 두었습니다.
<Suspense fallback={...}><TodoList milliseconds={3000} /></Suspense> {/* B:0 */}
<Suspense fallback={...}><TodoList milliseconds={500} /></Suspense> {/* B:1 */}
<Suspense fallback={...}><TodoList milliseconds={1500} /></Suspense> {/* B:2 */}
먼저 완료되는 순서대로 B:1 → B:2 → B:0으로 도착할 것이라고 예상했습니다.
shell에는 자리가 세 개 미리 잡혔습니다.
<!--$?--><template id="B:0"></template>
<div>loading...</div>
<!--/$-->
<!--$?--><template id="B:1"></template>
<div>loading...</div>
<!--/$-->
<!--$?--><template id="B:2"></template>
<div>loading...</div>
<!--/$-->
B:0, B:1, B:2가 선언 순서대로 있습니다.
이 시점에 완료된 컴포넌트는 하나도 없는데도 자리는 다 잡혀 있습니다.
콘텐츠는 완료 순서로 도착했습니다.
0.5초 S:1 → $RC("B:1","S:1")
1.5초 S:2 → $RC("B:2","S:2")
3.0초 S:0 → $RC("B:0","S:0")
선언 순서(0, 1, 2)가 아니라 완료 순서(1, 2, 0)입니다.
아직 만들어지지 않은 것은 보낼 수 없기 때문입니다.
B:0은 3초가 걸립니다. 0.5초 시점에는 그 HTML이 존재하지 않습니다.
서버가 보내고 싶어도 보낼 것이 없습니다.
실제로 각 조각은 해당 컴포넌트의 지연 시간과 같은 시점에 도착했습니다.
선언 순서를 지키려면 가장 느린 것이 끝날 때까지 전부 기다려야 하는데,
그러면 스트리밍이 아닙니다. 그것이 곧 5장의 onAllReady입니다.
주소가 붙어 있기 때문입니다.
$RC("B:0","S:0")은 “맨 위 자리에 넣어라”입니다.
id로 찾아가므로 언제 도착하든 상관없습니다.
택배가 순서와 무관하게 각자 주소로 배달되는 것과 같습니다.
| 순서 | |
|---|---|
자리 (<template id="B:n">) |
선언 순 — 0, 1, 2 |
내용 (<div hidden id="S:n">) |
완료 순 — 1, 2, 0 |
| 최종 화면 | 선언 순 (주소로 찾아가므로) |
자리는 선언 순서로 미리 잡고, 내용은 완료 순서로 채웁니다. 이 분리가 스트리밍을 가능하게 합니다.