티스토리 뷰

분류없음

Web Components 의 현 주소 (1)

한장현 2016.07.15 13:35

다음 글의 번역입니다. [The state of Web Components - Wilson Page]

If writer does not want this article, please contact me.(han41858@gmail.com)


 개발자들은 요즘 Web Components 에 대한 소식을 자주 접한다. Fronteers Conference 2011 에서 Alex Russel 가 처음 Web Components를 소개했다. 이 때 발표한 내용은 커뮤니티를 뒤흔들었고 미래 기술에 대해 이야기할 때 빠지지 않는 중요한 주제의 하나가 되었다.

 2013년에 Google에서는 Web Component 기반의 프레임워크인 Polymer를 발표했고, 기존의 불편함을 해소하는 새로운 API를 도입하면서 커뮤니티의 피드백을 받아 문법 설탕(역주 : sugar, syntax sugar를 의미하며 같은 로직을 간단하게 구현할 수 있는 문법) 을 추가하고 의견들을 받고 있다.

 4년이 지난 현재, Web Components는 많은 곳에 존재하고 있지만, 브라우저에서 Web Components의 '어떤 버전' 을 명시하고 있는 것은 Chrome 뿐이다. 주류 브라우저들이 Web Components를 관심있어 하기 전까지는 커뮤니티들의 환영을 받지는 못한 것 같다.


왜 이렇게 오래 걸렸나?

 간단하게 말해서, 벤더들이 동의하지 않았다.

 Web Components는 Google과 다른 브라우저들의 협상에 의한 결과물이다. 다른 협상과 마찬가지로, 참여자들의 열정이 없다면, 동의를 이끌어 낼 수 없을 것이다.

 Web Components는 어마어마한 제안이었다. 초기 API는 구현하기 복잡했고, 벤더들의 논쟁과 반대만이 있었다.

 그래도 Google은 계속해서 이것을 추진했고 피드백을 받으면서 커뮤니티들을 인수하기도 했지만, 다른 벤더들이 도입하기 전까지는 사용성에 문제가 있었다.

 Polyfills을 이용하면 Web Components를 도입하지 않은 브라우저에서도 Web Components를 동작하게 했지만 '완성품' 정도는 아니었다.

 이런 와중에, Microsoft는 Edge에서 동작하는 새로운 DOM API들을 추가하고 있었고, Apple도 Safari를 위한 것들에 집중하고 있었다.


Custom Elements

 Web Components의 모든 기술들 중에 Custom Elements는 가장 논란이 적다. 부분적인 UI의 모양과 동작을 정의하고 이 조각을 브라우저와 프레임워크에 관계없이 배포할 수 있다는 점은 이미 어느정도 지지를 받고 있다.


'업그레이드'

 전통적인 HTMLElement를 기존의 방식과 prototype을 이용하여 Custom Element로 만드는 것을 '업그레이드'라고 할 수 있다. 요즘은, elements 가 업그레이드 되면 createdCallback이 호출된다.

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

 여러 벤더들에 의해 5개의 제안이 있었지만, 모두가 동의하기 전까지는 2개가 유력해보인다.


'Dmitry'

 좀 더 이후의 버전의 createCallback 패턴은 ES6에서도 잘 동작한다. createdCallback 컨셉은 살아남았지만, 클래스에 대한 내용은 일부 변경되었다.

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

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

 요즘의 구현방식에서는 custom element는 HTMLUnknownElement로 생성되지만 이 객체는 미리 정의된 객체로 변환되고 createCallback를 호출한다.

 그 이후의 진행은 플랫폼이 어떻게 동작하느냐에 따라 다르다. Element는 처음에 'unknown' 이었고, 최종적으로 특정 객체로 변하지만 동작이 같지는 않기 때문에 개발자들에게는 혼란을 줄 수 있다.


동기방식의 생성자

 개발자에 의해 등록된 생성자(역주 : class constructor)는 custom element가 생성될때 파서에 의해 실행되고 트리에 들어간다.

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

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

 이 방식이 합리적으로 보이지만, 초기에 다운로드 된 문서에 있는 custom element가 registerElement 를 비동기로 호출한다면 업그레이드에 실패할 수 있기도 하다. 비동기 ES6 모듈의 영역에 접근하기 어려운 점이다.

 게다가 동기방식의 생성자는 .cloneNode() 와 연관된 플랫폼 이슈가 있다.

 2015년 6월에 있는 미팅에서 벤더들에 의해 결정이 될 것으로 보인다.


is=""

 is 속성을 이용하면 custom element에 대한 행동을 기본 엘리먼트를 통해 정의할 수 있다.

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


찬성 의견

  1. 기본 엘리먼트를 확장할 수 있다. (예를 들면 문자로 접근하는 방식이나 <form> 컨트롤, <template>).
  2. 이 방식은 엘리먼트에 '점진적인 향상' 의 의미를 부여하기 때문에 JavaScript 없이도 함수형으로 동작한다.


반대 의견

  1. 문법이 헷갈린다.
  2. 플랫폼의 접근성(accessibility) 요소를 사용하지 못하게 하는 방법이다.
  3. 기본 엘리먼트를 '적절하게' 확장하는 방법한다는 것이 무엇인지 모른다.
  4. 용도가 제한적이다. 곧이어 개발자들은 Shadow DOM을 소개하면서, 모든 내장 접근성 기능을 잃었다.


합의점
 is는 Custom Element 스펙에서 '껄끄러운 어떤 것'으로 여겨지고 있다. Google 은 is를 구현해 두었고 하위 요소들이 노출되기 전까지의 빈틈을 채우는 역할을 할 것으로 보고 있다. 요즘 Mozilla 와 Apple이 Custom Elements V1을 근시일 안에 도입하려고 하고, 플랫폼에 존재하는 '껄끄러운 어떤 것'을 해소하는 V2에 대한 방향을 잡고 있다.
 Custom Element로서의 HTML은 Domenic Denicola가 진행하고 있는 프로젝트이며, 빌트인 HTML 요소들을 custom element을 이용하여 새롭게 만들어 플랫폼에서 빠뜨린 DOM 요소들을 커버하려고 한다.



Shadow DOM

 Shadow DOM 은 벤더들 사이에서 가장 논쟁이 되고 있는 부분이다. 빠른 합의를 위해 이 기능은 'V1'과 'V2' 로 나뉘어져 진행되고 있다.


배포(Distribution)

 배포 단계는 한 Shadow DOM 객체가 상위 Shadow DOM 에 들어가 화면에 표시되는 단계이다. 이제 당신이 만든 컴포넌트를 다른 사람의 컴포넌트 안에 넣어서 사용할 수 있다.


현재 API

 현재 구현되어 있는 API는 명확하다. Shadow DOM 안에서 <content> 엘리먼트를 사용하여 위치할 부모를 지정할 수 있다.

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

 그러나 Apple과 Microsoft에서는 복잡함과 성능의 문제로 이러한 방식을 거부했다.


새로운 API

 벤더들이 직접 만나는 미팅에서도 API에 대한 명확한 합의가 이루어 지지는 않았고, 어쩔 수 없이 필수적인 것들만 합의하게 되었다.

 Microsoft, Google, Apple, Mozilla, 이렇게 4개의 벤더들은 2015년 7월을 목표로 새로운 API를 구체화하는 작업에 착수했다. 그 결과, 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 = '';

// 초기 호출
shadow.distribute();

// 이후 MutationObserver를 후킹

 이 방식의 큰 위험요소는 타이밍이다. MutationObserver 콜백이 실행되는 시점에, 자식 객체가 변경되어 재배포 된다면 레이아웃 속성을 찾지 못하고 잘못된 결과가 나올 수 있다.

myHost.appendChild(someElement);
someElement.offsetTop; //=> 이전 값

// mutation observer 콜백에 의해 재배포 (비동기)

someElement.offsetTop; //=> 새로운 값

 offsetTop에서 새로운 값을 참조하고 싶겠지만, 중간에 있는 콜백에 의한 레이아웃 재배포는 비동기로 실행되기 때문에 원하는 값을 얻을 수 없을 것이다.

 이런 코드가 항상 문제가 되지는 않겠지만, 스크립트나 브라우저는 스크롤과 같은 동작에서 내부적으로 offsetTop과 같은 값의 보정하기 때문에 종종 문제가 될 수 있다.

 이런 문제는 명확한 API에 대한 논의가 제대로 수행되기 전까지는 해결할 수 없을 것이다. 하지만 Apple 에서 제안한 "named slots" API 에서는 이 문제를 회피할 수 있다.


새로운 API의 정의 - 'Named Slots'

 'named slots' 은 이전에 'content select' API를 좀 더 단순화했으며, 배포하려고 하는 컴포넌트에 대해 명확한 설명을 요구한다.

 <x-page>에 대한 Shadow Root에 대해 정의는 방식은 아래와 같다.




some shadow content

 <x-page>를 사용하는 방법은 아래와 같다.


  
header
footer

my page title

my page content

 이 컴포넌트가 트리에 렌더링되어 사용자가 보는 모습은 아래와 같다.


  
header

my page title

my page content

footer
some shadow content

 브라우저는 shadow 부모(myXPage.children)를 직접적인 자식 객체로 보고 <slot> 객체의 속성과 맞는 이름이 있다면 shadowRoot를 연결할 것이다.

 같은 속성을 찾았다면 연관된 <slot> 엘리먼트와 연결되어 '배포' 된 것처럼 보일 것이다. 연결 단계에서 같은 속성을 찾지 못한 다른 자식 객체들은 그대로 이름 없는 <slot> 엘리먼트로 대체될 수 있다.

찬성 의견 :

  1. 배포는 좀 더 명확하고 알아보기 쉽다.
  2. 브라우저 엔진이 연산하기에 좀 더 간단하다.

반대 의견 :

  1. <select>와 같은 이미 있는 엘리먼트들은 어떻게 해야 하는지에 대한 내용이 없다.
  2. slot 속성을 모두 추가하는 것은 사용자에게 부담이 된다.
  3. 표현방식이 좀 이상하다.

'closed' vs. 'open'
 myHost.shadowRoot 의 방식을 통해 객체에 접근할 수 없을 때 shadowRoot 는 '닫혔다' 라고 한다. 닫힌 상태에서는 컴포넌트 작성자가 컴포넌트를 복제하거나 상속하여 다른 객체로 만드는 것을 방지하고, 클로저를 통해 값들을 내부적으로 감출수 있도록 한다.
 Apple은 이 사항에 대해 아주 중요하다고 느꼈다. Apple은 세부 구현을 위해 상속하는 것이 밖으로 노출되지 않아야 한다고 주장했고, 그래서 'closed' 모드가 필요하다고 했으며 '독립된' 커스텀 엘리먼트가 되기를 원했다.
 Google은 좀 다른데, 'closed' 상태의 shadow root는 접근성을 떨어뜨리고 컴포넌트 툴의 활용을 방해할 수 있다고 생각했다. shadowRoot에서 문제가 전혀 없을 것이라고 할 수 없을 것이고 그렇다면 사람들이 고치기를 원할 수 있지 않느냐인 것이다. JS와 DOM은 '열린' 상태이다. 좀 더 이야기 해보자.

 4월에 있었던 미팅에서 이런 '모드' 가 필요하다는 것이 확실해졌지만, 벤더들은 기본값이 '열린' 상태여야 하는지 '닫힌' 상태여야 하는지 합의하지 못했다. 그 결과로, V1 에서는 '모드' 에서 인자를 받기로 합의했고, 기본 값에 대해서는 미리 정할 필요가 없어졌다.

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


-- 내용이 많아서 두 개의 글로 나눕니다 --

댓글
댓글쓰기 폼
공지사항
Total
295,220
Today
56
Yesterday
69
링크
«   2018/11   »
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30  
글 보관함