사용자 정의 요소 사용하기
웹 컴포넌트 표준의 주요 기능 중 하나는 사용자 정의 페이지 기능을 제공하는 길고 중첩된 요소들의 묶음으로 만족하는 것보다는, HTML 페이지에서 기능을 캡슐화하는 사용자 정의 요소를 생성하는 능력입니다. 이 문서는 Custom Elements API의 사용을 소개합니다.
중요 내용 보기
웹 document의 사용자 정의 요소의 컨트롤러는 CustomElementRegistry 객체입니다. 이 객체는 페이지에 사용자 정의 요소를 등록할 수 있게 하고, 어떤 사용자 정의 요소가 등록되었는지 등에 대한 정보를 반환합니다.
페이지에 사용자 정의 요소를 등록하는 방법은 CustomElementRegistry.define() 메서드를 사용하는 것입니다. 이 메서드는 인자로 다음을 취합니다.
DOMString은 요소에 주는 이름을 나타냅니다. 사용자 정의 요소의 이름은 대시가 사용되는 것을 요구한다는 것에 주의하세요 (kebab-case). 이름은 하나의 단어일 수 없습니다.- 요소의 동작을 정의하는 class 객체.
- Optional
extends속성을 포함하는 옵션 객체인데, 이는 이 요소가 상속받는 내장 요소가 있다면, 그 내장 요소를 명시합니다 (오직 사용자 정의된 내장 요소에만 관계있습니다. 아래의 정의를 보세요).
예를 들어, 사용자 정의 단어 카운트 요소를 다음과 같이 정의할 수 있습니다.
customElements.define("word-count", WordCount, { extends: "p" });
이 요소는 word-count 라고 불리며, 이것의 클래스 객체는 WordCount 이고, 이것은 <p>요소를 확장합니다.
사용자 정의 요소의 클래스 객체는 표준 ES 2015 class 구문을 사용하여 작성됩니다. 에를 들어, WordCount 는 다음과 같이 구조화될 수 있습니다.
class WordCount extends HTMLParagraphElement {
constructor() {
// 항상 super를 생성자에서 먼저 호출합니다
super();
// 요소 기능은 여기 작성됩니다
...
}
}
이것은 단순히 간단한 예제이지만, 여기서 할 수 있는 더 많은 것이 있습니다. 클래스 내부에서 특정한 생명 주기 콜백을 정의할 수 있는데, 이 콜백은 요소의 생명 주기의 특정한 지점에서 실행됩니다. 예를 들어, connectedCallback 은 사용자 정의 요소가 문서에 연결된 요소에 추가될 때마다 호출되는 반면, attributeChangedCallback 은 사용자 정의 요소의 특성 중 하나가 추가되거나, 제거되거나, 변경될 때 호출됩니다.
아래의 생명 주기 콜백 사용하기 섹션에서 더 많은 것을 배울 수 있습니다.
두 종류의 사용자 정의 요소가 있습니다.
- 독립적인 사용자 정의 요소는 독립적입니다. 이 유형의 요소는 표준 HTML 요소를 상속받지 않습니다. 이러한 요소는 페이지에서 말 그대로 HTML 요소로 작성됨으로써 사용됩니다. 예를 들어
<popup-info>, 혹은document.createElement("popup-info"). - 사용자 정의된 내장 요소는 기본 HTML 요소를 상속받습니다. 이러한 요소를 생성하기 위해서는, (위의 예제에서 암시되었듯이) 어떤 요소를 이것이 확장하는지 명시해야 하며, 이러한 요소는 기본 요소를 작성함으로써 사용되나
is특성 (혹은 속성) 에 사용자 정의 요소의 이름을 명시해야 합니다. 예를 들어<p is="word-count">, 혹은document.createElement("p", { is: "word-count" }).
몇 가지 간단한 예제 살펴보기
이 지점에서, 어떻게 사용자 정의 요소가 생성되는지를 자세히 보여주는 몇 가지 간단한 예제를 살펴봅시다.
독립적인 사용자 정의 요소
독립적인 사용자 정의 요소의 예제를 살펴봅시다. <popup-info-box> (작동 예제도 볼 수 있습니다). 이것은 이미지 아이콘과 텍스트 문자열을 취하고, 아이콘을 페이지에 넣습니다. 아이콘이 포커스되었을 때, 이것은 텍스트를 팝업 정보 박스에 표시하여 추가적인 맥락 내 정보를 제공합니다.
우선, JavaScript 파일에서 PopUpInfo 라는 클래스를 정의하는데, 이 클래스는 포괄적인 HTMLElement 클래스를 확장합니다.
class PopUpInfo extends HTMLElement {
constructor() {
// 항상 super를 생성자에서 먼저 호출합니다
super();
// 요소 기능을 여기 작성합니다
...
}
}
앞선 코드 스니펫은 클래스에 대한 constructor() 정의를 포함하고 있는데, 이는 항상 super()를 호출함으로써 시작하여 올바른 프로토타입 체인이 확립되도록 합니다.
생성자 내부에서, 클래스의 인스턴스가 인스턴스화되었을 때 요소가 가질 모든 기능을 정의합니다. 이 경우 우리는 shadow root을 사용자 정의 요소에 부착하고, 몇 가지 DOM 조작을 사용하여 요소의 내부 shadow DOM 구조를 생성하는데, 이는 그리고서 shadow root에 부착됩니다. 그리고 마지막으로 몇 가지 CSS를 shadow root에 부착하여 shadow DOM을 꾸밉니다.
// shadow root을 생성합니다
this.attachShadow({ mode: "open" }); // 'this.shadowRoot'을 설정하고 반환합니다
// (중첩된) span 요소들을 생성합니다
const wrapper = document.createElement("span");
wrapper.setAttribute("class", "wrapper");
const icon = wrapper.appendChild(document.createElement("span"));
icon.setAttribute("class", "icon");
icon.setAttribute("tabindex", 0);
// 정의된 특성으로부터의 아이콘 혹은 기본 아이콘을 삽입합니다
const img = icon.appendChild(document.createElement("img"));
img.src = this.hasAttribute("src")
? this.getAttribute("src")
: "img/default.png";
const info = wrapper.appendChild(document.createElement("span"));
info.setAttribute("class", "info");
// 특성의 내용을 취하고 그것을 info span 내부에 넣습니다
info.textContent = this.getAttribute("data-text");
// shadow dom에 적용할 몇 가지 CSS를 생성합니다
const style = document.createElement("style");
style.textContent =
".wrapper {" +
// 간결함을 위해 CSS 생략됨
// 생성된 요소들을 shadow DOM에 부착합니다
this.shadowRoot.append(style, wrapper);
마지막으로, CustomElementRegistry 에 사용자 정의 요소를 앞에서 언급된 define() 메서드를 사용해 등록합니다. 매개변수에서 요소의 이름과, 그리고 나서 요소의 기능을 정의하는 클래스명을 명시합니다.
customElements.define("popup-info", PopUpInfo);
이 요소는 이제 페이지에서 사용 가능합니다. HTML 전체에서, 요소를 다음과 같이 사용합니다.
<popup-info
img="img/alt.png"
data-text="Your card validation code (CVC)
is an extra security feature — it is the last 3 or 4 numbers on the
back of your card."></popup-info>
참고 : 전체 JavaScript 소스 코드를 여기서 확인할 수 있습니다.
내부 스타일 대 외부 스타일
상기의 예제에서 <style> 요소가 사용되어 Shadow DOM에 스타일을 적용했으나, 대신 <link> 요소로부터 외부 스타일시트를 참조함으로써 스타일을 적용하는 것도 완벽히 가능합니다.
예를 들자면, popup-info-box-external-stylesheet 예제에서 이 코드를 확인해 보세요 (소스 코드도 볼 수 있습니다).
// 외부 스타일을 shadow dom에 적용하기
const linkElem = document.createElement("link");
linkElem.setAttribute("rel", "stylesheet");
linkElem.setAttribute("href", "style.css");
// 생성된 요소를 shadow dom에 부착하기
shadow.appendChild(linkElem);
<link> 요소는 shadow root의 페인트를 막지 않아, 스타일시트가 로딩되는 동안 스타일되지 않은 내용의 번쩍임 (FOUC, flash of unstyled content) 이 있을 수 있다는 점에 주의하세요.
많은 모던 브라우저들은 공통 노드로부터 복제되었거나 동일한 텍스트를 가지고 있는 <style> 태그에 대한 최적화를 구현하여 스타일 태그가 하나의 백업 스타일시트를 공유할 수 있게 합니다. 이 최적화로 인해 외부 스타일과 내부 스타일의 성능은 비슷할 것입니다.
사용자 정의된 내장 요소
이제 사용자 정의된 요소 예제를 살펴봅시다. expanding-list (작동 예제도 확인해 보세요). 이것은 정렬되지 않은 리스트를 확장/축소 메뉴로 바꿔 줍니다.
우선, 요소의 클래스를 이전과 같은 방식으로 정의합니다.
class ExpandingList extends HTMLUListElement {
constructor() {
// 항상 super를 생성자에서 먼저 호출합니다
super();
// 요소 기능을 여기 작성합니다
...
}
}
요소 기능을 여기서는 자세히 설명하지 않을 것이지만, 소스 코드를 확인해서 어떻게 작동하는지 발견할 수 있을 것입니다. 여기서의 차이는 이 요소가 HTMLElement가 아니라, HTMLUListElement 인터페이스를 확장한다는 것입니다. 그래서 이 요소는 독립된 요소이기보다는 <ul> 요소의 모든 특성을 가지고 있으며 그 위에 우리가 정의한 기능 또한 가지고 있습니다. 이것이 이 요소를 독립적인 요소보다는 사용자 정의된 내장 요소로 만들어주는 것입니다.
다음으로, 전과 같이 define() 메서드를 사용하여 요소를 등록하나, 이번엔 이 사용자 정의 요소가 어떤 요소를 상속받는지를 나타내는 옵션 객체를 포함합니다.
customElements.define("expanding-list", ExpandingList, { extends: "ul" });
웹 document에서 이 내장 요소를 사용하는 것은 또한 어느 정도 다르게 보입니다.
<ul is="expanding-list">
...
</ul>
<ul> 요소를 평범하게 사용하나, is 특성 내부에 사용자 정의 요소의 이름을 명시합니다.
참고 : 전체 JavaScript 소스 코드를 확인해볼 수 있습니다.
생명 주기 콜백 사용하기
몇 가지 다른 콜백을 사용자 정의 요소의 클래스 정의 내부에 정의할 수 있는데, 이 콜백들은 요소의 생명 주기의 각기 다른 지점에서 발생됩니다.
-
connectedCallback: 사용자 정의 요소가 문서에 연결된 요소에 추가될 때마다 호출됩니다. 이것은 노드가 이동될 때마다 발생할 것이며, 요소의 내용이 완전히 해석되기 전에 발생할 지도 모릅니다.참고 :
connectedCallback은 요소가 더 이상 연결되지 않았을 때 호출될 수도 있으므로, 확실하게 하기 위해선Node.isConnected를 사용하세요. -
disconnectedCallback: 사용자 정의 요소가 document의 DOM에서 연결 해제되었을 때마다 호출됩니다. -
adoptedCallback: 사용자 정의 요소가 새로운 document로 이동되었을 때마다 호출됩니다. -
attributeChangedCallback: 사용자 정의 요소의 특성들 중 하나가 추가되거나, 제거되거나, 변경될 때마다 호출됩니다. 어떤 특성이 변경에 대해 알릴지는 static getobservedAttributes메서드에서 명시됩니다.
이것들의 사용례를 봅시다. 아래의 코드는 life-cycle-callbacks 예제에서 취해졌습니다 (작동 예제도 볼 수 있습니다). 이것은 페이지에 고정된 크기의 색이 칠해진 사각형을 생성하는 작은 예제입니다. 사용자 정의 요소는 다음과 같이 생겼습니다.
<custom-square l="100" c="red"></custom-square>
클래스 생성자는 정말로 간단합니다. shadow DOM을 요소에 부착하고, 빈 <div> 와 <style> 요소를 shadow root에 부착합니다.
const shadow = this.attachShadow({ mode: "open" });
const div = document.createElement("div");
const style = document.createElement("style");
shadow.appendChild(style);
shadow.appendChild(div);
이 예제에서의 핵심 함수는 updateStyle() 입니다. 이 함수는 요소를 취하고, 요소의 shadow root을 얻고, shadow root의 <style> 요소를 찾고, width, height, background-color를 스타일에 추가합니다.
function updateStyle(elem) {
const shadow = elem.shadowRoot;
shadow.querySelector("style").textContent = `
div {
width: ${elem.getAttribute("l")}px;
height: ${elem.getAttribute("l")}px;
background-color: ${elem.getAttribute("c")};
}
`;
}
실제 갱신은 모두 생명 주기 콜백에 의해 다뤄지는데, 이 콜백들은 클래스 정의 내부에 메서드로 위치합니다. connectedCallback() 은 요소가 DOM에 추가될 때마다 실행됩니다. 여기서 updateStyle() 함수가 실행되어 사각형이 이것의 특성에 정의된 대로 꾸며지도록 합니다.
connectedCallback() {
console.log('Custom square element added to page.');
updateStyle(this);
}
disconnectedCallback() 과 adoptedCallback() 콜백은 콘솔에 간단한 메시지를 로그하여 언제 요소가 DOM에서 제거되었는지, 혹은 다른 페이지로 이동되었는지를 알립니다.
disconnectedCallback() {
console.log('Custom square element removed from page.');
}
adoptedCallback() {
console.log('Custom square element moved to new page.');
}
attributeChangedCallback() 은 요소의 특성 중 하나가 어떠한 방식으로 변경될 때마다 실행됩니다. 이 콜백의 매개변수에서 볼 수 있다시피, 특성의 이름, 이전 값, 새로운 값을 보고 특성의 변경 사항에 개별적으로 대응하는 게 가능합니다. 그러나, 이번 경우에는 개별적으로 대응하지 않고, 사각형의 스타일이 새로운 값에 따라 갱신될 수 있도록 단순히 updateStyle() 함수를 다시 호출합니다.
attributeChangedCallback(name, oldValue, newValue) {
console.log('Custom square element attributes changed.');
updateStyle(this);
}
attributeChangedCallback() 콜백이 특성이 변경되었을 때 발생되기 하기 위해서는, 해당 특성을 관찰해야만 합니다. 이 작업은 static get observedAttributes() 메서드를 사용자 정의 요소 클래스 내부에 명시함으로써 이루어집니다. 이 메서드는 관찰하기를 원하는 특성들의 이름을 포함하는 배열을 return 해야 합니다.
static get observedAttributes() { return ['c', 'l']; }
이 예제에서 이 메서드는 생성자의 바로 위에 위치해 있습니다.
참고 : 전체 JavaScript 소스 코드를 확인해 보세요.
트랜스파일러 대 클래스
ES2015 class는 레거시 브라우저를 목표로 하는 Babel 6 또는 TypeScript 에서 신뢰할만하게 트랜스파일될 수 없다는 점에 주의하세요. Babel 7을 사용하거나 Babel 6에 대해서 babel-plugin-transform-builtin-classes를 사용할 수 있고, 레거시 대신에 TypeScript의 ES2015를 목표로 할 수 있습니다.
라이브러리
사용자 정의 요소를 생성할 때 추상화의 단계를 올리는 것을 목표로 하는 웹 컴포넌트를 기반으로 하는 몇 가지의 라이브러리들이 있습니다. 이러한 라이브러리에는 FASTElement, snuggsi, X-Tag, Slim.js, Lit, Smart, Stencil, hyperHTML-Element, DataFormsJS, Custom-Element-Builder가 있습니다.