클라이언트 측 템플릿 표준화
소개
템플릿 개념은 웹 개발에 새로운 것이 아닙니다. 실제로 Django (Python), ERB/Haml (Ruby), Smarty (PHP)와 같은 서버 측 언어/엔진 템플릿은 오랫동안 사용되어 왔습니다. 하지만 지난 몇 년 동안 MVC 프레임워크가 폭발적으로 늘어났습니다 모두 약간 다르지만, 대부분은 표시 계층 (즉, DA 뷰)을 렌더링하는 일반적인 메커니즘을 공유합니다.
현실을 직시해야 합니다. 템플릿은 매우 유용합니다. 주변 사람들에게 물어보세요. 다음과 같이 정의도 따뜻하고 아늑한 느낌을 줍니다.
"...매번 다시 만들 필요는 없습니다..." 여러분에 관해서는 모르지만, 추가 작업을 피하는 것을 좋아합니다. 그렇다면 웹 플랫폼에서는 왜 개발자가 중요하게 생각하는 요소를 기본적으로 지원하지 않을까요?
WhatWG HTML 템플릿 사양이 답입니다. 클라이언트 측 템플릿을 위한 표준 DOM 기반 접근 방식을 설명하는 새로운 <template>
요소를 정의합니다. 템플릿을 사용하면 HTML로 파싱되고 페이지 로드 시 사용되지 않는 마크업의 프래그먼트를 선언할 수 있지만, 나중에 런타임 시 인스턴스화할 수 있습니다. 라파엘 와인스타인의 말:
이 URL은 어떤 이유로든 브라우저가 망가뜨리지 않았으면 하는 큰 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);
템플릿을 스탬프 아웃하면 콘텐츠가 '게시'됩니다. 이 예에서는 콘텐츠가 복제되고, 이미지가 요청되고, 최종 마크업이 렌더링됩니다.
데모
예: Inert 스크립트
이 예는 템플릿 콘텐츠의 비활성성을 보여줍니다. <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에서는 재사용 가능한 템플릿을 만들 수 있는 몇 가지 영리한 방법을 찾아냈습니다. 아래는 일반적인 2가지 경우입니다. 비교를 위해 이 도움말에 포함했습니다.
방법 1: 오프스크린 DOM
사람들이 오랫동안 사용해 온 한 가지 방법은 hidden
속성 또는 display:none
를 사용하여 '오프스크린' DOM을 만들고 뷰에서 숨기는 것입니다.
<div id="mytemplate" hidden>
<img src="logo.png">
<div class="comment"></div>
</div>
이 기법은 효과적이지만 몇 가지 단점이 있습니다. 이 기법에 대한 설명은 다음과 같습니다.
- DOM 사용 - 브라우저가 DOM을 인지합니다. 그것은 잘하는 일입니다. 손쉽게 클론할 수 있어요.
- 아무것도 렌더링되지 않음 -
hidden
를 추가하면 블록이 표시되지 않습니다. - Not inert - 콘텐츠가 숨겨진 경우에도 이미지에 대한 네트워크 요청이 계속 생성됩니다.
- 간편한 스타일 지정 및 테마 설정 - 스타일 범위를 템플릿으로 지정하려면 삽입 페이지에서 모든 CSS 규칙 앞에
#mytemplate
를 접두사로 추가해야 합니다. 이는 불안정하며 향후 이름 지정 충돌이 발생하지 않는다는 보장은 없습니다. 예를 들어 삽입 페이지에 이미 해당 ID를 가진 요소가 있는 경우 호스팅됩니다.
방법 2: 스크립트 오버로드
또 다른 기법은 <script>
를 오버로드하고 콘텐츠를 문자열로 조작하는 것입니다. John Resig는 아마도 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> (동영상) - 놀랍도록 종합적인 프리젠테이션을 제공합니다.