맞춤 요소 권장사항

맞춤 요소를 사용하면 자체 HTML 태그를 구성할 수 있습니다. 이 체크리스트에서는 고품질 요소를 빌드하는 데 도움이 되는 권장사항을 다룹니다.

맞춤 요소를 사용하면 HTML을 확장하고 자체 태그를 정의할 수 있습니다. 이는 매우 강력한 기능이지만 하위 수준이기도 합니다. 즉, 자체 요소를 가장 효과적으로 구현하는 방법이 항상 명확하지는 않습니다.

최고의 환경을 만드는 데 도움이 되도록 이 체크리스트를 마련했습니다. 올바르게 동작하는 맞춤 요소가 되기 위해 필요한 모든 요소를 분석합니다.

체크리스트

Shadow DOM

섀도우 루트를 만들어 스타일을 캡슐화합니다.

이유가 무엇인가요? 요소의 섀도우 루트에 스타일을 캡슐화하면 사용되는 위치에 관계없이 스타일이 작동합니다. 개발자가 다른 요소의 섀도우 루트 내부에 요소를 배치하려는 경우 특히 중요합니다. 이는 체크박스나 라디오 버튼과 같은 간단한 요소에도 적용됩니다. 섀도우 루트 내부에 있는 유일한 콘텐츠가 스타일 자체일 수도 있습니다.
<howto-checkbox> 요소

생성자에서 섀도우 루트를 만듭니다.

이유가 무엇인가요? 생성자는 요소에 관한 독점적인 지식을 가지고 있는 경우입니다. 다른 요소에 지장을 주지 않도록 구현 세부정보를 설정하는 것이 좋습니다. connectedCallback와 같은 이후 콜백에서 이 작업을 하면 요소가 분리되었다가 문서에 다시 연결되는 상황을 방지해야 합니다.
<howto-checkbox> 요소

요소가 만든 모든 하위 요소를 섀도우 루트에 배치합니다.

이유가 무엇인가요? 개발자 요소에 의해 생성된 하위 요소는 구현의 일부이며 비공개여야 합니다. 섀도우 루트를 보호하지 않으면 외부 JavaScript가 의도치 않게 이러한 하위 요소를 방해할 수 있습니다.
<howto-tabs> 요소

<slot>을 사용하여 Light DOM 하위 요소를 Shadow DOM에 프로젝션합니다.

이유가 무엇인가요? HTML 하위 요소가 구성요소의 구성 가능성을 높이므로 구성요소 사용자가 구성요소의 콘텐츠를 지정할 수 있도록 허용합니다. 브라우저에서 맞춤 요소를 지원하지 않는 경우 중첩된 콘텐츠는 계속 사용 가능하고 표시되고 액세스할 수 있습니다.
<howto-tabs> 요소

기본값 inline을 사용하지 않는 한 :host 디스플레이 스타일 (예: block, inline-block, flex)을 설정하세요.

이유가 무엇인가요? 맞춤 요소는 기본적으로 display: inline이므로 width 또는 height를 설정해도 효과가 없습니다. 이는 개발자가 놀랄 수도 있고 페이지 레이아웃과 관련된 문제를 일으킬 수도 있습니다. inline 표시를 선호하지 않는다면 항상 기본 display 값을 설정해야 합니다.
<howto-checkbox> 요소

숨겨진 속성을 고려하는 :host 디스플레이 스타일을 추가합니다.

이유가 무엇인가요? 기본 display 스타일(예: :host { display: block })을 사용하는 맞춤 요소는 낮은 특수성을 기본으로 제공하는 hidden 속성을 재정의합니다. 요소의 hidden 속성을 설정하여 display: none를 렌더링한다고 예상할 수 있습니다. 기본 display 스타일 외에도 :host([hidden]) { display: none }를 사용하여 hidden 지원을 추가합니다.
<howto-checkbox> 요소

속성 및 속성

작성자가 설정한 전역 속성을 재정의하지 않습니다.

이유가 무엇인가요? 전역 속성은 모든 HTML 요소에 존재하는 속성입니다. tabindex, role을 예로 들 수 있습니다. 맞춤 요소는 키보드에 포커스를 맞출 수 있도록 초기 tabindex을 0으로 설정하는 것이 좋습니다. 그러나 항상 내 요소를 사용하는 개발자가 이 값을 다른 값으로 설정했는지 항상 확인해야 합니다. 예를 들어 tabindex를 -1로 설정했다면 요소의 상호작용이 원치 않는다는 신호입니다.
<howto-checkbox> 요소 자세한 내용은 페이지 작성자를 재정의하지 마세요.

항상 원시 데이터 (문자열, 숫자, 불리언)를 속성 또는 속성으로 허용합니다.

이유가 무엇인가요? 기본 제공 요소와 같은 맞춤 요소는 구성 가능해야 합니다. 구성은 선언적으로, 속성을 통해 또는 자바스크립트 속성을 통해 명령적으로 전달될 수 있습니다. 모든 속성을 상응하는 속성에도 연결하는 것이 이상적입니다.
<howto-checkbox> 요소

원시 데이터 속성과 속성의 동기화를 유지하여 속성에서 속성으로 또는 그 반대로 반영하는 것을 목표로 합니다.

이유가 무엇인가요? 사용자가 요소와 어떻게 상호작용할지 알 수 없습니다. JavaScript에서 속성을 설정한 다음 getAttribute()와 같은 API를 사용하여 이 값을 읽을 수 있습니다. 모든 속성에 상응하는 속성이 있고 두 속성에 모두 반영되는 경우 사용자가 더 쉽게 요소를 사용할 수 있습니다. 즉, setAttribute('foo', value)를 호출하면 상응하는 foo 속성이 설정되며 그 반대의 경우도 마찬가지입니다. 물론 이 규칙에는 예외가 있습니다. 동영상 플레이어에서 currentTime와 같이 빈도가 높은 속성을 반영해서는 안 됩니다. 현명하게 판단하세요. 사용자가 속성 또는 속성과 상호작용할 것 같고 이를 반영하는 것이 부담스럽지 않다면 그렇게 하세요.
<howto-checkbox> 요소 자세한 내용은 재진입 문제 방지하기에 설명되어 있습니다.

리치 데이터 (객체, 배열)만 속성으로 허용하는 것을 목표로 합니다.

이유가 무엇인가요? 일반적으로 해당 속성을 통해 리치 데이터 (일반 자바스크립트 객체 및 배열)를 허용하는 기본 제공 HTML 요소의 예는 없습니다. 대신 메서드 호출 또는 속성을 통해 리치 데이터를 사용할 수 있습니다. 리치 데이터를 속성으로 허용하는 데는 몇 가지 명백한 단점이 있습니다. 큰 객체를 문자열로 직렬화하는 데 비용이 많이 들 수 있고 이 문자열화 프로세스에서 모든 객체 참조가 손실됩니다. 예를 들어 다른 객체 또는 DOM 노드에 대한 참조가 있는 객체를 문자열화하면 해당 참조가 손실됩니다.

속성에 리치 데이터 속성을 반영하지 않습니다.

이유가 무엇인가요? 리치 데이터 속성을 속성에 반영하려면 동일한 자바스크립트 객체를 직렬화하고 역직렬화해야 하므로 불필요하게 비용이 많이 듭니다. 이 기능으로만 해결할 수 있는 사용 사례가 없다면 사용하지 않는 것이 가장 좋습니다.

요소가 업그레이드되기 전에 설정되었을 수 있는 속성을 확인하는 것이 좋습니다.

이유가 무엇인가요? 요소를 사용하는 개발자는 요소의 정의가 로드되기 전에 요소의 속성 설정을 시도할 수 있습니다. 개발자가 구성요소를 로드하고 페이지에 스탬핑하고 속성을 모델에 결합하는 프레임워크를 사용하는 경우 특히 그렇습니다.
<howto-checkbox> 요소 자세한 내용은 속성을 지연 상태로 만들기를 참고하세요.

수업을 직접 적용하지 마세요.

이유가 무엇인가요? 상태를 표현해야 하는 요소는 속성을 사용하여 표현해야 합니다. 일반적으로 class 속성은 요소를 사용하는 개발자가 소유한 것으로 간주되며 직접 작성할 때 의도치 않게 개발자 클래스를 사용할 수 있습니다.

이벤트

내부 구성요소 활동에 대한 응답으로 이벤트를 전달합니다.

이유가 무엇인가요? 예를 들어 타이머 또는 애니메이션이 완료되거나 리소스 로드를 완료하는 경우와 같이 구성요소만 알고 있는 활동에 대한 응답으로 변경되는 속성이 구성요소에 있을 수 있습니다. 이러한 변경사항에 대한 응답으로 이벤트를 전달하여 구성요소의 상태가 다르다는 것을 호스트에 알리는 것이 좋습니다.

속성을 설정하는 호스트에 대한 응답으로 이벤트를 전달하지 않습니다 (하향식 데이터 흐름).

이유가 무엇인가요? 호스트에서 속성을 설정하는 것에 대한 응답으로 이벤트를 전달하는 것은 불필요합니다. 호스트는 그냥 상태를 설정하기 때문에 현재 상태를 알 수 있습니다. 속성을 설정하는 호스트에 대한 응답으로 이벤트를 전달하면 데이터 결합 시스템에 무한 루프가 발생할 수 있습니다.
<howto-checkbox> 요소

설명 동영상

페이지 작성자 재정의 안함

요소를 사용하는 개발자가 초기 상태의 일부를 재정의하려고 할 수 있습니다. 예를 들어 tabindex로 ARIA role 또는 포커스 가능 여부를 변경할 수 있습니다. 자체 값을 적용하기 전에 이러한 속성 및 기타 전역 속성이 설정되었는지 확인하세요.

connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', 0);

속성을 지연 상태로 만들기

개발자가 요소의 정의가 로드되기 전에 요소에 속성을 설정하려고 시도할 수 있습니다. 개발자가 구성요소를 로드하여 페이지에 삽입하고 속성을 모델에 결합하는 프레임워크를 사용하는 경우 특히 그렇습니다.

다음 예에서 Angular는 모델의 isChecked 속성을 체크박스의 checked 속성에 선언적으로 바인딩합니다. 방법 체크박스 정의가 지연 로드된 경우 요소가 업그레이드되기 전에 Angular가 선택된 속성을 설정하려고 시도할 수 있습니다.

<howto-checkbox [checked]="defaults.isChecked"></howto-checkbox>

맞춤 요소는 인스턴스에 속성이 이미 설정되어 있는지 확인하여 이 시나리오를 처리해야 합니다. <howto-checkbox>_upgradeProperty()라는 메서드를 사용하여 이 패턴을 보여줍니다.

connectedCallback() {
  ...
  this._upgradeProperty('checked');
}

_upgradeProperty(prop) {
  if (this.hasOwnProperty(prop)) {
    let value = this[prop];
    delete this[prop];
    this[prop] = value;
  }
}

_upgradeProperty()는 맞춤 요소의 자체 속성 setter를 섀도잉하지 않도록 업그레이드되지 않은 인스턴스에서 값을 캡처하고 속성을 삭제합니다. 이렇게 하면 요소의 정의가 최종적으로 로드될 때 즉시 올바른 상태가 반영될 수 있습니다.

재진입 문제 방지

attributeChangedCallback()를 사용하여 기본 속성에 상태를 반영하고 싶을 수 있습니다. 예를 들면 다음과 같습니다.

// When the [checked] attribute changes, set the checked property to match.
attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'checked')
    this.checked = newValue;
}

하지만 속성 setter도 속성에 반영되는 경우 무한 루프가 생성될 수 있습니다.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    // OOPS! This will cause an infinite loop because it triggers the
    // attributeChangedCallback() which then sets this property again.
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

대안은 속성 setter가 속성에 반영되도록 하고 getter가 속성을 기반으로 값을 결정하도록 하는 것입니다.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

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

이 예에서는 속성을 추가하거나 삭제해도 속성이 설정됩니다.

마지막으로 attributeChangedCallback()는 ARIA 상태 적용과 같은 부작용을 처리하는 데 사용할 수 있습니다.

attributeChangedCallback(name, oldValue, newValue) {
  const hasValue = newValue !== null;
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-checked', hasValue);
      break;
    ...
  }
}