배경
DHTML 커서 추적 애니메이션과 “주간 사이트” 배지가 처음으로 웹을 우아하게 만든 이후로, 재사용 가능한 코드는 웹 개발자들을 유혹해왔습니다. 그때부터 제3자 UI를 웹사이트로 통합하는 것은 골치아픈 일이되었습니다.
다른 사람들의 똑똑한 코드를 사용하다보면 JavaScript 또는 !important
를 포함하는 공포스러운 CSS 보일러플레이트와 충돌하게됩니다. React 세계나 다른 최신 프레임워크에서는 나은편이지만, 위젯을 재사용하기 위해 전체 프레임워크를 알아야한다는 오버헤드는 조금 과한 요구입니다. HTML5는 웹 플랫폼에서 많이 필요로하는 일반적인 UI 위젯을 추가해주는 <video>
와 <input type="date">
와 같은 몇 가지 새로운 엘리먼트를 소개했습니다. 하지만 충분히 일반적인 모든 웹 UI 패턴에 대해 새로운 표준 엘리먼트를 추가하는 것은 지속 가능한 옵션이 아닙니다.
이에 대한 응답으로, 유용한 웹 표준의 초안이 작성되었습니다. 각 표준은 몇 가지 독립적인 기능들을 갖지만 함께 사용될 때, 이 전에 불가능 했으며 흉내 내기도 쉽지 않았던 것들(기존의 HTML 처럼 모든 위치에 놓일 수 있는 사용자 정의 HTML 엘리먼트를 생성하는 기능)을 가능하게 해줍니다. 이러한 엘리먼트들은 풍부한 폼 컨트롤 또는 비디오 플레이어와 마찬가지로 이를 사용하고있는 사이트로부터 그 내부의 복잡성을 숨길수도 있습니다.
발전하는 표준
그룹으로써 이 표준들은 웹 컴포넌트로 알려져있습니다. 2018년에서 보면, 웹 컴포넌트가 이미 옛날 이야기라고 생각될수도 있습니다. 실제로 이 표준의 초기 버전은 하나 이상의 형태로 2014년부터 크롬에 자리잡았으며, 폴리필은 다른 브라우저들간의 차이를 어설프게 채워주고 있습니다.
표준 위원회에서 품질에 대한 논의를 진행한 이후, 웹 컴포넌트 표준은 초기 형태인 0 버전으로부터 모든 주요 브라우저에서 그 구현을 확인할 수 있는 더 성숙한 1 버전으로 개선되었습니다. Firefox 63은 주요 표준 중, 커스텀 엘리먼트와 Shadow DOM에 대한 지원을 추가했으며, 저는 지금이 HTML 발명기를 어떻게 가지고 놀 수 있을지에 대해 더 살펴볼 시간이라고 생각합니다!
웹 컴포넌트가 있다고 가정하면 많은 여러 리소스들을 사용할 수 있습니다. 이 글은 다양한 새로운 기능과 리소스를 소개하는 입문서입니다. 좀 더 깊게 보시려면 MDN 웹 문서와 Google 개발자 사이트에서 웹 컴포넌트에 대해 더 읽어보시길 바랍니다.
여러분만의 동작하는 HTML 엘리먼트를 정의하려면 이전에 브라우저가 개발자에게 주지 않았던 새로운 능력이 필요합니다. 각 섹션에서는 이전에 불가능했던 것들을 포함해 이들에 의존하고 있는 새로운 웹 기술들에 대해 살펴볼 것입니다.
<template>
엘리먼트: 복습
이 첫 번째 엘리먼트는 웹 컴포넌트 작업보다 먼저 필요로 하는 것으로, 다른 엘리먼트들에 비해 새로운 것이 아닙니다. 때때로 일부 HTML 만을 저장해야 할 때가 있습니다. 일부 마크업은 여러번 복제해야 하는 것일수도 있고, 일부 UI는 아직 생성할 필요가 없는 것일수도 있습니다. <template>
엘리먼트는 파싱된 DOM을 현재 document에 추가하지 않고도 HTML을 받아 파싱합니다.
<template>
<h1>이건 표출되지 않습니다!</h1>
<script>alert("이 알림은 뜨지 않습니다!");</script>
</template>
파싱된 HTML은 document가 아니라면 어디로 갈까요? 이는 HTML document의 일부를 포함하는 가벼운 래퍼로 가장 잘 이해되는 “document fragment”에 추가됩니다. Document fragment는 다른 DOM에 추가될 때 해체되므로 유지할 필요가 없는 컨테이너에서 나중에 필요한 엘리먼트들을 유지할 때 유용합니다.
“네, 좋습니다. 이제 저에게는 해체될 컨테이너안에 DOM 일부가 있습니다. 이 코드가 필요할 때 어떻게 사용할 수 있죠?”
template의 document fragment를 현재 document로 간단히 삽입하면 됩니다.
let template = document.querySelector('template');
document.body.appendChild(template.content);
이는 여러분이 document fragment를 해체한 후가 아니라면 잘 동작합니다! 위 코드를 두 번 실행하면 두 번째 실행에서 template.content
가 사라졌기때문에 에러가 날 것입니다. 대신, fragment를 삽입하기 전에 사본을 만들겠습니다.
document.body.appendChild(template.content.cloneNode(true));
cloneNode
메소드는 그 이름처럼 동작하며, 노드 자체만을 복사할지 모든 자식을 포함해 복사할지를 지정하는 아규먼트를 갖습니다.
template 태그는 HTML 구조를 반복해야하는 모든 상황에서 이상적입니다. 특히 컴포넌트의 내부 구조를 정의할 때 편리하며 그 이유로 <template>
은 웹 컴포넌트 클럽으로 입성하게되었습니다.
새로운 능력
- HTML을 갖지만 현재 document에 추가하지 않는 엘리먼트.
주제 리뷰
- Document Fragments
cloneNode
를 사용해 DOM 노드 복제하기
커스텀 엘리먼트
커스텀 엘리먼트는 웹 컴포넌트 표준의 상징입니다. 이름에서 말해주듯 개발자들이 그들만의 커스텀 HTML 엘리먼트를 정의할 수 있게 해줍니다. 이를 가능하고 쾌적하게 해주는것은 v0 문법이 훨씬 더 번거로운 ES6의 클래스 구문에서 상당히 비중있게 구성됩니다. JavaScript 또는 다른 언어에서의 클래스에 익숙하시다면, 다른 클래스로부터 상속받거나 다른 클래스를 확장하는 클래스를 정의하실 수 있습니다.
class MyClass extends BaseClass {
// 여기에 클래스의 정의가 옵니다
}
그럼, 이걸로 한 번 해볼까요?
class MyElement extends HTMLElement {}
이는 최근까지도 에러였습니다. 브라우저는 내장 HTMLElement
클래스나 그 하위 클래스를 확장하는 것을 허용하지 않았습니다. 커스텀 엘리먼트는 이 제한을 해제합니다.
브라우저는 <p>
태그가 HTMLParagraphElement
클래스에 매핑된다는것을 알고 있지만, 커스텀 엘리먼트 클래스에 매핑할 태그는 어떻게 알 수 있을까요? 내장 클래스 확장뿐만 아니라 이제 매핑 선언을 위한 “커스텀 엘리먼트 레지스트리”도 있습니다.
customElements.define('my-element', MyElement);
이제 페이지의 모든 <my-element>
는 새로운 MyElement
인스턴스와 연결됩니다. MyElement
생성자는 브라우저가 <my-element>
태그를 파싱할때마다 실행될 것입니다.
태그명 내의 대시는 무엇일까요? 음, 표준 기관들은 앞으로도 새로운 HTML 태그를 생성할 자유를 원할 것이며, 이는 개발자들이 <h7>
또는 <vr>
같은 태그를 생성할 수 없음을 의미합니다. 미래에 발생할 충돌을 피하기 위해, 모든 커스텀 엘리먼트는 반드시 대시를 포함해야 하며, 표준 기관들은 대시를 포함하는 새로운 HTML 태그를 만들지 않을 것을 약속했습니다. 충돌은 방지되었습니다!
커스텀 엘리먼트가 생생될때마다 생성자가 호출되는것을 포함해, 다양한 상황에서 커스텀 엘리먼트를 통해 호출되는 부가적인 “lifecycle” 메소드도 많이 있습니다.
connectedCallback
은 document에 엘리먼트가 추가될 때 호출됩니다. 이는 한 번 이상 발생할 수 있습니다. 엘리먼트가 이동되거나 제거된 후 다시 추가되는 경우가 그 예입니다.disconnectedCallback
은connectedCallback
과 반대입니다.attributeChangeCallback
은 화이트리스트의 어트리뷰트가 엘리먼트에서 수정될 때 발생합니다.
좀 더 풍부한 예제는 다음과 같습니다.
class GreetingElement extends HTMLElement {
constructor() {
super();
this._name = 'Stranger';
}
connectedCallback() {
this.addEventListener('click', e => alert(`Hello, ${this._name}!`));
}
attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName === 'name') {
if (newValue) {
this._name = newValue;
} else {
this._name = 'Stranger';
}
}
}
}
GreetingElement.observedAttributes = ['name'];
customElements.define('hey-there', GreetingElement);
페이지에서의 사용은 다음과 같습니다.
<hey-there>Greeting</hey-there>
<hey-there name="Potch">Personalized Greeting</hey-there>
그런데, 존재하는 HTML 엘리먼트를 확장하고 싶을땐 어떻게 해야할까요? 물론 가능합니다. 하지만 마크업 내에서의 사용은 상당히 다릅니다. 우리의 greeting을 버튼으로 만들고 싶다고 해봅시다.
class GreetingElement extends HTMLButtonElement
레지스트리에게 존재하는 태그를 확장한다고도 얘기해야 할 것입니다.
customElements.define('hey-there', GreetingElement, { extends: 'button' });
왜냐하면 우리는 존재하는 태그를 확장하고 있으며, 실제로 존재하는 태그를 우리의 커스텀 태그명을 대신해 사용하기 때문입니다. 새로운 특별 어트리뷰트인 is
를 사용해 브라우저에게 어떤 종류의 버튼을 사용하는지 알려줍니다.
<button is="hey-there" name="World">Howdy</button>
처음에는 좀 투박해보일 수 있습니다. 하지만 보조 기술들과 다른 스크립트들은 이 특별한 마크업이 없이는 우리의 커스텀 엘리먼트가 어떤 종류의 버튼인지 알지 못합니다.
이제부터 기존의 모든 웹 위젯 기술이 적용됩니다. 여러 이벤트 핸들러를 설정하고, 커스텀 스타일링을 추가할 수 있으며 <template>
을 사용해 내부 구조를 찍어낼 수도 있습니다. 사람들은 자신들의 코드와 함께 HTML 템플릿, DOM 호출, 심지어 아주 새로운 프레임워크를 통해 여러분의 커스텀 엘리먼트를 사용할 수 있으며, 그 중 일부는 그들의 가상 DOM 구현에서 커스텀 태그명을 지원합니다. 인터페이스가 표준 DOM 인터페이스이므로, 커스텀 엘리먼트를 사용해 진정으로 이동성을 갖춘 위젯을 구현할 수 있습니다.
새로운 능력
- 내장 ‘HTMLElement’ 클래스와 그 하위 클래스를 확장할 수 있는 능력
customElements.define()
을 통해 사용가능한 커스텀 엘리먼트 레지스트리- 엘리먼트 생성, DOM으로의 삽입, 어트리뷰트 변경 등을 위한 특별 lifecycle 콜백들
주제 리뷰
Shadow DOM
우리는 사용하기 쉬운 커스텀 엘리먼트를 만들었으며, 아주 세련된 스타일링을 할 수도 있습니다. 이것을 우리의 모든 사이트에서 사용하고, 다른 사람들과 공유해 그들도 사용할 수 있게 하려고합니다. 커스터마이즈된 <button>
엘리먼트가 다른 사이트의 CSS를 먼저 마주하게 될 때 발생하는 충돌에 대한 악몽을 어떻게 방지할 수 있을까요? Shadow DOM은 그에 대한 해결책을 제공합니다.
Shadow DOM 표준은 shadow root 개념을 소개합니다. 표면적으로 shadow root는 표준 DOM 메소드를 가지며 다른 DOM 노드와 마찬가지로 추가될 수 있습니다. Shadow root는 부모 노드를 포함하는 document에 그들의 컨텐츠를 나타내지 않는다는 점에서 빛납니다.
// attachShadow는 shadow root를 생성합니다.
let shadow = div.attachShadow({ mode: 'open' });
let inner = document.createElement('b');
inner.appendChild(document.createTextNode('그림자에 숨김'));
// shadow root는 appendChild 메소드를 지원합니다.
shadow.appendChild(inner);
div.querySelector('b'); // empty
위 예시에서, <div>
는 <b>
를 “포함하며” <b>
는 페이지에 렌더링되지만, 기존의 DOM 메소드로는 확인할 수 없습니다. 뿐만 아니라 이를 포함하고 있는 페이지의 스타일에서도 확인할 수 없습니다. 이는 shadow root 외부의 스타일은 내부로 넣을 수 없으며, shadow root 내부의 스타일은 밖으로 새어 나오지 않음을 의미합니다. 이러한 경계는 페이지의 다른 스크립트에서 shadow root의 생성을 감지할 수 있으며 shadow root에 대한 참조가 있을 경우 그 컨텐츠를 직접 쿼리 할 수도 있다는 점에서 보안 기능을 의미하지는 않습니다.
shadow root의 컨텐츠는 root에 <style>
(또는 <link>
)을 추가하여 스타일링됩니다.
let style = document.createElement('style');
style.innerText = 'b { font-weight: bolder; color: red; }';
shadowRoot.appendChild(style);
let inner = document.createElement('b');
inner.innerHTML = "I'm bolder in the shadows";
shadowRoot.appendChild(inner);
휴, 이제 정말 <template>
을 바로 사용할 수 있습니다! 어느쪽이든 <b>
는 root내의 스타일시트에 의해 영향을 받지만 <b>
태그에 매칭되는 다른 외부 스타일은 영향을 받지 않습니다.
커스텀 엘리먼트에 shadow가 아닌 컨텐츠가 있으면 어떻게 해야 하나요? <slot>
이라는 새로운 특별 엘리먼트를 사용해 함께 잘 동작하도록 만들 수 있습니다.
<template>
Hello, <slot></slot>!
</template>
템플릿이 shadow root에 추가되면
<hey-there>World</hey-there>
위 마크업은 이렇게 렌더링 됩니다.
Hello, World!
shadow root를 shadow가 아닌 컨텐츠와 합성할 수 있는 이 능력은 외부 환경에서 단순해보이나 복잡한 내부 구조를 갖는 풍부한 커스텀 엘리먼트를 사용할 수 있게해줍니다. 여러개의 slot, 명명된 slot, slot 컨텐츠를 대상으로하는 특별한 CSS 수도 클래스를 사용해, Slot은 여기에서 보여드린 것 보다 더 강력합니다. 좀 더 배워보시길 바랍니다!
새로운 능력
- “shadow root”라는 덜 잘 알려진 DOM 구조
- shadow root를 생성하고 접근하기 위한 DOM API들
- shadow root로 범위가 제한된 스타일
- shadow root와 범위가 제한된 스타일에서의 동작을 위한 새로운 CSS 수도 클래스
<slot>
엘리먼트
함께 모아 보기
멋진 버튼을 만들어봅시다! 우리는 창의력을 발휘하여 <fancy-button>
엘리먼트를 호출할 것입니다. 무엇이 버튼을 멋지게 할까요? 커스텀 스타일을 갖게 될 것이고, 아이콘을 제공할 수 있게 해줄 것이며 아주 멋있어 보이도록 만들것입니다. 버튼을 사용하는 사이트가 무엇이든 상관 없이 버튼의 스타일을 멋지게 유지하기 위해 shadow root내에 스타일을 캡슐화할 것입니다.
아래의 인터렉티브한 예제에서 완성된 커스텀 엘리먼트를 확인하실 수 있습니다. 커스텀 엘리먼트의 스타일 및 엘리먼트의 구조를 위한 HTML <template>
과 JS 정의 모두를 확인하시기 바랍니다.
결론
웹 컴포넌트를 구성하는 표준은 여러 하위 레벨 기능을 제공함으로써 사람들이 스펙이 쓰여진 시점에 누구도 예상하지 못한 방법으로 이를 합성할 것이라는 철학을 기반으로합니다. 커스텀 엘리먼트는 좀 더 쉽게 웹에서 VR 컨텐츠를 제작하고 여러 UI 툴킷을 생성하는 등의 작업을 위해 이미 사용되고 있습니다. 오랜 표준화 과정에도 불구하고 웹 컴포넌트의 새로운 약속은 제작자들에게 더 많은 능력을 제공합니다. 이제 이 기술은 브라우저에서 사용 가능하며 웹 컴포넌트의 미래는 여러분의 손에 달려있습니다. 무엇을 구축하시겠습니까?
이 글은 Potch의 The Power of Web Components의 한국어 번역입니다.
작성자: Seul Gi Choi
Open Source // Web // Javascript // Map engineer
댓글이 없습니다.