jQuery 에서는 CSS 셀렉터 와 엘리먼트를 선택해 처리하는 방법을 사용합니다.
그러나 이런 CSS 셀렉터 사용법을 좀더 잘 사용하면 처리속도의 향상을 가져옵니다.
여기에서 jQuery의 내부 처리와 같은 코드를 보고, jQuery를 빠르게 사용하기 위한 TIP 5 를
소개하고자 합니다.
1. 여러번 같은 실렉터를 실행하지 않는다.
2. 클래스만으로 지정하는 것을 금지한다.
3. #id 의 사용을 적극 권장한다.
4. 도중까지 결과를 재이용 한다.
5. 자식 셀렉터(>)를 잘 사용하면 처리가 빨라진다.
1. 여러번 같은 실렉터를 실행하지 않는다.
ㅁ 개선전
// 예제1
$("div.foo").addClass("bar");
$("div.foo").css("background","#ffffff");
$("div.foo").click(function(){ alert('foo'); } );
$("div.foo").addClass("bar");
$("div.foo").css("background","#ffffff");
$("div.foo").click(function(){ alert('foo'); } );
ㅁ 문제점
jQuery는 CSS 셀렉터를 쓸 때마다 DOM에서 셀럭터와 일치하는 엘리먼트를 찾습니다.
$("div.foo") 실행하면 뒤에서 jQuery는 아래과 같은 작업을 처리합니다.
// 셀렉터로 선택한 결과를 저장하는 배열
var ret = [];
// div 태그 목록을 열거
var elems = document.getElementsByTagName("div");
// 각각 클래스명이 foo 의 것을 ret 에 추가
for(var i = 0; i < elems.length; i++){
var classes = elems[i].className.split(" ");
if(classes.indexOf("foo") != -1){
ret.push(elems[i]);
}
}
HTML 안에 포함됨 div 태그를 열거하고, 각 클래스명을 조사해 가는 것입니다.
(Array.indexOf (은)는 비표준입니다만, 간단하게 쓰기 위해서 사용하고 있습니다)
즉, 모두의 코드와 같이 $("div.foo") (을)를 3회 써 버리면 위에서처럼 처리가 3회 실행되어 버립니다. 비효율적이지요.
ㅁ 개선방법1: 캐쉬
셀렉터의 실행 결과를 변수에 캐쉬해 둡니다. 2회 분의 $("div.foo") 실행시간을 단축시킬 수 있습니다.
// 코드 1-1
var foos = $("div.foo");
foos.addClass("bar");
foos.css("background", "#ffffff");
foos.click(function(){alert('foo');});
var foos = $("div.foo");
foos.addClass("bar");
foos.css("background", "#ffffff");
foos.click(function(){alert('foo');});
ㅁ 개선방법2: 메소드 체인
메소드 체인을 사용하면, jQuery 같아지고 처리 효율도 오릅니다.
// 코드 1-2
$("div.foo")
.addClass("bar")
.css("background", "#ffffff")
.click(function(){alert('foo');});
$("div.foo")
.addClass("bar")
.css("background", "#ffffff")
.click(function(){alert('foo');});
$("div.foo")실렉터의 실행 결과가 다음의 메소드에 차례로 인계됩니다. 임시 변수를 필요로 하지 않는 것도 장점입니다.
2. 클래스만을 지정하는 것은 금지
ㅁ 개선전
// 예제 2
$(".foo").css("display", "none");
$(".foo").css("display", "none");
ㅁ 문제점
클래스명 만을 지정하면, jQuery는 모든 HTML 노드를 열거하고, 각 클래스명을 확인합니다.
$(".foo") 의 배후는 다음과 같은 처리가 실행됩니다.
// 셀렉터로 선택한 결과를 저장할 배열
var ret = [];
// 모든 태그를 열거한다
var elems = document.getElementsByTagName("*");
// 각 클래스명이 foo 의 것을 ret 에 넣는다
for(var i = 0; i < elems.length; i++){
var classes = elems[i].className.split(" ");
if(classes.indexOf("foo") != -1){
ret.push(elems[i]);
}
}
var ret = [];
// 모든 태그를 열거한다
var elems = document.getElementsByTagName("*");
// 각 클래스명이 foo 의 것을 ret 에 넣는다
for(var i = 0; i < elems.length; i++){
var classes = elems[i].className.split(" ");
if(classes.indexOf("foo") != -1){
ret.push(elems[i]);
}
}
모든 태그를 열거하여 루프를 돌리기 때문에 비효율적입니다.
조금, 이야기가 빗나가지만 Firefox3 나 Opera9.5, Safari3 에는 getElementsByClassName() 메소드가 기본으로 포함되어 있습니다. 그 때문에 이러한브라우저는 빠른 $(".foo") 를 실행할 수 있는 능력을 가지고 있습니다. 그러나, jQuery 1.2.6 의 시점에서는 기본 getElementsByClassName()을 사용하고 있지 않습니다.
jQuery 의 차기 CSS 셀렉터인 Sizzle는 getElementsByClassName()가 정의되는 경우에 사용하도록 구현되어있는 것 같습니다.
ㅁ 개선방법: 태그를 명시 한다
태그를 명시합니다.
$("div.foo").css("display", "none");
모든 노드로부터가 아닌, 지정된 태그중에서 클래스명으로 검색이 좁히게 되어, 루프의 회수가 큰폭으로 줄어듭니다.
3. #id 를 적극적으로 사용한다
ㅁ개선전
<body>
<script src="jquery.js"></script>
<script>
$(function(){
$(".main").css("color", "red"); //문제 부분
});
</script>
<div class="main">
< ... >
</div>
</body>
<script src="jquery.js"></script>
<script>
$(function(){
$(".main").css("color", "red"); //문제 부분
});
</script>
<div class="main">
< ... >
</div>
</body>
ㅁ 문제점
반복적으로 언급해온 대로 jQuery는 클래스 이름으로 검색하는 것은 비효율적이다.
HTML 설계의 이야기가 되어 버립니다만, HTML 중 1번만 등장하지 않는 클래스명은 id 화 하는게 좋습니다. 그 편이 JavaScript 에서 취급하기에도 형편상 좋습니다.
ㅁ 개선방법
main 을 클래스가 아닌 id 로 변경합니다.
<body>
<script src="jquery.js"></script>
<script>
$(function(){
$("#main").css("color", "red"); // 해결부분
});
</script>
<div id="main">
< ... >
</div>
</body>
jQuery는 셀렉터에 id 가 지정되어 있을 경우에는 재귀적으로 탐색하지 않고 getElementById() 를 이용합니다. 따라서 모든 노드를 열거하는데 비해 비교적 빠른 처리를 할 수 있습니다.<script src="jquery.js"></script>
<script>
$(function(){
$("#main").css("color", "red"); // 해결부분
});
</script>
<div id="main">
< ... >
</div>
</body>
4. 도중까지의 결과를 재이용한다
ㅁ 개선전
<body>
<script src="jquery.js"></script>
<script>
$(function(){
$("#main div.entry").css( ... );
$("#main div.entry div.body") // 문제부분
.css( ... );
});
</script>
<div id="main">
<div class="entry">
<div class="header"> ... </div>
<div class="body"> ... </div>
</div>
<div class="entry">
<div class="header"> ... </div>
<div class="body"> ... </div>
</div>
</div>
</body>
<script src="jquery.js"></script>
<script>
$(function(){
$("#main div.entry").css( ... );
$("#main div.entry div.body") // 문제부분
.css( ... );
});
</script>
<div id="main">
<div class="entry">
<div class="header"> ... </div>
<div class="body"> ... </div>
</div>
<div class="entry">
<div class="header"> ... </div>
<div class="body"> ... </div>
</div>
</div>
</body>
ㅁ 문제점
아래를 읽어 보면 이 경우에 대해 알수 있습니다.
$("#main div.entry") // (1)
$("#main div.entry div.body") // (2)
$("#main div.entry div.body") // (2)
(2) 의 셀렉터에서는,
1. #main 을 탐색
2. 그 자손으로부터 div.entry 를 열거
3. 그 자손으로부터 div.body 를 열거
는 작업을 실행합니다. 이 중 1~2는 (1)(과)과 완전히 같은 처리입니다. (1)에서 구한 결과를 다시 사용하면 처리속도가 향상할 것입니다.
ㅁ 개선방법 : 캐쉬 전략
var entries = $("#main div.entry").css( ... );
$("div.body", entries).css( ... );
$("div.body", entries).css( ... );
$() 함수의 제2 인수에는 CSS 셀렉터를 찾는 기점을 지정할 수 있습니다. (1)의 결과에 포함된 엘리먼트의 자식에서 div.body 를 찾아 줍니다.
find() 메소드를 사용해도 괜찮을 것입니다.
var entries = $("#main div.entry").css( ... );
entries.find("div.body").css( ... );
entries.find("div.body").css( ... );
어라? 여기까지 오면 메소드 체인이 생길 것 같네요.
$("#main div.entry").css( ... )
.find("div.body").css( ... );
.find("div.body").css( ... );
ㅁ 응용 사례
div.head 도 찾고 싶은 경우에는 어떻게 하면 좋을 까요?
네, 이렇게 하면 좋겠네요.
var entries = $("#main div.entry").css( ... );
entries.find("div.body").css( ... );
entries.find("div.head").css( ... );
entries.find("div.body").css( ... );
entries.find("div.head").css( ... );
이 녀석도 메소드 체인 해보죠. end()를 사용하면, find()로 찾기 이전 상태에 되돌릴 수 있습니다.
$("#main div.entry")
.css( ... );
.find("div.body") // #main div.entry div.body 가 된다
.css( ... )
.end() // #main div.entry로 돌아온다
.find("div.head") // #main div.entry div.head 가 된다
.css( ... )
.end();
.css( ... );
.find("div.body") // #main div.entry div.body 가 된다
.css( ... )
.end() // #main div.entry로 돌아온다
.find("div.head") // #main div.entry div.head 가 된다
.css( ... )
.end();
여기까지 오면 곡예수준입니다만.....윈래 코드보다 더 빠르다는것은 틀림없습니다.
5. 자식 셀렉터(>)를 사용하면 빨라질수 있다.
ㅁ 개선전
<body>
<script src="jquery.js"></script>
<script>
$(function(){
$("#main div.entry").css( ... ); // ← 코코
});
</script>
<div id="main">
<div class="entry">
<div class="header"> ... </div>
<div class="body"> ... </div>
</div>
<div class="entry">
<div class="header"> ... </div>
<div class="body"> ... </div>
</div>
</div>
</body>
<script src="jquery.js"></script>
<script>
$(function(){
$("#main div.entry").css( ... ); // ← 코코
});
</script>
<div id="main">
<div class="entry">
<div class="header"> ... </div>
<div class="body"> ... </div>
</div>
<div class="entry">
<div class="header"> ... </div>
<div class="body"> ... </div>
</div>
</div>
</body>
ㅁ 문제점
' #main div.entry '는 #main 의 후에 자손 실렉터(스페이스)가 있습니다. 즉, #main 노드아래의 모든 div 노드로부터 entry 클래스를 찾아냅니다.
$("#main div.entry") 의 배후에서는 다음과 같은 처리가 실행되고 있습니다.
// 셀렉터로 선택한 결과를 저장하는 배열
var ret = [];
// #main을 찾는다
var main = document.getElementsById("main");
// #main의 아래에서 전체 div 를 열거한다
var elems = main.getElementsByTagName("div");
// 각 클래스명이 foo 의 것을 ret 에 넣는다
for(var i = 0; i < elems.length; i++){
var classes = elems[i].className.split(" ");
if(classes.indexOf("foo") != -1){
ret.push(elems[i]);
}
}
var ret = [];
// #main을 찾는다
var main = document.getElementsById("main");
// #main의 아래에서 전체 div 를 열거한다
var elems = main.getElementsByTagName("div");
// 각 클래스명이 foo 의 것을 ret 에 넣는다
for(var i = 0; i < elems.length; i++){
var classes = elems[i].className.split(" ");
if(classes.indexOf("foo") != -1){
ret.push(elems[i]);
}
}
elems 에는 div#main 의 아래의 모든 div 태그가 저장됩니다. 이 모두에 대한 클래스명을 확인하는 것이, 경우에 따라서는 늦어져 버립니다.
만약,div.entry 가 div#main 아래에만 존재한다면, "자손 실렉터"는 아니고 "자식 셀렉터"를 사용하면 효율적으로 동작할지도 모릅니다.
ㅁ개선방법
자식 셀렉터(>)를 사용합니다.
$("#main > div.entry").css( ... );
jQuery 그럼 자식 셀렉터가 나오면, 모든 자손이 아니고, 자식중에서 매치하는 것을 조사합니다. 손자와 그 자식에 대해서는 조사하지 않기 때문에, 고속화가 기대됩니다.
$("#main > div.entry") 의 배후에서는 다음과 같은 처리가 실행됩니다.
// 셀렉터로 선택한 결과를 저장하는 배열
var ret = [];
// #main 를 찾는다
var main = document.getElementsById("main");
// #main 자식 노드중에서
// 태그 이름이 DIV이고, 클래스명이 entry 의 것을 ret에 넣는다
var child = main.firstChild;
while(child){
var classes = elems[i].className.split(" ");
if (child.tagName == "DIV"
&& classes.indexOf("entry") != -1){
ret.push(child);
}
child = child.nextSibling;
}
꼭 자식 셀렉터를 사용하면 반드시 빨리된다 싶지 않지만 자식의 수에 비해 자손이 많은 경우, 자식셀럭터 쪽이 빨라집니다.var ret = [];
// #main 를 찾는다
var main = document.getElementsById("main");
// #main 자식 노드중에서
// 태그 이름이 DIV이고, 클래스명이 entry 의 것을 ret에 넣는다
var child = main.firstChild;
while(child){
var classes = elems[i].className.split(" ");
if (child.tagName == "DIV"
&& classes.indexOf("entry") != -1){
ret.push(child);
}
child = child.nextSibling;
}
ㅁ 실제 코드 시험
실제로 브라우저로 실행했을 때에 CSS 실렉터에 의해서 처리 속도가 얼마나 개선하는지를 확인해 봅시다.
시험은 블로그의 HTML 에서 실행해 보았습니다. HTML 구조는 다음과 같습니다.
<div id="days">
<div class="day">
<h2>2008년12월11일</h2>
<div class="body">
<div class="section">
<h3>타이틀</h3>
<p>본문</p>
</div>
</div>
</div>
<div class="day"> ... </div>
<div class="day"> ... </div>
</div>
<div class="day">
<h2>2008년12월11일</h2>
<div class="body">
<div class="section">
<h3>타이틀</h3>
<p>본문</p>
</div>
</div>
</div>
<div class="day"> ... </div>
<div class="day"> ... </div>
</div>
이러한 HTML 에 대해서,jQuery (을)를 실행해 보았습니다.
CSS 셀렉터 |
FireFox 2 |
IE7 | Opera9 | Safari(win) |
.body | 22.18ms | 19.85.ms | 5.32ms |
2.49ms |
div.body | 2.34.ms | 2.82.ms | 1.24ms |
0.49ms |
#days >div.day>div.body | 2.66.mx | 1.72ms | 1.25ms |
0.44ms |
* #days > div.day > div.body 는 아이 셀렉터를 2회 사용하고 있는데,
div.body 와 같을 정도의 속도로 실행 되어 있다.
마지막에
jQuery 는 라이브러리인 이상, DOM 을 직접 접근하는데 비해 늦어지는 것은 피할 수 없습니다.
아무래도 처리 속도가 신경이 쓰이는 경우는, jQuery 의 코드를 DOM 를 직접 컨트롤하는 코드로 변환하면 좋을 것입니다. 경험적으로 Firefox 나 IE 그리고 처리 속도가10배정도가 됩니다.
다만, 개발 효율설 측면에서는 처음에 jQuery 를 사용해 쓰기 시작하는 것을추천합니다.
jQuery 의 코드를 DOM 직접 변환하는 것은 간단합니다만, DOM 직접으로 개발을 진행시키는 것은 귀찮지요.