맞춤 요소 작업

소개

웹은 표현력이 매우 부족합니다. 자세히 알아보려면 Gmail과 같은 '최신' 웹 앱을 살펴보세요.

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 프리미티브 중 하나이지만 가장 중요할 가능성이 높습니다. 맞춤 요소로 잠금 해제된 기능이 없으면 웹 구성요소는 존재하지 않습니다.

  1. 새 HTML/DOM 요소 정의
  2. 다른 요소에서 확장되는 요소 만들기
  3. 맞춤 기능을 단일 태그로 논리적으로 결합
  4. 기존 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 요소뿐만 아니라 기타 맞춤 요소를 확장할 수 있습니다. 요소를 확장하려면 상속할 요소의 이름과 prototyperegisterElement()에 전달해야 합니다.

네이티브 요소 확장

일반 사용자 <button>가 마음에 들지 않는다고 가정해 보겠습니다. '메가 버튼'이 되도록 기능을 강화하려고 합니다. <button> 요소를 확장하려면 HTMLButtonElementprototype와 요소의 이름인 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은 사용자 지정 요소에 다음을 제공합니다.

  1. 구현 세부정보를 숨겨 사용자를 잔인한 구현 세부정보로부터 보호하는 방법입니다.
  2. 스타일 캡슐화는 무료입니다.

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&gt;.
</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&gt;.
</template>

<div class="demoarea">
  <x-foo-from-template></x-foo-from-template>
</div>

이 몇 줄의 코드는 많은 내용을 담고 있습니다. 어떤 일이 일어나는지 살펴보겠습니다.

  1. HTML에 새 요소(<x-foo-from-template>)가 등록되었습니다.
  2. 요소의 DOM이 <template>에서 생성되었습니다.
  3. Shadow DOM을 사용하여 요소의 무서운 세부정보를 숨깁니다.
  4. 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 스타일 지정은 방대한 주제입니다. 이에 대해 자세히 알아보려면 다른 도움말도 참고하시기 바랍니다.

: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의 어휘를 확장하고, 새로운 기술을 가르치고, 웹 플랫폼의 웜홀을 뛰어넘을 수 있는 도구를 제공합니다. 이를 Shadow DOM 및 <template>과 같은 다른 새로운 플랫폼 프리미티브와 결합하면 웹 구성요소의 상태를 알 수 있습니다. 마크업은 다시 섹시할 수 있습니다!

웹 구성요소를 시작하는 데 관심이 있다면 Polymer를 확인해 보세요. 목표를 향해 나아가는 데 충분합니다.