클라이언트 측 템플릿 표준화
소개
템플릿 개념은 웹 개발에 새로운 개념이 아닙니다. 사실 Django(Python), ERB/Haml(Ruby), Smarty(PHP)와 같은 서버 측 템플릿 언어/엔진은 오래전부터 사용되어 왔습니다. 하지만 지난 몇 년 동안 MVC 프레임워크가 급증했습니다. 모두 약간씩 다르지만 대부분은 프레젠테이션 레이어(즉, 뷰)를 렌더링하기 위한 공통 메커니즘인 템플릿을 공유합니다.
현실을 직시해야 합니다. 템플릿은 정말 좋습니다. 다른 직원에게 문의해 보세요. 정의만으로도 따뜻하고 포근한 느낌을 줍니다.
"…매번 다시 만들 필요는 없습니다…" 저는 불필요한 작업을 피하는 것을 좋아합니다. 그렇다면 개발자가 분명히 중요하게 생각하는 사항에 대한 네이티브 지원이 웹 플랫폼에 없는 이유는 무엇인가요?
WhatWG HTML 템플릿 사양이 답입니다. 클라이언트 측 템플릿에 관한 표준 DOM 기반 접근 방식을 설명하는 새로운 <template>
요소를 정의합니다. 템플릿을 사용하면 HTML로 파싱되고 페이지 로드 시 사용되지 않지만 나중에 런타임에 인스턴스화할 수 있는 마크업 프래그먼트를 선언할 수 있습니다. 라파엘 와인스틴의 말은 다음과 같습니다.
어떤 이유로든 브라우저가 건드리지 않았으면 하는 대량의 HTML을 배치하는 곳입니다.
라파엘 와인스타인 (사양 작성자)
특징 감지
<template>
를 기능 감지하려면 DOM 요소를 만들고 .content
속성이 있는지 확인합니다.
function supportsTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// Good to go!
} else {
// Use old templating techniques or libraries.
}
템플릿 콘텐츠 선언
HTML <template>
요소는 마크업에서 템플릿을 나타냅니다. 여기에는 '템플릿 콘텐츠'가 포함됩니다. 기본적으로 클론 가능한 DOM의 비활성 청크입니다.
템플릿은 앱의 전체 기간 동안 사용하고 재사용할 수 있는 발판이라고 생각하면 됩니다.
템플릿 콘텐츠를 만들려면 마크업을 선언하고 <template>
요소로 래핑합니다.
<template id="mytemplate">
<img src="" alt="great image">
<div class="comment"></div>
</template>
핵심 요소
<template>
에서 콘텐츠를 래핑하면 중요한 속성이 거의 없습니다.
콘텐츠는 활성화될 때까지 사실상 비활성 상태입니다. 기본적으로 마크업은 숨겨진 DOM이며 렌더링되지 않습니다.
템플릿 내의 콘텐츠는 부작용이 없습니다. 템플릿이 사용될 때까지 스크립트가 실행되지 않고 이미지가 로드되지 않고 오디오가 재생되지 않습니다.
콘텐츠가 문서에 포함되지 않은 것으로 간주됩니다. 기본 페이지에서
document.getElementById()
또는querySelector()
를 사용하면 템플릿의 하위 노드가 반환되지 않습니다.템플릿은
<head>
,<body>
또는<frameset>
내 어디서나 배치할 수 있으며 이러한 요소에서 허용되는 모든 유형의 콘텐츠를 포함할 수 있습니다. '모든 위치'는 HTML 파서가 허용하지 않는 위치, 즉 콘텐츠 모델 하위 요소를 제외한 모든 하위 위치에서<template>
를 안전하게 사용할 수 있음을 의미합니다.<table>
또는<select>
의 하위 요소로 배치할 수도 있습니다.
<table>
<tr>
<template id="cells-to-repeat">
<td>some content</td>
</template>
</tr>
</table>
템플릿 활성화
템플릿을 사용하려면 활성화해야 합니다. 그렇지 않으면 콘텐츠가 렌더링되지 않습니다.
가장 간단한 방법은 document.importNode()
를 사용하여 .content
의 깊은 사본을 만드는 것입니다. .content
속성은 템플릿의 핵심이 포함된 읽기 전용 DocumentFragment
입니다.
var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'logo.png';
var clone = document.importNode(t.content, true);
document.body.appendChild(clone);
템플릿을 스탬프 처리하면 콘텐츠가 '게시'됩니다. 이 예시에서는 콘텐츠가 클론되고 이미지 요청이 이루어지며 최종 마크업이 렌더링됩니다.
데모
예: 비활성 스크립트
이 예에서는 템플릿 콘텐츠의 비활성 상태를 보여줍니다. <script>
는 버튼을 누를 때만 실행되어 템플릿을 스탬프 처리합니다.
<button onclick="useIt()">Use me</button>
<div id="container"></div>
<script>
function useIt() {
var content = document.querySelector('template').content;
// Update something in the template DOM.
var span = content.querySelector('span');
span.textContent = parseInt(span.textContent) + 1;
document.querySelector('#container').appendChild(
document.importNode(content, true)
);
}
</script>
<template>
<div>Template used: <span>0</span></div>
<script>alert('Thanks!')</script>
</template>
예: 템플릿에서 Shadow DOM 만들기
대부분의 경우 마크업 문자열을 .innerHTML
로 설정하여 Shadow DOM을 호스트에 연결합니다.
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
이 접근 방식의 문제는 Shadow DOM이 복잡해질수록 더 많은 문자열 연결을 실행한다는 점입니다. 확장되지 않고 빠르게 지저분해지며
아이가 울기 시작합니다. 이 접근 방식은 처음에 XSS가 탄생한 과정이기도 합니다. <template>
를 사용하면 됩니다.
섀도 루트에 템플릿 콘텐츠를 추가하여 DOM을 직접 사용하는 것이 더 좋습니다.
<template>
<style>
:host {
background: #f8f8f8;
padding: 10px;
transition: all 400ms ease-in-out;
box-sizing: border-box;
border-radius: 5px;
width: 450px;
max-width: 100%;
}
:host(:hover) {
background: #ccc;
}
div {
position: relative;
}
header {
padding: 5px;
border-bottom: 1px solid #aaa;
}
h3 {
margin: 0 !important;
}
textarea {
font-family: inherit;
width: 100%;
height: 100px;
box-sizing: border-box;
border: 1px solid #aaa;
}
footer {
position: absolute;
bottom: 10px;
right: 5px;
}
</style>
<div>
<header>
<h3>Add a Comment
</header>
<content select="p"></content>
<textarea></textarea>
<footer>
<button>Post</button>
</footer>
</div>
</template>
<div id="host">
<p>Instructions go here</p>
</div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.appendChild(document.querySelector('template').content);
</script>
주의할 점
다음은 실제 환경에서 <template>
를 사용할 때 발생한 몇 가지 문제입니다.
- modpagespeed를 사용하는 경우 이 버그에 주의하세요. 인라인
<style scoped>
를 정의하는 템플릿은 PageSpeed의 CSS 재작성 규칙을 사용하여 헤더로 이동할 수 있습니다. - 템플릿을 '사전 렌더링'하는 방법은 없습니다. 즉, 애셋을 미리 로드하거나 JS를 처리하거나 초기 CSS를 다운로드할 수 없습니다. 이는 서버와 클라이언트 모두에 적용됩니다. 템플릿은 게시될 때만 렌더링됩니다.
중첩된 템플릿에는 주의하세요. 예상대로 작동하지 않습니다. 예를 들면 다음과 같습니다.
<template> <ul> <template> <li>Stuff</li> </template> </ul> </template>
외부 템플릿을 활성화해도 내부 템플릿은 활성화되지 않습니다. 즉, 중첩된 템플릿의 하위 요소도 수동으로 활성화해야 합니다.
표준으로 가는 길
어디에서 시작했는지 잊지 맙시다. 표준 기반 HTML 템플릿으로 전환하는 과정은 오랫동안 지속되었습니다 수년 동안 Google에서는 재사용 가능한 템플릿을 만들기 위한 몇 가지 영리한 트릭을 개발했습니다. 다음은 제가 경험한 두 가지 일반적인 문제입니다. 비교를 위해 이 도움말에 포함했습니다.
방법 1: 화면 밖 DOM
오랫동안 사용해 온 한 가지 접근 방식은 '오프스크린' DOM을 만들고 hidden
속성 또는 display:none
를 사용하여 뷰에서 숨기는 것입니다.
<div id="mytemplate" hidden>
<img src="logo.png">
<div class="comment"></div>
</div>
이 기법은 작동하지만 몇 가지 단점이 있습니다. 이 기법의 개요는 다음과 같습니다.
- DOM 사용 - 브라우저가 DOM을 알고 있습니다. 잘 하고 있습니다. 쉽게 클론할 수 있습니다.
- 렌더링되지 않음 -
hidden
를 추가하면 블록이 표시되지 않습니다. - 비활성 상태가 아님: 콘텐츠가 숨겨져 있더라도 이미지에 대한 네트워크 요청이 계속 이루어집니다.
- 불편한 스타일 지정 및 테마 설정 - 삽입 페이지는 스타일 범위를 템플릿으로 축소하기 위해 모든 CSS 규칙 앞에
#mytemplate
를 붙여야 합니다. 이는 취약하며 향후 이름 충돌이 발생하지 않는다는 보장은 없습니다. 예를 들어 삽입 페이지에 이미 해당 ID를 가진 요소가 있는 경우 연결됩니다.
방법 2: 오버로드 스크립트
또 다른 기법은 <script>
를 오버로드하고 콘텐츠를 문자열로 조작하는 것입니다. 2008년 존 리시그가 Micro Templating 유틸리티를 사용하여 이를 처음으로 보여주었을 것입니다.
이제 handlebars.js와 같은 신규 도구를 포함하여 다양한 도구가 있습니다.
예를 들면 다음과 같습니다.
<script id="mytemplate" type="text/x-handlebars-template">
<img src="logo.png">
<div class="comment"></div>
</script>
이 기법의 개요는 다음과 같습니다.
- 렌더링되지 않음 -
<script>
이 기본적으로display:none
이므로 브라우저가 이 블록을 렌더링하지 않습니다. - Inert - 스크립트 유형이 'text/javascript' 이외의 값으로 설정되어 있기 때문에 브라우저에서 스크립트 콘텐츠를 JS로 파싱하지 않습니다.
- 보안 문제 -
.innerHTML
사용을 권장합니다. 사용자 제공 데이터의 런타임 문자열 파싱은 쉽게 XSS 취약점으로 이어질 수 있습니다.
결론
jQuery가 DOM 작업을 매우 간단하게 만들었던 때를 기억하시나요? 그 결과 querySelector()
/querySelectorAll()
가 플랫폼에 추가되었습니다. 분명히 이득이죠? CSS 선택기로 DOM 가져오기를 대중화한 라이브러리이며 나중에 표준에서 채택했습니다. 항상 그런 것은 아니지만 그렇게 될 때는 좋습니다.
<template>
도 비슷한 사례인 것 같아요. 클라이언트 측 템플릿 작성 방식을 표준화하지만 더 중요한 점은 2008년 해킹이 필요하지 않다는 점입니다.
전체 웹 작성 프로세스를 더 합리적이고 유지 관리가 쉬우며 기능이 더 풍부하게 만드는 것은 항상 좋은 일이라고 생각합니다.
추가 리소스
- WhatWG 사양
- Web Components 소개
- <web>components</web>(동영상): 저의 멋지고 포괄적인 프레젠테이션입니다.