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

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

사용자설정 요소를 사용하면 웹 개발자가 새로운 HTML 태그를 생성하거나, 기존 HTML 태그를 보강하거나, 다른 개발자가 작성한 구성요소를 확장할 수 있습니다. API는 웹 구성요소의 기반입니다. 이 API는 단순하게 vanilla JS/HTML/CSS를 사용하여 재사용 가능한 구성요소를 생성할 수 있는 웹 표준 기반 방법을 제공합니다. 그 결과, 앱에서 사용되는 코드 수가 줄어들고, 모듈식 코드 사용이 가능하며, 코드 재사용 가능성이 늘어납니다.

소개

브라우저는 웹 애플리케이션을 구조화하는 데 훌륭한 도구를 제공합니다. HTML이라고 합니다. 이에 대해 들어봤을 것입니다. 이는 선언 및 이식이 가능하며, 완벽히 지원되고, 사용이 쉽습니다. HTML은 나름 훌륭하지만, 해당 어휘와 확장성은 한정되어 있습니다. HTML Living Standard에는 지금까지 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 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 요소의 인스턴스가 생성되거나 업그레이드됩니다. 상태 초기화, 이벤트 리스너 설정 또는 Shadow DOM 생성에 유용합니다. constructor에서 수행할 수 있는 작업에 대한 제한사항은 사양 을 참조하세요.
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 속성은 값을 DOM에 HTML 속성으로 다시 반영합니다. 예를 들어 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;
}

disabled 속성이 JS에서 변경될 때에는 사용자 선택기가 일치하도록 해당 특성이 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. Shadow DOM 덕분에 요소의 DOM은 요소에 로컬입니다.
  4. Shadow DOM 덕분에 이 요소의 내부 CSS의 범위는 이 요소로 지정됩니다.

Shadow DOM을 사용하고 있습니다. My markup was stamped from a <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">

또는 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)

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

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 (상태), Safari 10.1 (상태), Firefox 63 (상태)에는 Custom Elements v1이 있습니다. Edge는 개발을 시작했습니다.

맞춤 요소를 검색하려면 window.customElements가 있는지 확인합니다.

const supportsCustomElementsV1 = 'customElements' in window;

폴리필

브라우저 지원이 광범위하게 제공될 때까지 Custom Elements 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>)과 함께 사용하면 다음과 같이 웹 구성 요소의 장대한 그림을 볼 수 있습니다.

  • 재사용 가능한 구성요소를 만들고 확장하기 위한 교차 브라우저 (웹 표준)
  • 시작하는 데 라이브러리나 프레임워크가 필요하지 않습니다. Vanilla JS/HTML FTW!
  • 익숙한 프로그래밍 모델을 제공합니다. 이를 테면 DOM/CSS/HTML
  • 다른 신규 웹 플랫폼 기능(Shadow DOM, <template>, CSS 맞춤 속성 등)과 호환
  • 브라우저의 DevTools와 완벽하게 통합됨
  • 기존 접근성 기능 활용