컴포넌트
는 React, Vue 등의 SPA 라이브러리를 사용해본 개발자에겐 익숙한 개념일텐데요.
Web API에도 HTML 태그를 컴포넌트로 다룰 수 있는 기술이 있습니다.
웹 컴포넌트(Web Component)는 복잡한 HTML 코드를 캡슐화하여 재사용 가능한 커스텀 엘리먼트를 생성하고, Web/App에서 활용할 수 있도록 하는 여러 기술들의 모음입니다.
웹 컴포넌트를 만들기 위해서 알아야 할 몇 가지 개념을 살펴봅시다.
웹 컴포넌트 기술들은 DOM을 OOP 대상으로 바라볼 수 있게 해줍니다.
Custom Elements
: HTML element를 어떤 이름을 가진 태그로 정의하는 방법Shadow DOM
: DOM에서 외부의 영향을 받지 않고, 독립적으로 작동하는 DOMHTML Template
: HTML 마크업 템플릿의 임시 저장소class MyComponent extends HTMLElement {...}
customElements.define("my-component", MyComponent)
Custom Element
는 class로 정의를 해야 하는데, 이때 위와 같이 HTMLElement
을 extends합니다. 위 예시 속 customElements.define
의 customElements
는 무엇일까요?
window.customElements
는 window 내장 객체이며, 호출 시 CustomElementRegistry
객체의 참조(reference)를 리턴합니다. 즉, customElements.define
는 customElements
의 자체 메서드가 아닌 customElements
가 리턴한 CustomElementRegistry
객체가 가진 메서드인 것이죠.
앞서 Custom Element는 class로 정의해야 한다고 하였는데요. class로 정의된 Custom Element의 내부에는 Custom Element의 생명주기를 다루는 콜백(callback)을 정의할 수 있습니다.
class MyComponent extends HTMLElement {
// element 생성시
constructor() {
super();
...
}
// connectedCallback: component가 DOM에 추가 되면 가장 먼저 불리워짐
connectedCallback() {
.
}
// disconnectedCallback: MyComponent에 element가 제거될 때 마다 호출
disconnectedCallback() {
...
}
// observedAttributes: 변경을 감지할 attribute들을 모니터링하기 위한 메서드
static get observedAttributes() {
return [...];
}
// attributeChangedCallback: observedAttributes에서 모니터링중인 attribute가 변경되었을 때, 호출
attributeChangedCallback(name, oldValue, newValue) {
...
}
// adoptedCallback: element가 새로운 document로 이동될 때마다 호출
adoptedCallback() {
...
}
}
1. Custom Element class를 선언합니다.
component가 DOM에 추가 되면 가장 먼저 불리워지는 메서드인 connectedCallback
내부에 생성하고자 하는 element를 작성해줍니다.
class MyComponent extends HTMLElement {
connectedCallback() {
const BoxOne = document.createElement('div')
BoxOne.innerText = '안녕'
this.appendChild(BoxOne)
const BoxTwo = document.createElement('div')
BoxTwo.innerText = '하세요'
this.appendChild(BoxTwo)
}
}
2. Custom Element 정의하기
customElements.define('my-component', MyComponent)
3. 정의한 Custom Element 사용하기
<my-component></my-component>
4. 결과
<template>
태그는 HTML 마크업을 정의해 놓고 실제 사용하고자 하는 마크업에 가져다 쓸 수 있도록 하는 HTML 임시 보관소입니다.
//template 코드 예시
<template id="myGreeting">
<div>반갑습니다</div>
<div id="greeting">안녕하세요</div>
<style>
#greeting {
color: blue;
}
</style>
</template>
//사용 예시
this.shadowRoot.append(myGreeting.content.cloneNode(true));
slot은 컴포넌트 내 HTML 태그를 위치 시킬 슬롯을 나타내는 용도로 쓰입니다.
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
`;
}
});
</script>
<user-card>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
</user-card>
Shadow DOM을 사용하면 DOM에 비밀 공간을 만들 수 있습니다. Shadow DOM 트리는 shadow root로부터 시작되어 원하는 모든 요소의 안에 부착될 수 있는데요. Shadow DOM은 일반적인 DOM과 조작 방법(자식 요소 추가나 스타일을 넣는 등)에서 차이가 없지만, shadow DOM 내부의 어떤 코드도 외부에 영향을 줄 수 없는 캡슐화를 허용한다는 차이점이 있습니다.
Flattened Tree (for rendering): (렌더링을 위해) 평평해진 트리
앞서 생성한 Custom Element가 DOM에 추가 되면 가장 먼저 불리워질 connectedCallback 메서드 내부에this.attachShadow({ mode: "open" })
를 사용하여 shadow DOM을 생성해줍니다. 생성한 shadow DOM에 myGreeting template을 복제해서 부착해줍니다.
<body>
<my-component name="daisy"></my-component>
<template id="myGreeting">
<div>안녕하세요</div>
<div id="greeting">반갑습니다.</div>
<style>
#greeting {
color: blue;
}
</style>
</template>
<script>
class MyComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: "open" });
this.shadowRoot.append(myGreeting.content.cloneNode(true));
}
}
customElements.define("my-component", MyComponent);
</script>
</body>
개발자도구로 살펴보면 shadow DOM이 부착된 my-component에는 #shadow-root가 open 된 것을 확인할 수 있습니다.
이렇게 shadow DOM을 사용하면 shadow DOM 내부의 코드가 외부 DOM 요소에 영향을 미치지 않기에 캡슐화가 가능합니다. 예를 들어 shadow DOM 내부에서 정의한 스타일은 shadow DOM 내부에만 영향을 미칠 뿐 shadow DOM 바깥에 있는 element에는 영향을 미치지 않습니다.
앞서 살펴본 Web Component를 구성하는 요소를 조합한다면, Web API 만드로도 캡슐화, 재사용성을 가진 컴포넌트를 구현할 수 있습니다.
웹 컴포넌트를 구성하는 여러 요소를 살펴보았습니다. React 이전에 이미 웹 컴포넌트 만으로도 컴포넌트 Life cycle을 섬세하게 다룰 수 있는 기술이 존재했다는 점이 인상 깊었습니다. React의 소중함도 알게 되었네요.