← 목차6 / 7

06

boundary가 여러 개일 때

자리는 선언 순으로, 내용은 완료 순으로

HTML은 되돌릴 수 없다

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초       이 자리를 실제 목록으로 바꾸고 싶다  <- 그런데 이미 보냈다

이미 브라우저로 나간 것을 어떻게 바꿀 수 있을까요.

그래서 JavaScript를 쓴다

HTML로는 불가능하지만 DOM은 수정할 수 있습니다.

  1. 완성된 콘텐츠를 문서 맨 아래에 붙여 보낸다 (뒤에 덧붙이는 것은 가능하므로).
  2. 그냥 붙이면 화면 아래에 뜬금없이 나타나므로 hidden을 달아 안 보이게 한다.
  3. 작은 스크립트를 보내 숨겨둔 것을 원래 자리로 옮긴다.

생각보다 단순했습니다. 뒤에 붙이고, 감추고, 옮기는 것뿐입니다.

응답을 다시 읽어보면

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>를 만나면 즉시 실행하므로 도착하자마자 교체됩니다.

B와 S는 무엇의 약자인가

React 내부 렌더러(Fizz)의 자료구조 이름에서 왔습니다. Request 아래에 SuspenseBoundary가 있고, 그 아래에 Segment가 있는 구조입니다.

출처: Basic Fizz Architecture (facebook/react#20970)

브라우저가 보는 문서
Todo List
B:0<!--$?-->미해결loading...
서버가 보낸 응답
<div id="root">
<h1>Todo List</h1>
<!--$?--><template id="B:0"></template>
<div>loading...</div><!--/$-->
</div>

자리 표시자(B:0)와 fallback만 먼저 보낸다. 이 시점에 TodoList는 아직 완료되지 않았다.

1 / 4

boundary를 여러 개 두면

지금까지 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
최종 화면 선언 순 (주소로 찾아가므로)

자리는 선언 순서로 미리 잡고, 내용은 완료 순서로 채웁니다. 이 분리가 스트리밍을 가능하게 합니다.