맞춤 요소 작업

소개

웹에는 표현이 많이 부족합니다. 무슨 말인지 알아보려면 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를 상속하는 새 요소를 만듭니다. 이 경우 '버튼'은 다음과 같습니다.

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로 선언하거나 자바스크립트를 사용하여 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!');
  });
};

마크업 추가

JavaScript API를 받아 <x-foo>를 만들었지만 비어 있습니다. 렌더링할 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에서 검사 (마우스 오른쪽 버튼을 클릭하여 요소 검사 선택)하면 다음과 같이 표시됩니다.

▾<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 스타일 지정은 매우 중요한 주제입니다. 이에 대해 자세히 알고 싶다면 몇 가지 다른 자료를 참조해 보시기 바랍니다.

:unresolved를 사용한 FOUC 방지

FOUC를 완화하기 위해 맞춤 요소는 새로운 CSS 유사 클래스 :unresolved를 지정합니다. 브라우저가 createdCallback()를 호출하는 시점까지 확인되지 않은 요소를 타겟팅하는 데 사용합니다 (수명 주기 메서드 참고). 이렇게 되면 해당 요소는 더 이상 해결되지 않은 요소가 아닙니다. 업그레이드 프로세스가 완료되고 요소가 정의로 변환되었습니다.

: 'x-foo' 태그가 등록되면 페이드 인합니다.

<style>
  x-foo {
    opacity: 1;
    transition: opacity 300ms;
  }
  x-foo:unresolved {
    opacity: 0;
  }
</style>

:unresolvedHTMLUnknownElement에서 상속되는 요소가 아닌 확인되지 않은 요소에만 적용됩니다 (요소 업그레이드 방법 참고).

<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에서 사용하는 polyfill이 있습니다.

HTMLElementElement는 어떻게 되었나요?

표준화 작업을 해보신 분은 아시다시피 한때 <element>가 있습니다. 벌의 무릎이었어. 이를 사용하여 새 요소를 선언적으로 등록할 수 있습니다.

<element name="my-element">
    ...
</element>

안타깝게도 업그레이드 프로세스, 특수한 케이스, 아마겟돈과 같은 시나리오에는 타이밍 문제가 너무 많아 모두 해결할 수 없었습니다. <element> 서가는 서가에 있어야 합니다. 2013년 8월, 디미트리 글라즈코프는 public-webapps에 앱이 삭제된다는 소식을 게시하였으며, 당분간은 아니었습니다.

Polymer는 <polymer-element>를 사용하여 선언적 형태의 요소 등록을 구현합니다. 방법 document.registerElement('polymer-element')템플릿에서 요소 만들기에서 설명한 기법을 사용합니다.

결론

맞춤 요소는 HTML의 어휘를 확장하고, 새로운 기술을 익히고, 웹 플랫폼의 웜홀을 통과할 수 있는 도구를 제공합니다. Shadow DOM 및 <template>와 같은 다른 새로운 플랫폼 프리미티브와 결합하면 웹 구성요소의 그림을 깨닫기 시작합니다. 마크업이 또다시 섹시해질 수 있습니다!

웹 구성요소를 시작하는 데 관심이 있다면 Polymer를 확인해 보시기 바랍니다. 하지만 계속 동기를 부여해줄 만큼 충분해요.