맞춤 요소 v1 - 재사용 가능한 웹 구성요소

맞춤 요소를 사용하면 웹 개발자가 새 HTML 태그를 정의하고, 기존 태그를 확장하고, 재사용 가능한 웹 구성요소를 만들 수 있습니다.

웹 개발자는 맞춤 요소를 사용하여 새 HTML 태그를 만들거나 기존 HTML 태그를 보강하거나 다른 개발자가 작성한 구성요소를 확장할 수 있습니다. 이 API는 웹 구성요소의 기반입니다. 웹 표준 기반 방식을 통해 재사용 가능한 구성요소를 만들 수 있습니다. 이를 위해 베이스라인 JS/HTML/CSS만 사용하면 됩니다. 그 결과 앱에서 코드가 줄고 모듈식 코드가 사용되며 재사용이 늘어납니다.

소개

브라우저는 웹 애플리케이션을 구성하는 데 훌륭한 도구를 제공합니다. HTML이라고 합니다. 들어보셨을 수도 있습니다. 선언적이고 이식성이 있으며 잘 지원되고 사용하기 쉽습니다. HTML은 훌륭하지만 어휘와 확장성이 제한적입니다. HTML 실시간 표준에는 JS 동작을 마크업과 자동으로 연결하는 방법이 없었습니다. 지금까지는 말이죠.

맞춤 요소는 HTML을 현대화하고, 누락된 부분을 채우며, 구조를 동작과 번들로 묶는 해답입니다. HTML이 문제의 해결 방법을 제공하지 않는 경우 해결 방법을 제공하는 맞춤 요소를 만들 수 있습니다. 맞춤 요소는 HTML의 이점을 유지하면서 브라우저에 새로운 트릭을 가르칩니다.

새 요소 정의

새 HTML 요소를 정의하려면 JavaScript의 힘이 필요합니다.

customElements 전역은 맞춤 요소를 정의하고 브라우저에 새 태그를 알려주는 데 사용됩니다. 만들려는 태그 이름과 기본 HTMLElement를 확장하는 JavaScript class를 사용하여 customElements.define()를 호출합니다.

- 모바일 드로어 패널 <app-drawer> 정의:

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});

사용 예:

<app-drawer></app-drawer>

맞춤 요소를 사용하는 것은 <div> 또는 다른 요소를 사용하는 것과 다르지 않다는 점에 유의하세요. 인스턴스를 페이지에서 선언하거나 JavaScript에서 동적으로 만들거나 이벤트 리스너를 연결할 수 있습니다. 자세한 예는 계속 읽어보세요.

요소의 JavaScript API 정의

맞춤 요소의 기능은 HTMLElement를 확장하는 ES2015 class를 사용하여 정의됩니다. HTMLElement를 확장하면 맞춤 요소가 전체 DOM API를 상속받게 되며 클래스에 추가하는 모든 속성/메서드가 요소의 DOM 인터페이스의 일부가 됩니다. 기본적으로 이 클래스를 사용하여 태그의 공개 JavaScript API를 만듭니다.

- <app-drawer>의 DOM 인터페이스 정의:

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open', '');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // A getter/setter for a disabled property.
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    // Reflect the value of the disabled property as an HTML attribute.
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super();

    // Setup a click listener on <app-drawer> itself.
    this.addEventListener('click', e => {
      // Don't toggle the drawer if it's disabled.
      if (this.disabled) {
        return;
      }
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
    // ...
  }
}

customElements.define('app-drawer', AppDrawer);

이 예에서는 open 속성, disabled 속성, toggleDrawer() 메서드가 있는 드로어를 만듭니다. 또한 속성을 HTML 속성으로 반영합니다.

맞춤 요소의 멋진 기능은 클래스 정의 내의 this가 DOM 요소 자체를 참조한다는 것입니다. 즉, 클래스의 인스턴스입니다. 이 예시에서 this<app-drawer>를 참조합니다. 요소가 click 리스너를 자체에 연결하는 방법입니다 (😉). 이벤트 리스너로 제한되지 않습니다. 전체 DOM API는 요소 코드 내에서 사용할 수 있습니다. this를 사용하여 요소의 속성에 액세스하고 하위 요소 (this.children), 쿼리 노드(this.querySelectorAll('.items')) 등을 검사합니다.

맞춤 요소 만들기 규칙

  1. 맞춤 요소의 이름에는 대시 (-)가 포함되어야 합니다. 따라서 <x-tags>, <my-element>, <my-awesome-app>는 모두 유효한 이름이지만 <tabs><foo_bar>는 유효하지 않습니다. 이 요구사항은 HTML 파서가 맞춤 요소를 일반 요소와 구분할 수 있도록 하기 위한 것입니다. 또한 HTML에 새 태그가 추가될 때 전방 호환성을 보장합니다.
  2. 동일한 태그를 두 번 이상 등록할 수는 없습니다. 이렇게 하면 DOMException이 발생합니다. 브라우저에 새 태그를 알리면 됩니다. 반품 불가
  3. HTML에서는 몇 가지 요소만 자체 종료되도록 허용하므로 맞춤 요소는 자체 종료될 수 없습니다. 항상 닫는 태그(<app-drawer></app-drawer>)를 작성합니다.

맞춤 요소 리액션

맞춤 요소는 요소가 존재하는 흥미로운 시점에 코드를 실행하기 위한 특수한 수명 주기 후크를 정의할 수 있습니다. 이를 커스텀 요소 반응이라고 합니다.

이름 호출 시점
constructor 요소의 인스턴스가 생성되거나 업그레이드됩니다. 상태를 초기화하거나 이벤트 리스너를 설정하거나 섀도 DOM을 만드는 데 유용합니다. constructor에서 할 수 있는 작업에 대한 제한사항은 spec 을 참고하세요.
connectedCallback 요소가 DOM에 삽입될 때마다 호출됩니다. 리소스 가져오기 또는 렌더링과 같은 설정 코드를 실행하는 데 유용합니다. 일반적으로 이 시간까지 작업을 지연시키는 것이 좋습니다.
disconnectedCallback 요소가 DOM에서 삭제될 때마다 호출됩니다. 정리 코드를 실행하는 데 유용합니다.
attributeChangedCallback(attrName, oldVal, newVal) 관찰된 속성이 추가, 삭제, 업데이트 또는 대체될 때 호출됩니다. 요소가 파서에 의해 생성되거나 업그레이드될 때 초기 값을 위해 호출됩니다. 참고: observedAttributes 속성에 나열된 속성만 이 콜백을 수신합니다.
adoptedCallback 맞춤 요소가 새 document (예: document.adoptNode(el))로 이동했습니다.

리액션 콜백은 동기식입니다. 누군가 요소에서 el.setAttribute()를 호출하면 브라우저는 즉시 attributeChangedCallback()를 호출합니다. 마찬가지로 요소가 DOM에서 삭제된 직후에도 disconnectedCallback()이 수신됩니다 (예: 사용자가 el.remove()를 호출함).

예: <app-drawer>에 맞춤 요소 리액션을 추가합니다.

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

적절한 경우 반응을 정의합니다. 요소가 충분히 복잡하고 connectedCallback()에서 IndexedDB에 대한 연결을 여는 경우 disconnectedCallback()에서 필요한 정리 작업을 실행합니다. 하지만 주의해야 합니다. 모든 상황에서 DOM에서 요소가 삭제된다고 가정할 수는 없습니다. 예를 들어 사용자가 탭을 닫으면 disconnectedCallback()가 호출되지 않습니다.

속성 및 특성

속성을 속성에 반영

HTML 속성은 일반적으로 값을 HTML 속성으로 DOM에 다시 반영합니다. 예를 들어 JS에서 hidden 또는 id 값이 변경되면 다음과 같이 됩니다.

div.id = 'my-id';
div.hidden = true;

값이 실시간 DOM에 속성으로 적용됩니다.

<div id="my-id" hidden>

이를 '속성을 속성에 반영'이라고 합니다. HTML의 거의 모든 속성이 이렇게 합니다. 왜냐하면 속성은 요소를 선언적으로 구성하는 데도 유용하며 접근성 및 CSS 선택자와 같은 특정 API는 작동하는 데 속성을 사용합니다.

속성을 반영하는 것은 요소의 DOM 표현을 JavaScript 상태와 동기화 상태로 유지하려는 모든 경우에 유용합니다. 속성을 반영하는 한 가지 이유는 JS 상태가 변경될 때 사용자 정의 스타일이 적용되도록 하기 위해서입니다.

<app-drawer>을 참고하세요. 이 구성요소의 소비자는 구성요소가 사용 중지되었을 때 구성요소를 페이드 아웃하거나 사용자 상호작용을 방지할 수 있습니다.

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

JS에서 disabled 속성이 변경되면 사용자의 선택기가 일치하도록 해당 속성이 DOM에 추가되어야 합니다. 요소는 값을 동일한 이름의 속성에 반영하여 이러한 동작을 제공할 수 있습니다.

get disabled() {
  return this.hasAttribute('disabled');
}

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}

속성 변경사항 관찰

HTML 속성은 사용자가 초기 상태를 선언하는 편리한 방법입니다.

<app-drawer open disabled></app-drawer>

요소는 attributeChangedCallback를 정의하여 속성 변경에 반응할 수 있습니다. 브라우저는 observedAttributes 배열에 나열된 속성이 변경될 때마다 이 메서드를 호출합니다.

class AppDrawer extends HTMLElement {
  // ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}

이 예에서는 disabled 속성이 변경될 때 <app-drawer>에 추가 속성을 설정합니다. 여기서는 사용하지 않지만 attributeChangedCallback를 사용하여 JS 속성을 속성과 동기화할 수도 있습니다.

요소 업그레이드

점진적 개선 HTML

사용자설정 요소는 customElements.define()를 호출하여 정의된다는 것을 이미 알고 있습니다. 그렇다고 해서 맞춤 요소를 한 번에 모두 정의하고 등록해야 하는 것은 아닙니다.

맞춤 요소는 정의가 등록되기 전에 사용할 수 있습니다.

점진적 개선은 맞춤 요소의 기능입니다. 즉, 페이지에서 여러 개의 <app-drawer> 요소를 선언하고 나중에 customElements.define('app-drawer', ...)를 호출하지 않을 수 있습니다. 이는 브라우저가 알 수 없는 태그 덕분에 잠재적 맞춤 요소를 다르게 처리하기 때문입니다. define()를 호출하고 기존 요소에 클래스 정의를 부여하는 프로세스를 '요소 업그레이드'라고 합니다.

태그 이름이 정의되는 시점을 확인하려면 window.customElements.whenDefined()를 사용하면 됩니다. 요소가 정의될 때 해결되는 Promise를 반환합니다.

customElements.whenDefined('app-drawer').then(() => {
  console.log('app-drawer defined');
});

- 하위 요소 집합이 업그레이드될 때까지 작업 지연

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map((socialButton) => {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

요소 정의 콘텐츠

맞춤 요소는 요소 코드 내에서 DOM API를 사용하여 자체 콘텐츠를 관리할 수 있습니다. 이때 리액션이 유용합니다.

- 기본 HTML로 요소 만들기

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

이 태그를 선언하면 다음이 생성됩니다.

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// TODO: DevSite - 인라인 이벤트 핸들러를 사용했으므로 코드 샘플이 삭제됨

Shadow DOM을 사용하는 요소 만들기

Shadow DOM은 요소가 페이지의 나머지 부분과 별개로 DOM 청크를 소유, 렌더링, 스타일 지정할 수 있는 방법을 제공합니다. 한 태그에 전체 앱을 숨길 수도 있습니다.

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

맞춤 요소에서 Shadow DOM을 사용하려면 constructor 내에서 this.attachShadow를 호출합니다.

let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>:host { ... }</style> <!-- look ma, scoped styles -->
  <b>I'm in shadow dom!</b>
  <slot></slot>
`;

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
  // ...
});

사용 예:

<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  #shadow-root
  <b>I'm in shadow dom!</b>
  <slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>

사용자 맞춤 텍스트

// TODO: DevSite - 인라인 이벤트 핸들러를 사용했으므로 코드 샘플이 삭제됨

<template>에서 요소 만들기

잘 모르는 경우 <template> 요소를 사용하면 파싱되고 페이지 로드 시 비활성 상태이며 나중에 런타임에 활성화할 수 있는 DOM의 프래그먼트를 선언할 수 있습니다. 웹 구성요소 계열의 또 다른 API 원시입니다. 템플릿은 맞춤 요소의 구조를 선언하는 데 이상적인 자리표시자입니다.

예: <template>에서 생성된 Shadow DOM 콘텐츠로 요소 등록

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true));
    }
    // ...
  });
</script>

이 몇 줄의 코드가 강력한 기능을 제공합니다. 진행 중인 주요 사항을 알아보겠습니다.

  1. HTML에서 새 요소 <x-foo-from-template>를 정의합니다.
  2. 요소의 Shadow DOM이 <template>에서 생성됩니다.
  3. Shadow DOM 덕분에 요소의 DOM이 요소에 로컬입니다.
  4. Shadow DOM 덕분에 요소의 내부 CSS가 요소로 범위가 지정됩니다.

Shadow DOM에 있습니다. <템플릿>에서 마크업이 찍혔습니다.

// TODO: DevSite - 인라인 이벤트 핸들러를 사용했으므로 코드 샘플이 삭제됨

맞춤 요소 스타일 지정

요소가 Shadow DOM을 사용하여 자체 스타일을 정의하더라도 사용자는 페이지에서 맞춤 요소의 스타일을 지정할 수 있습니다. 이를 '사용자 정의 스타일'이라고 합니다.

<!-- user-defined styling -->
<style>
  app-drawer {
    display: flex;
  }
  panel-item {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  panel-item:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > panel-item {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-drawer>
  <panel-item>Do</panel-item>
  <panel-item>Re</panel-item>
  <panel-item>Mi</panel-item>
</app-drawer>

요소에 Shadow DOM 내에 정의된 스타일이 있는 경우 CSS 특수성이 어떻게 작동하는지 궁금하실 수 있습니다. 구체성 측면에서 사용자 스타일이 더 좋습니다. 항상 요소 정의 스타일을 재정의합니다. Shadow DOM을 사용하는 요소 만들기 섹션을 참고하세요.

등록되지 않은 요소의 사전 스타일 지정

요소가 업그레이드되기 전에 :defined 의사 클래스를 사용하여 CSS에서 요소를 타겟팅할 수 있습니다. 구성요소의 스타일을 미리 지정하는 데 유용합니다. 예를 들어 정의되지 않은 구성요소를 숨기고 정의되면 페이드 인하여 레이아웃이나 기타 시각적 FOUC를 방지할 수 있습니다.

- <app-drawer>가 정의되기 전에 숨깁니다.

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

<app-drawer>가 정의되면 선택기 (app-drawer:not(:defined))가 더 이상 일치하지 않습니다.

요소 확장

맞춤 요소 API는 새 HTML 요소를 만드는 데 유용하지만 다른 맞춤 요소나 브라우저의 내장 HTML을 확장하는 데도 유용합니다.

맞춤 요소 확장

다른 맞춤 요소를 확장하려면 클래스 정의를 확장하면 됩니다.

- <app-drawer>를 확장하는 <fancy-app-drawer>를 만듭니다.

class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    // ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    // ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

네이티브 HTML 요소 확장

더 멋진 <button>를 만들고 싶다고 가정해 보겠습니다. <button>의 동작과 기능을 복제하는 대신 커스텀 요소를 사용하여 기존 요소를 점진적으로 개선하는 것이 좋습니다.

맞춤설정된 내장 요소는 브라우저의 내장 HTML 태그 중 하나를 확장하는 맞춤 요소입니다. 기존 요소를 확장하면 얻을 수 있는 주요 이점은 모든 기능 (DOM 속성, 메서드, 접근성)을 얻을 수 있다는 것입니다. 기존 HTML 요소를 점진적으로 개선하는 것보다 프로그레시브 웹 앱을 작성하는 더 좋은 방법은 없습니다.

요소를 확장하려면 올바른 DOM 인터페이스에서 상속받는 클래스 정의를 만들어야 합니다. 예를 들어 <button>를 확장하는 맞춤 요소는 HTMLElement 대신 HTMLButtonElement에서 상속해야 합니다. 마찬가지로 <img>를 확장하는 요소는 HTMLImageElement를 확장해야 합니다.

- <button> 확장:

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend', (e) => div.remove());
  }
}

customElements.define('fancy-button', FancyButton, {extends: 'button'});

네이티브 요소를 확장할 때 define() 호출이 약간 변경됩니다. 필수 세 번째 매개변수는 브라우저에 확장할 태그를 알려줍니다. 이는 많은 HTML 태그가 동일한 DOM 인터페이스를 공유하기 때문에 필요합니다. <section>, <address>, <em> 등은 모두 HTMLElement를 공유합니다. <q><blockquote>는 모두 HTMLQuoteElement를 공유합니다. {extends: 'blockquote'}를 지정하면 브라우저에 <q> 대신 개선된 <blockquote>를 만들고 있다고 알릴 수 있습니다. HTML의 DOM 인터페이스 전체 목록은 HTML 사양을 참고하세요.

맞춤설정된 기본 제공 요소의 소비자는 여러 가지 방법으로 이를 사용할 수 있습니다. 네이티브 태그에 is="" 속성을 추가하여 선언할 수 있습니다.

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

JavaScript에서 인스턴스를 만듭니다.

// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);

또는 new 연산자를 사용합니다.

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

다음은 <img>를 확장하는 또 다른 예입니다.

- <img> 확장:

customElements.define('bigger-img', class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10);
  }
}, {extends: 'img'});

사용자는 이 구성요소를 다음과 같이 선언합니다.

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

또는 JavaScript에서 인스턴스를 만듭니다.

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);

기타 세부정보

알 수 없는 요소와 정의되지 않은 맞춤 요소 비교

HTML은 유연하고 작업하기 쉽습니다. 예를 들어 페이지에서 <randomtagthatdoesntexist>를 선언하면 브라우저는 이를 완벽하게 수락합니다. 비표준 태그가 작동하는 이유는 무엇인가요? 답은 HTML 사양에서 허용하기 때문입니다. 사양에서 정의되지 않은 요소는 HTMLUnknownElement로 파싱됩니다.

맞춤 요소의 경우는 그렇지 않습니다. 잠재적 맞춤 요소는 유효한 이름('-' 포함)으로 만들어진 경우 HTMLElement로 파싱됩니다. 맞춤 요소를 지원하는 브라우저에서 이를 확인할 수 있습니다. 콘솔을 실행합니다. Ctrl+Shift+J (또는 Mac의 경우 Cmd+Opt+J)를 누르고 다음 코드 줄을 붙여넣습니다.

// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true

API 참조

customElements 전역은 맞춤 요소를 사용하는 데 유용한 메서드를 정의합니다.

define(tagName, constructor, options)

브라우저에서 새 맞춤 요소를 정의합니다.

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

유효한 맞춤 요소 태그 이름이 주어지면 요소의 생성자를 반환합니다. 요소 정의가 등록되지 않은 경우 undefined을 반환합니다.

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

맞춤 요소가 정의될 때 결정되는 Promise를 반환합니다. 요소가 이미 정의된 경우 즉시 해결합니다. 태그 이름이 유효한 맞춤 요소 이름이 아닌 경우 거부합니다.

customElements.whenDefined('app-drawer').then(() => {
  console.log('ready!');
});

기록 및 브라우저 지원

지난 몇 년 동안 웹 구성요소를 사용해 왔다면 Chrome 36 이상에서 customElements.define() 대신 document.registerElement()를 사용하는 맞춤 요소 API 버전을 구현했음을 알고 계실 것입니다. 이제 이 버전은 v0이라는 지원 중단된 표준 버전으로 간주됩니다. customElements.define()는 새로운 핫한 기술이며 브라우저 공급업체에서 구현하기 시작했습니다. 맞춤 요소 v1이라고 합니다.

이전 v0 사양에 관심이 있다면 html5rocks 도움말을 확인하세요.

브라우저 지원

Chrome 54 (상태), Safari 10.1 (상태), Firefox 63 (상태)에는 맞춤 요소 v1이 있습니다. Edge에서 개발이 시작되었습니다.

맞춤 요소를 기능 감지하려면 window.customElements의 존재를 확인합니다.

const supportsCustomElementsV1 = 'customElements' in window;

폴리필

브라우저 지원이 널리 제공될 때까지 커스텀 요소 v1에는 독립형 폴리필이 제공됩니다. 하지만 webcomponents.js 로더를 사용하여 웹 구성요소 폴리필을 최적으로 로드하는 것이 좋습니다. 로더는 기능 감지를 사용하여 브라우저에 필요한 필수 폴리필만 비동기식으로 로드합니다.

설치 방법은 다음과 같습니다.

npm install --save @webcomponents/webcomponentsjs

사용:

<!-- Use the custom element on the page. -->
<my-element></my-element>

<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  function loadScript(src) {
    return new Promise(function(resolve, reject) {
      const script = document.createElement('script');
      script.src = src;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  WebComponents.waitFor(() => {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>

결론

맞춤 요소를 사용하면 브라우저에서 새 HTML 태그를 정의하고 재사용 가능한 구성요소를 만들 수 있는 새로운 도구를 사용할 수 있습니다. 이를 Shadow DOM 및 <template>와 같은 다른 새로운 플랫폼 프리미티브와 결합하면 Web Components의 전체적인 그림을 실현할 수 있습니다.

  • 재사용 가능한 구성요소를 만들고 확장하기 위한 크로스브라우저 (웹 표준)입니다.
  • 시작하는 데 라이브러리 또는 프레임워크가 필요하지 않습니다. 바닐라 JS/HTML이 최고야!
  • 익숙한 프로그래밍 모델을 제공합니다. DOM/CSS/HTML일 뿐입니다.
  • 다른 새로운 웹 플랫폼 기능 (Shadow DOM, <template>, CSS 맞춤 속성 등)과 잘 작동합니다.
  • 브라우저의 DevTools와 긴밀하게 통합됩니다.
  • 기존 접근성 기능을 활용하세요.