FishEye와 Crucible에서 jQuery 사용사례

jQuery 에 빠지다
Seb RuizFishEye에 대해 이야기 합니다. (2009년 8월)
요약: 큰 DOM을 가진 RICH 웹 어플리케이션은 성능에 무리를 주는 새 클래스를 생성하게 됩니다. Seb Ruiz가 최근 FishEyeCrucible 2.0 UI를 개발하며 겪은 일에 대해 알려줍니다.


처음에는



최근 리치 웹 어플리케이션을 개발하는 개발자가 일반적으로 부딪치는 문제가 강력한 기능을 가진 복잡한 JavaScript 를 사용하는 데이터양이 많은 페이지의 성능문제입니다.

파이어폭스가가 특정 웹 페이지에서 먹통이 된다면, 여러분의 경험을 개선시킬 수 있는 JavaScript 이슈가 있는 것입니다.

AJAX 호출에 의한 컨텐츠의 비동기 업로드와 업데이트, 에러 리포팅, 동적 비쥬얼라이징 등은 모두 실시간으로 데이터를 업데이트하며 복잡한 레이아웃을 처리합니다. FishEye 와 Crucible 팀은 이번 2.0 버전 릴리스를 하면서 이러한 것들을 모두 처리해야 했습니다. 그러나 정말로 이러한 기능을 추가하면서 문제가 되었던 것은 기능자체의 추가가 아니라 일반적인 브라우저에서의 성능이슈 였습니다.

이 포스트가 매우 큰 HTML 문서 구조와 복잡한 JavaScript를 연동하는데 도움이 될만한 팁이 되었으면 합니다.

일반적으로는 HTML 문서의 사이즈를 최소화하는 것이 좋은 방법입니다. 그러나, 문서 사이즈를 줄일 수 없는 경우가 가끔 발생합니다. 매우 사이즈가 큰 FishEye 주석(annotation) 페이지는 대략 80,000 DOM 엘리먼트가 페이지에 포함됩니다. – 사실 이것은 하나의 소스가 문법 하이라이팅을 포함하는 경우 10개 이상의 HTML 엘리먼트를 가지게 되는 것은 매우 흔한일입니다.

매우 큰 DOM를 만나지 않는다고 하여도, Jquery의 마법뒤에서 무슨일이 벌어지는지 알아보는 것은 매우 가치있는 일일 것입니다.


이벤트 바인더(event binder)


간단 jQuery 이벤트 바인더 실렉터는 아래와 같은 형태입니다.:



<script type="text/javascript"> \\
$(document).ready(function () \{ \\
$(".alert-on-click").bind("click", function () \{ \\
alert("Clicked element " + this); \\
\}); \\
\}); \\
</script>



이것은 JQuery에서 이벤트에 함수를 연결하는 다소 일반적인 방법입니다.
HTML 문서 로딩이 끝날 때, anonymous 함수가 실행되게 됩니다.
이것은 문서에서 'alert-on-click' 클래스를 가진 모든 엘리먼트를 찾아 click 이벤트에서 발생하는 함수를 연결합니다.


느린 클래스 셀렉터(selectors)


이 방법은 수천개의 DOM 엘리먼트를 가진 HTML 문서에 대해서는 문제가 생길 소지가 큽니다.
클래스 기반의 셀렉터에 효율적으로 동작하지 않는 웹 브라우저의 경우 엘레먼트를 찾기 위해 모든 DOM 트리 전체를 탐색하는 단점을 가지게 됩니다.
다른 웹브라우저의 경우는 상황이 좀 낫지만, ID 기반의 셀렉터에 비해 상대적으로 더 많은 부하를 발생하게 됩니다.


바인드 비용



만약 웹 어플리이션을 반드시 개발해야 하는데, 10,000 라인의 소스코드를 가진 리뷰를 표시해야 한다고 가정해 봅시다.
보기에 간단해 보입니다 – 각 소스 라인을 테이블에 넣어 출력하는 것이죠.
그리고 사용자가 소스라인을 클릭할 때마다 요청한 라인에 댓글을 생성하도록 해야 합니다.
간단합니다. Click 이벤트 핸들러를 각 소스 라인에 바인딩(연결)합니다.



<script type="text/javascript"> \\
$(document).ready(function () \{ \\
$("tr.sourceLine").bind("click", showCommentBoxFn); \\
\}); \\
</script>



그러나, 이 경우 브라우저는 10,000개의 tr 엘리먼트를 찾아야 하고, 10,000 번의 Bind 호출을 해야만 합니다.
이것은 명확히 페이지의 로딩 속도를 현저히 떨어뜨릴 것입니다. 더욱이, 메모리 관리차원에서도, Jquery가 페이지를 빠져 나갈 때 모든 연결된 엘리먼트를 해지하지 않을 가능성이 많습니다.
이것은 결국 느린 페이지 로드, 느린 언로드를 의미하는 것입니다.


연결(bind) 카우팅 방법



여기서는 여러분의 페이지가 로드될 때 얼마나 많은 수의 호출과 바인드(연결)가 발생하는지 확인하는 팁에 대해 설명합니다.
아래의 코드를 여러분의 JavaScript에 Jquery 바인트 이벤트 디버깅을 위해 끼워 넣으십시요. 출력결과를 확인하기 위해서는 firebug를 설치하셔야 합니다.



jQuery.fn.bind = function (bind) \{ \\
return function () \{ \\
console.count("jQuery bind count"); \\
console.log("jQuery bind %o", this); \\
return bind.apply(this, arguments); \\
\}; \\
\}(jQuery.fn.bind);






AJAX 호출과 함께 동기 바인드(Bind)



There is a significant amount of interactive functionality in FishEye/Crucible에는 중요한 인터렉티브 기능이 있습니다. 새로운 엘리먼트가 문서에 추가되면, 그것이 항상 적절히 Bind 되었는지를 확인해야 합니다.
버튼, 링크, 인터렉티브 엘리먼트는 모두 추적되어야 하며, 이것은 매우 귀찮은 작업입니다.
문서 구조의 약간의 변경이 기능을 깨뜨릴 수도 있는 것입니다.


전통적 바인딩(binding)에서 벗어나기



FishEye 와 Crucible에서 이 문제를 해결한 방법은 바로 jQuery 라이브 이벤트의 형태에서 이벤트 위임(delegation) 방법입니다.
JavaScript에서 엘리먼트에 의해 이벤트가 잡히지 않으면, 그 엘리먼트는 그 부모 혹은 그 부모의 부모로 계속 이어지게 됩니다.
이벤트 위임(event delegation)을 사용하면, 함수가 모든 현재와 미래의 특정 jQuery 셀렉터와 매칭되는 엘리먼트에 대해 바인드될 것입니다.

라이브 이벤트를 사용하는 것은 기존 바인딩을 사용하는 것처럼 간단합니다. 간단히 "bind" 대신 "live" 단어를 사용하면 됩니다.



<script type="text/javascript"> \\
$(document).ready(function () \{ \\
$("tr.sourceLine").live("click", showCommentBoxFn); \\
\}); \\
</script>



라이브 이벤트를 사용하면 페이지 로드 혹은 언로드시의 과도한 바인딩에 의해 발생하는 블록킹 인터페이스도 피할 수 있습니다.



라이브 이벤트 함정(Live event pitfalls)



라이브 이벤트를 사용할 때 몇가지 예상치 못한 문제를 발견했습니다.

라이브 이벤트와의 바인딩은 여전히 셀렉터 표현을 평가해야 할 필요가 있습니다. 다시말해 $(".class").live() 은 DOM이 매우 큰 경우 여전히 느려질 수 있는 것입니다.
우리는 이것을 페이지 보기가 초기화되고 라이브 이벤트가 바인딩 된 이후에 Ajax 호출과 대규모 데이터를 로딩하는 것으로 우회적으로 처리하였습니다.
마우스 이벤트(클릭, 더블클릭, 누르기 등)에 대한 라이브 이벤트를 이용하는 것은 원하지 않는 마우스 오른쪽 버튼을 누름으로서 발생하는 이벤트를 받아들입니다.
예를들면, 대상 URL 복사를 위해 링크에서 마우스 오른쪽 버튼을 클릭하는 것은 연결된 이벤트를 구동시키게 됩니다. 우리는 이 경우, jQuery의 라이브 함수를 재구현하여 오른쪽 마우스 버튼이 클릭되면 이벤트를 무시하도록 처리하였습니다. - 소스 참조

라이브 이벤트의 비결정 실행 순서로 인한 Quasi race conditions. 만약 엘리먼트가 하나 이상의 라이브 셀렉터와 매칭되면, 이 이벤트 함수의 순서는 보장될 수 없습니다. 예를들면:



<script type="text/javascript"> \\
$(document).ready(function () \{ \\
$("div.comment").live("click", function () \{ \\
markCommentAsRead(); \\
\}); \\
\\
$("div.comment a.reply").live("click", function () \{ \\
replyToComment(); \\
// there is no way to prevent a propagation to div.comment \\
\}); \\
\});
이 경우, <a class="reply"> 링크가 클릭되면, replyToComment() 와 markCommentAsRead() 가 실행될 것입니다.

댓글

이 블로그의 인기 게시물

Confluence 내의 스프레드 시트 기능이 필요하시다면 애드온을 활용해 보십시요

시스템에 숨어있는 "윤초" 버그에 대해 준비하십시요

Confluence 페이지의 분류와 관련된 잘 몰랐던 기능 3가지를 확인해 보십시요