새로운 HTML 및 JavaScript API로 순서를 벗어난 스트리밍과 부분 콘텐츠 업데이트를 더 쉽게 구현하는 방법을 소개합니다.


게시일: 2026년 5월 19일
웹은 출발점이었던 정적인 문서 중심 매체에서 이미 오래전에 벗어났습니다. 현대의 풍부한 웹 앱은 소통, 구매, 풍부한 콘텐츠 소비, 복잡한 일상 관리 등 다양한 이유로 모든 사람이 사용합니다.
HTML은 수많은 발전에도 불구하고, 여전히 콘텐츠 준비 여부나 사용자가 언제 소비하는지와는 거의 무관하게 위에서 아래로 순서대로 전달됩니다. CSS를 사용하면 콘텐츠의 순서를 바꿀 수 있지만, 종종 접근성 측면에서 큰 부작용이 따릅니다. JavaScript를 사용하면 다양한 API를 통해 DOM을 조작해 이러한 한계에서 어느 정도 벗어날 수 있지만, 대개 장황한 문법이나 HTML에 연결할 DOM 트리 구성이 필요합니다.
웹은 클라이언트-서버라는 매체 특성상 성능이 매우 중요하지만, HTML의 이러한 순차적 특성을 우회하려다 보니 종종 최적이 아닌 선택이 이루어지고 이는 성능 저하로 이어집니다. 여기에는 페이지 전체가 준비될 때까지 기다리거나 무거운 프레임워크를 사용해 비동기 방식으로 컴포넌트를 전달하는 방식이 포함됩니다. JavaScript 프레임워크의 높은 인기는 웹 개발자들이 웹의 기원인 경직된 문서 중심 사고방식보다 컴포넌트 기반 모델을 선호한다는 점을 보여줍니다.
Chrome 팀은 이 문제를 검토해 왔으며 Declarative Partial Updates라는 이름으로 웹 플랫폼에 새로운 기능을 추가해 왔습니다.
새로운 두 가지 API 집합은 HTML 문서 자체 안에서 순서를 벗어나거나, 새로운 JavaScript API를 통해 기존 문서에 HTML을 더 쉽게 동적으로 삽입하는 방식으로, HTML을 덜 선형적으로 전달하기 쉽게 만들어 줍니다. 이 기능들은 Chrome 148부터 chrome://flags/#enable-experimental-web-platform-features 플래그를 사용해 개발자 테스트가 가능하며, 아직 이를 지원하지 않는 브라우저에서도 바로 사용할 수 있도록 폴리필도 제공됩니다.
웹 플랫폼에 대한 이러한 추가 사항은 다른 브라우저 공급업체와 표준화 경로에서 긍정적인 피드백을 받으며 표준화가 진행 중입니다. 관련 표준도 이 새로운 API를 포함하도록 업데이트되는 과정에 있습니다.
첫 번째 변경 집합은 <template> HTML 요소와 처리 명령 플레이스홀더를 사용하는 새로운 순서를 벗어난 스트리밍 API입니다. 예를 들면 다음과 같습니다.
<div>
<?marker name="placeholder">
</div>
...
<template for="placeholder">
Here is some <em>HTML content</em>!
</template>
처리 명령은 XML에서 오래전부터 존재했지만, HTML에서는 주석처럼 취급되어 무시되어 왔습니다. 이 새로운 API는 이를 바꾸어 HTML에도 처리 명령을 도입합니다. 브라우저가 <?marker name="placeholder"> 처리 명령을 보더라도 즉시 아무 작업도 하지 않습니다. 이는 이전과 비슷하지만, 나중에 참조할 수 있습니다.
<template> 요소는 name 속성을 사용해 해당 처리 명령을 찾아 콘텐츠를 대체합니다. 이 경우 파싱이 끝난 뒤 DOM은 다음과 같이 됩니다.
<div>
Here is some <em>HTML content</em>!
</div>
대체를 위한 <?marker> 속성 외에도 <?start> 및 <?end> 범위 마커가 있어, 템플릿이 처리되기 전에 임시 플레이스홀더 콘텐츠를 표시할 수 있습니다.
<div>
<?start name="another-placeholder">
Loading…
<?end>
</div>
...
<template for="another-placeholder">
Here is some <em>HTML content</em>!
</template>
이 경우 <template>가 나타날 때까지 Loading…가 표시되고, 이후 새로운 콘텐츠로 대체됩니다.
여러 번 업데이트할 수 있도록 템플릿 안에 처리 명령을 포함하는 것도 가능합니다.
<ul id="results">
<?start name="results">
Loading…
<?end>
</ul>
...
<template for="results">
<li>Result One</li>
<?marker name="results">
</template>
...
<template for="results">
<li>Result Two</li>
<?marker name="results">
</template>
...
이 경우 파싱 후 다음과 같은 HTML이 됩니다.
<ul id="results">
<li>Result One</li>
<li>Result Two</li>
<?marker name="results">
</ul>
문서에 나중에 <template for="results">가 더 추가될 수 있으므로 마지막 처리 명령이 끝에 남아 있습니다.
이 영상에서는 스트리밍 HTML로 기본 사진 앨범 애플리케이션을 구현합니다.
순서를 벗어난 스트리밍으로 구현한 사진 앨범 데모(source)
상태와 사진 모두 초기 레이아웃 이후 HTML로 스트리밍됩니다.
스트리밍 HTML과 결합된 이 순서를 벗어난 패칭 HTML에는 많은 사용 사례가 있습니다.
<template for> API를 사용하면 정적 콘텐츠도 HTML에서 직접 비슷한 방식으로 처리할 수 있습니다. JavaScript 프레임워크 역시 더 상호작용적인 아일랜드나 컴포넌트 처리를 위해 이를 사용할 수 있습니다.이는 일부 사용 사례에 불과하며, 개발자들이 이 새로운 API를 어떻게 활용할지 매우 기대됩니다.
이 API에는 알아두어야 할 몇 가지 제한 사항과 미묘한 점이 있습니다.
<template for>는 보안상의 이유로 같은 부모 요소 안에 있는 처리 명령만 업데이트할 수 있습니다. <template for>를 <body> 요소에 직접 추가하면 전체 문서(<head> 포함)에 접근할 수 있습니다.<?end> 처리 명령은 선택 사항이며, 없으면 <?start> 요소와 포함 요소의 끝 사이의 콘텐츠가 대체됩니다.<template for>의 스트리밍이 시작된 뒤 처리 명령을 이동하면 새 콘텐츠가 이전 위치로 계속 스트리밍되는 등 예기치 않은 결과가 발생할 수 있습니다.setHTML이나 innerHTML 같은 메서드로 <template for>를 동적으로 삽입할 때, 파싱 시 템플릿의 “부모”는 중간 문서 프래그먼트입니다. 즉, 이러한 메서드로 HTML을 삽입해도 기존 DOM을 수정할 수 없고, 패칭은 프래그먼트 내부에서 “제자리”로 일어납니다. 하지만 streamHTMLUnsafe 같은 메서드(곧 설명합니다!)로 스트리밍할 때는 중간 프래그먼트가 없으므로 템플릿이 기존 콘텐츠를 대체할 수 있습니다.현재 검토 중인 향후 추가 가능성은 다음과 같습니다.
<template for="footer" patchsrc="/partials/footer.html">.<template for=icon safe><svg id="from-untrusted-source">...</svg></template>Chrome 팀은 다른 브라우저에 이 기능이 적용되기 전에도 사이트가 바로 이 새 기능을 사용할 수 있도록 template-for-polyfill을 공개했으며, 이는 npm에서 사용 가능합니다.
브라우저의 HTML 파서를 직접 업데이트할 수 없기 때문에 몇 가지 제한 사항이 있지만, 가장 일반적인 사용 사례는 지원합니다. 사이트는 다른 브라우저에서도 반드시 테스트해야 합니다.
모든 콘텐츠를 HTML로 전달할 수 있는 것은 아닙니다. Chrome이 이 영역에서 진행 중인 작업의 두 번째 부분은 JavaScript를 통해 콘텐츠를 더 쉽게 업데이트하도록 만드는 것입니다.
이미 JavaScript를 사용해 기존 문서에 HTML을 동적으로 주입하는 여러 방법이 있습니다.
setHTMLsetHTMLUnsafeinnerHTML 및 outerHTML settercreateContextualFragmentinsertAdjacentHTML하지만 이들 모두는 약간씩 다른 방식으로 동작하며, 개발자가 항상 고려하지는 않는 미묘한 점과 차이가 있습니다.
<script> 태그를 이스케이프하는 방식으로 잠재적으로 위험한 HTML을 정화하는가?<script>는 실행되어야 하는가?개발자 중 이 API들을 보고 각 항목에 대해 자신 있게 답할 수 있는 사람은 많지 않을 것입니다.
큰 제약 중 하나는 HTML을 스트리밍할 수 있게 해 달라는 요청이 있었음에도, 미리 알고 있는 완전한 HTML 집합에만 사용할 수 있다는 점입니다. 실제로 이는 삽입 전에 전체 콘텐츠를 모두 다운로드해야 한다는 뜻인데, HTML의 강점 중 하나는 콘텐츠를 즉시 스트리밍할 수 있다는 점입니다. 이는 페이로드를 분할하거나 document.write 같은 임시방편적이고 폐기 예정인 메서드를 사용하는 제한적인 방식으로 우회할 수 있지만, 그 자체의 문제를 낳습니다.
Chrome은 이를 정리하고 스트리밍 기능도 도입하는 새 API 모음과 기존 setHTML, setHTMLUnsafe 확장을 제안했습니다.
기존 HTML 앞이나 뒤에 콘텐츠를 삽입하는 메서드와, 설정하거나 대체하는 메서드가 있습니다. 각 메서드에는 스트림 대응 버전이 있습니다.
| 동작 | 정적 | 스트리밍 |
|---|---|---|
| 요소의 HTML 콘텐츠 설정 | setHTML(html, options); | streamHTML(options); |
| 전체 요소를 이 HTML로 대체 | replaceWithHTML(html, options); | streamReplaceWithHTML(options); |
| 요소 앞에 HTML 추가 | beforeHTML(html, options); | streamBeforeHTML(options); |
| 요소의 첫 번째 자식으로 HTML 추가 | prependHTML(html, options); | streamPrependHTML(options); |
| 요소의 마지막 자식으로 HTML 추가 | appendHTML(html, options); | streamAppendHTML(options); |
| 요소 뒤에 HTML 추가 | afterHTML(html, options); | streamAfterHTML(options); |
새로운 삽입 및 스트리밍 메서드
곧 설명할 Unsafe 버전도 있습니다. 수가 많아 보일 수 있지만(특히 Unsafe 대응 버전까지 포함하면), 일관된 이름 규칙 덕분에 앞서 언급한 서로 무관한 메서드들보다 각 메서드의 역할이 더 분명합니다.
정적 버전은 선택적 options와 함께 DOM String 인수로 새 HTML을 받습니다.
const newHTML = "<p>This is a new paragraph</p>";
const contentElement = document.querySelector('#content-to-update');
contentElement.setHTML(newHTML);
스트리밍 버전은 getWriter()와 함께 Streams API를 사용합니다.
const contentElement = document.querySelector('#content-to-update');
const writer = contentElement.streamHTMLUnsafe().getWriter();
// Example stream of updating content
while (true) {
await writer.write(`<p>${++i}</p>`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
writer.close();
또는 pipe chains를 사용해 fetch 응답에서 바로 연결할 수도 있습니다.
const contentElement = document.querySelector('#content-to-update');
const response = await fetch('/api/content.html');
response.body
.pipeThrough(new TextDecoderStream())
.pipeTo(contentElement.streamHTMLUnsafe());
또한 중간 TextDecoderStream() 단계 없이 직접 스트리밍할 수 있는 편의 메서드 추가도 계획 중입니다.
options 인수를 사용하면 사용자 지정 sanitizer를 지정할 수 있으며, 기본값은 기본 sanitizer 구성을 뜻하는 default입니다. 사용 방법은 다음과 같습니다.
const newHTML = "<p>This is a new paragraph</p>";
const contentElement = document.querySelector('#content-to-update');
// Only allows basic formatting
const basicFormattingSanitzer = new Sanitizer({ elements: ["em", "i", "b", "strong"] });
contentElement.setHTML(newHTML, {sanitizer: basicFormattingSanitzer});
각 API에는 “unsafe” 버전도 있습니다.
| 동작 | 정적 | 스트리밍 |
|---|---|---|
| 요소의 HTML 콘텐츠 설정 | setHTMLUnsafe(html,options); | streamHTMLUnsafe(options); |
| 전체 요소를 이 HTML로 대체 | replaceWithHTMLUnsafe(html, options); | streamReplaceWithHTMLUnsafe(options); |
| 요소 앞에 HTML 추가 | beforeHTMLUnsafe(html, options); | streamBeforeHTMLUnsafe(options); |
| 요소의 첫 번째 자식으로 HTML 추가 | prependHTMLUnsafe(html, options); | streamPrependHTMLUnsafe(options); |
| 요소의 마지막 자식으로 HTML 추가 | appendHTMLUnsafe(html, options); | streamAppendHTMLUnsafe(options); |
| 요소 뒤에 HTML 추가 | afterHTMLUnsafe(html, options); | streamAfterHTMLUnsafe(options); |
“unsafe” 삽입 및 스트리밍 메서드
이러한 “unsafe” 메서드는 기본적으로 sanitizer를 끄며(원한다면 사용자 지정 sanitizer를 지정할 수 있음), 선택적 runScripts 옵션을 통해 스크립트 실행도 허용합니다(기본값은 false).
setHTML과 마찬가지로 setHTMLUnsafe는 기존 메서드이지만, 이제 스크립트 실행과 함께 사용할 수 있도록 runScripts 옵션 매개변수가 추가되었습니다.
const newHTML = `<p>This is a new paragraph</p>
<script src=script.js></script>`;
const contentElement = document.querySelector('#content-to-update');
contentElement.setHTMLUnsafe(newHTML, {runScripts: true});
메서드 이름에 “unsafe”라는 표현을 사용한 이유는 이 메서드를 사용하지 말라는 뜻이 아니라, 잠재적 위험을 개발자에게 상기시키고 필요에 따라 정화나 스크립트 제한을 고려하도록 하기 위해서입니다.
이것이 얼마나 “unsafe”한지는 입력을 얼마나 신뢰할 수 있는지에 따라 다릅니다. Unsafe 정적 메서드는 모두 html 인수로 DOM String과 TrustedHTML 모두를 받을 수 있고, sanitizer 사용도 허용합니다. 다만 runScript의 목적 자체가 스크립트 허용이므로 기본적으로는 sanitizer를 사용하지 않습니다.
이 새로운 API는 개발자가 기존 페이지에 HTML을 더 쉽게 추가할 수 있게 하며, 일관된 이름과 옵션을 가진 새 API를 제공합니다. 스트리밍 API는 새 콘텐츠 전체가 준비될 때까지 기다리지 않아도 된다는 성능상의 이점을 플랫폼에 가져옵니다.
사용 사례는 다음과 같습니다.
다시 말하지만 이것들은 몇 가지 예에 불과하며, 여러분이 어떤 것을 만들어낼지 기대하고 있습니다!
이 새로운 API에도 알아두어야 할 몇 가지 제한 사항과 미묘한 점이 있습니다.
createParserOptions 메서드를 사용해야 하며, 이를 통해 모든 HTML 설정 작업에 sanitizer를 주입할 수 있습니다. 자세한 내용은 trusted types 통합 설명서를 참고하세요.<template for>와 비슷하게, 스트리밍 대상 요소를 이동하면 예기치 않은 결과나 스트림 오류가 발생할 수 있습니다.streamHTMLUnsafe는 여러 면에서 메인 파서와 비슷하게 동작하며, 메인 문서에 추가되는 <template for> 지시를 처리하고 defer 스크립트는 스트림 끝까지 지연합니다.Chrome 팀은 다른 브라우저에 이 기능이 적용되기 전에도 사이트가 바로 이 새 기능을 사용할 수 있도록 html-setters-polyfill을 공개했으며, 이는 npm에서 사용 가능합니다.
이 폴리필은 스트리밍하지 않고, 대신 완료될 때까지 버퍼링한 뒤 적용합니다. 기능 자체보다는 API 형태에 대한 폴리필에 더 가깝습니다.
또한 안전한 콘텐츠 설정은 Safari에서 지원되지 않는 setHTML 및 Sanitizer API에 의존합니다.
이 둘은 별개의 API이지만, 진정한 힘은 결합했을 때 나옵니다. 새로운 <template for> 요소를 HTML에 스트리밍함으로써, DOM에 대한 별도의 JavaScript 참조로 각각을 직접 지정하지 않고도 콘텐츠의 서로 다른 부분을 동적으로 업데이트할 수 있습니다.
기본적인 SPA 스타일 페이지 로드는 처리 명령이 들어 있는 윤곽 페이지를 먼저 로드한 뒤, 각 새 페이지의 템플릿을 HTML 하단으로 스트리밍해 해당 처리 명령 슬롯에 끼워 넣는 방식으로 구현할 수 있습니다.
분명히 이 두 API 모두에 더 많은 가능성과 사용 사례가 있으니, 우리의 (제한된!) 상상력에 얽매이지 마세요. 부분 업데이트를 더 쉽게 관리할 수 있게 되면 일부 상용구 코드를 줄이고, 업데이트를 더 쉽게 만들며, 웹의 새로운 가능성을 열 수 있습니다!