04
출력도 TTFB도 정상인데 화면만 다른 경우
스트리밍이 동작하는 것은 확인했습니다. 그런데 동작하지 않을 때는 어떻게 알아챌 수 있을까요.
3장에서 스트림에 개입하는 구조를 만들었습니다. 이 구조를 일부러 잘못 쓰면 스트리밍이 죽습니다. 그러면 무엇이 그것을 알려주는지 확인해봤습니다.
3장에서 만든 개입 구조는 이랬습니다.
while (true) {
const { done, value } = await reader.read();
if (done) break;
controller.enqueue(value); // 읽는 즉시 내보낸다
}
여기서 enqueue를 배열에 모으는 것으로 바꿔봅니다.
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value); // 모은다
}
for (const chunk of chunks) {
controller.enqueue(chunk); // 다 모인 뒤 한 번에
}
모았다가 한 번에 내보내면 스트리밍이 아니게 됩니다. 그러면 무엇이 달라지는지 확인해봤습니다.
예상했을 때는 TTFB가 2초로 튈 것으로 생각했습니다.
다 모이면 한 번에 처리하는 것이니, getTodoList가 완료되기 전까지는 화면에 아무것도 보이지 않겠다고 이해했기 때문입니다.
curl -N localhost:3001
3장에서 본 응답과 한 글자도 다르지 않았습니다.
서버가 만드는 HTML은 달라진 것이 없습니다. 언제 내보내느냐만 바뀌었을 뿐이었습니다.
TTFB: 0.002676s / Total: 2.007220s
TTFB가 2초로 튈 것이라고 예상했는데 그렇지 않았습니다.
이유는 코드에 있었습니다. header를 먼저 enqueue하고 그다음부터 모으기 시작합니다. 그러니 첫 바이트는 header가 즉시 내보낸 것입니다.
TTFB는 첫 바이트가 언제 나갔는지만 측정합니다. React 콘텐츠가 2초 뒤에 한꺼번에 나갔다는 사실은 잡히지 않습니다.
브라우저로 열어보면 다릅니다.
loading...이 보이지 않습니다.
브라우저는 HTML을 받는 즉시 파싱하고 그립니다. 다 받을 때까지 기다리지 않습니다.
loading...이 포함된 shell이 도착 → 즉시 그림 → 2초 동안 표시 → 목록으로 교체loading...을 그리자마자 곧바로 교체 → 눈에 안 보일 만큼 스쳐감그리고 그 2초 동안 빈 화면으로 보입니다.
| 빈 화면 | loading... |
목록 | |
|---|---|---|---|
| 정상 | ~6ms | 6ms ~ 2000ms | 2000ms ~ |
| 버퍼링 | ~2000ms | 스쳐감 | 2000ms ~ |
이 결과는 현재 회사에서 진행하는 프로젝트에서 처음 SSR 프로젝트를 맡았을 때 겪었던 현상과 동일했습니다.
| 관측 수단 | 스트리밍이 죽은 것을 잡았나 |
|---|---|
| curl 응답 원문 | 아니오. 완전 동일 |
| TTFB | 아니오. 정상으로 보임 |
loading... 표시 여부 |
예 |
| 빈 화면 시간 | 예 |
응답도 같고 숫자도 같은데 사용자 경험만 다릅니다.
이것이 스트리밍의 본질이기도 합니다. 서버가 보내는 바이트는 결국 같습니다. 다른 것은 언제 보내느냐뿐이고, 그 차이는 사용자 화면에서만 드러납니다.
성능 지표로 말하면 FCP(First Contentful Paint)가 달라진 것입니다. 총 로딩 시간이 아니라 첫 픽셀이 언제 뜨느냐입니다.
그렇다면 스트리밍은 항상 좋은 것일까요.