소개
웹은 표현력이 매우 부족합니다. 의미를 파악하려면 Gmail과 같은 '최신' 웹 앱을 살펴보세요.
<div>
수프는 현대적이지 않습니다. 하지만 웹 앱을 빌드하는 방법은 다음과 같습니다. 안타깝습니다.
플랫폼에 더 많은 것을 요구해야 하지 않나요?
섹시한 마크업 한번 해 보자
HTML은 문서를 구조화하는 데 훌륭한 도구이지만, 어휘는 HTML 표준에서 정의하는 요소로 제한됩니다.
Gmail의 마크업이 좋지 않은 경우 어떻게 해야 하나요? 아름다운 경우:
<hangout-module>
<hangout-chat from="Paul, Addy">
<hangout-discussion>
<hangout-message from="Paul" profile="profile.png"
profile="118075919496626375791" datetime="2013-07-17T12:02">
<p>Feelin' this Web Components thing.
<p>Heard of it?
</hangout-message>
</hangout-discussion>
</hangout-chat>
<hangout-chat>...</hangout-chat>
</hangout-module>
참신한 아이디어네요. 이 앱도 완전히 이해가 됩니다. 의미가 있으며 이해하기 쉽고 무엇보다도 유지관리가 가능합니다. 미래의 나/여러분은 선언적 백본을 살펴보기만 하면 정확히 어떤 역할을 하는지 알 수 있습니다.
시작하기
사용자설정 요소를 사용하면 웹 개발자가 새로운 유형의 HTML 요소를 정의할 수 있습니다. 이 사양은 웹 구성요소의 범위 내에서 제공되는 여러 새로운 API 원시 중 하나이지만 가장 중요한 API일 수 있습니다. 맞춤 요소로 잠금 해제된 기능이 없으면 웹 구성요소가 존재하지 않습니다.
- 새 HTML/DOM 요소 정의
- 다른 요소에서 확장되는 요소 만들기
- 맞춤 기능을 논리적으로 단일 태그로 번들로 묶기
- 기존 DOM 요소의 API 확장
새 요소 등록
맞춤 요소는 document.registerElement()
를 사용하여 생성됩니다.
var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());
document.registerElement()
의 첫 번째 인수는 요소의 태그 이름입니다.
이름에 대시 (-)가 포함되어야 합니다. 예를 들어 <x-tags>
, <my-element>
, <my-awesome-app>
는 모두 유효한 이름이지만 <tabs>
와 <foo_bar>
는 유효하지 않습니다. 이 제한사항을 통해 파서는 사용자설정 요소를 일반 요소와 구별할 수 있을 뿐만 아니라 HTML에 새 태그가 추가될 때 향후 호환성을 보장할 수 있습니다.
두 번째 인수는 요소의 prototype
를 설명하는 객체 (선택사항)입니다.
여기에서 요소에 맞춤 기능 (예: 공개 속성 및 메서드)을 추가할 수 있습니다.
자세한 내용은 나중에 설명해 드리겠습니다.
기본적으로 맞춤 요소는 HTMLElement
에서 상속됩니다. 따라서 이전 예시는 다음과 같습니다.
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype)
});
document.registerElement('x-foo')
호출은 브라우저에 새 요소에 관해 알려주고 <x-foo>
인스턴스를 만드는 데 사용할 수 있는 생성자를 반환합니다.
또는 생성자를 사용하지 않으려면 다른 요소 인스턴스화 기법을 사용할 수 있습니다.
요소 확장
사용자설정 요소를 사용하면 기존 (네이티브) HTML 요소와 다른 사용자설정 요소를 확장할 수 있습니다. 요소를 확장하려면 상속할 요소의 이름과 prototype
를 registerElement()
에 전달해야 합니다.
네이티브 요소 확장
일반 사용자 <button>
가 마음에 들지 않는다고 가정해 보겠습니다. '메가 버튼'이 되도록 기능을 강화하려고 합니다. <button>
요소를 확장하려면 HTMLButtonElement
의 prototype
와 요소의 이름인 extends
를 상속하는 새 요소를 만듭니다. 이 경우 'button':
var MegaButton = document.registerElement('mega-button', {
prototype: Object.create(HTMLButtonElement.prototype),
extends: 'button'
});
네이티브 요소에서 상속하는 맞춤 요소를 유형 확장 맞춤 요소라고 합니다.
'요소 X는 Y입니다'라고 말하는 방법으로 특수 버전의 HTMLElement
에서 상속됩니다.
예:
<button is="mega-button">
맞춤 요소 확장
<x-foo>
맞춤 요소를 확장하는 <x-foo-extended>
요소를 만들려면 프로토타입을 상속하고 상속할 태그를 지정하면 됩니다.
var XFooProto = Object.create(HTMLElement.prototype);
...
var XFooExtended = document.registerElement('x-foo-extended', {
prototype: XFooProto,
extends: 'x-foo'
});
요소 프로토타입을 만드는 방법에 관한 자세한 내용은 아래의 JS 속성 및 메서드 추가를 참고하세요.
요소 업그레이드 방법
HTML 파서가 비표준 태그에 대해 경고하지 않는 이유를 궁금해 한 적이 있나요?
예를 들어 페이지에서 <randomtag>
를 선언해도 됩니다. HTML 사양에 따르면 다음과 같습니다.
<randomtag>
님, 죄송합니다. 비표준이며 HTMLUnknownElement
에서 상속합니다.
맞춤 요소의 경우는 그렇지 않습니다. 유효한 맞춤 요소 이름이 있는 요소는 HTMLElement
에서 상속합니다. 콘솔 Ctrl + Shift + J
(Mac의 경우 Cmd + Opt + J
)을 실행하고 다음 코드 줄을 붙여넣으면 이 사실을 확인할 수 있습니다. 이 코드는 true
를 반환합니다.
// "tabs" is not a valid custom element name
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype
// "x-tabs" is a valid custom element name
document.createElement('x-tabs').__proto__ == HTMLElement.prototype
해결되지 않은 요소
맞춤 요소는 document.registerElement()
를 사용하여 스크립트에 의해 등록되므로 브라우저에서 정의가 등록되기 전에 선언하거나 만들 수 있습니다. 예를 들어 페이지에서 <x-tabs>
을 선언했지만 나중에 document.registerElement('x-tabs')
를 호출할 수 있습니다.
요소가 정의로 업그레이드되기 전에는 해결되지 않은 요소라고 합니다. 유효한 맞춤 요소 이름을 가지고 있지만 등록되지 않은 HTML 요소입니다.
다음 표를 참고하면 도움이 될 수 있습니다.
이름 | 상속 위치 | 예 |
---|---|---|
해결되지 않은 요소 | HTMLElement |
<x-tabs> , <my-element> |
알 수 없는 요소 | HTMLUnknownElement |
<tabs> , <foo_bar> |
요소 인스턴스화
요소를 만드는 일반적인 기법은 맞춤 요소에도 적용됩니다. 다른 표준 요소와 마찬가지로 HTML에서 선언하거나 JavaScript를 사용하여 DOM에서 만들 수 있습니다.
맞춤 태그 인스턴스화
다음과 같이 선언합니다.
<x-foo></x-foo>
JS에서 DOM 만들기:
var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
alert('Thanks!');
});
new
연산자를 사용합니다.
var xFoo = new XFoo();
document.body.appendChild(xFoo);
유형 확장 프로그램 요소 인스턴스화
유형 확장 프로그램 스타일 맞춤 요소를 인스턴스화하는 것은 맞춤 태그와 매우 유사합니다.
다음과 같이 선언합니다.
<!-- <button> "is a" mega button -->
<button is="mega-button">
JS에서 DOM 만들기:
var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true
보시다시피 이제 is=""
속성을 두 번째 매개변수로 사용하는 오버로드된 버전의 document.createElement()
가 있습니다.
new
연산자를 사용합니다.
var megaButton = new MegaButton();
document.body.appendChild(megaButton);
지금까지 document.registerElement()
를 사용하여 브라우저에 새 태그를 알리는 방법을 알아봤습니다. 하지만 이 방법은 큰 효과가 없습니다. 속성과 메서드를 추가해 보겠습니다.
JS 속성 및 메서드 추가
맞춤 요소의 강력한 점은 요소 정의에서 속성과 메서드를 정의하여 맞춤설정된 기능을 요소와 번들로 묶을 수 있다는 것입니다. 이는 요소의 공개 API를 만드는 방법이라고 생각하면 됩니다.
전체 예는 다음과 같습니다.
var XFooProto = Object.create(HTMLElement.prototype);
// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
alert('foo() called');
};
// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});
// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');
// 5. Add it to the page.
document.body.appendChild(xfoo);
물론 prototype
를 구성하는 방법은 수만 가지가 있습니다. 이와 같은 프로토타입을 만드는 것을 좋아하지 않는다면 다음과 같이 더 압축된 버전을 사용해 보세요.
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype, {
bar: {
get: function () {
return 5;
}
},
foo: {
value: function () {
alert('foo() called');
}
}
})
});
첫 번째 형식은 ES5 Object.defineProperty
를 사용할 수 있습니다. 두 번째는 get/set을 사용할 수 있습니다.
수명 주기 콜백 메서드
요소는 존재하는 동안 흥미로운 시점을 활용하기 위한 특수 메서드를 정의할 수 있습니다. 이러한 메서드는 적절하게 수명 주기 콜백이라고 합니다. 각 옵션에는 고유한 이름과 목적이 있습니다.
콜백 이름 | 호출 시점 |
---|---|
createdCallback | 요소의 인스턴스가 생성됩니다. |
attachedCallback | 인스턴스가 문서에 삽입되었습니다. |
detachedCallback | 문서에서 인스턴스가 삭제됨 |
attributeChangedCallback(attrName, oldVal, newVal) | 속성이 추가, 삭제 또는 업데이트됨 |
예: <x-foo>
에서 createdCallback()
및 attachedCallback()
정의
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};
var XFoo = document.registerElement('x-foo', {prototype: proto});
모든 수명 주기 콜백은 선택사항이지만 적절한 경우 정의합니다.
예를 들어 요소가 충분히 복잡하고 createdCallback()
에서 IndexedDB에 대한 연결을 연다고 가정해 보겠습니다. DOM에서 삭제되기 전에 detachedCallback()
에서 필요한 정리 작업을 실행합니다. 참고: 사용자는 탭을 닫을 수 있으므로 이 메서드에 의존해서는 안 되며, 최적화 후크로 생각하면 됩니다.
또 다른 사용 사례 수명 주기 콜백은 요소에 기본 이벤트 리스너를 설정하는 것입니다.
proto.createdCallback = function() {
this.addEventListener('click', function(e) {
alert('Thanks!');
});
};
마크업 추가
<x-foo>
를 만들고 JavaScript API를 제공했지만 빈 화면이 표시됩니다. 렌더링할 HTML을 제공해 줘야 할까요?
여기에서 수명 주기 콜백이 유용합니다. 특히 createdCallback()
를 사용하여 요소에 기본 HTML을 부여할 수 있습니다.
var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.createdCallback = function() {
this.innerHTML = "**I'm an x-foo-with-markup!**";
};
var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});
이 태그를 인스턴스화하고 DevTools에서 검사 (마우스 오른쪽 버튼 클릭, Inspect Element 선택)하면 다음이 표시됩니다.
▾<x-foo-with-markup>
**I'm an x-foo-with-markup!**
</x-foo-with-markup>
Shadow DOM에서 내부 캡슐화
Shadow DOM은 그 자체만으로 콘텐츠를 캡슐화하는 강력한 도구입니다. 맞춤 요소와 함께 사용하면 마법 같은 일이 벌어집니다.
Shadow DOM은 사용자 지정 요소에 다음을 제공합니다.
- 구현 세부정보를 숨겨 사용자를 잔인한 구현 세부정보로부터 보호하는 방법입니다.
- 스타일 캡슐화…무료로 제공됩니다.
Shadow DOM에서 요소를 만드는 것은 기본 마크업을 렌더링하는 요소를 만드는 것과 같습니다. 차이점은 createdCallback()
에 있습니다.
var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.createdCallback = function() {
// 1. Attach a shadow root on the element.
var shadow = this.createShadowRoot();
// 2. Fill it with markup goodness.
shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};
var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});
요소의 .innerHTML
를 설정하는 대신 <x-foo-shadowdom>
의 그림자 루트를 만든 다음 마크업으로 채웠습니다.
DevTools에서 'Shadow DOM 표시' 설정을 사용 설정하면 확장할 수 있는 #shadow-root
가 표시됩니다.
▾<x-foo-shadowdom>
▾#shadow-root
**I'm in the element's Shadow DOM!**
</x-foo-shadowdom>
섀도우 루트입니다.
템플릿에서 요소 만들기
HTML 템플릿은 맞춤 요소의 세계에 잘 맞는 또 다른 새로운 API 원시입니다.
예: <template>
및 Shadow DOM에서 생성된 요소 등록
<template id="sdtemplate">
<style>
p { color: orange; }
</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.
</template>
<script>
var proto = Object.create(HTMLElement.prototype, {
createdCallback: {
value: function() {
var t = document.querySelector('#sdtemplate');
var clone = document.importNode(t.content, true);
this.createShadowRoot().appendChild(clone);
}
}
});
document.registerElement('x-foo-from-template', {prototype: proto});
</script>
<template id="sdtemplate">
<style>:host p { color: orange; }</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.
</template>
<div class="demoarea">
<x-foo-from-template></x-foo-from-template>
</div>
이 몇 줄의 코드는 강력한 효과를 제공합니다. 현재 진행 중인 모든 상황을 살펴보겠습니다.
- HTML에 새 요소
<x-foo-from-template>
를 등록했습니다. - 요소의 DOM이
<template>
에서 생성되었습니다. - 요소의 무서운 세부정보가 Shadow DOM을 사용하여 숨겨집니다.
- Shadow DOM은 요소 스타일 캡슐화를 제공합니다 (예:
p {color: orange;}
가 전체 페이지를 주황색으로 변환하지 않음).
좋았어!
맞춤 요소 스타일 지정
다른 HTML 태그와 마찬가지로 맞춤 태그 사용자는 선택기로 스타일을 지정할 수 있습니다.
<style>
app-panel {
display: flex;
}
[is="x-item"] {
transition: opacity 400ms ease-in-out;
opacity: 0.3;
flex: 1;
text-align: center;
border-radius: 50%;
}
[is="x-item"]:hover {
opacity: 1.0;
background: rgb(255, 0, 255);
color: white;
}
app-panel > [is="x-item"] {
padding: 5px;
list-style: none;
margin: 0 7px;
}
</style>
<app-panel>
<li is="x-item">Do</li>
<li is="x-item">Re</li>
<li is="x-item">Mi</li>
</app-panel>
Shadow DOM을 사용하는 요소의 스타일 지정
Shadow DOM을 사용하면 훨씬 더 깊은 곳으로 들어가게 됩니다. Shadow DOM을 사용하는 사용자 지정 요소는 Shadow DOM의 큰 이점을 상속받습니다.
Shadow DOM은 요소에 스타일 캡슐화를 주입합니다. Shadow Root에 정의된 스타일은 호스트에서 누수되지 않으며 페이지에서 스며들지 않습니다. 맞춤 요소의 경우 요소 자체가 호스트입니다. 스타일 캡슐화의 속성을 사용하면 맞춤 요소가 자체 기본 스타일을 정의할 수도 있습니다.
Shadow DOM 스타일 지정은 방대한 주제입니다. 자세한 내용은 다음 도움말을 참고하세요.
- Polymer 문서의 '요소 스타일 지정 가이드'
- 'Shadow DOM 201: CSS 및 스타일 지정'을 참고하세요.
:unresolved를 사용한 FOUC 방지
FOUC를 완화하기 위해 맞춤 요소는 새로운 CSS 의사 클래스 :unresolved
를 지정합니다. 브라우저가 createdCallback()
를 호출하는 시점까지 해결되지 않은 요소를 타겟팅하는 데 사용합니다 (수명 주기 메서드 참고).
그러면 요소가 더 이상 해결되지 않은 요소가 아닙니다. 업그레이드 프로세스가 완료되고 요소가 정의로 변환되었습니다.
예: 'x-foo' 태그가 등록될 때 페이드 인:
<style>
x-foo {
opacity: 1;
transition: opacity 300ms;
}
x-foo:unresolved {
opacity: 0;
}
</style>
:unresolved
는 HTMLUnknownElement
에서 상속받는 요소가 아닌 해결되지 않은 요소에만 적용됩니다 (요소 업그레이드 방법 참고).
<style>
/* apply a dashed border to all unresolved elements */
:unresolved {
border: 1px dashed red;
display: inline-block;
}
/* x-panel's that are unresolved are red */
x-panel:unresolved {
color: red;
}
/* once the definition of x-panel is registered, it becomes green */
x-panel {
color: green;
display: block;
padding: 5px;
display: block;
}
</style>
<panel>
I'm black because :unresolved doesn't apply to "panel".
It's not a valid custom element name.
</panel>
<x-panel>I'm red because I match x-panel:unresolved.</x-panel>
기록 및 브라우저 지원
기능 감지
기능 감지는 document.registerElement()
가 있는지 확인하는 문제입니다.
function supportsCustomElements() {
return 'registerElement' in document;
}
if (supportsCustomElements()) {
// Good to go!
} else {
// Use other libraries to create components.
}
브라우저 지원
document.registerElement()
는 Chrome 27 및 Firefox ~23에서 플래그 뒤에 처음으로 제공되었습니다. 하지만 그 이후로 사양이 많이 발전했습니다. Chrome 31은 업데이트된 사양을 처음으로 완전히 지원합니다.
브라우저 지원이 우수해질 때까지 Google의 Polymer와 Mozilla의 X-Tag에서 사용하는 폴리필이 있습니다.
HTMLElementElement은 어떻게 되었나요?
표준화 작업을 따라온 분들은 <element>
가 있었음을 알고 계실 것입니다.
정말 좋았어요. 이를 사용하여 새 요소를 선언적으로 등록할 수 있습니다.
<element name="my-element">
...
</element>
안타깝게도 업그레이드 프로세스, 특이 사례, 아마겟돈과 같은 시나리오에 대한 타이밍 문제가 너무 많아 모든 문제를 해결할 수 없습니다. <element>
를 보류해야 했습니다. 2013년 8월, Dimitri Glazkov는 public-webapps에 게시하여 적어도 당분간은 삭제될 것이라고 발표했습니다.
Polymer는 <polymer-element>
를 사용하여 선언적 요소 등록 형식을 구현한다는 점에 유의하세요. 방법 document.registerElement('polymer-element')
및 템플릿에서 요소 만들기에 설명된 기법을 사용합니다.
결론
사용자설정 요소는 HTML의 어휘를 확장하고, 새로운 방법을 HTML에 알려주고, 웹 플랫폼의 웜홀을 통해 이동하는 도구를 제공합니다. 이를 Shadow DOM 및 <template>
와 같은 다른 신규 플랫폼 원시 기능과 결합하면 웹 구성 요소의 그림을 볼 수 있습니다. 마크업이 다시 섹시해질 수 있습니다.
웹 구성요소를 시작하는 데 관심이 있다면 Polymer를 확인해 보세요. 시작하기에는 충분합니다.