Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

DOM 확장(Extension)의 잘못된 점

Perfection kills라는 블로그를 운영하면서 Prototype.js의 core팀의 일원이기도 한 kangax님이 What's wrong with extending the DOM이라는 글을 올리면서 Prototype.js의 DOM Extension은 Prototype.js각 한 가장 큰 실수중의 하나(Dom extension is one of the biggest mistakes Prototype.js has ever done.)라고 지적했습니다.

제가 좋아하는 Prototype.js에 디자인에 대한 얘기이기도 하면서 DOM Extension에 대한 내용들이 일부는 공감을 안할 수 있겠지만 꽤 중요하다고 생각되어 kangax의 글을 옮겨적습니다. 항상 그렇듯이 발로한 번역이기에 영어에 알레르기가 있으시지 않다면 원문을 읽으시기를 권해드립니다.

아래는 kangax가 쓴 글의 내용입니다.



저는 최근에 웹에서 DOM extension에 대한 이야기가 얼마나 적은지에 대해서 알고 놀랬습니다.  이 겉으로 보기에는 유용해 보이는 프렉티스의 단점들이 무엇을 불안하게 만드는지 잘 알려져있지 않은것 같습니다. 이러한 정보의 부족은 왜 현재 존재하는 스크립트들과 라이브러리들이 이러한 함정에 아직도 빠지는지 잘 설명해 주고 있습니다. 그래서 왜 DOM을 확장하는 것이 일반적으로 좋지 않은 생각인지 그것과 관계된 문제들을 보여주면서 설명하려고 하고 가능한 대안이 어떤것이 있는지 볼 것입니다.

가장 먼저 DOM확장(DOM extension)이란 무엇인가? 그리고 어떻게 동작하는가?를 봐야합니다.


DOM확장은 어떻게 동작하는가?(How DOM extension works)
DOM확장은 DOM오브젝트에 커스텀 메서드와 프로퍼티들을 추가하는 간단한 프로세스입니다. 커스텀 프로퍼티들은 구현상에 존재하지 않는 것들을 말합니다. 그러면 DOM 오브젝트는 무엇인가? Element, Event, Document, 또는 다른 많은 DOM 인터페이스를 구현하는 호스트 객체(host object)입니다. 확장하는 동안 메서드와 프로퍼티들은 오브젝트에 직접 추가되거나 그들의 프로포토입에 추가될 수 있습니다.(이것을 적절하게 지원하는 환경하에서만...)

대부분의 일반적인 오브젝트 확장은 아마도 DOM 엘레먼트(Element interface를 구현한)일 것이고 Prototype.js와 Mootools같은 자바스크립트 라이브러리들에 의해서 대중화되었습니다. Event 오브젝트(Event interface를 구현한), documents(Document interface)도 종종 잘 확장됩니다.

엘리먼트 오브젝트의 prototype이 노출되는 환경에서 DOM확장의 예는 아래와 같습니다.

Element.prototype.hide = function() {
this.style.display = 'none';
};
...
var element = document.createElement('p');

element.style.display; // ''
element.hide();
element.style.display; // 'none'

소스에서 볼 수 있는 것처럼 "hide"펑션은 Element.prototye의 hide프로퍼티로 처음 할당되었습니다. 그리고 element에서 직접 호출되었고 엘리먼트의 display 스타일은 "none"으로 설정되었습니다.

이렇게 동작하는 이유는 Element.prototype에 의해서 참조된 객체가 실제로 P엘리먼트의 prototype chain의 오브젝트중 하나이기 때문입니다. hide프로퍼티가 호출되었을때 그것은 Element.prototype객체에서 찾을수 있을때까지 prototype chain전체를 찾습니다.

사실 최근의 브라우저들에서 P엘리먼트의 프로토타입 체인을 검사하면 보통 아래와 같습니다.

// "^" denotes connection between objects in prototype chain
document.createElement('p');
    ^
HTMLParagraphElement.prototype
    ^
HTMLElement.prototype
    ^
Element.prototype
    ^
Node.prototype
    ^
Object.prototype
    ^
null


P엘리먼트의 프로토타입 체인에서 가장 가까운 조상객체는 HTMLParagraphElement.prototype에 의해서 참조됩니다. 이것은 한 엘리먼트의 독특한 타입을 가진 오브젝트이고 P엘리먼트에서는 HTMLParagraphElement.prototype이고 DIV엘리먼트에서는 HTMLDivElement.prototype이고 A엘리먼트에서는 HTMLAnchorElement.prototype입니다.

이러한 이름은 사실 DOM Level 2 HTML 스펙에서 정의된 인터페이스에 해당합니다. 이 스펙에서 이러한 인터페스으들간의 상속도 정의하고 있습니다. 예를 들어 "...HTMLParagraphElement인터페이스는 HTMLElement인터페이스의 모든 프로퍼티와 펑션을 가진다...", "...HTMLElement인터페이스는 Element인터페이스의 모든 프로퍼티와 펑션을 가진다" 등입니다.

아주 명백하게도 paragraph 엘리먼트의 "prototype 객체"에서 프로퍼티를 만들면 이 프로퍼티는 anchor엘리먼트에서는 이용할 수 없습니다.

HTMLParagraphElement.prototype.hide = function() {
this.style.display = 'none';
};
...
typeof document.createElement('a').hide; // "undefined"
typeof document.createElement('p').hide; // "function"

이것은 anchor엘리먼트의 프로토타입 체인은 HTMLParagraphElement.prototype에 의해서 참조된 객체를 전혀 포함하고 있지 않기 때문입니다. 하지만 대신 HTMLAnchorElement.prototype에 의해 참조된 객체를 포함하고 있습니다. 이것은 고치기 위해서 객체의 프로퍼티를 프로토타입체인의 더 엎에서 할당할 수 있습니다.(HTMLElement.prototype, Element.prototype, Node.prototype등에 의해서 참조된 객체같은...)

유사하게 Element.prototype에서 프로퍼티를 추가하는 것은 모든 노드들에서 이용가능하도록 만드는 것이 아니라 엘리먼트타입의 노드에서만 이용가능하도록 만듭니다. 모든 노드(text노트, comment노트같은)에서 프로퍼티를 같기를 원한다면 Node.prototype의 프로퍼티로 할당할 필요가 있습니다. text, comment노드에 대해 말하는 것은 어떻게 인터페이스 상속이 그것들은 찾느냐는 것입니다.


document.createTextNode('foo'); // < Text.prototype < CharacterData.prototype < Node.prototype
document.createComment('bar'); // < Comment.prototype < CharacterData.prototype < Node.prototype

이러한 DOM오브젝트의 prototype들의 노출이 보장되지 않는다는 것을 이해하는 것은 중요합니다. DOM Level2 스펙에서는 단지 인터페이스를 정의하고 그들간의 상속에 대해서만 정의하고 있습니다. Element인터페이스를 구현하는 모든 객체들의 프로토타입 객체를 참조하는 글로벌 Element프로퍼티가 존재해야 한다는 것은 정의되어 있지 않고 Node인터페이스를 구현한 모든객체의 프로토타입 객체를 참조하는 글로벌 Node프로퍼티가 반드시 존재해야 한다는 것도 없습니다.

IE7(또는 그이하)은 이러한 환경의 한 예입니다. IE7은 글로벌 Node, Element, HTMLElement, HTMLParagraphElement등등을 노출하지 않습니다. Safari 2.x 또한 그러합니다.

그러면 글로벌 "프로토타입" 오브젝트를 노출하지 않는 환경에서 우리는 무엇을 하여야 하는가? 해결방법은 DOM객체를 직접 확장하는 것입니다.


Element.prototype.hide = function() {

this.style.display = 'none';

};

...

var element = document.createElement('p');



element.style.display; // ''

element.hide();

element.style.display; // 'none'



무엇이 잘못되었는가?(What went wrong?)
프로토타입 객체를 통해서 DOM엘리먼트를 확장할수 있다는 것은 매력적으로 보입니다. Javascript의 프로토타입 특성(prototypal nature)의 이점을 취할때 DOM스크립팅은 아주 객체지향적이 됩니다. 사실 DOM확장은 몇년전에는 아주 유용한 것처럼 보였습니다. Prototype.js는 이것을 prototype.js 아키텍쳐의 가장 중요한 부분으로 만들었습니다. 그러나 겉으로 보기에는 무해한 프렉티스뒤로 숨긴 것은 큰 문제를 일으켰습니다. 크로스 브라우징 스크립팅의 시대가 왔을때 이러한 접근의 단점은 다른 이점보다 훨씬 컸습니다. DOM확장은 Prototype.js가  가장 큰 실수중의 하나입니다.(DOM extension is one of the biggest mistakes Prototype.js has ever done.)

그럼 무엇이 문제인가?


표준스펙의 부재(Lack of specification)
이미 언급했던 대로 "프로토타입 오브젝트"의 노출은 스펙의 일부가 아닙니다. DOM Level 2는 단지 인터페이스와 상속관계만을 정의하고 있습니다. DOM Level 2를 완전하게 따라 구현하기 위해서는 그러한 글로벌 Node, Element, HTMLElement등등의 오브젝트를 노출할 필요가 없으며 어떤 다른 방법으로라도 그것들을 노출해야 한다는 요구사항도 없습니다. 항상 DOM오브젝트를 수동으로 확장할 가능성이 주어신 것은 큰 이슈처럼 보이지는 않습니다. 그러나 수동 확장한다는 것은 오히려 느리고 불편한 프로세스입니다. 그리고 빠른 "프로토타입 오브젝트"기반의 확장은 소수의 브라우저들 사이에서만의 사실상 표준의 일부일 뿐,  그것이 차후에 비전통적인 플램폼들 사이에서(모바일디바이스 같은) 적응력(adoption)과 이식성(portability)이 필요할 때가 왔을때 이러한 프렉티스를 신뢰할 수 없게 만듭니다.


Host오브젝트가 룰을 가지고 있지 않다.(Host objects have no rules)
DOM확장의 다음 문제는 DOM오브젝트는 Host오브젝트라는 것입니다. 호스트오브젝트는들은 가장 안좋은 것들입니다. 스펙(ECMA 262 3rd. ed)에 따라서 호스트 오브젝트는 다른 어떤 오브젝트도 상상할 수 없는 것들을 하도록 허락합니다. 관련 인용문(8.6.2)은 다음과 같습니다.

호스트오브젝트는 이러한 내장메서드들을 어떤 구현족송적 비헤이비어와 함께 구현할 것이거나 호스트 오브젝트는 오직 일부 내장메서드만을 구현할 것입니다. 내장 메서드들의 스펙은 [Get], [Put], [Delete]등등에 대해서 이야기 합니다. 이것은 내장 메서드 비헤이비어(internal methods behavior)는 구현종속적이라는 것을 이야기 합니다. 이것은 [Get]메서드의 호출에서 에러를 던지는 것은 호스트오브젝트에게는 아주 정상적이라는 것을 의미하고 단지 이론뿐인 것이 아닙니다. IE에서 명확하게 [Get]호스트 오브젝트가 에러를 던지는 예를  쉽게 관찰할 수 있습니다.


document.createElement('p').offsetParent; // "Unspecified error."
new ActiveXObject("MSXML2.XMLHTTP").send; // "Object doesn't support this property or method"

DOM오브젝트를 확장하는 것은 지뢰밭을 걷는 것과 같습니다. 정의에 따라서 예측할 수 없고 완전히 산만한 방법으로 동작하는 것을 허락하는 것이 동작합니다. 그리고 이것이 그냥 폭발하는 것만이 아니라 더 나쁜 시나리오인 조용한 실패의 가능성도 있습니다. 산만한 비헤이비어의 예는 프로퍼티의 할당에서 예외를 던지는 특정한 케이스가 있는 applet, object, embed엘리먼트들입니다.  유사한 재앙은 XML노드에서도 발생합니다.


var xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.loadXML('<foo>bar</foo>');
xmlDoc.firstChild.foo = 'bar'; // "Object doesn't support this property or method"

"Invalid procedure call or argument"예외를 던지는 document.styleSheets[99999]나 "Member not found"예외를 던지는 document.createElement('p').filters같은 IE에서의 다른 실패케이스들도 있습니다.  그러나 MSHTML DOM이 유일한 문제가 아닙니다. Mozilla에서 event오브젝트의 "target"프로퍼티를 overwrite하려고 하면 프로퍼티가 오직 getter(set이 불가능하고 readonly만 되는)만 가지고 있다고 TypeError예외를 던집니다.  WebKit에서도 같은 것을 하면 할당후에 오리지날 객체를 참조하려고 조용히 실패합니다.

event오브젝트의 작동을 위해서 API를 생성하려고 할때 간결하고 설명적인 이름에 집중하는 대신에 이러한 readonly프로퍼티들에 대해서 고려할 필요가 있습니다.

가능한한 호스트오브젝트를 건드리는 것은 피하는 것이 좋은 습관입니다. 정의에 따른 아키텍쳐에 기반하려는 시도는 그렇게 산발적으로 동작하는 것을 어렵게 하는 좋은 아이디어입니다.


충돌의 기회(Chance of collisions)
DOM엘리먼트 확장에 기반한 API는 scale하기 어렵습니다. 코어API 메서드를 추가하거나 변경하려고 하는 라이브러리 개발자와 도메인에 특정한 확장을 추가하려는 라이브러리 사용자 모두에게 scale하는 것은 어렵습니다. 이 이슈의 근원은 충돌의 기회와 유사합니다. 인기있는 브라우저에서 DOM구현은 일반적으로 자신만의 API를 가지고 있습니다. 안좋은 점은 이러한 API가 정적(static)일 뿐만 아니라 새버전의 브라우저에서 처럼 끊임없이 변경된다는 것입니다. 어떤 것들은 사용이 중지되고(deprecated) 어떠 것들은 추가되거나 수정됩니다. 이러한 결과로 DOM오브젝트에서 주어진 프로퍼티와 메서드의 셋은 약간 이동하는 타겟입니다.

오늘날 엄청나게 많은 사용환경이 주어진 것은 어떤 프로퍼티가 DOM의 일부가 이미 아니라고 말하는 것이 블가능하도록 만들었습니다. 그리고 만약 이것이 덮어씌워질 수 있다면? 또는 그것을 사용하려고 시도했을때 에러를 던진다면? 그것이 호스트 객체라는 것을 기억하세요! 그리고 만약 우리가 그것을 조용하게 덮어쓸수 있다면 DOM의 다른 부분의 영향이 얼마나 될까요?  모든 것이 예상한대로 아직 동작하는가? 만약 어떤 브라우저의 한 버전에서 모든 것이 정상적이라면 다음버전이 같은이름의 프로퍼티를 소개하지 않는다는 보장이 있는가? 이런한 질문들은 계속 할 수 있습니다.

Prototype를 깨뜨리는 독점적인(proprietary) 확장의 예시는 IE에서 textarea의 wrap프로퍼티(Element#wrap 메서드와 충돌하는)와 Opera에서 control엘리먼트로에서 발생하는 select메서드(Element#select메서드와 충돌하는)입니다. 비록 이 두 케이스가 문서화되었다고 할지라도 이러한 예외들을 기억하는 것은 피곤한 일입니다.

독점적인 확장은 유일한 문제가 아닙니다. HTML5는 새로운 메서드와 프로퍼티들을 제공합니다. 그리고 인기있는 브라우저들의 대부분은 이미 그것들을 구현하기 시작했습니다. 어떤 점에서 WebForms는 replace프로퍼티를 input엘리먼트에 정의했고 Opera는 그들의 브라우저에 추가하기로 결정했습니다. 그리고 그것은 Element#replace메서드와 충돌하기 때문에 Prototype을 깨뜨립니다.

하지만 더 있습니다.

오래된 DOM Level 0 전통때문에 form엘리먼트의 form컨트롤에 간단하게 그들의 이름으로 접근하는 접근하는 편리한 방법이 있습니다. 이것이 의미하는 것은 표준적인 elements컬렉션을 사용하는 대신에 다른과 같이 form컨트롤에 접근할 수 있다는 것입니다.


<form action="">
    <input name="foo">
</form>
...
<script type="text/javascript">
    document.forms[0].foo; // non-standard access
    // compare to
    document.forms[0].elements.foo; // standard access
</script>


로그인폼의 유효성을 검사하고 submit하기 위해서 login메서드로 form엘리먼트를 확장한다고 하겠습니다. login이라는 이름을 가진 form컨트롤이 있다면 다음에 발생할 일은 별로 좋지 않습니다.


<form action="">
    <input name="login">
    ...
</form>
...
<script type="text/javascript">
    HTMLFormElement.prototype.login = function(){ 
        return 'logging in'; 
    };
    ...
    $(myForm).login(); // boom!
    // $(myForm).login references input element, not `login` method
</script>


모든 명명된 폼컨트롤은 프로토타입 체인을 통해서 상속된 프로퍼티를 감춥니다. 충돌의 기회와 폼엘리먼트상의 예상하지 않은 에러는 더욱 높아졌습니다.

상황은 그들의 이름에 의해서 document에 직접 접근될수 있는 명명된(named) 폼엘리먼트와 좀더 유사합니다.


<form name="foo">
    ...
</form>
    ...
<script type="text/javascript">
    document.foo; // [object HTMLFormElement]
</script>


document객체를 확장했을 때 확장과 함께 폼네임 충돌의 추가적인 위험이 있습니다. 그리고 다루기 힘든 많은 양의 HTML을 가진 레거시 애플리케이션에서 스크립트를 돌린다는 것은 이러한 이름을 변경하고 삭제하는 것은 하찬은 일은 아닐것입니다.

prefixing전략의 어떤 종류를 사용하는 것은 문제를 덜수 있습니다. 그러나 아마도 추가적인 잡음을 가져올 것입니다.

소유하고 있지 않은 오브젝트를 수정하지 마라는 것은 충돌을 피하는 최고의 레시피입니다. 이 규칙을 깨뜨리는 것은 이미 Prototype이 document.getElementsByClassName을 덮어쓰고 커스텀구현을 하였을 때 문제를 갖도록 만들었습니다. 이 규칙을 따르는 것은 또한 같은 환경에서 다른 스크립트들과 잘 동작한다는 것을 의미합니다.(DOM오브젝트를 수정했던지에 상관없이)


성능 저하(Performance overhead)
앞에서 본 것 처럼 엘리먼트 확장을 지원하지 않는 IE 6,7, safari 2.x등과 같은 브라우저들은 수동 오브젝트 확장을 요구한다. 이 문제는 수동확장은 느리고 불편하고 scale되지 않는다는 것입니다. 이것은 오브젝트가 메서드와 프로퍼티들의 많은 수가 자주 확장될 필요가 있기 때문에 느립니다. 그리고 아리러니하게도 이러한 브라우저들은 가장 느린 것들입니다. 이것은 오브젝트가 동작하기 위해서 우선 확장될 필요가 있기 때문에 불편합니다. document.createElement('p').hide() 대신에 $(docuement.createElement('p'))hide()처럼 사용할 필요가 있습니다. 어쨌든 이것은 Prototype.js의 입문자에게 가장 일반적인 걸림돌 중 하나입니다. 마지막으로 수동확장은 API메서드들을 추가하는 것이 아주 선형적(linearly)으로 성능에 영향을 미치기 때문에 scale되기 어렵습니다. 만약 Element.prototype에 100개의 메서드가 있다면 엘리먼트에 100개의 할당이 이루어져야하고 200개의 메서드가 있다면 200개의 할당이 이루어져야 합니다.

또다른 성능문제는 이벤트오브젝트에 있습니다. Prototype.js는 이벤트와 그것들을 특정 메서드의 세트와 확장하는 유사합 접근을 따릅니다. 브라우저에서 mousemove, mouseover, mouseout, resize같은 이벤트들은 말그대로 초당 수십번씩 발생할 수 있습니다. 그것들을 각각 확장하는 것은 믿을수없을정도로 비용이 드는 프로세스입니다. 그리고 무엇을 위해서 인가요? 단지 이벤트오브젝트에서 한 메서드를 호출하기 위해서입니다.

마지막으로 엘리먼트를 한번 확장하기 시작하면 라이브러리 API는 대부분 어디서나 확장된 엘리먼트를 return해주어야 할 필요가 있을 것입니다. 그 결과로 $$같은 메서드를 쿼리하는 것은 한 쿼리에서 모든 각각의 엘리먼트를 확장할 수 있습니다. 수백 또는 수천개의 엘리먼트를 사용할 때 이러한 프로세스의 퍼포먼스 오버헤드를 상상하는 것은 쉽습니다.


IE DOM은 엉망입니다.(IE DOM is a mess)
이전 세션에서 보여준 것처럼 수동 DOM확장은 엉망입니다. 그러나 IE에서 수동 DOM확장은 더욱 좋지 않습니다. 여기 그 이유가 있습니다.

우리가 모두 알고 있는대로 IE에서 호스트와 native오브젝트사이의 순회참조는 leak이 발생하고 피하는 것이 최선입니다. 그러나 DOM엘리먼트에 메서드를 추가하는 것은 이러한 순회참조를 만들어내는 것으로 가는 첫 발걸음입니다. 그리고 ID의 오래된버전부터 "오브젝트 프로토타입"을 노출하지 않아서 많은 것을 해야하고 직접 엘리먼트를 확장해야 합니다. 순회참조과 누출(leaks)은 거의 피할 수 없습니다. 사실 Prototype.js는 라이프타임의 대부분에서 그것들로부터 고통을 겪습니다.

또다른 문제는 IE DOM이 프로퍼티와 어트리뷰트를 서로 매핑하는 방법입니다. 어트리뷰트들이 프로퍼티처럼 같은 네임스페이스안에 있다는 사실은 충돌의 기회와 모든 종류의 기대하지 않은 불일치 증가시킵니다. 엘리먼트가 커스텀 "show"어트리뷰트를 가지고 있으면 그 다음에 Prototype.js에 의해서 확장됩니다. 아마도 놀랄것이지만 show 속성은 Prototype의 Element#show메서드에 의해서 덮어쓰여질 것입니다. extendedElement.getAttribute('show')는 show속성의 값이 아닌 펑션의 참조를 리턴할 것입니다. 유사하게 extendedElement.hasAttribute('hide')는 비록 엘리먼트에 hide속성을 커스텀하지 않았더라도 true가 될 것입니다.

마지막으로 덜 알려진 단점중 하나는 DOM엘리먼트에 프로퍼티를 추가하는 것이 IE에서 reflow를 야기시킨다는 사실입니다. 그래서 엘리먼트의 확장은 아주 비용이 큰 동작입니다. DOM내에서 어트리뷰트와 프로퍼티의 부족한 매핑이 주어진다는 것은 수긍이 가는 부분입니다.


보너스: 브라우저 버그들(Bonus: browser bugs)
지금까지 했던것들이 충분치 않다면 여기 몇가지 버그가 더 있습니다.

사파리 3.x의 버전대에서는 뒤로가기 버튼으로 이전페이지로 이동할때 모든 호스트오브젝트를 없애버리는 버그가 있습니다. 불행하게도 이 버드는 찾을수 없는 것이라서 이슈를 발생킵니다. Prototype.js는 더 끔찍한 일을 저지릅니다. 그것은 WebKit의 위 버전들에서 발생하고 명백하게 window에  "unload"이벤트를 부여함으로써 bfcache1가 불가능하도록 합니다. bfcache를 무력화시키는 것은 뒤로/앞으로 버튼으로 페이지를 이동했을때 브라우져가 캐시로부터 페이지를 복구하는 대신에 다시 불러와야 한다는 것을 의미합니다.

또다른 버그는 IE8에서 HTMLObjectElement.prototype, HTMLAppletElement.prototype과 관련이 있습니다. object와 applet멜리먼트는 그러한 프로토타입객체로부터 상속되지 않습니다. HTMLObjectElement.prototype의 프로퍼트를 할당할 수는 있지만 이 프로퍼티는 object엘리먼트상에서 절대 해결되지 않습니다. applet도 마찬가지 입니다. 그 결과로 이러한 엘리먼트는 항상 추가적인 오버헤드를 발생하며 수동으로 확장되어야 합니다.

IE8은 또한 다른 일반적인 구현들과 비교했을 때  prototye객체의 서브셋만을 노출시킵니다. 예를 들어 HTMLParagraphElement.prototype과 Element.prototype은 있지만 HTMLElement(그리고 HTMLElement.prototype) 또는 Nodo(와 Node.prototype)는 없습니다. IE8에서 Element.prototype는 또한 Object.prototype를 상속받지 않습니다. 이것은 버그가 아니가 아니고 원래 그런것이지만 그럼에도 불구하고 존재하지않는 Node를 확장하려는 시도가 좋을게 없다는 것을 기억하고 있어야 합니다.


이것을 해결할 랩퍼(Wrappers to the rescue)
DOM확장의 이 모든 난잡함에 대한 가장 일반적인 대안 중 하나는 객체 랩퍼(object wrappers)입니다. 이것은 jQuery가 처음부터 가진 접근방법이고 소스의 다른 라이브러리들이 후에 같은 방법을 취했습니다. 아이디어는 아주 간단합니다. 엘리먼트나 이벤트를 직접 확장하는 대신에 그것들은 감싸는 랩퍼를 만들고 적절하게 메서드를 위임합니다.(delegate) 충돌도 없고 호스트오브젝트와 관련된 정신착란과 같은 것을 다룰 필요도 없습니다. 누출을 관리하는 것이 더 쉽고 적적히 동작하지 않는 MSHTML DOM을 동작시킬수 있고 더 좋은 성능에다가 정상적인 유지보수와 어려움없이 scaling할 수 있습니다.

그리고 아직 당신은 절차적인 접근을 피할수 있습니다.


Protytype 2.0
Prototype.js의 실수는 차기 메인버전에서는 피할것이라는 것은 좋은 소식입니다. 내가 관계되는 한 모든 코어개발자들은 위에서 언급한 문제점들을 이해하고 있습니다. 랩퍼접근 방식은 앞으로 나아가기 위해서 적절한 방식입니다. Mootools같은 다른 DOM확장 라이브러리들은 어떤 계획을 가지고 있는지 잘 모릅니다. 내가 보는 것 내에서는 그것들은 이미 event에 랩퍼를 사용하고 있습니다만 아직 엘리먼트는 확장하고 있습니다. 저는 확실하게 그것들이 조만간에 이 무모한 짓을 그만두기를 바랍니다.


통제된 환경(Controlled environments)
지금까지 크로스브라우징 스크립팅 라이브러리의 관점에서 DOM확장을 살펴보았습니다. 그 배경하에서 이 아이디어가 얼마나 성가신 문제인지는 명백하지만 통제된 환경에 대해서는 어떠합니까? 스크립트가 오직 Gecko, Webkit이나 다른 현대적인 비MSHTML DOM에 기반한 한개 혹은 2개의 환경에서만 돌아갈 때를 말합니다. 아마도 특정브라우저를 통해서 접근되는 인트라넷 애플리케이션이나 데스크탑 WebKit기반의 앱이 그러한 것들입니다.

이 경우에는 상황은 명백하게 더 좋습니다. 자 위에서 리스트된 점들을 봅시다.

스펙의 부재는 차기 에디션들이나 다른 플래폼들과의 호환성에 대해서 걱정할 필요가 없기 때문에 약간 관계없게 되었습니다. 비MSHTML DOM환경의 대부분은 DOM오브젝트 프로토타입을 꽤 오랫동안 노출하고 조만간 그것을 버릴것 처럼 보이지 않습니다. 하지만 변경될 가능성은 아직 있습니다.

충돌에 대한 점은 아직 유효합니다. 이러한 환경은 자신만의 API를 가지고 비표준 폼컨트롤 접근을 지원합니다. 그리고 끊임없이 새로운 HTML5의 특징들을 구현하고 있습니다. 소유하고 있지 않은 오브젝트를 수정하는 것은 아직 나쁜 아이디어이고 찾기 어려운 버그과 불일치로 이끌수 있습니다.

이러한 DOM은 prototype기반의 DOM확장을 지원하기 때문에 성능저하는 실제적으로 존재하지 않습니다. DOM오브젝트를 벗어나 메서드를 호출하거나 프로퍼티에 접근하려고 추가적인 어떤 객체도 생성할 필요가 없기 때문에 성능은 랩퍼접근방식과 비교했을때 실제로 더 좋아질수 있습니다.

통제된 환경에서 DOM을 확장하는 것은 그렇게 하기에 완벽히 좋은 것처럼 보입니다. 하지만  충돌과 관련된 주요 문제가 있다고 하더라도 대신에 랩퍼를 사용하라고 조언해주고 싶습니다. 이것은 앞으로 나아가기 위한 더 안전한 방법이고 미래에 유지보수에 대한 오버헤드로부터 당신을 구할 것입니다.


맺는 말(Afterword)
희망스럽게도 이제 당신은 우아한 접근처럼 보이는 것 뒤에 있는 사실을 모두 명확하게 볼 수 있습니다. 다음번 Javascript framework를 디자인할 때 DOM을 확장하지 말라고 하세요. 방해가되는 API를 유지보수하는 모든 문제들들과 불필요한 성능저하를 겪는 일로부터 당신 스스로를 구하세요. 그 반면에 DOM을 확장하는 자바스크립트 라이브러리의 사용을 고려하고 있다면 즉시 그만두고 위험을 취할 의도가 있는지 자문해 보세요.


글을 정말 엄청나게 길게 적었습니다 ㅠ..ㅠ 어제 포스팅하려고 하다가 결구 다 못하고 2일에 걸쳐서 포스팅을 적어버렸군요. ㅎㅎ
사실 사용하면서 jQuery의 랩퍼방식과 Prototype.js의 DOM확장에 대해서 크게 차이를 체감하면서 사용하고 있지는 않았습니다. jQuery에 대한 이해가 적어서 그런지 몰라도 어쨌든 jQuery에서도 랩핑된 객체를 돌려받아서 사용하게 되니까요. 어쨌든 DOM확장은 Prototype.js의 큰 장점이었고 이것은 상당히 OOP스런 느낌을 주었다는 것이 개인적인 생각이었습니다.
글에 달린 Andrew Dupont의 댓글을 보아도 완전히 kangax와 같은 입장은 아닌듯 하기도 합니다.(어느정도 공감은 하지만 kangax처럼 강력히 주장하고 있지는 않는 상황이 아닐까 싶기도 합니다.

아무튼 댓글들에도 찬성한다 반대한다는 말도 있지만 DOM확장을 좋아하는 사람들도 있었는데 Prototype.js의 특징을 잃어버릴 수 있는 선택일 수도 있다는 생각도 듭니다. 이미 과거보다는 많이 밀려난 프레임워크인데(안타깝게도요 ㅠ..ㅠ) jQuery와 같은 접근을 취한다면 Prototype.js의 특징은 더욱 없어지겠지요. 어쨌든 DOM에 대한 많은 내용들만으로도 충분히 읽어볼 만한 가치가 있는 글이었네요.

  1. Back-Forward Cache. 뒤로가기 앞으로가기를 눌렀을때의 캐쉬를 말합니다. [Back]
2010/04/09 03:02 2010/04/09 03:02