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

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

맞춤 요소를 사용하면 웹 개발자가 새 HTML 태그를 생성하거나 기존 HTML 태그를 보강하거나 다른 개발자가 작성한 구성요소를 확장할 수 있습니다. API는 웹 구성요소의 기반입니다. 바닐라 JS/HTML/CSS만을 사용하여 재사용 가능한 구성요소를 생성할 수 있는 웹 표준 기반 방법을 제공합니다. 그 결과 앱에서 사용되는 코드가 줄어들고, 모듈식 코드가 더 많이 재사용됩니다.

소개

브라우저는 웹 애플리케이션을 구조화하는 데 탁월한 도구를 제공합니다. 이를 HTML이라고 합니다. 들어보셨을 것입니다. 선언적이고 이식 가능하며 잘 지원되고 작업하기 쉽습니다. HTML이 아무리 훌륭하더라도 어휘와 확장성은 제한적입니다. HTML Living Standard에는 지금까지는 JS 동작을 마크업과 자동으로 연결하는 방법이 항상 없었습니다.

맞춤 요소는 HTML을 현대화하고, 누락된 부분을 채우고, 구조를 동작과 번들로 묶는 해답입니다. HTML을 사용하여 문제를 해결할 수 없는 경우, 해결책을 제공할 수 있는 맞춤 요소를 만들 수 있습니다. 맞춤 요소는 HTML의 이점을 유지하면서 새로운 기능을 브라우저에 알려줍니다.

새 요소 정의

새 HTML 요소를 정의하려면 자바스크립트의 기능이 필요합니다.

customElements 전역 변수는 맞춤 요소를 정의하고 브라우저에 새 태그에 관해 알리는 데 사용됩니다. 만들려는 태그 이름과 기본 HTMLElement를 확장하는 자바스크립트 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.children)를 검사하고 노드를 쿼리(this.querySelectorAll('.items'))하는 등의 작업을 하려면 this를 사용합니다.

맞춤 요소 생성에 관한 규칙

  1. 맞춤 요소의 이름에는 대시 (-)가 포함되어야 합니다. 따라서 <x-tags>, <my-element>, <my-awesome-app>는 모두 유효한 이름이지만 <tabs><foo_bar>는 그렇지 않습니다. 이는 HTML 파서가 맞춤 요소와 일반 요소를 구별할 수 있도록 하기 위한 것입니다. 또한 새 태그가 HTML에 추가될 때 이후 버전과의 호환성도 보장합니다.
  2. 동일한 태그는 두 번 이상 등록할 수 없습니다. 그렇게 하려고 하면 DOMException이 발생합니다. 브라우저에 새 태그를 알렸으면 이제 완료된 것입니다. 되돌릴 수는 없습니다.
  3. HTML에서는 일부 요소만 스스로 닫을 수 있으므로 맞춤 요소는 자체적으로 닫힐 수 없습니다. 항상 닫는 태그(<app-drawer></app-drawer>)를 작성합니다.

맞춤 요소 반응

맞춤 요소는 존재하는 동안 코드를 실행하기 위한 특별한 수명 주기 후크를 정의할 수 있습니다. 이를 맞춤 요소 반응이라고 합니다.

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

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

예: <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에 다시 반영합니다. 예를 들어 hidden 또는 id의 값이 JS에서 변경되면 다음과 같습니다.

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

값은 속성으로 라이브 DOM에 적용됩니다.

<div id="my-id" hidden>

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

속성을 반영하는 것은 요소의 DOM 표현이 자바스크립트 상태와 동기화된 상태를 유지하려는 모든 경우에 유용합니다. 속성을 반영해야 하는 한 가지 이유는 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()를 사용하면 됩니다. 요소가 정의된 시점을 확인하는 프로미스를 반환합니다.

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. 이 요소의 DOM은 Shadow DOM 덕분에 요소에 로컬입니다.
  4. Shadow DOM 덕분에 이 요소의 내부 CSS의 범위가 해당 요소로 지정됩니다.

Shadow DOM에 있습니다. 내 마크업이 <template>에서 스탬프되었습니다.

// 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))가 더 이상 일치하지 않습니다.

요소 확장

Custom Elements 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">

자바스크립트로 인스턴스를 생성할 수도 있습니다.

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)

사용자설정 요소가 정의된 시점을 확인하는 프로미스를 반환합니다. 요소가 이미 정의된 경우 즉시 확인합니다. 태그 이름이 유효한 맞춤 요소 이름이 아닌 경우 거부합니다.

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

방문 기록 및 브라우저 지원

지난 몇 년 동안 웹 구성요소를 사용해 왔다면 Chrome 36 이상에서 customElements.define() 대신 document.registerElement()를 사용하는 Custom Elements API 버전을 구현했음을 알 수 있습니다. 이 버전은 현재 지원 중단된 표준 버전인 v0으로 간주됩니다. customElements.define()는 새로운 인기 분야이며 브라우저 공급업체에서 구현하기 시작하는 요소입니다. 이를 사용자설정 요소 v1이라고 합니다.

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

브라우저 지원

Chrome 54 (status), Safari 10.1 (status), Firefox 63 (status)에는 맞춤 요소 v1이 있습니다. Edge는 개발을 시작했습니다.

맞춤 요소의 기능을 감지하려면 window.customElements가 있는지 확인합니다.

const supportsCustomElementsV1 = 'customElements' in window;

폴리필

브라우저 지원이 광범위하게 제공될 때까지 맞춤 요소 v1에서는 독립형 폴리필을 사용할 수 있습니다. 하지만 webcomponents.js 로더를 사용하여 웹 구성요소 polyfill을 최적으로 로드하는 것이 좋습니다. 로더는 기능 감지를 사용하여 브라우저에 필요한 필수 폴리필만 비동기식으로 로드합니다.

설치합니다.

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>와 같은 다른 새로운 플랫폼 프리미티브와 결합하여 웹 구성요소의 전체적인 그림을 파악하기 시작합니다.

  • 재사용 가능한 구성요소를 만들고 확장하기 위한 교차 브라우저 (웹 표준)
  • 라이브러리나 프레임워크가 없어도 시작할 수 있습니다. Vanilla JS/HTML FTW!
  • 익숙한 프로그래밍 모델을 제공합니다. DOM/CSS/HTML뿐입니다.
  • 다른 새로운 웹 플랫폼 기능 (Shadow DOM, <template>, CSS 맞춤 속성 등)과 함께 잘 작동합니다.
  • 브라우저의 DevTools와 긴밀하게 통합됩니다.
  • 기존 접근성 기능을 활용합니다.