Web Component 표준의 현재 상황

웹 컴포넌트(Web Component)가 개발자들에게 알려진지후 지금까지 꽤 오랜 시간이 지났습니다. 웹 컴포넌트는 Alex RussellFronteers Conference 2011에서 처음 발표하면서 알려졌습니다. 웹 컴포넌트의 개념은 커뮤니티를 흔들었고 뒤이어 많은 이야기와 토론이 이어졌습니다.

구글은 2013년 웹 컴포넌트 기반의 폴리머(Polymer) 프레임워크를 출시해서 새로운 웹 컴포넌트 API를 제시했습니다. 구글은 커뮤니티의 피드백을 수용하며 폴리머 프레임워크를 개선하고 있습니다.

4년이 지난 지금, 웹 컴포넌트는 마땅히 널리 사용되고 있어야 합니다. 하지만 현실에서는 크롬만이 웹 컴포넌트를 ‘일부 버전’이나마 지원하는 유일한 브라우저입니다. 폴리필(polyfill)이 있어도 개발자들은 주요 브라우저들이 웹 컴포넌트를 지원하기 전에는 웹 컴포넌트를 사용하려하지 않을 것입니다.

왜 이렇게 오래 걸리는 걸까요?

긴 이야기를 짧게 요약하자면, 브라우저 벤더들이 동의하지 않기 때문입니다.

웹 컴포넌트는 구글의 작품이었습니다. 처음 릴리즈되었을 때 다른 브라우저 벤더들과의 협의가 부족했습니다. 현실 세계의 대부분 협상이 그렇듯, 초대 받지 못했다고 느끼는 당사자들은 열정을 갖기 힘들며 합의에 이르기도 힘듭니다.

초기 웹 컴포넌트는 모호한 제안이었습니다. (비록 합당한 이유가 있었지만) 초기 API는 하이레벨 관점에서 정의되어 구현하기 어려웠습니다. 이로인해 브라우저 벤더들간에 논쟁과 의견 충돌이 많았습니다.

구글은 계속 노력했고, 사용자 피드백을 구했고, 커뮤니티를 설득했습니다. 하지만 다른 브라우저 벤더들의 참여 없이는 사용성이 제한된다는 사실을 늦게 깨달았습니다.

폴리필은 이론적으로 웹 컴포넌트를 지원하지 않는 브라우저에서도 웹 컴포넌트를 사용할 수 있게 하는 기술입니다. 하지만 이정도로는 ‘상용 개발에 적합’하지 않습니다.

이런 사정 외에도, 마이크로소프트는 (완료를 앞두고 있는) 엣지(Edge) 브라우저 개발 때문에 새로운 DOM API를 추가할 수 없는 상황이었고, 애플(Apple)사파리 브라우저에 다른 웹 컴포넌트 대안 기술을 추가하는데 집중하고 있었습니다.

Custom Element

웹 컴포넌트를 구성하는 기술들 중에서, Custom Element는 별다른 논란이 없는 기술입니다. UI 부품이 보여지고 동작하는 방식을 정의해서, 정의된 UI 부품을 브라우저들 사이에서 또는 프레임워크들 사이에서 공유하는 것의 가치에 모두가 공감하고 있습니다.

‘업그레이드(Upgrade)’

‘업그레이드’란 어떤 엘리먼트가 평범한 HTMLElement에서 새롭게 정의된 라이프사이클과 새롭게 정의된 prototype을 갖는 빛나는 Custom Element로 변화하는 것을 뜻합니다. 현재 스펙에 따르면 엘리먼트가 업그레이드될 때 해당 엘리먼트에 정의된 createdCallback이 호출됩니다.

var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() { ... };
document.registerElement('x-foo', { prototype: proto });

‘업그레이드’에 관해서 브라우저 벤더들이 내놓은 5개의 제안이 있습니다. 그중 2개의 제안이 가장 유력합니다.

‘Dmitry’

createdCallback 패턴이 진화된 버전으로 ES6의 class와 잘 어울립니다. createdCallback 개념을 사용하지만, 좀더 일반적인 형태의 sub-classing 방식을 사용합니다.

class MyEl extends HTMLElement {
  createdCallback() { ... }
}

document.registerElement("my-el", MyEl);

현재 브라우저에 구현되어 있는 방식에 따르면, Custom Element는 HTMLUnknownElement로 생성되어 존재하기 시작합니다. 그리고 시간이 지난뒤 Custome Element의 prototype이 개발자가 등록한 prototype으로 교체(또는 ‘swizzled’)되고 createdCallback이 호출됩니다.

이 방식의 단점은 플랫폼 본연의 동작 양식과 다르다는 것입니다. 엘리먼트들이 처음에는 ‘unknown’이었다가 어느 순간 최종 형태로 변환(transform)되는 것에서 개발자들이 혼란을 느낄 수 있습니다.

Synchronous constructor

개발자에 의해 등록된 생성자가 Custom Element가 생성되고 트리에 추가되는 시점에 파서에 의해 호출됩니다.

class MyEl extends HTMLElement {
  constructor() { ... }
}

document.registerElement("my-el", MyEl);

합리적인 측면이 있지만, 만약 Custom Element의 registerElement 코드를 담고 있는 스크립트 문서가 비동기적으로 뒤늦게 로드되는 상황이라면, 맨처음 다운로드된 문서 안에 있는 Custom Element는 업그레이드에 실패하게 됩니다. 이는 ES6가 약속하는 비동기 모듈 세상으로 나아가는데 도움이 되지 않습니다.

덧붙여 Synchronous Constructor에는 .cloneNode()에 관한 플랫폼 이슈가 딸려 있습니다.

여기에 대한 결정은 벤더들이 2015년 7월에 가질 대면 미팅(face-to-face meeting) 자리에서 내려질 예정입니다.

is=””

is 속성은 Custom Element가 일반적인 빌트인(built-in) 엘리먼트 위에서 동작할 수 있는 층(layer)을 제공해줍니다.

<input type="text" is="my-text-input">

찬성의견

  1. 어떤 엘리먼트의 빌트인(built-in) 기본 기능을 확장할 수 있게 허용합니다 (eg. Accessibility 관련 특성, <form> 컨트롤, <template>).
  2. 어떤 엘리먼트를 ‘점진적으로 개선’할 수 있는 수단을 제공합니다. 그리고 JavaScript 없이도 개선된 기능을 사용할 수 있게 합니다.

반대의견

  1. 문법이 혼란스럽습니다.
  2. 우리가 만드는 플랫폼이 Accessibility 수단을 충분히 제공하고 있지 않다는 내재된 문제를 회피하게 만듭니다.
  3. 빌트인 엘리먼트를 확장할 적당한 방법이 없다는 내재된 문제를 회피하게 만듭니다.
  4. 유즈케이스가 제한적입니다. 개발자가 Shadow DOM을 도입하는 순간, 모든 빌트인 Accessibility 수단을 사용할 수 없게 됩니다.

공감대

많은 사람들이 is가 Custom Element 스펙의 ‘군더더기’라고 생각하고 있습니다. 구글은 이미 is를 구현했지만 이를 저수준 수단(lower-level primitive)들이 제공될 때까지의 임시방편이라고 생각하고 있습니다. 지금 현재 모질라(Mozilla)애플은 Custom Elements V1을 먼저 출시하고 관련 문제는 V2에서 해결함으로써 ‘군더더기’로 플랫폼을 더럽히는 것을 피하려 하고 있습니다.

Custom Element로 HTML 새로 만들기는 Domenic Denicola가 시작한 프로젝트로 브라우저에 내장된 HTML 엘리먼트들을 Custom Element로 재구축하려는 시도입니다. 이를 통해 플랫폼이 놓치고 있는 DOM 재료가 무엇인지 밝혀내는 것을 목표로합니다.

Shadow DOM

Shadow DOM은 브라우저 벤더들 사이에서 가장 격렬한 논쟁 대상이었습니다. 논쟁이 심했기 때문에 합의에 빨리 도달하기 위해 기능을 ‘V1’과 ‘V2’ 2단계로 나누어야 했습니다.

배포(Distribution)

배포(distribution)는 shadow host의 자손들을 Shadow DOM 안에 있는 slot에 ‘투영시켜(project)’ 보여지게 만드는 절차입니다. 이것은 컴포넌트 사용자가 저장한 컨텐츠를 컴포넌트 개발자가 컴포넌트 안에서 이용할 수 있게 만드는 기능입니다.

현재의 API

현재의 API는 완전히 선언문적(declarative)인 API입니다. 컴포넌트 개발자는 Shadow DOM 안에서 host의 자손들을 보여지게 할 위치를 지정하기 위해 <content>라는 특별한 엘리먼트를 사용합니다.

<content select="header"></content>

애플마이크로소프트가 복잡성과 성능에 대한 염려 때문에 이 방식을 지지하고 있습니다.

새로운 명령문적(imperative) API

아쉽게도 대면 미팅 자리에서 선언문적 API에 대한 완전한 합의가 이루어지지 못했습니다. 그래서 브라우저 벤더들은 명령문적(imperative) 해법을 마련하는데 동의했습니다.

4개 브라우저 벤더 (마이크로소프트, 구글, 애플, 모질라) 모두가 새로운 API 스펙의 결정 시한을 2015년 7월로 설정했습니다. 지금까지 3개의 제안이 제시되었습니다. 3개의 제안을 가장 간단 형태로 표현하면 다음과 같습니다.

var shadow = host.createShadowRoot({
  distribute: function(nodes) {
    var slot = shadow.querySelector('content');
    for (var i = 0; i < nodes.length; i++) {
      slot.add(nodes[i]);
    }
  }
});

shadow.innerHTML = '<content></content>';

// Call initially ...
shadow.distribute();

// then hook up to MutationObserver

가장 큰 장애물은 타이밍입니다. 만약 host 자손들이 변경될 때 우리가 MutationObserver 콜백에서 재배포(redistribute)를 하는 상황이라면, 레이아웃 속성에 대한 요청이 잘못된 결과를 반환할 수 있습니다.

myHost.appendChild(someElement);
someElement.offsetTop; //=> old value

// distribute on mutation observer callback (async)

someElement.offsetTop; //=> new value

offsetTop을 호출하면 비동기 레이아웃 처리가 배포(distribution) 처리보다 먼저 일어날 것입니다!

이것이 별일 아니라고 생각할 지도 모릅니다. 하지만 스크립트와 브라우저 내부 모듈들은 종종 offsetTop 값에 크게 의지합니다 (예를 들면 view 안의 엘리먼트들을 스크롤시킬 때). 그래서 offsetTop 값은 정확해야 합니다.

만약 이 문제를 해결할 수 없다면 우리는 아마도 선언문적인 API에 대한 토론으로 다시 복귀할지도 모릅니다. 아마도 현재의 <content select> 스타일이거나 새롭게 제안된 ‘named slots’ API (애플이 제안함) 스타일 중 하나가 될 것입니다.

새로운 선언문적 API – ‘Named Slots’

‘named slots’은 현재의 ‘content select’ API보다 간단한 형태의 API입니다. 컴포넌트 사용자는 반드시 명시적으로 자신의 컨텐츠에 배포될 slot을 표시(label)해야 합니다.

<x-page>의 Shadow Root

<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
<div>some shadow content</div>

<x-page>의 사용

<x-page>
  <header slot="header">header</header>
  <footer slot="footer">footer</footer>
  <h1>my page title</h1>
  <p>my page content<p>
</x-page>

합성된/렌더링된 트리 (사용자가 보게되는 것)

<x-page>
  <header slot="header">header</header>
  <h1>my page title</h1>
  <p>my page content<p>
  <footer slot="footer">footer</footer>
  <div>some shadow content</div>
</x-page>

브라우저는 shadow host (myXPage.children)의 바로 아래 자손들을 보고 그들중 어느 하나라도 host의 shadowRoot 안에 있는 <slot> 엘리먼트 이름과 같은 slot 속성을 갖고 있는지 확인합니다.

맞는 것이 발견되면, 해당 노드는 해당 <slot> 엘리먼트의 자리에 시각적으로 ‘배포(distribute)’ 됩니다. 이 검색 과정이 종료될 때까지 배포되지 않고 남은 자손들은 (만약 존재한다면) 디폴트 (unamed) <slot> 엘리먼트로 배포됩니다.

찬성의견
  1. 배포 방식이 좀더 명시적이고 이해하기 쉽습니다. 덜 ‘신비’합니다.
  2. 배포 방식이 엔진 입장에서 더 계산하기 수월합니다(간단합니다).
반대의견
  1. <select> 같은 빌트인 엘리먼트가 어떻게 동작하는지 설명하지 않습니다.
  2. slot 속성으로 컨텐트를 장식하기 위해 사용자가 더 많은 작업을 해야 합니다.
  3. 표현성이 떨어집니다.

‘closed’ vs. ‘open’

shadowRoot가 ‘closed’이면 myHost.shadowRoot로 접근할 수 없습니다. 이 덕분에 컴포넌트 개발자는 컴포넌트 사용자가 세세한 구현 내용을 뽑아내는 것에 대해 약간 안심할 수 있습니다. 마치 클로저를 이용해서 private 속성을 만드는 것과 비슷한 경우입니다.

애플은 이것이 반드시 보호해야 하는 중요한 기능이라고 생각했습니다. 애플은 세세한 구현 내용을 절대 외부에 노출되지 않게 보호되어야 한다고 주장했습니다. 그래서 ‘isolated’ custom elements를 만들 때 ‘closed’ 모드는 필수 기능이어야 한다고 주장했습니다.

반면 구글은 ‘closed’ shadow root가 접근성을 훼손할 뿐 아니라 컴포넌트의 유즈케이스를 제한한다고 생각했습니다. 구글은 shadowRoot를 우연히 엉망으로 만드는 것은 불가능하며 만약 shadowRoot에 대한 변경 요구가 있다면 거기엔 충분한 이유가 있을 거라고 주장했습니다. JS/DOM이 공개되는 것처럼 shadow root도 같은 방식으로 취급되면 좋겠다는 주장입니다.

4월 미팅에서 분명해진 것이 있습니다. 전진해야 한다는 것입니다. ‘mode’는 기능으로 제공되어야 합니다. 하지만 브라우저 벤더들은 이것의 디폴트 값을 ‘open’으로 해야 하는지 ‘closed’로 해야 하는지 합의하기 어려웠습니다. 결과적으로 V1에서는 ‘mode’를 필수 파라메터로 정하되 디폴트 값은 두지 않기로 합의했습니다.

element.createShadowRoot({ mode: 'open' });
element.createShadowRoot({ mode: 'closed' });

Shadow piercing combinator

‘piercing combinator’는 특별한 CSS ‘combinator’로 shadow root 외부에서 shadow root 내부에 있는 엘리먼트를 타겟으로 지정할 수 있습니다. /deep/ 이 그 사례이며 나중에 >>>로 개명되었습니다.

.foo >>> div { color: red }

웹 컴포넌트를 처음 스펙으로 정할 때에는 Shadow piercing combinator가 필요할 것이라고 생각했습니다. 하지만 이들이 어떻게 사용되는지 지켜본 결과 문제점만 드러났습니다. Shadow piercing combinator로 인해 웹 컴포넌트의 큰 장점인 스타일 격리 기능이 쉽게 허물어지곤 했습니다.

성능

Shadow DOM 내부에서 엔진이 어떤 외부의 셀렉터나 상태도 고려할 필요가 없는 상황 아래서는 스타일 연산을 믿을 수 없을 정도로 빠르게 수행할 수 있습니다. 바로 piercing combinator 때문에 이런 류의 최적화가 불가능해집니다.

대안

Shadow piercing combinator를 폐기한다는 것이 사용자가 외부에서 컴포넌트의 외관을 바꿀 수 없다는 것을 의미하지는 않습니다.

CSS custom-properties (variables)

Firefox OS에서 우리는 CSS Custom Properties를 사용해서 특정 스타일 속성을 노출했습니다. 그래서 외부에서 해당 스타일 속성을 지정하거나 오버라이드시킬 수 있었습니다.

외부 (컴포넌트 사용자):

x-foo { --x-foo-border-radius: 10px; }

내부 (컴포넌트 개발자):

.internal-part { border-radius: var(--x-foo-border-radius, 0); }
Custom pseudo-elements

우리는 또 몇몇 브라우저 벤더들이 custom pseudo selector 정의 기능의 재도입에 관심을 표하는 것을 보았습니다. 이를 통해 주어진 내부 요소들의 스타일을 정의할 수 있습니다 (현재 우리가 <input type=”range”> 구성요소들의 스타일을 정의하는 방식과 유사합니다.).


x-foo::my-internal-part { ... }

이는 Shadow DOM V2 스펙의 일부로 고려될 것입니다.

Mixins – @extend

여기 SASS의 @extend 행동양식을 CSS에 도입하기 위해 제안된 스펙이 있습니다. 이 스펙은 컴포넌트 개발자가 컴포넌트 사용자에게 속성(property)들을 위한 ‘bag(가방)’을 제공하려 할 때 유용한 도구가 될 것입니다. 컴포넌트 사용자는 속성 ‘bag’을 통해 컴포넌트 내부에 있는 특정 요소들을 조작할 수 있습니다.

외부 (컴포넌트 사용자):


.x-foo-part {
  background-color: red;
  border-radius: 4px;
}

내부 (컴포넌트 개발자):


.internal-part {
  @extend .x-foo-part;
}

여러개의 shadow root

동일한 엘리먼트 위에 한개 이상의 shadow root를 만드는 이유가 뭔가요?라고 질문하는 사람들이 있습니다. 답은: 상속(inheritance) 때문입니다.

어떤 <x-dialog> 컴포넌트를 개발하는 상황을 생각해봅시다. 모든 마크업, 스타일, 그리고 다이얼로그 윈도를 열고 닫는 로직 등을 이 컴포넌트 내부에 작성합니다.

<x-dialog>
  <h1>My title</h1>
  <p>Some details</p>
  <button>Cancel</button>
  <button>OK</button>
</x-dialog>

shadow root는 사용자가 div.inner 안에 제공한 컨텐츠를 <content>태그가 삽입된 지점으로 가져옵니다.

<div class="outer">
  <div class="inner">
  <content></content>
  </div>
</div>

이제 <x-dialog-alert>을 만들고 싶어졌습니다. 이것은 <x-dialog>와 똑같이 보여지고 동작하지만 좀 더 제한된 API만 가지고 있는 컴포넌트입니다. 예를 들면 alert('foo') 같은 API를 생각하고 있습니다.

<x-dialog-alert>foo</x-dialog-alert>
var proto = Object.create(XDialog.prototype);

proto.createdCallback = function() {
  XDialog.prototype.createdCallback.call(this);
  this.createShadowRoot();
  this.shadowRoot.innerHTML = templateString;
};

document.registerElement('x-dialog-alert', { prototype: proto });

새로운 컴포넌트는 자기 자신의 shadow root를 가질 것입니다. 하지만 이 컴포넌트는 부모 클래스의 shadow root 위에서 동작하도록 설계되어 있습니다. <shadow>는 ‘이전의(older)’ shadow root를 의미합니다. 우리는 그 안에 컨텐츠를 투영(project)할 수 있습니다.

<shadow>
  <h1>Alert</h1>
  <content></content>
  <button>OK</button>
</shadow>

일단 여러개의 shadow root 라는 개념에 익숙해지면, 이 개념의 강력함을 알게 될 것입니다. 단점은 이 개념으로 인해 복잡도가 아주 많이 증가하고 고려해야 할 예외 상황(edge cases)도 늘어난다는 점입니다.

multiple shadow 없는 상속

여러개의 shadow root 없이도 상속은 여전히 가능합니다. 하지만 그러려면 부모 클래스의 shadow root를 직접 수정해야 합니다.


var proto = Object.create(XDialog.prototype);

proto.createdCallback = function() {
  XDialog.prototype.createdCallback.call(this);
  var inner = this.shadowRoot.querySelector('.inner');

  var h1 = document.createElement('h1');
  h1.textContent = 'Alert';
  inner.insertBefore(h1, inner.children[0]);

  var button = document.createElement('button');
  button.textContent = 'OK';
  inner.appendChild(button);

  ...
};

document.registerElement('x-dialog-alert', { prototype: proto });

이 방식의 단점은 다음과 같습니다.

  1. 아름답지 못합니다.
  2. 당신의 상속 컴포넌트가 부모 컴포넌트의 세세한 구현 방식에 의존적이게 됩니다.
  3. 부모 컴포넌트의 shadow root가 ‘closed’ 라면 이 방식을 사용할 수 없습니다. this.shadowRootundefined일 것이기 때문입니다.

HTML Import

HTML Import는 하나의 .html 문서에 정의된 모든 재료(asset)을 다른 스코프(scope)로 불러오는 수단을 제공합니다.

<link rel="import" href="/path/to/imports/stuff.html">

일전에 이야기한 바와 같이, 모질라현재로서는 HTML Import를 구현할 계획이 없습니다. 이는 우선 모질라가 외부에 있는 재료를 가져오는 또다른 방법을 내놓기 전에 ES6의 module이 정립되는 양상을 지켜보고 싶기 때문입니다. 그리고 모질라는 HTML Import가 절실히 필요하다고 생각하고 있지 않습니다.

우리는 Firefox OS에서 일년 넘도록 웹 컴포넌트를 구현해왔습니다. 그 과정에서 기존의 모듈 문법(AMD 또는 Common JS)를 이용해서 의존성 트리(dependency tree) 문제를 해결하고, 엘리먼트를 등록하고, 일반적인 <script> 태그로 로드 하는 것 만으로도 많은 일을 할 수 있다는 것을 알게 됐습니다.

HTML Import 자체는 보다 간결하고 보다 선언문적인 워크플로우에 잘 맞습니다. 오래된 <element>폴리머(Polymer)의 현재 등록(registration) 문법처럼 말이죠.

이런 간결함에도 불구하고 Import가 의존성 관리 솔루션으로써 필요한 조작성을 충분히 제공하지 못한다는 비판이 커뮤니티로부터 제기되었습니다.

결론을 내리기 몇 달 전에, 모질라는 플랙(flag)을 설정하면 동작하는 방식으로 HTML Import를 구현했지만 불완전한 스펙 때문에 고생했습니다.

HTML Import에 어떤 일이 일어날까요?

애플Isolated Custom Elements 제안은 자체적인 도큐먼트 스코프를 갖는 Custom Element를 제공하기 위해 HTML Import 스타일의 접근 방식을 이용합니다. 아마도 거기에 HTML Import의 미래가 있을 것 같습니다.

모질라는 Custom Element를 불러오는 방식과 앞으로 다가올 ES6 module API 방식을 조화롭게 맞추는 방법을 탐구하려고 합니다. 우리는 HTML Import가 지금은 불가능한 무엇인가를 해결해준다고 밝혀질 경우/그때 HTML Import를 구현할 것입니다.

마치며

웹 컴포넌트는 요즘의 브라우저에 거대한 기능을 구현하는 것이 얼마나 어려운지 보여주는 좋은 예입니다. 추가된 모든 API는 영원히 존재합니다. 그래서 다음에 출현하는 API의 걸림돌로 남습니다.

비유하자면 엉켜있는 커다란 끈뭉치를 헤쳐서, 끈을 더 길게 더한 다음, 다시 뭉쳐 놓는 것과 같습니다. 이 끈뭉치, 즉 우리 플랫폼은, 점점 더 커지고 점점 더 복잡해집니다.

웹 컴포넌트는 3년 넘게 계획 단계에 머물러 있습니다. 하지만 우리는 낙관적입니다. 마무리가 멀지 않았습니다. 모든 주요 브라우저 벤더들이 참여하고 있으며, 태도 또한 적극적이고, 남은 문제들을 해결하기 위해 아주 많은 시간을 투자하고 있습니다.

이제 웹을 컴포넌트화 할 준비를 합시다!

그밖에

이 글은 이 쓴 The state of Web Components의 한국어 번역본입니다.

작성자: ingeeKim

"누구에게나 평등하고 자유로운 웹"에 공감하는 직장인.

ingeeKim가 작성한 문서들…


댓글이 없습니다.

댓글 쓰기