jQuery를 사용하면 좋은 JavaScript 기반 웹 애플리케이션을 쉽고 간단하게 작성할 수 있습니다. 하지만 좋은 웹 애플리케이션을 우수한 웹 애플리케이션으로 만들려면 몇 가지 추가 단계가 필요합니다. 이 기사에서는 웹 애플리케이션의 품질을 최종적으로 한 단계 더 높일 수 있는 방법에 대해 자세히 설명합니다.
이 jQuery 시리즈에서는 JavaScript 기반 웹 애플리케이션을 개발하는 과정에 대해 설명하고 있다. 이러한 기사를 읽기 전까지 jQuery에 대해 들어본 적이 없었다고 하더라도 이제는 jQuery를 사용하여 좋은 웹 애플리케이션을 개발하는 데 필요한 모든 스킬과 지식을 모두 갖추고 있어야 한다. 하지만 좋은 애플리케이션으로는 부족하여 우수한 웹 애플리케이션이 필요한 경우도 있다. 이 요구를 충족하기 위해서는 몇 가지 추가 단계가 필요하다. 이러한 단계는 개발자의 애플리케이션에 추가되어 크고 작은 애플리케이션에서 원활하게 실행되면서 모든 사용자에게 적합한 기능을 제공하기 위한 것이다. 이러한 단계는 웹 애플리케이션이 반짝반짝 빛나도록 광택을 내는 최종 단계의 작업이다.
이 기사에서는 코드의 성능을 향상시키는 방법에 대해 살펴본 후 jQuery 라이브러리에서 자주 다루어지지 않고 있는 주제에 대해서도 설명한다. 이러한 항목은 복잡한 애플리케이션에서 중요한 역할을 수행하게 되는 플러그인으로 모든 애플리케이션에 필요한 항목이며 웹 애플리케이션 코드를 쉽게 작성하는 데 도움이 되는 좋은 설계 팁이기도 한다. 그리고 마지막 섹션에서는 최근에 릴리스되어 라이브러리에 몇 가지 새 기능이 추가된 jQuery 1.3의 새 기능에 대해 설명한다.
이 기사에서 제공하는 대부분의 팁은 이 기사에 첨부된 샘플 애플리케이션(간단한 이메일 웹 애플리케이션)에서도 볼 수 있다(다운로드 참조). 이 애플리케이션은 시리즈의 첫 번째 기사에서 제공한 애플리케이션과 같기 때문에 익숙할 것이다. 하지만 첫 번째 기사의 애플리케이션과 비교하여 얼마나 달라졌는지, 성능은 얼마나 향상되었는지 그리고 진정으로 우수한 웹 애플리케이션이 되기 위해 어떠한 광택 작업이 수행되었는지 등을 알아야 한다.
Events 모듈에는 bind()
와 unbind()
라는 두 함수가 있다. 하지만 이들 함수를 사용하면 다른 모든 이벤트 메소드에 대한 작업이 중복되는 결과가 나타난다. 결국, click()
메소드를 페이지 요소에 간단히 연결할 수 있다면 bind("click")
를 호출하는 것은 키 입력만 낭비하는 것에 불과할 것이다. 물론 이들 함수는 특정 상황에서 유용하게 사용할 수 있으며 올바르게 사용하면 애플리케이션의 성능을 획기적으로 향상시킬 수도 있다. 이들 함수는 모듈에 있는 수많은 이벤트 메소드와 마찬가지로 이벤트를 특정 페이지 요소에 연결하는 기능을 제공한다. 하지만 이들 함수는 페이지 요소에서 이벤트를 제거하는 기능도 제공한다. 이렇게 하려는 이유가 무엇일까? 이제 웹 애플리케이션을 보면서 이 상황에서 이들 함수를 사용하는 방법을 살펴보자.
Listing 1에서는 성능 개선을 위해 수정하기 전인 최초 코드를 보여 주며 앞으로 이 코드를 수정해 나갈 것이다.
$(document).ready(function(){ // cache this query since it's a search by CLASS selectable = $(":checked.selectable"); // when the select/deselect all is clicked, do this function $("#selectall").click(selectAll); // whenever any individual checkbox is checked, change the text // describing how many are checked selectable.click(changeNumFilters); // calculate how many are initially checked changeNumFilters(); }); var selectable; function changeNumFilters() { // this needs to be checked on every call // since the length can change with every click var size = $(":checked.selectable").length; if (size > 0) $("#selectedCount").html(size); else $("#selectedCount").html("0"); } // handles the select/deselect of all checkboxes function selectAll() { var checked = $("#selectall").attr("checked"); selectable.each(function(){ var subChecked = $(this).attr("checked"); if (subChecked != checked) { $(this).click(); } }); changeNumFilters(); } |
이 코드는 몇몇 기사에서 필자가 작업했던 위젯과 거의 비슷하기 때문에 비교적 간단한 편이다. 필자는 첫 번째 기사에서 기본적인 형태의 "select/deselect all" 위젯을 보여 주었다. 성능 기사에서는 선택 쿼리를 캐시하고 CLASS를 이용한 검색을 최소화하여 성능을 향상시키는 방법을 살펴보았다. 그래도 아직까지 문제가 남아 있다. 100개의 행으로 구성된 테이블에서 "select/deselect all" 선택란을 눌러보면 성능이 매우 나쁘다는 것을 알 수 있다. 실제로 필자의 브라우저에서 이 코드를 실험해 본 결과 모든 항목을 선택하는 데 평균 3.4초의 시간이 걸렸다. 이는 응답성이 좋다고 보기에는 어려운 결과이다. 또한 이 문제에 대한 모든 최적화 작업을 수행하고난 후에도 성능이 좋다고 보긴 어려울 것이다.
알고리즘을 좀 더 자세히 살펴보면서 잘못된 부분이 있는지 찾아보자. 이 코드에서는 페이지의 모든 선택란을 반복하면서 선택란의 현재 "checked" 상태가 "select/deselect all" 선택란과 같은지 여부를 확인하고 있다. 같지 않으면 해당 선택란에 대한 "click"을 호출하여 "select/deselect all" 선택란의 상태와 일치시킨다. 여기서 잠깐 생각해 보자. 이들 선택란에도 함수가 연결되어 있기 때문에 각 click에서 changeNumFilters()
함수가 호출된다. 이 알고리즘을 좀 더 자세히 살펴보면 changeNumFilters()
가 101번까지도 호출될 수 있다. 이제 성능이 좋지 못했던 원인을 어느 정도 이해할 수 있을 것이다. 실제로, 클릭할 때마다 선택된 메시지의 수를 업데이트할 필요 없이 프로세스의 끝에서 한 번만 실행해도 충분하다. 그렇다면 선택란을 클릭할 때 이 메소드가 호출되지 않게 하려면 어떻게 해야 할까?
물론 여기에서 unbind()
메소드를 사용할 수 있다. 아마도 이미 알고 있었을 것이다. 선택란을 클릭하기 전에 unbind()
를 호출하면 click()
호출에서 changeNumFilter()
메소드가 호출되지 않는다. 정말 좋은 방법이다. 이것이 바로 여러분의 목표였다. 이젠 더 이상 101번씩 호출되지는 않을 것이다. 하지만 이제부터는 한 번만 실행되기 때문에 선택란에 대한 click 호출을 완료한 후에 bind 메소드를 사용하여 각 선택란에 click 메소드를 다시 연결해야 한다. Listing 2에서 업데이트된 버전을 보여 준다.
// handles the selection/unselection of all checkboxes function selectAll() { var checked = $("#selectall").attr("checked"); selectable.unbind("click", changeNumFilters); selectable.each(function(){ var subChecked = $(this).attr("checked"); if (subChecked != checked) { $(this).click(); } }); selectable.bind("click", changeNumFilters); changeNumFilters(); } |
이러한 최적화가 적용되면 선택란 실행 시간이 약 900ms로 단축되면서 상당한 성능 향상을 얻을 수 있다. 지금까지 수행한 작업은 알고리즘의 작동 방법을 정확히 살펴본 후 코드에서 문제가 있는 부분을 찾아내서 해결한 것이 전부였다. 함수를 100번 호출하는 대신 한 번만 호출해도 원하는 결과를 얻을 수 있었다. 정말 멋진 일이다. 이 함수의 동작을 자세히 분석하고 고민한 결과 더 빠르고 효과적인 함수를 만들어낼 수 있었다. 하지만 지금까지 살펴보았던 이 알고리즘을 지금보다도 훨씬 더 빠르게 실행할 수 있는 방법이 있다는 사실이 밝혀졌다. 이 빠른 알고리즘을 바로 알려줬다면 이 함수를 기사의 재료로 사용할 수 없었을 것이다. 다행히 이러한 상황을 예제로 사용할 수 있었기 때문에 코드에서 bind/unbind 기능을 사용했을 때의 효과를 한 눈에 볼 수 있었던 것이다.
중요: 기본 이벤트를 실행하지 않고 싶거나 페이지 요소에 이벤트를 임시로 연결 또는 제거하려는 상황에서 bind/unbind를 사용한다.
Listing 3에서는 사용자의 코드에 이 위젯이 있을 경우 이 알고리즘을 매우 빠르게 작성할 수 있는 방법을 보여 준다. 여기서는 지금까지 시도했던 다른 방법보다 월등하게 빠른 속도인 40ms 이내로 실행된다.
function selectAll() { var checked = $("#selectall").attr("checked"); selectable.each(function(){ $(this).attr("checked", checked); }); changeNumFilters(); } |
1.3 버전의 jQuery에 추가된 새 기능 중 live()
및 die()
함수는 아주 좋은 기능으로 평가되고 있다. 잘 설계된 웹 애플리케이션에서 이들 함수가 수행하는 역할을 이해하기 위해서는 예제를 실행해보는 것이 가장 효과적인 방법일 것이다. 테이블의 모든 셀에 두 번 클릭을 연결하는 경우를 가정해 보자. 숙련된 jQuery 개발자라면 document.ready()
함수에 Listing 4와 같은 방법으로 이 설정을 작성해야 한다는 것을 알고 있을 것이다.
$("tr.messageRow").dblclick(function() { if ($(this).hasClass("mail_unread")) { $(this).removeClass("mail_unread"); } }); |
이 설계에는 한 가지 문제점이 있다. 그 문제점은 바로 messageRow
클래스를 사용하여 테이블의 모든 행에 두 번 클릭 이벤트를 연결하고 있다는 것이다. 하지만 새 행을 테이블에 추가하게 되면 어떻게 될까? 예를 들어, 페이지가 다시 로드되지 않은 상태에서 추가 메시지가 Ajax를 통해 페이지에 로드된 경우 그러한 행을 볼 수 있다. 이 경우 코드가 작성된 대로 작동하지 않기 때문에 문제가 발생한다. 작성해 놓은 이벤트는 페이지가 로드될 때 표시되는 모든 기존 tr.messageRow
요소에 바인딩되어 있지만 페이지의 수명 시간 동안 작성한 새 tr.messageRow
에는 바인딩되어 있지 않다. 이와 같은 코드를 작성한 개발자는 코드가 작동하지 않기 때문에 실망이 클 것이다. 초보 jQuery 프로그래머라면 코드가 작동하지 않은 이유를 찾기 위해 여러 시간 동안 골치 아픈 디버깅 작업에 매달린 후에야 jQuery 설명서에서 이 사실을 우연히 발견하게 될지도 모른다. (이 모습이 바로 필자의 작년 모습이다.)
jQuery 1.3 전까지는 이 문제점을 세 가지 방법으로 해결했었지만 세 방법 모두 좋은 방법이라고 하기에는 무리가 있었다. (하지만 계속해서 jQuery 1.2.x를 사용하는 개발자에게는 여전히 유효한 방법이다.) 첫 번째 방법은 새 메시지가 추가될 때마다 선택된 요소에 이벤트를 다시 연결하는 재초기화 기법이었다. 두 번째 방법은 이전 섹션에서 살펴본 bind/unbind 메소드를 활용하는 방법이었다. Listing 5에서 이 두 방법을 보여 준다.
// first technique to deal with "hot" page elements, added during the page's // lifetime $("#mailtable tr #"+message.id).addClass("messageRow") .dblclick(function() { if ($(this).hasClass("mail_unread")) { $(this).removeClass("mail_unread"); } // second technique to deal with "hot" page elements $("#mailtable tr #"+message.id).addClass("messageRow") .bind("dblclick", (function() { if ($(this).hasClass("mail_unread")) { $(this).removeClass("mail_unread"); } |
두 방법 모두 비효율적이라고 말할 수 있을 것이다. 코드를 반복해야 하고 새 페이지 요소가 추가될 가능성이 있는 페이지의 모든 지점을 찾은 후 해당 지점에서 "뜨거운 요소(hot element)" 문제를 처리해야 한다. 이는 좋은 프로그래밍 방법이 아니다. 게다가 지금 사용하고 있는 언어는 jQuery이다. 이 언어는 모든 작업을 쉽게 처리할 수 있을 뿐 아니라 필요한 모든 기능을 제공할 수 있어야 한다.
다행스럽게도 이러한 문제점을 해결할 수 있는 것처럼 보이는 플러그인이 있었다. 이 플러그인의 이름은 LiveQuery 플러그인이며 특정 페이지 요소를 이벤트에 바인딩하는 기능을 제공한다. 하지만 이 기능은 "live" 패션에서만 사용할 수 있다. 따라서 페이지 작성 시에 있던 요소와 페이지의 수명 시간 동안에 작성된 요소를 포함한 모든 페이지 요소가 이벤트를 발생시킬 수 있다. 이 플러그인을 사용하면 정적 페이지 작업을 수행할 때와 마찬가지로 동적 페이지 작업을 쉽게 수행할 수 있으므로 이 플러그인은 UI 개발자에게 매우 유용하고 중요한 플러그인이었다. 웹 개발자에게 이 플러그인은 실제로 "반드시 가지고 있어야 하는" 플러그인 중 하나였다.
jQuery 코어 팀에서는 이 플러그인을 1.3 릴리스에 통합하면서 그 중요성을 확실하게 인정했다. 이 "live" 기능은 이제 jQuery 코어에 포함되어 있으므로 모든 개발자가 활용할 수 있다. 이 플러그인은 1.3의 최초 릴리스에 채택되지 않은 몇 가지 이벤트를 제외하면 1.3 코어 코드에서 거의 정확히 중복된다. 필자의 견해로는 누락된 이벤트가 jQuery의 후속 릴리스에서 등장할 것이라고 확신한다. 이 플러그인을 사용하여 코드를 변경하는 방법을 살펴보자.
$("tr.messageRow").live("dblclick", function() { if ($(this).hasClass("mail_unread")) { $(this).removeClass("mail_unread"); } |
이 작은 변경 사항을 코드에 적용하게 되면 페이지에 있는 모든 tr.messageRow
요소를 두 번 클릭할 때 이 코드가 트리거된다. 앞에서 설명했던 것처럼 이 동작은 dblclick()
함수를 사용했을 때는 볼 수 없었던 동작이다. 따라서 대부분의 이벤트 메소드에 live()
메소드를 사용하는 것이 매우 효과적이라는 것을 알 수 있다. 실제로 필자는 Ajax 또는 사용자 상호 작용을 통해 페이지 요소를 동적으로 작성하는 모든 페이지에서는 대체 이벤트 메소드 대신 live()
함수를 사용해야 한다고 생각한다. 이는 발생할 수 있는 버그를 버리고 코드를 쉽게 작성할 수 있는 효과적인 방법이기에 주저할 필요 없이 선택해야 한다.
중요: 이벤트를 동적 페이지 요소에 연결할 대는 항상 live()
메소드를 사용한다. 이 메소드를 사용하면 이벤트도 페이지 요소처럼 동적으로 생성할 수 있다.
서버에 대한 Ajax 호출의 사용 여부는 Web 2.0 회사의 자체 평가 척도가 되었다. 이 기사에서 자주 언급했던 것처럼 jQuery에서 Ajax를 사용하는 방법은 일반적인 메소드 호출을 호출하는 것과 마찬가지로 쉽다. 즉, 클라이언트측 JavaScript 함수를 호출하듯이 서버측 Ajax 함수를 호출할 수 있다. Ajax의 장점이 많기는 하지만 서버에 대한 Ajax 호출이 너무 많을 경우 약간의 부작용이 발생할 수 있다. 웹 애플리케이션에 Ajax 호출이 너무 많으면 문제가 발생할 가능성이 매우 높아진다.
첫 번째 문제점은 일부 브라우저에서는 열린 서버 연결의 수가 제한된다는 것이다. Internet Explorer의 현재 버전에서는 서버에 대한 열린 동시 연결 수가 2개만 허용된다. Firefox에서는 8개의 연결이 허용되지만 여전히 열린 연결 수에 대한 제한이 있다. 웹 애플리케이션에서 Ajax 호출에 대한 제어를 놓칠 경우, 특히 서버측 호출이 시간을 많이 사용하는 호출일 경우 열린 연결의 수가 2개를 초과하게 된다. 이 문제점은 웹 애플리케이션 설계자가 설계를 잘못했거나 사용자가 요청에 대한 제어를 놓쳤을 경우에 발생할 수 있다. 두 경우 모두 좋은 상태가 아니라는 점은 확실하며 브라우저에서 허용할 연결과 허용하지 않을 연결을 결정해야 하는 상황도 바람직한 상황이 아니다.
게다가 비동기(Ajax의 "A") 호출이기 때문에 사용자가 보낸 요청의 순서와 같은 순서로 서버에서 리턴한다는 보장이 없다. 좀 더 자세히 생각해 보자. 이는 거의 동시에 2개의 Ajax 호출을 실행할 경우 서버의 응답이 호출 순서대로 돌아온다고 확신할 수 없다는 말이다. 따라서 두 번째 호출이 첫 번째 호출에 종속되어 있어서 첫 번째 호출이 먼저 종료되어야 한다면 오류가 발생할 수 있다. 첫 번째 호출이 데이터를 검색하고 두 번째 호출이 클라이언트측에서 이 데이터를 처리하는 시나리오를 가정해 보자. 두 번째 호출이 첫 번째 Ajax 호출보다 빨리 리턴된다면 오류가 발생할 수 있다. 이는 응답 속도를 보장할 수 없기 때문이다. 이 문제가 4번 정도 더 발생하면 문제가 훨씬 더 빨리 시작된다는 것을 알 수 있다.
jQuery 제작자는 이 문제점의 가능성을 인식하고 있었으며 당시 약 1%의 웹 애플리케이션에만 발생할 것이라고 예상했다. 하지만 1%에 해당하는 개발자를 위한 해결 방법이 필요했기에 그는 Ajax Queue와 Ajax Sync를 작성하여 이 문제점을 해결하는 데 사용할 수 있는 플러그인을 작성했다. Ajax Queue와 Ajax Sync는 작동 방법이 매우 비슷하다. Queue는 Ajax 호출을 한 번에 하나씩 저장하고 선행 호출이 리턴될 때까지 기다린 후 후속 호출이 시작된다. Sync는 호출을 즉시 보내지만 선행 호출이 이미 리턴된 경우에만 호출 함수에 리턴한다.
이렇게 하면 클라이언트측에서 Ajax 호출을 제어하고 응답이 클라이언트측 코드에 되돌려 보내지는 방법을 제어 및 조절하여 오버로드 문제점을 해결할 수 있다. 이제 클라이언트에서 응답이 수신되는 순서를 알 수 있으므로 이벤트의 순서를 예측할 수 있는 코드를 작성할 수 있다. 이제 Listing 7의 예제를 통해 이 플러그인의 작동 방법과 코드에서 이 플러그인을 사용하는 방법을 살펴보자. 그리고 이 플러그인이 여러 Ajax 호출이 선행 Ajax 호출에 종속되는 중요 정보를 가지고 있는 1%만을 위한 것임을 염두에 두자. 이 예제는 그러한 상황을 보여 주는 예제는 아니지만 플러그인을 사용할 수 있는 방법을 보여 준다. (이 플러그인이 필요한 실제 상황을 반영하면서도 이해하기 쉬운 좋은 예제를 작성하기는 어려울 것이다.)
var newRow = "<tr id='?'>" + "<td><input type=checkbox value='?'></td>" + "<td>?</td>" + "<td>?</td>" + "<td>?</td>" + "<td>?</td></tr>"; $("#mailtable").everyTime(30000, "checkForMail", function(){ // by using the Ajax Queue here, we can be sure that we will check for mail // every 30 seconds, but ONLY if the previous mail check has already returned. // This actually would be beneficial in a mail application, if one check for // new mail takes longer than 30 seconds to respond, we wouldn't want the // next Ajax call to kick off, because it might duplicate messages (depending // on the server side code). // So, by using the Ajax Queue plug-in, we can ensure that our Web client // is only checking for new mail once, and will never overlap itself. $.ajaxQueue({ url: "check_for_mail.jsp", success: function(data) { var message = eval('(' + data + ')'); if (message.id != 0) { var row = newRow.replace("?", message.id); row = row.replace("?", message.id); row = row.replace("?", message.to); row = row.replace("?", message.from); row = row.replace("?", message.subject); row = row.replace("?", message.sentTime); $("#mailtable tbody").prepend(row); $("#mailtable #"+message.id).addClass("mail_unread").addClass("messageRow"); $("#mailtable #"+message.id+ " td").addClass("mail"); $("#mailtable :checkbox").addClass("selectable"); } } }); |
중요: 애플리케이션에 서로 기능이 겹칠 수 있는 여러 Ajax 호출이 있을 경우 Ajax Queue 또는 Ajax Sync를 사용한다.
8월 25일 고급 jQuery(2)가 업데이트 됩니다.