이번 글은 웹 컴포넌트 소개 연재 4번째로 Template Element과 HTML Imports에 대한 글이다. 서두에 밝히자면, 이 두 표준의 흐름은 웹 컴포넌트 개발에서 제외될 것으로 보인다. 따라서 두 표준을 자세히 알아보기보다는 이것들이 웹 컴포넌트로써 사용되게 된 배경과 한계점, 그리고 Polymer Summit 2017에서 나온 현재의 동향을 이야기로 풀어보았다. 직접 실습할 내용도 없으니 가볍게 이야기 읽듯 따라가 보자.
자바스크립트에서 HTML Element를 만드는 일은 일반적이다. 고민 없이 엘리먼트를 하나 만들어서 body에 붙여보자. document.createElement
, appnedChild
두 개의 함수 실행으로 간단히 할 수 있다.
const wrapper = document.createElement('div');
const content = document.createElement('div');
...
anotherElement.innerText = someText;
...
document.body.appendChild(wrapper);
wrapper.appendChild(content);
...
그러나 복잡한 DOM을 구성할 때 이런 방식을 적용하면 코드는 너무 길어져 버린리고, DOM 구조를 파악하기도 괴롭다. 유지보수 하기도 힘들어지므로 이렇게 쓰고 싶지는 않다. 복잡한 템플릿을 작성할 때에는 innerHTML
을 사용하는 것이 편하다. 아래의 코드는 더욱 직관적이면서 유지보수 가능한 형태로 보인다.
document.querySelect("#target").innerHTML = [
'<div class="wrapper">',
'<div class="content">',
...("<div>" + someText + "</div>"),
..."</div>",
"</div>"
].join("");
그러나 이 방법도 리스트처럼 반복적인 엘리먼트를 생성해야 하거나, 조건에 따라 템플릿이 바뀌어야 하는 경우 등의 다양한 요구사항을 해결하기에는 부족하다. 더불어 내부의 엘리먼트에 접근하기 위해서는 querySelector
를 사용해야하는 점도 불편하다.
문자열의 간단한 조합에서 해결할 수 없는 Loop, If 등의 요구사항들을 해결하기 위해 템플릿 엔진들이 사용되기 시작했다. Mustache, Handlebars 등 템플릿 엔진들은 비슷한 목적으로 쓰이면서도 각자의 개성을 가지고 있다. 유명한 Vue.js, Angular, Polymer(과거) 들도 카테고리에 속한다 할 수 있다. 이 템플릿 엔진 방식은 여전히 많은 상황에서 유용하게 사용되고 있다.
<!-- angular: loop -->
<header ng-repeat-start="item in items">
Header {{ item }}
</header>
<!-- handlebars: conditional -->
<div class="entry">
{{#if author}}
<h1>{{firstName}} {{lastName}}</h1>
{{/if}}
</div>
프론트엔드 개발이 컴포넌트화 되면서, 컴포넌트들이 그리는 여러 개의 템플릿들을 효율적으로 처리해주는 React(JSX + Virtual DOM)가 인기를 얻었다. JSX는 ESCAScript의 확장으로 스크립트 내부에 직접 템플릿을 작성할 수 있게 해주며, Virtual DOM은 주어진 상태에 따라 DOM의 일부분만 업데이트 해주는 역할을 한다. 템플릿 관점에서 보자면, React는 상태에 따라 템플릿을 컴포넌트 단위로 업데이트 해주는 일을 한다고 할 수 있다. JSX + Virtual DOM은 현재 가장 인기있는 템플릿 처리 방식이라 말할 수 있다.
// Virtual DOM example
var tree = render(count); // We need an initial tree
var rootNode = createElement(tree); // Create an initial root DOM node ...
document.body.appendChild(rootNode); // ... and it should be in the document
setInterval(function() {
count++;
var newTree = render(count);
var patches = diff(tree, newTree);
rootNode = patch(rootNode, patches);
tree = newTree;
}, 1000);
// jsx example
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
render(element);
드디어 Template Element의 이야기 이다. 템플릿 엔진들은 유용하지만 문자열로 처리되기에 어쩔 수 없는 문제점들을 내포한다.
XSS 공격에 노출될 위험이 있으며, innerHTML
의 사용이 강제된다. DOM API들을 템플릿에 사용할 수 없다는 것들이다.
이러한 문제들을 해결하기 위해 템플릿의 문자열 처리를 지양하고 엘리먼트로 처리하는 방법으로 Template Element 가 표준으로 만들어 졌다.
<template id="productrow">
<tr>
<td class="record"></td>
<td></td>
</tr>
</template>
const template = document.querySelector("#productrow");
const clone = document.importNode(template.content, true);
clone.querySelector(".record").innerText = "####";
tableElement.appendChild(clone);
Template Element는 자바스크립트 코드로 많은 양의 코드를 적지 않아도 되고, 조건에 따라 DOM의 변경도 가능하다. 이러한 변경은 강력한 DOM API들을 그대로 사용할 수 있어 편리하다. Template Element는 DOM에 한 번 정의되면 필요할 때마다 복사하여 붙여넣기 때문에 성능도 훌륭하다.
이러한 장점들으로 Template Element는 웹 컴포넌트를 구성하는 표준이 되었다. Template Element는 스크립트와 스타일도 포함할 수 있다. 스크립트와 스타일은 템플릿에 있을때는 적용되지 않지만, 복사되어 Document에 붙을 때에 적용된다. Shadow DOM과 시너지를 일으켜 웹 컴포넌트의 템플릿 기능을 수행하는 데 충분한 장점이라 할 수 있다.
<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<style>
.outer {
border: 2px solid brown;
background: red;
}
.name {
color: black;
}
</style>
<div class="outer">
<div class="name">
Bob
</div>
</div>
</template>
var shadow = document.querySelector("#nameTag").createShadowRoot();
var template = document.querySelector("#nameTagTemplate");
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);
그러나, Template Element는 템플릿이 HTML로 작성되어야 한다는 것이 오히려 단점이 되기도 한다. 컴포넌트의 컨트롤러에 해당하는 자바스크립트와 템플릿 뷰에 해당하는 HTML이 분리되어야 한다는 점이다. 컴포넌트와 모듈이 정확히 동의어라고 할 수는 없지만, 모듈로서 분리되어 재사용 될 수 있어야 의미가 있다. 그런데 Template Element와 Custom Elements를 컴포넌트로 구성하려면 HTML, JS 두 개의 파일이 필요하다. 이 방식은 웹 컴포넌트를 모듈로 만들기에 큰 걸림돌이다.
이제 HTML Imports가 역할을 할 때가 되었다. HTML Imports는 스크립트 기반의 CommonJS, RequireJS와는 달리 HTML 기반의 의존성 해결사 역할을 한다. HTML에서 HTML을 읽어와 붙여주기에 아래처럼 깔끔한 처리도 가능하다. HTML Imports의 문법은 스타일을 연결하는 것과 유사하지만 스크립트, 스타일, HTML 엘리먼트들의 의존성을 한 번에 해결해 주는 강력한 방법이다.
<!-- main page -->
<head>
<link rel="import" href="bootstrap.html" />
</head>
<!-- bootstrap.html -->
<link rel="stylesheet" href="bootstrap.css" />
<link rel="stylesheet" href="fonts.css" />
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
<script src="bootstrap-tooltip.js"></script>
<script src="bootstrap-dropdown.js"></script>
HTML Imports는 HTML과 자바스크립트 두 개의 의존성을 해결해야만 하는 숙명의 웹 컴포넌트의 구원자이다. 이를 적용하여 웹 컴포넌트를 구성하면 아래의 모습이 된다. 이전 시간까지 알아보았던 모든 웹 컴포넌트 표준들(Custom Elements, Shadow DOM, Template Element, HTML Imports)을 모두 조합해보자. 아래 보이는 HTML로 컴포넌트를 구성하는 모습은 모양과 처리 방법은 다르나 Polymer와 Vue.js에서도 비슷한 모습이다.
<!-- add dependencies -->
<link rel="import" href="./dependent-element.html" />
<!-- web component template -->
<template id="nameTagTemplate">
<style>
.outer {
border: 2px solid brown;
background: red;
}
.name {
color: black;
}
</style>
<div class="outer">
<div class="name">
Bob
</div>
<dependent-element></dependent-element>
</div>
</template>
<!-- web component class -->
<script>
class NameTag extends HTMLElement {
...
const shadowRoot = this.attachShadow({mode: 'open'});
const template = document.querySelector('#nameTagTemplate');
const clone = document.importNode(template.content, true);
shadowRoot.appendChild(clone);
...
}
customElements.define('name-tag', NameTag);
</script>
구글을 필두로 한 웹 컴포넌트 그룹은 위의 모양으로 Custom Elements, Shadow DOM, Template Element, HTML Imports 4개의 표준을 웹 컴포넌트 표준이라 정했다. 이후 Template Element는 가장 먼저 안정화 되었고 IE를 제외한 모든 브라우저에서 native로 지원한다. Custom Element와 Shadow DOM은 구현에 있어 몇 가지 의견을 수렴하면서 v1 스펙이 정의되어, IE를 제외한 브라우저에서 이미 지원되고 있거나 개발 중이다. 그런데 HTML Imports에서 문제가 생겼다. Firefox가 HTML Imports를 지원하지 않겠다고 선언한 것을 시작으로 다른 브라우저들도 구현에 나서지 않고 있기 때문이다.
Mozilla will not ship an implementation of HTML Imports. We expect that once JavaScript modules — a feature derived from JavaScript libraries written by the developer community — is shipped, the way we look at this problem will have changed. We have also learned from Gaia and others, that lack of HTML Imports is not a problem as the functionality can easily be provided for with a polyfill if desired. Mozilla and Web Components
웹 컴포넌트 옹호론자들은 꾸준히 HTML Imports를 구현할 것을 요구했지만, 실현되지 않을 것으로 보인다. HTML Imports에 여러가지 문제가 제기되었는데 그 중 몇 가지는 아래와 같다. 브라우저 제작사들은 이들 이유가 타당하다고 판단한 것 같다. - The Problem With Using HTML Imports For Dependency Management
위에서 제기한 이유들 외에도 실제 프로젝트를 구성해보려면 바로 만나게 되는 큰 장벽이 있다. Webpack을 사용할 수 없다. 이미 표준 같은 힘을 지닌 웹팩, 롤업과 같이 사용할 수 없는 점은 치명적이다. 현재는 polymer-webpack-loader가 나와있지만 겨우 2달 되었고 직접 사용해본 결과 기본적인 문제들의 해결이 필요해 보인다. 결국, 웹 컴포넌트를 작성하기 위해서는 polymer등의 지원 프레임워크와 독립적인 도구들을 사용해야 한다는 결론으로 흐르게 된다.
HTML Imports는 브라우저 지원이 안 될 것이라는 것, 웹팩 생태계와 따로 놀고 있다는 점등을 인식하고 결국 방향이 바뀌고 있다. 본인 의견으로도 Polymer가 웹팩 로더만 빠르게 지원했어도 더 나은 결과가 나오지 않았을까 생각한다. 지난주 있었던 Polymer Summit 2017에서는 Polymer 3.0 preview를 발표하며 새로운 방향을 보여주었다. 바로 HTML Imports를 포기하는 것이다. HTML Imports는 ES Modules로 대체되었고, HTML Template는 ES Template literals로 바뀌었다. 이로써 webpack 생태계의 시민이 된 것이다. 의존성 문제는 해결되었지만, 템플릿은? 결국, 문자열 기반의 템플릿으로 돌아가면 Template Element의 장점을 잃어버리게 되는 것 아닌가? 다행히도 ES6 Template literals는 단순한 문자열이 아니고 템플릿 역할을 수행할 멋진 가능성을 가지고 있다.
el.innerHTML = `
<div>
<h1>${title}</h1>
<body>${content}</body>
</div>
`;
이것 으로는 부족하다 여길 것이다. 다행히도 최근 hyper(HTML), lit-html, hyperx등의 Template Literals를 사용한 프로젝트가 인기를 얻고 있다. lit-html의 설명에 따르면 이는 Template Literals를 받아 Template Element를 생성한다. 만약 같은 템플릿으로 엘리먼트를 재생성하면, 이미 생성한 Template Element를 사용해서 효율을 높이는 방향으로 만들어졌다고 한다. 문자열을 사용하되 Template Element의 장점을 흡수하고자 하는 것이다. 크기도 minified기준으로 2kb가 채 안 된다고 하니, 바닐라 자바스크립트를 지향하는 경우에도 템플릿으로는 Template Literals를 이용한 이들 프로젝트를 사용하는 것이 좋아 보인다.
종합하면, 모양새는 Custom Element와 Shadow DOM이 자리은 지키고, HTML Imports의 하차로 인해 Template Element까지 쓰지 못하게 되었다. 그리고 빈 자리를 표준인 ES Modules와 Template Literals가 차지할 것으로 보인다. 그리고 Template Literals를 활용한 프로젝트들을 주시할 필요가 있겠다. Polymer Summit의 발표가 보여준 방향이 웹 컴포넌트의 방향을 정한다 볼 수 없으나, 많은 사람이 고민하고 있던 문제들이 해결되는 모습이다. 무엇보다 웹팩의 시민이 되었으니 앞으로 Web Components의 행보가 탄력을 받을 것으로 예상해본다.
오늘 Template Element와 HTML Imports에 대해 생각하며, 이미 힘을 잃은 이 두 기술을 어떻게 쓸지 고민이 많아 시작이 쉽지 않았다. 두 기술을 자세히 설명하는 것은 의미 없을 것으로 생각해 프론트엔드에서의 템플릿의 흐름, 의존성과의 연관성, 최근의 흐름에 대해 하나의 이야기로 풀어 마무리한다. 다행인 것은 최근의 흐름이 웹 컴포넌트에 관심이 있던 한 명으로서 웹 컴포넌트가 좋은 방향으로 진행 되는것 같아 기쁘다.
또한 이 글을 재미있게 읽었다면 지난주 Polymer Summit 2017의 lit-html 세션영상을 추천한다. 이 글 역시 해당 세션의 흐름을 따라가며 작성하였고, 이 영상은 Template Literals에 대한 이해와 React에 대한 이야기, 그리고 이들을 이용해서 lit-html이 어떻게 영리하게 문제를 해결하는지를 담고 있다. 이들 Template Literals 라이브러리들이 웹 컴포넌트를 React보다 멋진 모습을 만들어 줄 것이라는 기대도 생긴다.
이번을 웹 컴포넌트 마지막 글로 계획했다. 그러나 한 번 더 바뀐 흐름을 따라, 어떻게 웹 컴포넌트 개발을 시작할 수 있는지 튜토리얼 성격의 글을 여러분과 같이 시작해 볼 예정이다.