June 19, 2026
웹은 시작 당시의 정적, 문서 중심 매체에서 오래전에 벗어났습니다. 현대적이고 풍부한 웹 앱은 소통, 구매, 다양한 콘텐츠 소비, 복잡한 일상 관리 등 수많은 이유로 모든 사람이 사용합니다.
HTML은 모든 발전에도 불구하고 콘텐츠가 준비된 시점이나 사용자가 소비하는 시점과 관계없이 여전히 위에서 아래로 순서대로 전달됩니다. CSS로 콘텐츠 순서를 바꿀 수 있지만 접근성 측면에서 상당한 부작용이 발생할 수 있습니다. 자바스크립트는 다양한 API로 DOM을 조작하여 이 제약에서 어느 정도 벗어나지만, HTML에 연결하려면 장황한 구문이나 DOM 트리 구성이 필요한 경우가 많습니다.
웹은 클라이언트-서버 특성상 성능이 매우 중요하지만, HTML의 순서대로 전달되는 특성을 우회하기 위해 최적이 아닌 선택을 하는 경우가 많아 성능이 저하됩니다. 전체 페이지가 준비될 때까지 기다리거나 컴포넌트를 비동기 방식으로 전달하기 위해 무거운 프레임워크를 사용하는 것이 그 예입니다. 자바스크립트 프레임워크의 인기는 웹 개발자들이 웹의 기원인 엄격한 문서 중심 멘탈 모델보다 컴포넌트 기반 모델을 선호한다는 것을 보여줍니다.
Chrome 팀은 이 문제를 고민해 왔으며, 웹 플랫폼에 선언적 부분 업데이트(Declarative Partial Updates)라는 이름의 새로운 기능을 개발해 왔습니다.
두 가지 새로운 API 세트로 HTML을 보다 비선형적인 방식으로 더 쉽게 전달할 수 있습니다. HTML 문서 내에서 순서에 구애받지 않거나, 새로운 자바스크립트 API를 활용하여 기존 문서에 HTML을 동적으로 삽입하는 방식 모두 지원합니다. Chrome 148부터 chrome://flags/#enable-experimental-web-platform-features 플래그를 사용하여 개발자 테스트를 진행할 수 있습니다. 폴리필도 제공되어 아직 지원하지 않는 브라우저에서도 이 새로운 API를 바로 사용할 수 있습니다.
웹 플랫폼에 추가된 이 기능들은 다른 브라우저 공급업체와 표준화 기관의 긍정적인 피드백을 바탕으로 표준화가 진행 중입니다. 관련 표준은 이 새로운 API를 포함하도록 업데이트되고 있습니다.
첫 번째 변경 사항은 <template> HTML 요소와 처리 명령어(processing instruction) 자리표시자를 사용하는 새로운 비순서 스트리밍 API(out-of-order streaming APIs)입니다.
<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로 구현한 기본 사진 앨범 애플리케이션입니다.
비순서 스트리밍으로 구현된 사진 앨범 데모 (소스)
초기 레이아웃 이후 상태와 사진이 모두 HTML로 스트리밍됩니다.
스트리밍 HTML과 결합된 이 비순서 패치 HTML에는 다양한 사용 사례가 있습니다.
<template for> API를 사용하면 정적 콘텐츠를 HTML에서 직접 유사한 방식으로 처리할 수 있습니다. 자바스크립트 프레임워크도 이를 활용하여 더 인터랙티브한 아일랜드를 만들거나 컴포넌트를 처리할 수 있습니다.몇 가지 예시에 불과하며, 개발자들이 이 새로운 API를 어떻게 활용할지 기대됩니다.
API에는 몇 가지 제한 사항과 주의할 점이 있습니다.
<template for>는 보안상의 이유로 동일한 부모 요소 내의 처리 명령어만 업데이트할 수 있습니다. <body> 요소에 직접 <template for>를 추가하면 <head>를 포함한 전체 문서에 접근할 수 있습니다.<?end> 처리 명령어는 선택 사항이며, 생략하면 <?start> 요소와 포함 요소의 끝 사이에 있는 콘텐츠가 교체됩니다.<template for>가 스트리밍을 시작한 후 처리 명령어를 이동하면 새 콘텐츠가 기존 위치로 계속 스트리밍되는 등 예기치 않은 결과가 발생할 수 있습니다.setHTML이나 innerHTML 같은 메서드로 <template for>를 동적으로 삽입할 때, 파싱 시 템플릿의 “부모”는 중간 문서 프래그먼트(document fragment)입니다. 따라서 이 메서드로 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이 이 영역에서 진행하는 작업의 두 번째 부분은 자바스크립트로 콘텐츠를 업데이트하는 과정을 더 쉽게 만드는 것입니다.
자바스크립트로 기존 문서에 HTML을 동적으로 삽입하는 방법은 이미 여러 가지가 있습니다.
setHTMLsetHTMLUnsafeinnerHTML 및 outerHTML settercreateContextualFragmentinsertAdjacentHTML그러나 이 API들은 모두 개발자가 항상 인지하지 못할 수 있는 미묘한 차이와 다른 동작 방식을 가지고 있습니다.
<script> 태그를 이스케이프하는 등 잠재적으로 위험한 HTML을 새니타이징(sanitizing)하는지<script>를 실행해야 하는지솔직히 이 API들을 보고 각각에 대한 질문에 자신 있게 답할 수 있는 개발자는 거의 없을 것입니다.
큰 제한 사항은 HTML 스트리밍을 허용해야 한다는 요청이 있었음에도 불구하고, 미리 알고 있는 완전한 HTML 세트에만 사용할 수 있다는 점입니다. 실질적으로 콘텐츠를 바로 스트리밍할 수 있다는 HTML의 강점에도 불구하고, 삽입 전에 전체 콘텐츠를 다운로드해야 합니다. 페이로드를 분할하거나 document.write 같은 임시방편의 사용 중단된 메서드를 활용하여 제한적으로 우회할 수 있지만, 이 방법들도 자체적인 문제를 야기합니다.
Chrome은 기존 setHTML과 setHTMLUnsafe를 정리하고 스트리밍 기능을 도입하는 새로운 API 모음을 제안했습니다.
기존 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 버전까지 포함하면 많아 보일 수 있지만, 일관된 명명 규칙 덕분에 앞서 언급한 서로 관련 없는 메서드들에 비해 각 메서드의 기능을 훨씬 명확하게 파악할 수 있습니다.
정적 버전은 새 HTML을 DOM 문자열 인수로 받으며 선택적 옵션도 지정할 수 있습니다.
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();fetch 응답에서 파이프 체인(pipe chains)을 활용할 수도 있습니다.
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를 지정할 수 있으며, 기본값은 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); |
“안전하지 않은” 삽입 및 스트리밍 메서드
“안전하지 않은” 메서드들은 기본적으로 새니타이저를 비활성화하며(원하는 경우 커스텀 새니타이저 지정 가능), 기본값이 false인 선택적 runScripts 옵션으로 스크립트 실행도 허용할 수 있습니다.
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 정적 메서드는 모두 DOM 문자열 또는 TrustedHTML을 html 인수로 받으며 새니타이저를 사용할 수도 있습니다. 다만 runScripts의 목적 자체가 스크립트 실행을 허용하는 것이므로 기본적으로 새니타이저가 사용되지 않습니다.
이 새로운 API들은 일관된 이름과 옵션으로 기존 페이지에 HTML을 더 쉽게 추가할 수 있게 해줍니다. 스트리밍 API는 새 콘텐츠가 모두 준비될 때까지 기다리지 않아도 되는 성능상의 이점을 제공합니다.
사용 사례는 다음과 같습니다.
몇 가지 예시에 불과하며, 여러분이 어떤 아이디어를 내놓을지 기대됩니다!
이 새로운 API들에도 몇 가지 제한 사항과 주의할 점이 있습니다.
createParserOptions 메서드를 사용해야 합니다. 신뢰할 수 있는 타입 통합에 관한 자세한 내용은 설명자를 참고하세요.<template for>와 마찬가지로, 스트리밍 중인 요소를 이동하면 예기치 않은 결과나 스트림 오류가 발생할 수 있습니다.streamHTMLUnsafe는 기본 문서에 추가될 때 <template for> 명령어를 처리하고 defer 스크립트를 스트림 끝까지 지연하는 등 여러 면에서 기본 파서와 더 유사하게 동작합니다.Chrome 팀은 다른 브라우저에 이 기능이 도입되기 전에도 바로 사용할 수 있도록 html-setters-polyfill을 출시했으며 npm에서 사용 가능합니다.
이 폴리필은 스트리밍을 지원하지 않으며, 완료될 때까지 버퍼링한 후 적용합니다. 기능 자체보다는 API 형태에 대한 폴리필에 가깝습니다.
안전한 콘텐츠 설정은 setHTML과 Safari에서 지원하지 않는 Sanitizer API에 의존합니다.
두 가지 별개의 API지만, 함께 사용할 때 진정한 효과를 발휘합니다. 새로운 <template for> 요소를 HTML로 스트리밍하면 DOM에 대한 별도의 자바스크립트 참조 없이도 콘텐츠의 여러 부분을 동적으로 업데이트할 수 있습니다.
기본적인 SPA 방식의 페이지 로드는 처리 명령어가 포함된 개요 페이지를 로드한 다음, 각 새 페이지의 템플릿을 HTML 하단으로 스트리밍하여 해당 처리 명령어에 삽입하는 방식으로 구현할 수 있습니다.
두 API 모두 더 많은 가능성과 사용 사례가 있으니, (제한된!) 상상력에 제약을 받지 마세요. 부분 업데이트를 더 쉽게 관리할 수 있게 함으로써 보일러플레이트 코드를 줄이고, 업데이트를 단순화하며, 웹의 새로운 잠재력을 열어줄 것입니다.
🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article(https://kofearticle.substack.com/)을 구독해주세요!