최근 몇 년 동안 웹 개발자들은 스트리밍 HTML과 자바스크립트를 사용하는 기술을 활용하기 시작했습니다. 이 글에서는 스트리밍 HTML의 장점과 이를 통해 성능을 극대화할 수 있는 방법을 알아보겠습니다. 구체적인 사례와 함께 스트리밍 기술이 어떻게 웹사이트의 성능을 향상시키는지 살펴봅시다.
스트리밍 HTML의 기본 원리
스트리밍 HTML은 초기 로딩 동안 브라우저가 HTML 청크를 받는 대로 처리하는 방식입니다. 이를 통해 페이지 로드 시간을 단축하고 사용자 경험을 향상시킬 수 있습니다.
헤더 설정
서버에서 스트리밍을 활성화하려면 다음과 같은 헤더를 설정해야 합니다:
{
"transfer-encoding": "chunked",
"vary": "Accept-Encoding",
"content-type": "text/html; charset=utf-8"
}
응답에서는 ReadableStream을 사용합니다. 예를 들어, Bun을 사용할 경우 다음과 같이 설정할 수 있습니다:
const encoder = new TextEncoder();
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('<html lang="en">'));
controller.enqueue(encoder.encode('<head />'));
controller.enqueue(encoder.encode('<body>'));
controller.enqueue(encoder.encode('<div class="foo">Bar</div>'));
controller.enqueue(encoder.encode('</body>'));
controller.enqueue(encoder.encode('</html>'));
controller.close();
},
})
);
브라우저는 스트리밍 도중 작은 JS 스크립트를 실행하여 HTML 콘텐츠를 동적으로 변경할 수 있습니다.
스트리밍 중 HTML 콘텐츠 변경
스트리밍 HTML의 주요 이점 중 하나는 HTML 콘텐츠를 동적으로 변경할 수 있다는 것입니다. React Suspense는 이러한 방식을 사용하는 대표적인 예입니다. HTML의 나머지 부분을 로드하는 동안 플레이스홀더를 보여주고, 나중에 누락된 콘텐츠를 로드하여 사용자에게 더욱 나은 경험을 제공합니다.
return new Response(
new ReadableStream({
async start(controller) {
const suspensePromises = [];
controller.enqueue(encoder.encode('<html lang="en">'));
controller.enqueue(encoder.encode('<head>'));
controller.enqueue(encoder.encode('<script src="unsuspense.js"></script>'));
controller.enqueue(encoder.encode('</head>'));
controller.enqueue(encoder.encode('<body>'));
controller.enqueue(encoder.encode('<div id="suspensed:1">Loading...</div>'));
suspensePromises.push(
computeExpensiveChunk().then((content) => {
controller.enqueue(encoder.encode(`<template id="suspensed-content:1">${content}</template>`));
controller.enqueue(encoder.encode(`<script>unsuspense('1')</script>`));
})
);
controller.enqueue(encoder.encode('<div class="foo">Bar</div>'));
controller.enqueue(encoder.encode('</body>'));
controller.enqueue(encoder.encode('</html>'));
await Promise.all(suspensePromises);
controller.close();
},
})
);
unsuspense.js 파일은 window.unsuspense를 통해 스트리밍 중에 실행되며, 플레이스홀더를 실제 콘텐츠로 교체합니다.
실시간 내비게이션과 RPC
2023년부터 크롬은 뷰 트랜지션 API를 지원하기 시작했습니다. 이 API를 사용하면 싱글 페이지 애플리케이션(SPA)처럼 부드러운 전환 효과를 제공하면서도 DOM 콘텐츠를 한 번에 업데이트할 수 있습니다.
window.navigation.addEventListener('navigate', navigate);
function navigate(event) {
const url = new URL(event.destination.url);
const decoder = new TextDecoder();
if (location.origin !== url.origin) return;
event.intercept({
async handler() {
const res = await fetch(url.pathname);
const doc = document.implementation.createHTMLDocument();
const stream = res.body.getReader();
await document.startViewTransition(() => {
document.documentElement.replaceWith(doc.documentElement);
document.documentElement.scrollTop = 0;
}).ready;
while (true) {
const { done, value } = await stream.read();
if (done) break;
await document.startViewTransition(() => doc.write(decoder.decode(value))).ready;
}
},
});
}
DOM 비교 알고리즘
DOM 비교 알고리즘은 브라우저가 HTML을 스트리밍하면서 실시간으로 DOM을 업데이트할 수 있도록 돕습니다. 여러 오픈소스 라이브러리가 이러한 기능을 제공합니다.
- morphdom
- set-dom
- diffhtml
- diffDOM
- nanomorph
- incremental-dom
이들 라이브러리는 대부분 너비 우선 탐색(BFS)을 사용하여 DOM 트리를 탐색하고 업데이트합니다. 반면, 스트리밍 중에는 깊이 우선 탐색(DFS) 순서로 HTML 노드가 도착합니다. 이를 처리하기 위해 parse-html-stream 라이브러리를 사용할 수 있습니다.
import parseHTMLStream from 'parse-html-stream';
window.navigation.addEventListener('navigate', navigate);
function navigate(event) {
const url = new URL(event.destination.url);
const decoder = new TextDecoder();
if (location.origin !== url.origin) return;
event.intercept({
async handler() {
const res = await fetch(url.pathname);
const doc = document.implementation.createHTMLDocument();
const stream = res.body.getReader();
await document.startViewTransition(() => {
document.documentElement.replaceWith(doc.documentElement);
document.documentElement.scrollTop = 0;
}).ready;
for await (const node of parseHTMLStream(reader)) {
console.log(node.nodeName);
}
},
});
}
결론
스트리밍 HTML과 DOM 비교 알고리즘은 웹 개발의 미래를 밝게 비추고 있습니다. 이 기술을 활용하면 초기 로딩 시간을 단축하고, 사용자 경험을 크게 향상시킬 수 있습니다. 또한, 개발자는 복잡한 추가 요청 없이도 HTML 콘텐츠를 동적으로 업데이트할 수 있습니다.