티스토리 뷰
Custom Element
클래스에서 설명하는 사용자 지정 HTML 요소를 자체 방법과 속성, 이벤트 등으로 만들 수 있습니다. 사용자 지정 요소가 정의되면 내장된 HTML 요소와 동등하게 사용할 수 있습니다.
HTML Dictionary은 풍부하지만 무한하지는 않기 때문에 이 선택이 좋습니다.
HTML Dictionary에는 easy-tabs, sliding-carousel, beautiful-upload등이 없습니다… 우리가 필요로 할 다른 tag들에 대해 생각해봅시다.
그를 특별 클래스로 정의한 다음 항상 HTML의 일부인 것처럼 사용할 수 있습니다.
두 가지 종류의 사용자 지정 요소가 있습니다:
- Autonomous custom elements자율 사용자 지정 요소 - 추상적인
HTMLElement클래스를 확장하는 "완전히 새로운" 요소 - Customized built-in elements맞춤형 내장 요소 -
HTMLButtonElement등을 기반으로 하는 맞춤형 버튼 같은 내장 요소를 기반으로 확장합니다.
먼저 자율 요소를 다룬 다음 맞춤형 내장 요소로 이동합니다.
사용자 지정 요소를 만들려면 브라우저에 표시 방법, 요소를 페이지에 추가하거나 삭제할 때 수행할 작업 등 몇 가지 세부 정보를 알려줘야 합니다.
이는 특별한 방법으로 수업을 구성하여 이루어집니다. 방법이 거의 없고 모두 선택 사항이기 때문에 쉽습니다.
전체 목록이 포함된 스케치는 다음과 같습니다:
class MyElement extends HTMLElement {
constructor() {
super();
// element 생성
}
connectedCallback() {
// 브라우저는 요소가 문서에 추가될 때 이 메서드를 호출합니다
// (요소를 반복적으로 추가/removed하는 경우 여러 번 호출할 수 있습니다)
}
disconnectedCallback() {
// 브라우저는 문서에서 요소가 제거되면 이 메서드를 호출합니다
// (요소를 반복적으로 추가/removed하는 경우 여러 번 호출할 수 있습니다)
}
static get observedAttributes() {
return [/* 변경 사항을 모니터링할 속성 이름 배열 */];
}
attributeChangedCallback(name, oldValue, newValue) {
// 위에 나열된 속성 중 하나가 수정되면 호출됩니다
}
adoptedCallback() {
// 요소를 새 문서로 이동할 때 호출됩니다
// (document.adaptNode에서 발생하며, 거의 사용되지 않습니다)
}
// 다른 요소 방법 및 속성이 있을 수 있습니다
}
그런 다음 요소를 등록해야 합니다:
//브라우저에 가서 <my-element>가 우리의 새로운 클래스로서 serve되는 것을 알립니다.
customElements.define("my-element", MyElement);
이제 태그 my-element가 있는 모든 HTML 요소에 대해 MyElement의 인스턴스가 생성되고 앞서 언급한 방법이 호출됩니다. 또한 document.createElement('my-element') 도 사용할 수 있습니다.
- 사용자 지정 요소 이름에는 하이픈이 포함되어야 합니다. -
사용자 지정 요소 이름에는 하이픈이 있어야 합니다. -,exmy-element,super-button은 유효한 이름이지만myelement는 아닙니다.
이는 내장된 HTML 요소와 사용자 지정 HTML 요소 간에 이름 충돌이 발생하지 않도록 하기 위한 것입니다.
Example: “time-formatted”
<time>element가 이미 날짜/시간에 대한 HTML의 요소입니다. 하지만 자체적으로 포맷을 수행하지는 않습니다.
<time-formatted> element를 만들어봅시다. 언어 인식 형식으로 시간을 표시하는 요소입니다:
<script>
class TimeFormatted extends HTMLElement { // (1)
connectedCallback() {
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
}
customElements.define("time-formatted", TimeFormatted); // (2)
</script>
<!-- (3) -->
<time-formatted datetime="2019-12-01"
year="numeric" month="long" day="numeric"
hour="numeric" minute="numeric" second="numeric"
time-zone-name="short"
></time-formatted>
- 클래스에는 한 가지 방법만 있습니다.
connectedCallback()– 브라우저는 다음과 같이 부릅니다.<time-formatted>요소가 페이지에 추가되고(또는 HTML 파서가 감지할 때) 내장된 Intl을 사용합니다.브라우저 전반에서 잘 지원되는 DateTimeFormat 데이터 형식을 사용하면 형식이 좋은 시간을 표시할 수 있습니다. - 다음까지 새 요소를 등록해야 합니다.
customElements.define(tag, class)`. - 그런 다음 어디에서나 사용할 수 있습니다.
Custom elements 업그레이드
만일 브라우저가 어떤 <time-formatted> 을 customElements.define이전에 직면했다면,이는 오류가 아닙니다. 그러나 이 element는 표준이 아닌 태그와 마찬가지로 아직 알려지지 않았습니다.
이러한 "정의되지 않은" 요소는 CSS 선택기로 스타일링할 수 있습니다. :not(:defined).
customElement.define이 호출되었을 때, 그들은 "upgraded"됩니다."upgraded": TimeFormatted의 새로운 인스턴스는 각각 만들어지고, connectedCallback 를 호출합니다. 그리고 마침내 다음의 상태가 됩니다. :defined.
사용자 지정 요소에 대한 정보를 얻으려면 다음과 같은 방법이 있습니다:
customElements.get(name)–name이 지정된 사용자 지정 요소의 클래스를 반환합니다.customElements.whenDefined(name)–name이 주어진 커스텀 엘리멘트가 define된 후에 resolve된 프라미스를 (값 없이) 반환한다.
constructor가 아니라 connectedCallback에서 렌더링 된다. ,
위의 예에서 요소 콘텐츠는 connectedCallback에서 렌더링(생성)됩니다.
왜 constructor이 아닌가?
이유는 간단합니다: constructor이 호출되었을 때, 아직 이르다고 판단합니다. element가 생성되었지만 브라우저는 아직 이 단계에서 속성을 처리/할당하지 않았습니다: getAttribute를 호출하면 null이 반환될 것 입니다. 따라서 실제로 렌더링할 수 없습니다.
또한, 생각해보면 정말 필요할 때까지 작업을 지연시키는 것이 성과 측면에서 더 좋습니다.
connectedCallback은 요소가 문서에 추가될 때 트리거됩니다. 자식으로써 다른 요소에 추가되었을 뿐만 아니라 실제로 페이지의 일부가 됩니다. 따라서 분리된 DOM을 구축하고 요소를 생성하여 나중에 사용할 수 있도록 준비할 수 있습니다. 페이지에 들어갈 때만 실제로 렌더링됩니다.
Observing attributes 옵저버 속성
<time-formatted>가 현재 실행되는 도중, 요소가 렌더링된 후에는 추가 속성 변경이 아무런 영향을 미치지 않습니다. HTML 요소치고는 이상한 일입니다. 일반적으로 a.href,와 같은 속성을 변경할 때는 변경 사항이 즉시 가시화될 것으로 예상합니다. 그럼 이 문제를 해결해 보겠습니다.
observedAttributes() static getter 안에서 그 속성들의 목록을 제공함으로써 우리는 속성을 관찰할 수 있습니다.
이러한 속성의 경우, attributeChangedCallback 를 수정할 때 호출합니다. 이것은 성능상의 이유로 속성에 대해 트리거되지 않습니다.
여기 속성이 변경될 때 자동으로 updates되는 새로운 <time-formatted>가 있습니다.
<script>
class TimeFormatted extends HTMLElement {
render() { // (1)
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
connectedCallback() { // (2)
if (!this.rendered) {
this.render();
this.rendered = true;
}
}
static get observedAttributes() { // (3)
return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name'];
}
attributeChangedCallback(name, oldValue, newValue) { // (4)
this.render();
}
}
customElements.define("time-formatted", TimeFormatted);
</script>
<time-formatted id="elem" hour="numeric" minute="numeric" second="numeric"></time-formatted>
<script>
setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
</script>
오전 2:15:14
- 렌더링 로직이
render()도우미 메서드로 이동한다. - 요소를 페이지에 삽입하면 한 번 호출합니다.
- 속성 변경의 경우 다음에 나열되어 있습니다. -
observedAttributes(),attributeChangedCallback트리거. - ...요소를 다시 renders합니다.
- 마지막에는 라이브 타이머를 쉽게 만들 수 있습니다.
Rendering order
HTML parser가 DOM을 구축하면 부모가 자녀보다 먼저 요소가 차례로 처리됩니다.
예: <outer><inner></inner></outer>,그리고나서 <outer> 요소가 생성되어 먼저 DOM에 연결된 다음 <inner>.
이는 맞춤형 요소에 중요한 결과로 이어집니다.
예를 들어, 사용자 지정 요소가 액세스를 시도하는 경우 connectedCallback안의 innerHTML이 아무것도 얻지 못하는 결과를 확인해봅시다.
아무것도 얻지 못하는 결과:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
alert(this.innerHTML); // empty (*)
}
});
</script>
<user-info>John</user-info>
실행하면 alert는 비어 있습니다.
바로 그 stage에 자식이 없고 돔이 미완성이기 때문입니다. HTML 파서가 사용자 지정 요소에 연결되었습니다. <user-info>, 그리고 자식들에게 진행할 예정이지만 아직 진행되지 않았습니다.
사용자 지정 요소에 정보를 전달하려면 속성을 사용할 수 있습니다. 즉시 사용할 수 있습니다.
또는 자녀가 정말 필요한 경우 자녀에 대한 액세스를 지연 없이 setTimeout로 연기할 수 있습니다.
작동합니다:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
setTimeout(() => alert(this.innerHTML)); // John (*)
}
});
</script>
<user-info>John</user-info>
이제 alert 줄을 서서 (*) 는 HTML 구문 분석이 완료된 후 비동기식으로 실행할 때 "John"을 보여줍니다. 필요한 경우 자식을 처리하고 초기화를 완료할 수 있습니다.
반면에 이 솔루션도 완벽하지 않습니다. 중첩된 사용자 지정 요소도 setTimeout을 사용합니다. 스스로 초기화한 다음 줄을 서 있습니다: 외부 setTimeout을 먼저 트리거하고 내부 트리거를 트리거합니다.
따라서 외부 요소는 내부 요소보다 먼저 초기화를 완료합니다.
이를 예로 들어 설명하겠습니다:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
alert(`${this.id} connected.`);
setTimeout(() => alert(`${this.id} initialized.`));
}
});
</script>
<user-info id="outer">
<user-info id="inner"></user-info>
</user-info>
출력 순서:
- 외부 연결됨.
- 내부 연결.
- 외부 초기화됨.
- 내부 초기화됨.
외부 요소가 초기화를 완료하는 것을 명확하게 볼 수 있습니다. (3) 내면의 것 앞에 (4).
중첩된 요소가 준비된 후 트리거되는 내장 콜백은 없습니다. 필요한 경우 자체적으로 구현할 수 있습니다. 예를 들어, 내부 요소는 initialized과 같은 이벤트를 디스패치할 수 있습니다. 그리고 바깥쪽 사람들은 귀를 기울이고 반응할 수 있습니다.
Customized built-in elements 맞춤형 내장 요소
<time-formatted>과 같은 새로운 요소를 생성합니다. 이것에는 연관된 의미론이 없습니다. 검색 엔진에는 알려지지 않았으며 접근성 장치로는 이를 처리할 수 없습니다.
하지만 그런 것들이 중요할 수 있습니다. 예를 들어, 검색 엔진은 실제로 시간을 표시한다는 사실에 관심이 있을 것입니다. 특별한 종류의 버튼을 만들고 있다면 기존 버튼을 재사용하는 것은 어떨까요? <button> 기능처럼요.
클래스에서 상속하여 내장된 HTML 요소를 확장하고 사용자 지정할 수 있습니다.
예를 들어, 버튼은 다음과 같은 인스턴스입니다. HTMLButtonElement, 그것을 기반으로 합니다.
HTMLButtonElement를 클래스와 함께 확장합니다.
class HelloButton extends HTMLButtonElement { /* custom element methods */ }
- 세 번째 인수를 제공합니다.
customElements.define, 태그를 지정합니다:
customElements.define('hello-button', HelloButton, {extends: 'button'});
동일한 DOM 클래스를 공유하는 태그가 다를 수 있으므로 필요한 extends를 지정합니다.
- 마지막에 사용자 지정 요소를 사용하려면 일반 요소
<button>를 삽입합니다. 추가로 태그 안에 is="hello-button"을 설정합니다.
결과:
<button is="hello-button">...</button>
전체 예는 다음과 같습니다:
<script>
// The button that says "hello" on click
class HelloButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => alert("Hello!"));
}
}
customElements.define('hello-button', HelloButton, {extends: 'button'});
</script>
<button is="hello-button">Click me</button>
<button is="hello-button" disabled>Disabled</button>
새로운 버튼은 내장 버튼을 확장합니다. 따라서 disabled 과 같은 스타일과 표준 기능을 유지합니다.
References
HTML Living Standard: https://html.spec.whatwg.org/#custom-elements.
Compatiblity: https://caniuse.com/#feat=custom-elements.
https://ko.javascript.info/custom-elements 의 글을 번역한 내용입니다.
'Programming > JavaScript' 카테고리의 다른 글
| Vanila 프로젝트 후기 (1) | 2024.12.31 |
|---|---|
| [JavaScript] Throttle과 Debounce (1) | 2024.12.15 |
| [JavaScript] 비동기 통신 - AJAX와 fetch (5) | 2024.12.01 |
| [JavaScrpit] 렉시컬 환경(Lexical Environment)과 클로저(closure) (2) | 2024.11.17 |
| [JavaScript]호이스팅(Hoisting) (2) | 2024.11.11 |
