라즈베리파이반

라즈베리파이 등 컴퓨터계열 게시판입니다.

제목웹 컴포넌트(Web Component)2022-02-23 22:17
작성자user icon Level 4

88x31.png


1. 렌더링(Rendering) 과정


웹 컴포넌트를 알아보기 이전에 웹브라우저의 렌더링 과정에 대해 알아보겠습니다.

렌더링은 요청받은 내용을 브라우저 화면에 표시하는 것을 말합니다. 렌더링 엔진에 의하여 HTML, XML 문서와 이미지 등을 표시할 수 있습니다. 크롬의 경우 웹킷(Webkit)이라는 렌더링 엔진을 사용합니다.


렌더링 과정은 다음과 같습니다.

mb-file.php?path=2022%2F02%2F23%2FF4592_2.png


HTML 문서는 HTML 파서를 통해 DOM 트리를 형성합니다. DOM은 문서 객체 모델(Document Object Model)로, HTML 문서의 인터페이스입니다. 자바스크립트는 DOM 트리를 통해 DOM의 조작이 가능합니다. 스타일시트는 CSS 파서를 통해 스타일 규칙을 형성하며, 어테치먼트(Attachment)에 의해 DOM 트리와 합쳐져 렌더트리를 형성합니다. 형성된 렌더트리는 배치(Layout)와 그리기(Drawing) 과정을 통해 화면상에 표시됩니다.

mb-file.php?path=2022%2F02%2F23%2FF4591_1.png
 


2. 웹 컴포넌트(Web Component)란?


웹 컴포넌트는 웹 표준을 기반으로 개발자가 자체적인 HTML Element를 생성할 수 있도록 하는 클라이언트 사이드 컴포넌트입니다. 코드에서 원하는 부분을 캡슐화하여 재사용 가능한 HTML Element를 만들 수 있습니다. 아래 그림에서 보여주듯이 컴포넌트 기반 프로그래밍을 하면 이미 만들어진 컴포넌트를 조합하여 화면구성이 가능합니다.mb-file.php?path=2022%2F02%2F23%2FF4593_3.png

출처: https://hanamon.kr/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-component%EB%9E%80/


HTML Element는 운영체제와 웹브라우저에 따라 다르게 표시될 수 있습니다. 그러나 웹 컴포넌트를 통해 Custom Element를 만들면 다른 환경에서도 같은 화면이 나타나도록 할 수 있습니다. 아래 그림은 웹브라우저에 따른 Select Element를 나타냅니다.


mb-file.php?path=2022%2F02%2F23%2FF4595_4.png
 

한때는 jQuery를 통해 제한적으로 동적인 페이지를 구성했습니다. 스마트폰의 등장으로 SPA 방식의 어플리케이션 개발이 활성화되면서 AJAX 사용이 증가함에 따라 코드가 복잡해졌습니다. 이를 해결하기 위해 부트스트랩(Bootstrap)이나 리액트(React)같은 다양한 프레임워크가 등장했습니다. 하지만 프레임워크는 프로젝트 규모가 커질수록 어플리케이션이 무거워지고 리소스 사용을 클라이언트에게 전가합니다. 또한 프레임워크에 대한 의존성이 있어서 다른 프레임워크 사이의 호환성이 떨어집니다. 웹 컴포넌트는 웹표준을 통해 코딩하기때문에 호환성이 높은 웹 어플리케이션을 개발할 수 있습니다.



3. 웹 컴포넌트 구성


웹 컴포넌트는 Templates, Shadow DOM, HTML Imports, Custom Element로 구성됩니다.


(1) Template


Template는 Element를 재사용하고 모듈화 시키기 위해 사용하는 기술요소입니다.


자바스크립트를 통해 템플릿을 생성하면 다음과 같습니다.

<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="utf-8">

</head>

<body>

<div id="header"></div>

</body>

</html>


<script>

const header = document.getElementById('header');

const h1 = document.createElement('h1');


h1.innerText = "헤더";

header.appendChild(h1);

</script>


편하고 직관적이지만 코드가 길어지면 유지 및 보수가 힘들어집니다.


템플릿을 사용하면 다음과 같이 작성할 수 있습니다.

<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="utf-8">

</head>

<body>

<div id="header"></div>

</body>

<template id="template">

<h1>헤더</h1>

</template>

</html>


<script>

if ('content' in document.createElement('template')) {


const template = document.getElementById('template');

const clone = document.importNode(template, true);


document.getElementById('header').appendChild(clone.content);

} else {

console.log('None supports');

}

</script>


스크립트때문에 코드가 더 복잡해보이지만 중요한 부분은 녹색으로 표시한 template 요소입니다.

template 요소는 비활성화된 상태로 document-fragment로 캡슐화 되어있습니다. 이 상태에서는 화면에 나타나지 않으며, template 내의 어떠한 마크업이나 스크립트도 실행되지 않습니다. 

mb-file.php?path=2022%2F02%2F23%2FF4596_5.png 


이를 활성화 하기위해서는 imprtNode를 통해 template 요소의 content를 deep-copy 하여 특정 요소에 붙여넣어주면 됩니다.


(2) Custom Element


Custom Element는 HTML Element를 개발자가 직접 정의하여 사용할 수 있도록 제공되는 기능입니다. ShadowDom과 함께 사용하여 웹 컴포넌트의 재사용성을 늘릴 수 있도록 해줍니다. CustomElementRegistry 객체를 통해 등록하며, 내부에는 lifecycle이 존재합니다.


<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="utf-8">

</head>

<body>

<div-header></div-header>

</body>

</html>


<script>

class DivHeader extends HTMLElement {

// 생성자 함수

constructor() {

super();

this.setAttribute('title', '타이틀');

}

// Custom Element가 생성될때 호출

connectedCallback() {

this.render();

}


// 해당요소가 새로운 문서로 이동될때 호출

adoptCallback() { }


// 요소의 속성이 변경될때 호출

attributeChangedCallback(attrName, oldVal, newVal) {

this.render();

}


// attributeChangedCallback 함수에서 관찰할 항목을 리턴

static get observedAttributes() {

return ['title'];

}


// title 속성 리턴

get title() {

return this.getAttribute('title');

}


// Custom Element가 제거될때 호출

disconnectedCallback() {

console.log('div-header element removed.');

}


// 렌더링 메소드

render() {

this.innerHTML = `

<h1>${this.title}</h1>

`

}

}


window.customElements.define('div-header', DivHeader);

</script>


Custom Element는 'div-header' 처럼 임의의 태그네임을 생성할 수 있습니다. HTMLElement 클래스를 상속받으며, 태그네임은 '-'를 반드시 포함해야합니다.


lifecycle hook는 위에 주석에 표기해두었습니다.

만약 observedAttributes나 attributeChangedCallback이 정의되지 않으면 해당 요소의 지정된 속성이 변경되어도 텍스트가 변경되지 않습니다. (다시 말하면, render 메소드가 실행되지 않습니다.)


개발자 콘솔을 통해 해당 스크립트를 실행해보세요. 텍스트가 변경되는 것을 확인할 수 있습니다.

document.getElementsByTagName('div-header')[0].setAttribute('title','변경된 타이틀');

mb-file.php?path=2022%2F02%2F23%2FF4597_ezgif.com-gif-maker.gif
 

(3) Shadow DOM


ShadowDOM은 HTML과 CSS의 스코프를 다른 컴포넌트나 메인페이지와 분리하여 서로 충돌하지 않도록 독립된 DOM 트리를 만드는 기술요소입니다. 

mb-file.php?path=2022%2F02%2F23%2FF4598_6.png
 

위의 그림과 같이 Shadow Host가 생성되면 DOM 트리의  shadow root가 생성되고 그 아래에 독립적인 Shadow Tree를 형성합니다. 스코프가 분리되었기때문에 일반적으로 document.querySelector를 통해 Shadow DOM의 자식노드에 접근이 불가합니다.


스코프를 분리하는 기술은 이전에도 iframe이 있었습니다. 그러나 iframe은 HTTP 요청이 한차례 더 발생하여 불필요한 리소스 낭비가 발생하며 같은 도메인의 경우에만 접근이 가능합니다.


Shadow Tree는 attachShadow를 통해 생성가능합니다.


<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="utf-8">

<style>

h1 {color: red;}

</style>

</head>

<body>

<div-header></div-header>

</body>

</html>


<script>

class DivHeader extends HTMLElement {

// 생성자 함수

constructor() {

super();

this.attachShadow({ mode: 'open'}); 

this.setAttribute('title', '타이틀');

}

// Custom Element가 생성될때 호출

connectedCallback() {

this.render();

}


// 해당요소가 새로운 문서로 이동될때 호출

adoptCallback() { }


// 요소의 속성이 변경될때 호출

attributeChangedCallback(attrName, oldVal, newVal) {

this.render();

}


// attributeChangedCallback 함수에서 관찰할 항목을 리턴

static get observedAttributes() {

return ['title'];

}


// title 속성 리턴

get title() {

return this.getAttribute('title');

}


// Custom Element가 제거될때 호출

disconnectedCallback() {

console.log('div-header element removed.');

}


// 렌더링 메소드

render() {

this.shadowRoot.innerHTML = `

<h1>${this.title}</h1>

`

}

}


window.customElements.define('div-header', DivHeader);

</script>


아까 작성한 Custom Element에서 생성자 함수에 attachShadow를 통해 Shadow Tree를 생성했습니다.

메인페이지의 스타일시트를 보면 h1 요소의 색상은 빨간색으로 설정했습니다. Shadow Tree를 생성하지 않으면 div-header 요소 아래 자식노드인 h1 노드는 빨간색 텍스트가 출력될 것입니다.

mb-file.php?path=2022%2F02%2F23%2FF4599_7.png
 

Shadow Tree를 생성하면 div-header 요소는 shadow root가 되어 h1 스코프가 분리되므로 빨간색 텍스트가 아니라 검은색 텍스트가 됩니다. Shadow DOM의 자식노드는 shadowRoot를 통해 접근가능합니다. render 메소드를 확인하세요. 

mb-file.php?path=2022%2F02%2F23%2FF4600_8.png
 

개발자 도구를 통해 돔트리를 확인하면 div-header 요소의 자식노드가 shadow-root로 캡슐화되어 분리된 것을 확인할 수 있습니다.


Shadow DOM의 slot 요소에 별도 이름을 설정하여 정해진 위치에 오도록 만들수도 있습니다.

<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="utf-8">

<style>

h1 {color: red;}

</style>

</head>

<body>

<div-header>

<h1 slot="second">두번째 타이틀</h1>

<h2 slot="first">첫번째 타이틀</h2>

</div-header>

</body>

</html>


<script>

class DivHeader extends HTMLElement {

// 생성자 함수

constructor() {

super();

this.attachShadow({ mode: 'open'});

this.setAttribute('title', '타이틀');

}

// Custom Element가 생성될때 호출

connectedCallback() {

this.render();

}


// 해당요소가 새로운 문서로 이동될때 호출

adoptCallback() { }


// 요소의 속성이 변경될때 호출

attributeChangedCallback(attrName, oldVal, newVal) {

console.log(attrName);

this.render();

}


// attributeChangedCallback 함수에서 관찰할 항목을 리턴

static get observedAttributes() {

return ['title'];

}


// title 속성 리턴

get title() {

return this.getAttribute('title');

}


// Custom Element가 제거될때 호출

disconnectedCallback() {

console.log('div-header element removed.');

}


// 렌더링 메소드

render() {

this.shadowRoot.innerHTML = `

<style> h1{color: blue;} </style> 

<h1>${this.title}</h1>

<slot name="first"></slot>

<slot name="second"></slot>

`

}

}


window.customElements.define('div-header', DivHeader);

</script>

mb-file.php?path=2022%2F02%2F23%2FF4602_9.png

DOM에서 div-header 요소 아래에 h1, h2 요소가 자식노드로 존재합니다. 각각 slot 속성값이 "second", "first" 입니다. DOM Tree에 따르면 두번째 타이틀, 첫번째 타이틀 순서로 표시되어야하겠지만 div-header는 shadow-root이므로 Shadow Tree에 따라 타이틀, "first" 슬롯, "second" 슬롯 순서로 표시됩니다. 참고로 "second" 슬롯은 DOM에서 h1 요소에 해당하므로 DOM의 스타일시트에 따라 빨간색 텍스트가 표시되고, Shadow DOM의 h1 요소는 Shadow DOM의 스타일시트에 따라 파란색 텍스트가 표시됩니다.


(4) HTML Import


HTML Import는 HTML 기반 의존성을 해결해주는 기능입니다. 파이어폭스가 지원하지 않으면서 다른 브라우저들도 지원하지 않게 되어 현재는 ES Modules를 통해 의존성을 해결합니다.


#웹 컴포넌트# Custom Element# Shadow DOM# Template
댓글
자동등록방지
(자동등록방지 숫자를 입력해 주세요)