오늘은 이전에 썼던 글 WebComponents: Keep calm and #UseThePlatform에 이어지는 것으로, Web Components의 주요 표준 중 하나인 Custom Elements에 대한 글이다.
<div>
대신 <current-time>
처럼 적절한 이름의 태그를 사용할 수 있다사용법은 아래의 코드를 보는 것이 빠르겠다. 아래의 코드를 크롬이나 사파리에 올려보자. 이외의 브라우저에서는 아래의 브라우저 지원과 Polyfill 섹션을 참조하자.
<!DOCTYPE html>
<html>
<head>
<script src="../src/CurrentTime.js"></script>
</head>
<body>
<current-time>
<!-- fallback value -->
6/11/2017, 11:55:49
</current-time>
</body>
</html>
class CurrentTime extends HTMLElement {
constructor() {
// 클래스 초기화. 속성이나 하위 노드는 접근할 수는 없다.
super();
}
static get observedAttributes() {
// 모니터링 할 속성 이름
return ["locale"];
}
connectedCallback() {
// DOM에 추가되었다. 렌더링 등의 처리를 하자.
this.start();
}
disconnectedCallback() {
// DOM에서 제거되었다. 엘리먼트를 정리하는 일을 하자.
this.stop();
}
attributeChangedCallback(attrName, oldVal, newVal) {
// 속성이 추가/제거/변경되었다.
this[attrName] = newVal;
}
adoptedCallback(oldDoc, newDoc) {
// 다른 Document에서 옮겨져 왔음
// 자주 쓸 일은 없을 것.
}
start() {
// 필요에 따라 메서드를 추가할 수 있다.
// 이 클래스 인스턴스는 HTMLElement이다.
// 따라서 `document.querySelector('current-time').start()`로 호출할 수 있다.
this.stop();
this._timer = window.setInterval(() => {
this.innerText = new Date().toLocaleString(this.locale);
}, 1000);
}
stop() {
// 이 메서드 역시 CurrentTime클래스의 필요에 의해 추가했다.
if (this._timer) {
window.clearInterval(this._timer);
this._timer = null;
}
}
}
// <current-time> 태그가 CurrentTime 클래스를 사용하도록 한다.
customElements.define("current-time", CurrentTime);
아래에는 참고로 비슷한 역할을 수행하도록 Custom Elements를 사용하지 않고 작성한 코드이다.
<!DOCTYPE html>
<html>
<head>
<script src="../src/CurrentTime.js"></script>
</head>
<body>
<div class="current-time">
<!-- fallback value -->
6/11/2017, 11:55:49
</div>
</body>
</html>
class CurrentTime {
constructor(el) {
this._el = el;
this._init();
this.start();
}
_init() {
// 속성 변경을 모니터
this._localeChangedObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (
mutation.type === "attributes" &&
mutation.attributeName === "locale"
) {
this.locale = this._el.getAttribute("locale");
}
});
});
this._localeChangedObserver.observe(this._el, {
attributes: true,
attributeFilter: ["locale"]
});
// 엘리먼트가 DOM에서 제거되었는지 모니터
this._disconnectedObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (
mutation.type === "childList" &&
Array.prototype.slice.call(mutation.removedNodes).indexOf(this._el) >=
0
) {
this.dispose();
}
});
});
this._disconnectedObserver.observe(this._el.parentNode, {
childList: true
});
}
start() {
this.stop();
this._timer = window.setInterval(() => {
this._el.innerText = new Date().toLocaleString(this.locale);
}, 1000);
}
stop() {
if (this._timer) {
window.clearInterval(this._timer);
this._timer = null;
}
}
dispose() {
this.stop();
this._localeChangedObserver.disconnect();
this._disconnectedObserver.disconnect();
}
static create(el) {
return new CurrentTime(el);
}
}
document.addEventListener(
"DOMContentLoaded",
() => {
document.querySelectorAll(".current-time").forEach(el => {
CurrentTime.create(el);
});
},
false
);
천천히 글을 읽어보기로 했다면 아래의 W3C에서 발췌한 서문부터 시작해 본다.
This specification describes the method for enabling the author to define and use new types of DOM elements in a document. - W3C: Custom Elements
W3C의 Custom Elements 표준 서문은 이를 새로운 DOM Elements를 정의하고 사용하는 방법이라 말하고 있다. 필자에 따라서는 Missing Piece of HTML(HTML에 빠진 조각), Association of a tag with a class(태그와 클래스의 연동) 이라고도 한다.
HTML은 우리 모두 잘 알다시피 웹 페이지 문서의 구조를 설명하기 위해 태어난 언어로, 현대의 웹 애플리케이션을 만들기 위해 컴포넌트로써 사용하는 것은 어색하다. HTML 표준은 우리가 애플리케이션을 만들 때 필요한 컴포넌트들의 정의는 포함하지 않기 때문이다.
우리가 웹 애플리케이션을 작성할 때 가장 많이 사용하는 태그는 무엇인가? 기억을 돌이킬 필요도 없이 div
태그이다. 아래는 gmail의 마크업이지만, 우리 모두 이렇게 사용하고 있음을 부정할 사람은 없을 것이다. 때에 따라서 figure
, code
, nav
등의 태그들을 조금 더 섞어 줄 수 있겠으나, 그 의미적인 태그들이 우리가 div
를 쓰고 있는 이유를 없애 주지는 않는다.
'div' soup: 묻지도 따지지도 말고 몽땅 div에 넣고 저어라!
한번은 왜 div
인가? 하는 물음을 가지고 다른 방법을 찾아보기도 했지만, HTML 표준에 따르자면 다른 태그들도 마땅치 해결책이 되지는 않았다. 비슷한 용도로 쓰이는 span
으로 만들라 한들 문제가 될 리 없으나, 인라인 태그 안에 블록 태그를 넣는 것은 HTML 스펙을 무시하는 일이니 결국 어떤 컴포넌트던지 블록 태그인 div
에 담는 것으로 돌아오고 만다.
Authors are strongly encouraged to view the div element as an element of last resort, for when no other element is suitable. - W3C HTML5.1: The div element > 아무런 엘리먼트도 어울리지 않으니
div
를 사용한다
할 수만 있다면, 의미 없는 div를 남발하는 것보다 의미 있는 태그 이름을 주는 쪽이 좋지 않을까? 기존 HTML 표준은 div
이외에 마땅한 해결책을 주지 않았지만, 아래는 Custom Elements를 사용하여 적절한 이름을 가지게 한 마크업의 예시이다. 이쁘다!
<div class="user-profile">
<div class="layout card small">...</div>
</div>
<user-profile>
<card-layout type="small">...</card-layout>
</user-profile>
div 인형들을 실로 묶어 자바스크립트가 연출하는 꼭두각시 인형극 - [Polymer 2.0: Under the Hood - Rob Dodson](https://www.youtube.com/watch?v=9vYJ8K6AKc)_
위에서부터 얘기했지만 사실 div
엘리먼트는 자바스크립트와 연관이 없다. "음? 무슨 말이지? 자바스크립트로 웹 애플리케이션 잘 만들고 있는데!" 물론 그렇다. 다시 고쳐 말하자면 엘리먼트들은 자바스크립트를 알지 못하며, 자바스크립트는 엘리먼트들을 조정해 꼭두각시 인형처럼 연출한다. 아래의 익숙한 코드들을 살펴보자.
// 자바스크립트 컨트롤을 묶을 엘리먼트를 받아와야 한다.
constructor(el) {
this._el = el;
}
...
// 이 클래스 인스턴스가 `유지하는 엘리먼트`의 innerText
this._el.innerText = 'text';
...
// 클래스 인스턴스와 엘리먼트의 라이프사이클은 다르다.
// bootstrap
document.addEventListener('DOMContentLoaded', () => {
CurrentTime.create(document.querySelector('.current-time'));
}, false);
// finalize
this._disconnectedObserver = new MutationObserver(mutations => { ... this.dispose() ... });
this._disconnectedObserver.observe(this._el.parentNode, {
childList: true
});
우리에게 너무나도 익숙하기에 당연하게 느껴지겠지만 아래의 Custom Elements를 정의하는 코드와 비교하며 어떠한 점이 다른지 찾아보자. 아래의 자세한 로직은 곧 설명할 테니 걱정 말자!
위의 코드에서는 자바스크립트에게 엘리먼트를 패스해주기 전까지 서로 어떠한 관계도 없다. 자바스크립트에게 패스해 준 이후에도 엘리먼트는 프로퍼티에 저장해 두고 조작해야 할 3인칭 대상이다. 또한 클래스 인스턴스와 엘리먼트는 라이프 사이클을 달리하기에 항상 부트스트랩 코드가 필요하며 경우에 따라서는 길고 고통스러운 MutationObserver 코드를 작성해야 하기도 한다. (심지어 대상 엘리먼트가 DOM에서 제거되었는지 알기 위해서는 대상 엘리먼트의 상위 엘리먼트까지 살펴야 한다!)
class CurrentTime extends HTMLElement {...}
...
this.innerText = 'text';
...
// connect
connectedCallback() { ... }
// disconnect
disconnectedCallback() { ... }
Custom Elements의 코드에서는 클래스의 인스턴스 this
가 엘리먼트 인스턴스 자체이다. 엘리먼트와 클래스 인스턴스는 동일한 라이프 사이클을 가지기에, 엘리먼트의 라이프 사이클에 맞추기 위한 어떠한 부트스트랩 코드나 MutationObserver가 필요하지 않다. 첫번째 코드는 우리에게 익숙하지만 두번째 코드는 생소하지만 무척이나 간결하고도 자연스럽다.
Custom Elements는 웹 애플리케이션을 작성하는데 필요한 HTML의 빠진 조각을 자연스러우면서도 직관적인 방법으로 제공해준다.
무엇을 하고 싶은지 알아봤으니, 이제 Custom Elements를 등록하는 방법부터 차근차근 알아보겠다. 아래는 가장 간단한 코드로 Custom Elements를 등록하는 방법이다. 이는 window
의 CustomElementRegistry에 custom-time
태그와 주어진 클래스를 묶는 역할을 한다.
window.customElements.define("current-time", class extends HTMLElement {});
<current-time></current-time>
이것으로 HTML에서 <current-time>
태그를 사용할 수 있게 되었다. 아무런 로직을 주지 않았으므로 별 의미 없는 일이 되었지만 엄연히 current-time
은 HTML에서 사용될 수 있는 Custom Element가 되었다.
-
를 포함하자주의할 점은 태그의 이름인데, Custom Elements는 특별한 이름 규칙을 필요로 한다. 쉽게 설명하면 글자 가운데 -
를 하나 이상 포함해야 한다는 것이다.
W3C Custom Elements: 올바른 이름 규칙
<tui-editor></tui-editor>
<my-element></my-element>
<super-awsome-carousel></super-awsome-carousel>
<tuieditor></tuieditor> /* `-` 없음 */ <font-face></font-face> /* 예약된 태그
이름 SVG */ <missing-glyph></missing-glyph> /* 예약된 태그 이름 SVG */
이러한 제약을 가지는 이유는 HTML파서가 자바스크립트에서 선언된 Custom Elements를 모르는 상황에서도 Custom Elements가 될지도 모르는 태그들을 구분하기 위해서이다. HTML표준에 정의되어 있지 않으면서도 Custom Elements 이름 규칙에 맞지 않는 태그들은 HTMLUnknownElement
인터페이스가 할당된다. 그러나 Custom Elements들은 HTMLElement
로부터 직접 상속되어야 하므로 Custom Elements 이름 규칙에 맞는 것들의 HTMLUnknownElement
상속을 막기 위해서이다.
WHATWG: 태그 이름에 따라 HTMLElement 인터페이스가 할당되는 방법
여태까지의 예제에서도 그러했지만, Custom Elements 스펙은 기본적으로 es6 클래스를 등록하도록 정의 되어있다(es5 prototype 형태를 사용하는 방법은 뒤에서 다룬다). 그리고 당연하지만 Custom Elements 클래스 constructor
는 여타 es6 클래스의 constructor
와 다를 바 없이 동작한다.
아래의 'yey!'메시지는 예상할 수 있는 대로 new CurrentTime()
을 통해 새로운 인스턴스를 생성할 때마다 확인할 수 있다. 또한 DOM에 <current-time>
엘리먼트가 추가될 때마다도 메시지를 보여줄 것이다.
<current-time></current-time>
class CurrentTime extends HTMLElement {
constructor() {
super(); // 항상 맨 앞에!
console.log("yey!");
}
}
window.customElements.define("current-time", CurrentTime);
다만 사용법에 있어서 주의할 점이 있는데, 전통적인 방법에서 우리는 아래의 코드처럼 constructor
에서 DOM을 조작하는 데 익숙하다. 이것이 가능한 이유는 우리가 Document의 'DOMContentLoaded'이벤트를 받아 DOM이 로드되고 나서 class를 초기화하기에 constructor
가 실행되는 시점에서 엘리먼트는 DOM에 붙어있는 상태이다. 따라서 constructor
에서 어떠한 DOM 조작을 해도 무방하다.
class CurrentTime {
constructor(el) {
super();
this._initDOM(el); // DOM 조작들
}
}
그러나 HTMLElement를 상속받은 Custom Elements의 constructor의 실행 시점은 아직 DOM에 추가지되지 않은 상태이다. 그렇기에 아래처럼 constructor
에서는 어떠한 DOM 조작도 할 수 없다. 그러므로, 이곳에서는 DOM과 무관한 클래스 인스턴스 자체의 준비만 해야 한다.
constructor() {
super(); // 항상 맨 앞에!
console.log(this.parentNode); // null
console.log(this.firstChild); // null
console.log(this.innerHTML); // ""
console.log(this.getAttribute('locale')); // null
this.setAttribute('locale', 'ko-KR'); // 에러: Uncaught DOMException: Failed to construct 'CustomElement': The result must not have attributes
this.innerText = 'Arr'; // 에러: Uncaught DOMException: Failed to construct 'CustomElement': The result must not have children
}
connectedCallback
과 disconnectedCallback
은 HTMLElement
를 상속받은 경우, 이 Custom Element가 DOM에 추가되거나/제거될 때마다 실행된다. 이때마다 인스턴스 객체가 생성/파괴되는 것은 아니므로, DOM을 수정함에 따라 얼마든지 여러 번 실행될 수 있다.
또한 한번 생성된 객체는 자동으로 파괴되지 않으므로 disconnectedCallback
에서 적절히 인스턴스 객체를 정리하는 작업을 해 줄 필요도 있다.
connectedCallback
의 실행 시점은 이 인스턴스가 DOM에 추가된 후이기 때문에 DOM을 조작하는 작업을 이곳에서 하는 것이 알맞다. 밑의 코드를 실행해보면 예상하는 대로 속성/부모/자식의 값을 수정할 수 있다.
이 콜백들을 사용할 때에도 주의할 점이 있는데, connectedCallback
이 실행되는 시점에 이 엘리먼트가 DOM에 추가되어 있기는 하지만, 자식 엘리먼트들은 아직 DOM에 추가되지 않았다. 따라서 자식 엘리먼트를 수정할 수는 있더라도, HTML에서 삽입한 자식 엘리먼트들에 접근할 수는 없다는 점에 유의하자.
별도로 자식 엘리먼트들에 접근하는 방법은 뒤에서 알아보겠다.
<current-time locale="ko-KR">
자식!
</current-time>
class CurrentTime extends HTMLElement {
...
connectedCallback() {
// 이 엘리먼트는 DOM에 추가되었다.
console.log(this.parentNode); // ok "<body></body>"
console.log(this.firstChild); // null <--- 아직 자식 엘리먼트에 접근할 수는 없다.
console.log(this.innerHTML); // "" <--- 아직 자식 엘리먼트에 접근할 수는 없다.
console.log(this.getAttribute('locale')); // ok "ko=KR"
this.setAttribute('locale', 'en-US'); // ok
this.innerText = 'Arr'; // ok
}
...
disconnectedCallback() {
// 이 엘리먼트가 DOM에서 제거되었다.
// connectedElement에서 수행한 셋업을 청소하는 일을 하자
}
}
window.customElements.define('current-time', CurrentTime);
이 콜백 메서드들은 MutationObserver
의 chlidList
를 통해 처리하는 방식보다 간결하고 직관적인 방법을 제공해준다. 아래의 MutationObserver
를 통한 방식과 비교해보자.
...
this._disconnectedObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList' &&
Array.prototype.slice.call(mutation.removedNodes).indexOf(this._el) >= 0) {
...
}
});
});
this._disconnectedObserver.observe(this._el.parentNode, {
childList: true
});
...
이 MutationObserver
가 IE11부터 지원하고, 비슷한 기능을 제공하는 MutationEvents
는 deprecated 되었다. 이러한 점들과 Custom Elements 역시 IE11부터 지원하는 점을 고려해 볼 때, MutationObserver
를 사용해야 할 프로젝트에서는 Custom Elements 도입을 적극 고려해 보아도 좋겠다는 생각이다.
(Mutation Events는 성능에도 문제가 있다 - Mutation Events Replacement: The story so far / existring points of consensus)
attributeChangedCallback
은 위의 콜백들과 마찬가지로 MutationObserver
에서 attributes
를 모니터링하는 기능을 한다. 주어진 이름으로 메서드를 등록해 놓으면 속성이 변경될 때마다 콜백 메서드가 실행된다. 한가지 잊지 말아야 할 것은 observedAttributes
에서 관심 있는 속성들의 이름을 나열해 주어야 한다는 것이다.
...
static get observedAttributes() {
// 모니터링 할 속성 이름
return ['locale'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
// 속성이 추가/제거/변경되었다.
this[attrName] = newVal;
}
...
이 콜백 메서드는 해당 엘리먼트가 다른 Document에서 옮겨져 올 때 수행된다. document.adoptNode()
가 이 엘리먼트를 대상으로 실행되었을 때 호출된다고 설명되어 있다. 아직 특별한 쓰임새는 보이지 않으므로 알고만 넘어가자.
adoptedCallback(oldDoc, newDoc) {
// 다른 Document에서 옮겨져 왔음
}
이로써 Custom Elements의 스펙을 기준으로 하나씩 구현에 필요한 내용들을 살펴보았다. Custom Elements가 새로운 개념을 도입하기에 독자에 생소하고 어려워 보일 수 있다고 본다. 하지만 위의 내용들을 정리해 보면 결코 많은 내용을 담고 있지 않다. 더불어 엘리먼트와 인스턴스 콜백들의 주의점 들은 엘리먼트의 라이프 사이클을 고려해보면 당연하다고도 볼 수 있는 내용들이니 겁내지 말자.
-
를 포함해야 한다.Chrome: 지원
Edge: 개발 예정, 현재 Polyfill로 지원
Firefox: 개발 중, 현재 Polyfill로 지원
Safari: 지원
IE11: 네이티브 지원 없음, 현재 Polyfill로 지원
Chrome Android, Mobile Safari: 지원
모바일을 포함 IE11 이상의 최신 브라우저에서 Polyfill을 사용하면 Custom Elements를 사용할 수 있다. 쉬운 접근은 webcomponents-loader.js
를 사용하는 것이며, 만약 프로젝트 소스들을 babel로 트랜스파일 하고있다면 custom-elements-es5-adapter.js
를 추가해준다.
구글 스타일 답다고 해야할지 모르겠으나 Polyfill 사용에 다양한 옵션을 제공해 주는데 크게 아래의 3가지 방법으로 해결 가능하다.
webcomponents-hi-ce.js
, webcomponents-hi-sd-ce.js
등의 Custom Elements의 약자인 ce
가 포함된 파일을 사용하거나, 모든 폴리필을 포함한 webcomponents-lite.js
를 사용하면 된다.우리는 종종 브라우저 지원을 넓히기 위해 es5로 자바스크립트를 트랜스파일 하는데, Custom Elements 표준은 customElements.define
에 es6 class를 요구한다. 이러한 경우에는 custom-elements-es5-adapter.js
를 추가적으로 로드할 필요가 있다.
주의할 점은 custom-elements-es5-adapter.js
는 네이티브로 브라우저에서 Custom Elements를 지원하는데, 자바스크립트 파일을 es5로 트랜스파일 한 경우에 필요한 것이지, IE11에 필요한 것은 아니라는 것이다.
위의 내용은 주요한 개념에 따라 움직이는 것들이기에 확실히 이해해두면 좋다. 이제부터는 위에서 알아본 주요한 스펙 이외에 실제 Custom Elements를 구현할 때 만날 수 있는 내용들을 조금 더 자세히 알아볼 것인데, 바로 이해하지 않더라도 추후 참고할 수 있을 정도로 읽어두자.
만일 Custom Elements를 customElements.define
을 통해 정의하였으나 DOM에서는 사용되지 않는 경우가 있을 수 있다. 그러나 한번 정의되고 나면 나중에 DOM에 해당 이름의 엘리먼트가 추가되는 경우라도 곧바로 주어진 엘리먼트를 묶어주며, 알맞은 콜백 메서드들을 정상적으로 호출한다.
반대로 DOM에 엘리먼트가 존재하지만 customElements.define
메서드를 통해 선언되지 않았다면, DOM에 있는 엘리먼트는 span
과 마찬가지로 동작하며, 추후 customeElements.define
을 통해 선언되면 주어진 클래스를 바로 묶어준다.
이러한 과정을 특별히 upgrade라고 한다.
이 upgrade 과정은 특별히 중요한데 우리가 'DOMContentLoaded'이벤트를 보고 인스턴스를 초기화하는 이유와 마찬가지로 DOM이 생성된 이후에 자바스크립트가 수행될 경우 DOM에 있는 Custom Elements는 스타일도, 동작도 못 가진 채로 화면에 노출될 수 있기 때문이다.
이러한 경우 :defined pseudo 클래스를 사용해 아래처럼 customeElements.define
을 통해 정의되기 이전까지 해당 엘리먼트를 가려둘 수도 있고, 필요에 따른 스타일을 적용해 볼 수 있겠다.
current-time:not(:defined) {
display: hidden;
}
connectedCallback
메서드에서 자식 엘리먼트들에 접근할 수 없다는 사실은 위에서 알아본 바 있다. 이 콜백 메서드에서 자식 엘리먼트들이 DOM에 추가되었으며, define
되었는지 알기 위해서는 조금 다른 전략이 필요하다.
MutationObserver
, Promise
등 여러 방법들이 있겠으나 가장 간단하고 확실한 방법은 자식 Custom Elements들의 connectedCallback
에서 부모 Custom Element의 메서드를 수행시켜 주거나 Custom Event를 발생시키는 것이다.
DOM을 수정할 connectedCallback
이 이러한 특성을 가지므로 Introducing Custom Elements의 Asynchronously Defining Custom Elements 부분에서 얘기하는 전략을 더 읽어보는 것도 도움이 된다.
class CurrentTimeText extends HTMLElement {
...
connectedCallback() {
// call parents callback
this.parentNode.childReady(this);
// or
this.parentNode.dispatchEvent(new Event('childReady'));
}
...
}
위의 어느 위의 방법이 마음에 들지 않을 수 있는 독자도 있을 것이다. 잠시 참아보자. 다음 글에서 소개할 Shadow DOM과 함께 보다 아름다운 모습으로 해결하는 방법을 얘기하겠다.
window
가 생성될 때, 브라우저는 CustomElementRegistry
인스턴스를 초기화하여, window.customElements
에 할당한다. 이 객체의 3가지 인스턴스 메서드들은 다음과 같다.
W3C: CustomElementRegistry
window.customElements.define: 태그 이름에 자바스크립트 클래스를 Custom Elements로 등록한다.
window.customElements.define("current-time", CurrentTime);
window.customElements.get: 태그 이름으로 정의된 클래스를 가져온다.
const CurrentTime = window.customElements.get("current-time");
const anotherTime = new CurrentTime();
window.customElements.whenDefined: 태그 이름의 Custom Elements가 등록될 때 이벤트를 받는다.
customElements.whenDefined("current-time").then(() => {
console.log("current-time defined!");
});
<current-time>
/* fallback html */ 13:00
</current-time>
Custom Elements를 이해하지 못하는 IE8~10 레거시 브라우저에서 <current-time>
커스텀 태그이름을 가진 엘리먼트는 HTMLUnknownElement
인터페이스를 가지게 되며, innerHTML
을 그대로 렌더링 하게 된다. 또한 HTMLUnknownElement
역시 스타일을 입힐 수 있으므로, 레거시 브라우저를 고려해야 하는 환경에서는 span
이라 생각하고 fallback html을 자식으로 가지고 있는 것이 좋다.
이는 Github에서 사용하고 있는 Custom Elements의 실패 전략인데, 동적으로 Custom Elements의 콘텐츠가 변화하지 않더라도, 페이지가 서버를 통해 리프레시 될 때 서버에서 랜더링한 적절한 콘텐츠를 보여줄 수 있도록 하는 방법이다.
Autonomouse custom elements는 여태까지 우리가 알아보아온 HTMLElement
를 직접 상속받는 Custom Elements의 형태이고, Customized built-in elements는 HTML 표준에 정의된 div
, input
table
등의 빌트인 엘리먼트들을 상속하는 형태이다.
빌트인 엘리먼트를 상속받는 이유는 해당 엘리먼트의 동작을 그대로 상속받으면서도 이외의 기능이나 스타일을 추가하기 위함이다. 이는 다만 조금(많이) 번거로운 문법을 요구한다.
<button is="current-time">6/11/2017, 11:55:49</button>
class CurrentTime extends HTMLButtonElement {
...
}
customElements.define('current-time', CurrentTime, {extends: 'button'});
악! HTML에서는 is
라는 녀석이 붙고, 클래스 extends
에서 분명 HTMLButtonElement
를 상속받음을 알려주었음에도 불구하고, 또다시 customElements.define
에서 버튼을 상속받고 있음을 또 알려준다. 이것이 정말 최선입니까
표준에 대한 논의 글을 따라가보면 HTML과 자바스크립트 생태계를 고려한 어쩔 수 없는 문법이라는 것인데, HTML 파서, 자바스크립트 파서는 브라우저만 존재하는 것도 아니고, button
이 각 파서들에게 button
으로 인식되기 위해서는 이러한 문법이 필요하다는 것이다. Autonomouse custom elements가 -
를 반드시 포함하면서 HTMLElement
를 바로 상속하는 명시적인 방법임에 반해, HTML 파서들에게(브라우저가 아닌) Customized built-in elements는 button
혹은 table
등 상속받은 엘레먼트로 인식되어야만 한다는 이유이다.
이유는 어쩔 수 없다 하겠으나, 이 사태에 대해 우리 또한 느낄 수 있듯이 각 브라우저 진영에서도 이 난잡한 문법에 대한 논의들이 있었다. 논의 끝에 어쩔 수 없는 문법을 마지못해 동의한 것으로 보이지만, 아직 제대로 구현된 브라우저가 없는 만큼 이러한 표준이 있다는 것만 알고 넘어가자.
사실 HTML 표준에서 정한 이름 이외의 태그를 사용하는 일은 이 문서에서 사용한 Custom Elements 표준이 아니라 하더라도 이전부터(이름만 바꾸는 일이라면 IE8부터도!) 사용 가능했다. 궁금하다면 지금 에디터를 열어 HTML문서를 하나 작성해서 띄워봐도 좋겠다.
HTML 표준에 따르면 태그 이름에 따라 적절한 HTMLElement
하위 인터페이스를 연결하며, 표준 이름의 어느것 에도 맞지 않는 태그에는 HTMLUnknownElement
인터페이스를 할당한다. HTMLUnknownElement
이름이 무언가 오류처럼 느껴질지 모르겠으나, HTMLSpanElement
와 다를 바 없다.
HTMLSpanElement
는 HTMLElement
를 상속하며, display
속성으로 inline
값을 가지는 인터페이스인데, HTMLElement
의 display
기본 속성값이 inline
이므로, 동일하게 HTMLElement
를 상속하는 HTMLUnknownElement
역시 HTMLSpanElement
와 다를 바 없다. 즉 <lol>
태그는 <span>
태그와 다를 바 없다. 즉 특별한 이름의 <random>
, <lol>
등을 CustomElements 표준 이전에도 적절한 스타일을 할당하여 얼마든지 사용 가능했다.
Determine HTML5 Element Support in Javascript: HTMLUnknownElement은 HTMLUnknownElement
에 대한 재미있는 시점을 보여주기도 한다.
필자는 Custom Elements의 튜토리얼들을 보며 "아! 이런 나이스 한!"이라며 이걸 어느 프로젝트에 적용해볼까 전전긍긍해왔던 기억이 있다. 하지만 이 표준을 지원하는 브라우저는 Chrome뿐이었고, 표준도 v0에서 v1으로 바뀌며 계속 제대로 써볼 기회는 손에 잡히지 않았었다. 물론 그사이 Chrome 베이스의 Electron 프로젝트에서 무작정 시도해 보기도 했지만! 이제는 IE11이상의 프로젝트라면 충분히 도전해 볼 환경이 되었다고 생각된다. 특히나 MutationObserver의 사용이 빈번한 프로젝트라면 과감히 Custom Elements를 시도해 볼만 하다.
다음 글로는 Web Components의 두 번째 표준 Shadow DOM에 대해서 써보려 한다. Web Components의 표준들은 각각 쓰임새도 있지만, 같이 쓰이면 더욱 시너지가 있는 만큼 Custom Elements에 Shadow DOM이 결합되며 어떻게 더 힘을 발휘하는지 기대해주면 좋겠다.