저자: Matthew Eernisse, 한동훈 역
원문: A Simpler Ajax Path

내가 웹 응용 프로그램 분야에 일하기 시작했던 때는 매우 불운했던 시절로 돌아간다. 데스크탑 응용 프로그램처럼 동작하는 응용 프로그램을 만든다는 것은 프레임셋안에 프레임셋이 들어가고, 그 안에 다시 프레임셋이 들어간다거나 대여섯단계 이상 중첩시킨 테이블을 이용한 미궁처럼 뒤얽힌 레이아웃과 씨름하는 것을 의미했다.

다행히도 표준을 준수하는 브라우저, CSS, DHTML, DOM의 출현과 함께 웹 개발자들의 상황은 꾸준히 나아지고 있다. 광범위한 브로드밴드 액세스는 웹 응용 프로그램을 보다 빠르게 만들어주었다. 브라우저에 배포할 수 있는 보다 다양한 기능과 보다 강력한 상호운영성을 보다 쉽게 이끌어 낼 수 있는 XMLHttpRequest 객체가 등장했다.

XMLHttpRequest 객체는 새로운 것이 아니지만 최근에 웹 응용 프로그램 개발에 대한 새로운 접근방법의 핵심으로 부각되고 있다. 최근에 가장 많이 인용되고 있는 Ajax(비동기 자바스크립트와 XML)는 플리커(Flickr), 아마존의 A9.com, 웹 기반 인터액티비티(interactivity)의 떠오르는 별인 Google MapsGoogle Suggest 같은 사이트에서 기능을 십분 활용하고 있다. 멋진 Ajax 사용자들은 어떤 원동력을 갖고 있는 것처럼 보인다. - 웹로그 Ajaxian, 오라일리 미디어와 Adaptive Path가 공동으로 주관한 Ajax Summit을 포함해서 Ajax는 모든 장소에서 등장하고 있다.

멋진 약어이든 아니든간에, 나의 웹캐스트 라디오 방송국 EpiphanyRadio에 재생목록 검색 기능을 추가하려고 결정했을 때, XMLHttpRequest 객체가 제공하는 기능들을 보여줄 수 있는 좋은 기회라고 생각했다.

아니나 다를까, 데스크탑 응용 프로그램 처럼 동작하는 웹 응용 프로그램을 만들기 위해 XMLHttpRequest 객체를 이용하는 것은 너무나 쉬웠다. 물론, 사용자 입력을 위해 웹 폼과 같은 도구들도 그대로 사용할 수 있다. 또한, 골치아픈 디버깅을 줄이기 위해 서버측 에러를 처리할 수 있는 방법도 찾아낼 수 있었다.

XMLHttpRequest 객체 개요

XMLHttpRequest를 사용하면 iframe 트릭을 사용하거나 페이지를 리로드하지 않고 자바스크립트를 사용해서 서버에 HTTP 요청(GET과 POST)을 보낼 수 있다. 마이크로소프트는 인터넷 익스플로러 버전 5 부터 ActiveX 객체 형태로 XMLHttpRequest 객체를 구현했으며, 모질라 프로젝트는 모질라 1.0에, 애플은 사파리 1.2에 이를 추가했다.

이름이 의미하는 것과 달리 XMLHttpRequest 객체는 XML 이상의 기능을 수행한다. XMLHttpRequest를 사용해서 어떤 종류의 데이터든 처리할 수 있다. 염두해둘것은 서버로부터의 응답을 자바스크립트가 처리한다는 것이다. 예제에서는 브라우저에 DSV(delimiter-separated values, 구분자로 값을 분리한 형식) 형식으로 데이터를 반환한다.

POST로 폼데이터 보내기

XMLHttpRequest 객체를 다루는 다른 기사에서는 GET 요청을 어떻게 사용하는지 설명하고 있다. XMLHttpRequest 객체는 POST 요청을 사용할 수 있으며, 웹 응용 프로그램을 위한 보다 유용한 도구로 만들어준다.

POST 메서드를 사용하려면 데이터를 XMLHttpRequest 객체에 쿼리 스트링형식(예, ArtistName=Kolida&SongName=Wishes)으로 전달하고, 객체가 서버에 보통의 POST 형식으로 전송한다. 웹폼 요소로부터 한번에 데이터를 가져오고, 쿼리 스트링 형식으로 형식화하기 위해 자바스크립트를 사용한다. 나의 재생목록 검색 함수를 위해 이 같은 작업을 한다면 다음과 같은 코드를 사용할 것이다.

var searchForm = document.forms['searchForm'];
var artistName = searchForm.ArtistName.value;
var songName = searchForm.SongName.value;
var queryString = '?ArtistName=' + escape(artistName) + '&SongName=' +
escape(songName);

주의: 값에 URL 인코딩을 위해 escape를 사용해야한다.

Larry Wall의 프로그래머의 위대한 세가지 미덕을 염두에 두면서 폼에 있는 모든 데이터를 쿼리 스트링으로 바꿔주는 프로세스를 자동화하는 일반 함수를 작성했다. 이 함수를 사용하면 매번 번거로운 작업들을 하지 않고 XMLHttpRequest 객체에 폼 데이터를 POST할 수 있다. 이 같은 방법으로 기존 응용 프로그램 코드에 XMLHttpRequest 객체 사용을 보다 쉽게 통합할 수 있다.
함수의 첫부분을 살펴보자.

function formData2QueryString(docForm) {

        var strSubmit       = '';
        var formElem;
        var strLastElemName = '';
        
        for (i = 0; i < docForm.elements.length; i++) {
                formElem = docForm.elements[i];
                switch (formElem.type) {
                        // Text, select, hidden, password, textarea elements
                        case 'text':
                        case 'select-one':
                        case 'hidden':
                        case 'password':
                        case 'textarea':
                                strSubmit += formElem.name +
                                '=' + escape(formElem.value) + '&'
                        break;

변수 docForm은 폼에 대한 참조이며, 데이터를 가져오기 위해 사용한다. 이런 방식은 다양한 장소에서 함수를 재사용할 수 있게 해준다.
이 함수는 폼의 elements 컬렉션을 반복하면서 각 element의 type을 사용해서 어떻게 값을 가져올지를 결정한다. 각 요소들에 대해서 쿼리 스트링 변수 strSubmit에 이름과 값을 저장한다. 그 다음에 POST를 위해 XMLHttpRequest 객체에 이 문자열을 전달한다.

폼 요소에 쓰이는 대부분의 타입들은 value 속성을 사용해서 값을 가져올 수 있지만 라디오 버튼과 체크 박스는 다른 작업을 해야한다. 체크 박스의 경우에 콤마(,)로 값을 구분하는 문자열을 만들었지만, 필요하다면 원하는 형태로 문자열을 처리할 수 있다. 이 함수를 사용하면 XMLHttpRequest 객체를 사용하는 것과 폼에서 사용자 데이터를 가져오는 데 있어 많은 시간을 절약할 수 있다.

전체 함수는 다운로드할 수 있다.

객체 생성하기

자바스크립트로 IE에서 객체를 생성하려면 new ActiveXObject( "Microsoft.XMLHTTP")를 사용하며, 모질라/파이어폭스/사파리에서는 new XMLHttpRequest()를 사용한다. 함수의 처음 절반은 객체를 생성하고 사용하는 부분이다.

function xmlhttpPost(strURL, strSubmit, strResultFunc) {

        var xmlHttpReq = false;
        
        // Mozilla/Safari
        if (window.XMLHttpRequest) {
                xmlHttpReq = new XMLHttpRequest();
                xmlHttpReq.overrideMimeType('text/xml');
        }
        // IE
        else if (window.ActiveXObject) {
                xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
        }

이 함수를 재사용할 수 있는 일반 함수로 만들기 위해 세 개의 매개변수를 사용한다. 서버에서 처리중인 페이지에 대한 URL, 데이터를 전송하기 위해 쿼리 스트링 형식으로 저장된 데이터, 서버로부터의 응답을 처리하기 위한 자바스크립트 함수의 이름(나중에 eval을 사용해서 호출된다)을 매개변수로 사용한다.

모질라/사파리 코드에서 overrideMimeType 메서드를 호출하는 것을 볼 수 있다. 이 부분이 없으면 서버에서 XML아 아닌 것을 반환할 때 특정 버전의 모질라가 멈춰버리는 것을 일부 사람들이 경험했다고 한다.(개인적으로는 이 문제가 발생하지 않았기 때문에 이 문제를 확인할 수는 없다)

만약, 보다 낡은 버전의 브라우저들을 지원하려 한다면 xmlHttpReq 변수를 테스트하고, 객체가 존재하지 않으면 전송하려는 데이터를 다른 메서드에 전달하면 된다.

데이터를 POST하기

함수의 나머지 부분은 서버에 요청을 전송하는 부분이다.

     xmlHttpReq.open('POST', strURL, true);
        xmlHttpReq.setRequestHeader('Content-Type',
                'application/x-www-form-urlencoded');
        xmlHttpReq.onreadystatechange = function() {
                if (xmlHttpReq.readyState == 4) {
                        eval(strResultFunc + '(xmlHttpReq.responseText;);');
                }
        }
        xmlHttpReq.send(strSubmit);
}

open 메서드는 매개변수를 세개 사용한다. 첫번째는 요청 방법을 지정하고, 두번째는 처리중인 페이지이며, 세번째는 async 플래그를 지정한다. 이 플래그는 요청을 보낸후에 즉시 실행을 계속할 것인지 또는 응답을 받은 후에 계속 실행할 것인지를 결정한다.
참고: HTTP 1.1 스펙에 따르면, 요청 메서드는 대소문자를 구분한다. 요청 메서드를 소문자로 입력한 경우에 인터넷 익스플로러를 사용할 때는 문제가 되지 않지만 모질라에서는 요청이 실패한다.

onreadystatechange 속성은 readyState 속성이 바뀔 때 실행할 콜백 함수를 지정한다.(예를 들어, xmlHttpReq.onreadystatechange = handleResponse; ) readyState 값이 4로 변경될 때 요청이 완료된다. 위 코드는 결과를 전달할 별도의 함수를 사용하는 대신에 익명 함수를 사용하여 응답이 돌아오고, 결과값을 처리할 함수에 문자열로 전달할 때까지 기다린다.

마지막으로 send 메서드가 실제 요청을 전송한다. 서버에 전송할 데이터를 매개변수로 사용한다. 이 함수에서는 서버에 전송할 데이터는 앞에서 formData2QueryString 함수를 사용해서 폼 데이터에서 생성한 쿼리 스트링이다.

서버 응답

앞에서 언급한 것처럼 XMLHttpRequest 객체는 이름이 의미하는 것과는 달리 XML이 아닌 다른 형식의 데이터를 이용할 수 있다. 내 인터넷 라디오 방송국을 위한 검색 함수는 간단한 테이블 형식으로 트랙목록을 반환하기 때문에 다른 데이터 형식을 사용할 수 있다는 점이 마음에 든다. XML 형식 대신에 DSV 형식으로 결과를 반환하는 것은 반환데이터의 크기와 복잡도를 획기적으로 줄여줄 뿐만 아니라 데이터를 해석하는 것도 단순하게 해준다.(에릭 레이몬드(Eric S. Raymond)가 The Art of Unix Programming의 Data File Metaformats 챕터에서 얘기한 것처럼 "XML은 복잡한 데이터 형식에 잘 어울리지만... 그럼에도 불구하고 단순한 용도로는 지나치게 복잡하다) 구분자로 무엇이든 사용할 수 있지만 여기서는 파이프(|) 문자를 사용했다.

재생목록 검색 기능을 위한 백엔드 코드로는 대부분의 사이트와 마찬가지로 mod_ruby로 운영중인 Ruby를 사용했다. 루비는 PHP나 Perl과 같이 개발자들에게 대중적이지 않지만 루비의 유연성, 확장성, 깔끔한 구문은 이상적인 웹 개발 플랫폼이 될 수 있다.

nRowCount    = sth.size
strContent += nRowCount.to_s + 10.chr + 10.chr
sth.each do |row|
        strContent += row['artist'] + '|' + row['song'] + '|' +
                row['album'] + '|' + row['comment'] + 10.chr
end

이 예제에서 sth는 Ruby DBI 모듈에서 수행되는 데이터베이스 쿼리 결과를 갖고 있는 배열이다. sth.each do |row|는 루비 개발자가 아닌 사람들에게는 낯설어 보이지만, 루비가 제공하는 가장 강력한 기능중에 하나인 iterator/block 조합을 사용한 것이다. 여기서는 독자가 추측한 것처럼 다른 언어의 foreach와 상당히 비숫하다.

10.chr는 라인피드 문자다. 이 코드는 로우 카운트 다음에 라인피드 문자를 2개 출력하고, 각 로우에 대해 파이프(|) 문자로 필드를 구분한 문자열을 작성한다. 예제 검색 결과는 다음과 같다.

4

Kush|New Life With Electricity|The Temptation Sessions||
Kush|Plaster Paris (Part Two)|The Temptation Sessions||
Kush|Reverse (Part One)|The Temptation Sessions||
Kush|The Beauty of Machines at Work|The Temptation Sessions||

마지막에 파이프(|) 문자 2개는 comment 컬럼에 대한 빈필드를 가리킨다.

응답 처리하기

서버에서 응답이 돌아올 때 XMLHttpRequest 객체는 responseXML(XML 문서 형태)와 responseText(문자열) 속성을 사용해서 액세스할 수 있다. 여기서는 XML의 복잡한 부분은 생략했기 때문에, 코드는 원래 함수로부터 해당 라인과 함께 반환된 데이터를 처리하고 표시하기 위한 자바스크립트에서 responseText를 전달한다.

eval(strResultFunc + '(xmlHttpReq.responseText;);');

xmlhttpPost 함수에서 전달된 함수 이름은 eval을 사용해서 실행하고, XMLHttpRequest 객체의 매개변수로 responseText를 전달한다.
코드에서 문자열을 배열로 자른후에, 테이블 형태로 보여주기 위해 다양한 방법들을 사용할 수 있다. DOM의 테이블 조작 방법에 대한 대단한 팬이 아닌한 innterHTML을 사용하는 보다 쉬운 접근방법을 택했다.(DOM은 XML과 마찬가지로 내 취향에는 지나치게 장황하다) 재생목록 검색 결과를 처리하기 위해 사용한 자바스크립트는 다음과 같다.

function displayResult(strIn) {

        var strContent = '<table>';
        var strPrompt = '';
        var nRowCount = 0;
        var strResponseArray;
        var strContentArray;
        var objTrack;
        
        // Split row count / main results
        strResponseArray = strIn.split('nn');
        
        // Get row count, set prompt text
        nRowCount = strResponseArray[0];
        strPrompt = nRowCount + ' row(s) returned.';
        
        // Actual records are in second array item --
        // Split them into the array of DB rows
        strContentArray = strResponseArray[1].split('n');
        
        // Create table rows
        for (var i = 0; i < strContentArray.length-1; i++) {
                // Create track object for each row
                objTrack = new trackListing(strContentArray[i]);
                // ----------
                // Add code here to create rows --
                // with objTrack.arist, objTrack.title, etc.
                // ----------
        }
        
        strContent += '</table>';
        // ----------
        // Use innerHTML to display the prompt with rowcount and results
        // ----------
}

서버에 있는 루비 코드는 라인피드 문자 2개를 사용해서 실제 데이터가 있는 로우와 로우 카운트를 분리시켰다. 따라서, 이 함수는 전체 결과를 라인피드 문자 2개로 자른 다음에 배열로 사용했다.

배열에서 실제 데이터 로우는 두번째 항목에 있다. 데이터 로우는 로우를 구분하는 라인피드를 1개씩 갖고 있기 때문에 각 항목에서 라인피드 문자 1개를 자르는 것은 페이지에 작성할 배열 데이터를 만드는 것이다. 테이블 내용을 생성하기 위해 코드는 이 배열을 반복하면서 각 로우에 대해 trackListring 객체를 생성하고, HTML 테이블 로우를 만들기 위해 객체를 사용한다. trackListring 함수는 trackListring 객체들을 생성한다.

function trackListing(strEntry) {
        var strEntryArray = strEntry.split('|');
        this.artist       = strEntryArray[0];
        this.title        = strEntryArray[1];
        this.album        = strEntryArray[2];
        this.label        = strEntryArray[3];
}

각 로우에서 파이프로 구분된 문자열을 잘라내고, 각 오트젝트에 대해 데이터베이스의 컬럼 이름에 해당하는 속성을 설정한다. 이 과정을 생략하고 각 컬럼에 대해 이름 대신 숫자를 사용할 수 있지만, 이름을 사용하는 것이 참조하기 보다 쉬운 방법일 것이다.

에러 처리하기

XMLHttpRequest 객체는 굉장히 좋은 장점을 갖고 있다. XMLHttpRequest 객체를 사용하면 자바스크립트에서 브라우저의 페이지를 로딩하지 않고 서버와 직접 커뮤니케이션할 수 있다. 그러나, 이런 장점이 단점이 될 수도 있다. 브라우저 윈도우에 직접 에러를 반환하는 언어를 사용해서 작업하고 있다면, XMLHttpRequest 객체를 사용해서 페이지를 디버깅하는 것이 잘못된 것처럼 느껴질 수 있다. 서버 에러 로그에 쉽게 접근할 수 없는 환경에서 작업한다면 더욱 그럴 것이다.

XMLHttpRequest 객체는 서버에서 반환하는 숫자 코드 값(예, 404, 500, 200)을 갖는 status 속성을 갖고 있다. 또한, 간단한 문자열을 전달하는 statusText 속성도 있다. 서버측 오류(코드 500)가 발생한 경우 메시지는 단순히 "내부 서버 오류(Internal Server Error)"를 갖고 있으며, 사용자에게 보여주기엔 괜찮을지 몰라도 디버깅에 사용하기엔 의미가 없다.

서버에서 반환하는 500 에러 페이지는 에러 종류, 에러가 발생한 라인 번호, 에러에 대한 전체 추적정보와 같은 유용한 디버그 정보들이 포함된다. 불행히도, XMLHttpRequest 객체에서는 이런 내용들이 자바스크립트 문자열 변수 하나에 파묻혀버린다.

500 에러 메시지의 전체 페이지를 가져오고 보다 우아하게 디버깅을 수행하는 것은 간단하다. 이를 위해, XMLHttpRequest를 생성하고 이용하는 이전 함수에 다음 코드를 추가한다.

if (xmlHttpReq.readyState == 4) {
           strResponse = xmlHttpReq.responseText;
           switch (xmlHttpReq.status) {
                   // Page-not-found error
                   case 404:
                           alert('Error: Not Found. The requested URL ' +
                                   strURL + ' could not be found.');
                           break;
                   // Display results in a full window for server-side errors
                   case 500:
                           handleErrFullPage(strResponse);
                           break;
                   default:
                           // Call JS alert for custom error or debug messages
                           if (strResponse.indexOf('Error:') > -1 ||
                                   strResponse.indexOf('Debug:') > -1) {
                                   alert(strResponse);
                           }
                           // Call the desired result function
                           else {
                                   eval(strResultFunc + '(strResponse);');
                           }
                           break;
           }
   }

case 구문은 xmlHttpReq.status 값에 따라 서버로부터의 응답을 다르게 처리한다. 오류가 발생한 경우에는 응답을 오류 처리 함수에 전달하지만, 사용자에게 간단한 에러 메시지를 보여주거나 개발자에게 간단한 디버그 메시지를 출력하기 위해 자바스크립트의 alert를 사용할 수도 있다.( 물론, 이러한 에러들을 div에 멋지게 만든 텍스트로 보여줄 수도 있다)

다음은 500 에러 페이지를 생성하는 함수다.

function handleErrFullPage(strIn) {

        var errorWin;

        // Create new window and display error
        try {
                errorWin = window.open('', 'errorWin');
                errorWin.document.body.innerHTML = strIn;
        }
        // If pop-up gets blocked, inform user
        catch(e) {
                alert('An error occurred, but the error message cannot be' +
                        ' displayed because of your browser's pop-up blocker.n' +
                        'Please allow pop-ups from this Web site.');
        }
}

팝업 차단기가 보편적이기 때문에 try/catch는 중요하다. 사용자가 팝업을 차단하고 있다면, 사용자가 에러를 보고 적절하게 알려줄 수 있게 하기 위한 옵션을 제공해야 한다.

사용자가 팝업을 허용하고 있다면, 서버측 500 오류는 버그를 추적하고 제거하는데 유용한 온갖 종류의 정보를 보여주는 새 창을 보게될 것이다.(현재 창에 에러 메시지를 출력하도록 할수도 있지만, 나의 재생목록 검색은 작은 팝업창이기 때문에 전체 메시지를 제대로 보여줄 수 없다. 대부분의 표준 500 에러 페이지는 전체화면 크기를 요구한다)

참고

XMLHttpRequest 객체는 브라우저에 대한 일반 요청처럼 요청과 함께 쿠키를 전송하기 때문에 서버측 세션 작업을 위해 특별한 작업을 하지 않아도 된다.

다양한 요청을 위해 동일한 객체를 사용할 행운이 별로 없었기 때문에 각 요청에 대해 새로운 XMLHttpRequest 객체를 사용한다. 각 요청에 대해 새로운 객체를 생성하는 것이 복수 요청, 비동기 요청을 수행할 때 문제를 일으키지 않을 것이다.

(역주: 하나의 객체를 공유하는 것은 얘기치 않은 병목현상을 일으킬 수 있다)

XMLHttpRequest 객체로 만든 요청은 브라우저 히스토리에 영향을 주지 않는다. 사용자가 브라우저의 "뒤로가기" 버튼을 클릭했을 때 이전 상태로 돌아가는 변화가 일어나지 않기 때문에 사용자가 혼란스러워할 수 있다. 사용자 액션에 해당하는 히스토리 단계가 필요하다면 HTTP 요청을 만들기 위해 iframe을 사용하는 것을 권한다. iframe을 사용하여 HTTP 요청을 하는 것은 각 요청에 대해 히스토리 항목을 생성한다.

결론 및 앞으로의 계획

XMLHttpRequest 객체는 웹 개발자가 보다 응답성있고, 동적인 웹 응용 프로그램을 만드는데 도움을 주는 다양한 기술을 제공하며, 데스크탑 응용 프로그램처럼 수행하게 해준다. 객체를 다루기 위한 formData2QueryString, handleErrFullPage 함수와 같은 몇가지 사전작업만 있으면, 여러분의 개발 프로세스를 크게 변경하지 않고도 AJAX 액션을 이용할 수 있다.

물론, AJAX를 시작했다면 각자의 응용 프로그램에서 AJAX가 가장 적합한 부분을 찾아보는 것이다. 내 경우엔 이미 EpiphanyRadio에서 이전에 재생한 곡 기능에 AJAX를 활용하고 있다. SHOUTcase 서버에 주기적으로 Song History 페이지를 갱신하는 XMLHttpRequest 객체를 가질 수 있다. HTML 페이지 응답에서 곡 목록을 자바스크립트로 가져오고, div에 DHTML로 목록을 작성한다. 이전에 재생된 곡 목록은 브라우저에서 페이지를 재로딩하지 않고 계속해서 업데이트된다.

나는 로그인 페이지에서 완전히 다른 페이지로 재전송하는 대신 원래 로그인 화면에 멋지게 만든 로그인 에러 메시지를 보여주기 위해 AJAX를 청취자 로그인 페이지에도 사용한다. SHOUTcast 사이트 메인페이지를 폴링해서 현재 청취자 숫자를 지속적으로 갱신하고, 똑똑한 정규식을 사용하여 응답에서 숫자들을 가져올 수 있다.

참고자료