Showing posts with label Ajax. Show all posts
Showing posts with label Ajax. Show all posts

Thursday, July 02, 2009

AJAX 강의 3장 - 서버와 통신하기(요청/응답 처리

 

3.1 responseText 속성을 이용한 단순 문자열 다루기

 

 2장에 이어 이번에는 innerHTML 속성을 이용한 샘플 코드를 살펴보자.

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Using responseText with innerHTML</title>
   
<script type="text/javascript">
var xmlHttp;

function createXMLHttpRequest() {
    if (window.ActiveXObject) {
        xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else if (window.XMLHttpRequest) {
        xmlHttp = new XMLHttpRequest();
    }
}
   
function startRequest() {
    createXMLHttpRequest();
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.open("GET", "innerHTML.xml", true);
    xmlHttp.send(null);
}
   
function handleStateChange() {
    if(xmlHttp.readyState == 4) {
        if(xmlHttp.status == 200) {
            document.getElementById("results").innerHTML = xmlHttp.responseText;
        }
    }
}
</script>
</head>

<body>
    <form action="#">
        <input type="button" value="Search for Today's Activities" onclick="startRequest();"/>
    </form>
    <div id="results"></div>
</body>
</html>

 

<3-1 innerHTML.html 의 내용>

 

 

<table border="1">
    <tbody>
        <tr>
            <th>Activity Name</th>
            <th>Location</th>
            <th>Time</th>
        </tr>
        <tr>
            <td>Waterskiing</td>
            <td>Dock #1</td>
            <td>9:00 AM</td>
        </tr>   
        <tr>
            <td>Volleyball</td>
            <td>East Court</td>
            <td>2:00 PM</td>
        </tr>   
        <tr>
            <td>Hiking</td>
            <td>Trail 3</td>
            <td>3:30 PM</td>
        </tr>   
    </tbody>
</table>

 

<3-2 innerHTML.xml 의 내용>

 

 

2장 <2-1> 와 <3-1>의 가장 큰 차이점이라면 XHR 객체의 responseText 속성값을 이용하여 div 엘리먼트에 문자열을 할당하는 부분이다. 아래 그림은 innerHTML.html 을 실행한 결과이다.


 

<3-3 innerHTML 실행 결과>


 

 
 
3.2 responseXML 속성을 이용한 DOM 시작하기
 
지금까지의 예제에서는 간단하고 단순한 문자열을 처리하는데 적합한 XHR 객체의 resonseText 속성만을 살펴보았다. 하지만 대단히 복잡한 응답데이터의 경우는 단순한 문자열로 처리할 수 없으며 XML 형식으로 처리하는 것이 훨씬 논리적이고 효율적일 것이다. 그러면 어떻게 브라우저는 서버로부터 받은 XML 형식의 데이터를 처리할 수 있는 것일까? XML 문서는 W3C 의 DOM 을 이용해서 처리된다. DOM 을 지원하는 브라우저들은 당연히 XML 문서를 다루는 많은 API 를 구현하고 있기 때문이다.
 
DOM 은 HTML 과 XML 을 다루는 API 를 제공하고 있으며, 스크립트를 통해서 다큐먼트에 접근할 수 있도록 정의되어 있다. 자바스크립트는 DOM 에 접근할 수 있고 DOM 을 다룰수 있는 스크립팅 언어이다. 다큐먼트의 모든 요소들은 DOM 의 부분들이기 때문에 요소의 속성과 메소드들은 자바스크립트로 제어가 가능하다.
 
 
다음은 XML 문서를 처리하기 위한 DOM 요소의 속성을 살펴보도록 하자.
 
childNodes : 현재 요소의 자식을 배열로 표현한다.
firstChild : 현재 요소의 첫번째 자식이다.
lastChild : 현재 요소의 마지막 자식이다.
nextSibling : 현재 요소와 바로 다음의 요소를 의미한다.
nodeValue : 해당 요소의 값을 읽고 쓸 수 있는 속성을 정의한다.(=data)
parentNode : 해당 요소의 부모노드이다.
previousSibling : 현재 요소와 바로 이전의 요소를 의미한다.
 
 
다음은 XML 다큐먼트를 다루는 유용한 DOM 요소의 메소드를 살펴보자.
 
getElementById(id) : 다큐먼트에서 특정한 id 속성값을 가지고 있는 요소를 반환한다.
getElementsByTagName(name) : 특정한 태그 이름을 가지고 있는 자식 요소로 구성된 배열을 리턴한다.
hasChildNodes() : 해당 요소가 자식 요소를 포함하고 있는지를 나타내는 Boolean 값을 리턴한다.
getAttribute(name) : 특정한 name 에 해당하는 요소의 속성값을 리턴한다.
 
 
이번에는 XHR 객체의 responseXML 속성을 이용한 예제를 살펴봄으로써 XML 다큐먼트를 다루기 위한 DOM 객체의 속성과 메소드에 대해서 알아본다.
 
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Parsing XML Responses with the W3C DOM</title>
   
<script type="text/javascript">
var xmlHttp;
var requestType = "";
function createXMLHttpRequest() {
    if (window.ActiveXObject) {
        xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else if (window.XMLHttpRequest) {
        xmlHttp = new XMLHttpRequest();
    }
}
   
function startRequest(requestedList) {
    requestType = requestedList;
    createXMLHttpRequest();
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.open("GET", "parseXML.xml", true);
    xmlHttp.send(null);
}
   
function handleStateChange() {
    if(xmlHttp.readyState == 4) {
        if(xmlHttp.status == 200) {
            if(requestType == "north") {
                listNorthStates();
            }
            else if(requestType == "all") {
                listAllStates();
            }
        }
    }
}
 
function listNorthStates() {
    var xmlDoc = xmlHttp.responseXML;
   var northNode = xmlDoc.getElementsByTagName("north")[0];
   
    var out = "Northern States";
    var northStates = northNode.getElementsByTagName("state");
   
    outputList("Northern States", northStates);
}
function listAllStates() {
    var xmlDoc = xmlHttp.responseXML;
   var allStates = xmlDoc.getElementsByTagName("state");
   

    outputList("All States in Document", allStates);
}
function outputList(title, states) {
    var out = title;
    var currentState = null;
    for(var i = 0; i < states.length; i++) {
        currentState = states[i];
        out = out + "\n- " + currentState.childNodes[0].nodeValue;
    }
   
    alert(out);
}
</script>
</head>
<body>
    <h1>Process XML Document of U.S. States</h1>
    <br/><br/>
    <form action="#">
        <input type="button" value="View All Listed States" onclick="startRequest('all');"/>
        <br/><br/>
        <input type="button" value="View All Listed Northern States" onclick="startRequest('north');"/>
    </form>
</body>
</html>

<3-4 parseXML.html 의 내용>
 
 
<?xml version="1.0" encoding="UTF-8"?>
<states>
    <north>
        <state>Minnesota</state>
        <state>Iowa</state>
        <state>North Dakota</state>
    </north>
    <south>
        <state>Texas</state>
        <state>Oklahoma</state>
        <state>Louisiana</state>
    </south>
    <east>
        <state>New York</state>
        <state>North Carolina</state>
        <state>Massachusetts</state>
    </east>
    <west>
        <state>California</state>
        <state>Oregon</state>
        <state>Nevada</state>
    </west>
</states>
 
<3-5 parseXML.xml 의 내용>
 
일단 <3-4>의 실행결과를 살펴본 후 핵심원리를 파악해 보자
 
 
 
 


 

 
<3-6 parseXML.html 을 실행했을때의 그림>
 
그림 <3-6> 은 parseXML.html 을 실행했을 때의 화면으로 View All Listed States 버튼을 눌렀을때의 결과 및 View All Listed Northern States 버튼을 눌렀을때의 결과 화면을 아래에 표시하였다.
 
 
 


 

 

<3-7 View All Listed States 버튼을 눌렀을때의 결과 화면>

 

 


 

 

<3-8 View All Listed Northern States 버튼을 눌렀을때의 결과 화면>

 

예제 3-4 는 다소 길어보이나 XHR 객체의 responseXML 속성을 이용한 DOM 객체를 다루는 속성 및 메소드를 다루고 있다는 면에서 반드시 이해하고 넘어가야만 한다. 소스 패턴은 지금까지의 예제와 비슷하므로 DOM 속성 및 메소드가 사용된 주요 부분만 설명하겠다.

 

listAllStates() 메소드의 아래 라인을 주목해 보자.

var xmlDoc = xmlHttp.responseXML;

=> XHR 객체는 responseXML 속성을 이용해서 서버로부터의 XML 결과 다큐먼트를 다룰수 있는 DOM 속성 및 메소드를 사용할 수 있게 해준다는 것을 알 수 있다.


var allStates = xmlDoc.getElementsByTagName("state");

=>XML 결과 다큐먼트로부터 state 자식 엘리먼트들로 구성된 배열을 얻어와 allStates 변수에 할당하는 로직이다.

 

listNorthStates() 메소드를 살펴보자.

var northNode = xmlDoc.getElementsByTagName("north")[0];

=> XML 다큐먼트에서 north 앨리먼트는 유일하게 하나만 존재하므로 자식 앨리먼트로 구성된 배열중에서 첫번째(0) 배열값을 얻어와야 한다. 위 식은 아래와 같이 수정해도 결과는 같다.

var northNode = xmlDoc.getElementsByTagName("north").item(0);

 

outputList() 메소드를 살펴보자.

out = out + "\n- " + currentState.childNodes[0].nodeValue;

=> 이부분은 각각의 state 앨리먼트의 첫번째 자식 노드의 값을 out 변수에 계속 연결하는 부분이다. state 앨리먼트의 값을 표현하고 있는 부분도 XML 에서는 하나의 text 엘리먼트이다. 따라서 각각의 state 엘리먼트의 첫번째 text 자식 엘리먼트를 childNodes[0] 으로 표시한 것이며 그 값을 가져오기 위해서 nodeValue 속성이 사용된 것이다. nodeValue 는 아래와 같이 data 속성을 사용해도 같은 결과를 얻는다.

out = out + "\n- " + currentState.childNodes[0].data;

 

 

 

3.3 Dynamic DOM 객체 다루기

 

지금까지는 DOM 의 기초적인 속성 및 메소드들을 다루어 봤다. 이런 속성으로는 다이나믹한 웹페이지를 구성하는데 한계가 있다. 웹페이지 전체가 리로딩 되지 않고 적절한 시점에 필요한 부분만 서버와 통신하여 데이터가 수정되는 동적인 웹페이지를 만들려면 더 다양한 DOM 의 속성을 익혀야 한다. 자, 그럼 컨텐츠를 동적으로 생성할 수 있게 해주는 W3C DOM 의 속성과 메소드에는 어떤것들이 있는지 알아보자.

document.createElement(tagName) : tagName 으로된 엘리먼트를 생성한다. div 를 메소드 파라미터로 입력하면 div 엘리먼트가 생성된다.

document.createTextNode(text) : 정적 텍스트를 담고 있는 노드를 생성한다.

<element>.appendChild(childNode) : 특정 노드를 현재 엘리먼트의 자식 노드에 추가시킨다. (예를들어 select 엘리먼트에 option 엘리먼트 추가)

<element>.getAttribute(name) : 속성명이 name 인 속성값을 반환한다.

<element>.setAttribute(name, value) : 속성값 value 를 속성명이 name 인 곳에 저장한다.

<element>.insertBefore(newNode, tartgetNode) : newNode 를  tartgetNode 전에 삽입한다.

<element>.removeAttribute(name) : 엘리먼트에서 name 속성을 제거한다.

<element>.removeChild(childNode) : 자식 엘리먼트를 제거한다.

<element>.replaceChild(newNode, oldNode) : oldNode 를 newNode 로 치환한다.

<element>.hasChildNodes() : 자식 노드가 존재하는지 여부를 판단한다. 리턴형식은 Boolean 이다.

 

여기서 한가지 짚고 넘어갈 부분이있다. 현재 거의 모든 브라우저는 DOM 을 지원하고 있으며 API 또한 비슷하게 동작하도록 구현되어 있다. 정확히 말하자면 DOM API 의 구현이 브라우저마다 다소 차이가 있다는 말이다. 가장 호환이 안되는 브라우저는 다름아닌 인터넷 익스플로어다. AJAX 의 핵심은 XMLHttpRequest 객체이고 가장 먼저 이 객체를 구현하고 제공한 브라우저가 IE5임을 감안할때 상당히 아이러니컬하지 않은가? 2000년 IE 가 전세계 브라우저의 시장점유율 95% 차지할 즈음, IE 에 대적할 만한 브라우저는 존재하지 않았다. 마이크로소프트가 다양한 웹표준 구현을 포기했던 시점이 바로 이때부터다. 이때까지 XHR 의 사용은 당연히 저조할 수밖에 없었다. 하지만 모질라와 사파리가 잇따라 광범위하게 지원을 시작하게 되자 상황은 변하기 시작했고,  사용율이 극히 부진했던 XHR 객체는 W3C 의 표준으로까지 자리매김하게 되었다. 더우기 구글맵, 구글 Suggest, Gmail, Flickr, Netflix 등에서 AJAX 를 사용하자 이제는 명실상부한 웹의 표준으로 거듭나게 되었다. 너무 늦었지만 Microsoft 쪽에서도 많은 분발을 촉구한다. 그러면 IE 에서 문제가 되고 있는 DOM 객체의 특징을 간략히 살펴보자.

첫째, IE 에서는 <table> 에 <tr> 을 추가할때 appendChilde() 메소드를 사용하더라도 <tr> 은 나타나지 않는다. 따라서 <tr> 을 <tbody> 에 추가해 주는 방식을 사용해야 모든 브라우저에서 동작하는 코드를 작성할 수 있다.

둘째, IE 에서는 setAttribute() 메소드에 class 속성을 이용할 수 없다. setAttribute("class", "newClassName") 한 후에 다시 setAttribute("className", "newClassName") 을 해야 모든 브라우저의 호환성을 보장할 수 있다.

셋째, IE 에서는 style 속성에 setAttribute() 메소드를 이용할 수 없다. <element>.setAttribute("style, font-weight:bold;") 라고 하는 대신에 <element>.style.cssText = "font-weight:bold"  라고 해 줘야 모든 브라우저에서 제대로 작동한다.

 

다음은 DOM 의 동적 속성 및 메소드를 이용해서 다이나믹한 웹페이지를 생성하는 예제를 다루어 보겠다.

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Dynamically Editing Page Content</title>

<script type="text/javascript">
var xmlHttp;

function createXMLHttpRequest() {
    if (window.ActiveXObject) {
        xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else if (window.XMLHttpRequest) {
        xmlHttp = new XMLHttpRequest();
    }
}
   
function doSearch() {
    createXMLHttpRequest();
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.open("GET", "dynamicContent.xml", true);
    xmlHttp.send(null);
}
   
function handleStateChange() {
    if(xmlHttp.readyState == 4) {
        if(xmlHttp.status == 200) {
            clearPreviousResults();
            parseResults();
        }
    }
}

function clearPreviousResults() {
    var header = document.getElementById("header");
    if(header.hasChildNodes()) {
        header.removeChild(header.childNodes[0]);
    }

    var tableBody = document.getElementById("resultsBody");
    while(tableBody.childNodes.length > 0) {
        tableBody.removeChild(tableBody.childNodes[0]);
    }
}

function parseResults() {
    var results = xmlHttp.responseXML;

    var property = null;
    var address = "";
    var price = "";
    var comments = "";

    var properties = results.getElementsByTagName("property");
    for(var i = 0; i < properties.length; i++) {
        property = properties[i];
        address = property.getElementsByTagName("address")[0].firstChild.nodeValue;
        price = property.getElementsByTagName("price")[0].firstChild.nodeValue;
        comments = property.getElementsByTagName("comments")[0].firstChild.nodeValue;
       
        addTableRow(address, price, comments);
    }
   
    var header = document.createElement("h2");
   var headerText = document.createTextNode("Results:");
   header.appendChild(headerText);
   document.getElementById("header").appendChild(header);
   
   document.getElementById("resultsTable").setAttribute("border", "1");
}


function addTableRow(address, price, comments) {
    var row = document.createElement("tr");
   var cell = createCellWithText(address);
   row.appendChild(cell);
   

    cell = createCellWithText(price);
    row.appendChild(cell);
   
    cell = createCellWithText(comments);
    row.appendChild(cell);
   
    document.getElementById("resultsBody").appendChild(row);
}

function createCellWithText(text) {
    var cell = document.createElement("td");
    var textNode = document.createTextNode(text);
    cell.appendChild(textNode);
   
    return cell;
}
</script>
</head>

<body>
  <h1>Search Real Estate Listings</h1>
 
  <form action="#">
    Show listings from
        <select>
            <option value="50000">$50,000</option>
            <option value="100000">$100,000</option>
            <option value="150000">$150,000</option>
        </select>
        to
        <select>
            <option value="100000">$100,000</option>
            <option value="150000">$150,000</option>
            <option value="200000">$200,000</option>
        </select>
    <input type="button" value="Search" onclick="doSearch();"/>   
  </form>


  <span id="header">
 
  </span>

  <table id="resultsTable" width="75%" border="0">
    <tbody id="resultsBody">
    </tbody>
  </table>
</body>
</html>

<3-9 dynamicContent.html 의 내용>

 

<?xml version="1.0" encoding="UTF-8"?>
<properties>
    <property>
        <address>812 Gwyn Ave</address>
        <price>$100,000</price>
        <comments>Quiet, serene neighborhood</comments>
    </property>   
    <property>
        <address>3308 James Ave S</address>
        <price>$110,000</price>
        <comments>Close to schools, shopping, entertainment</comments>
    </property>   
    <property>
        <address>98320 County Rd 113</address>
        <price>$115,000</price>
        <comments>Small acreage outside of town</comments>
    </property>   
</properties>

 

<3-10 dynamicContent.xml 의 내용>

 

 

 
<3-10 샘플 3-9의 실행결과 화면>
 

샘플 3-9를 보면 동적 메소드가 적용된 부분은 굵게 표시를 해 놓았다. 이부분을 이전 샘플들과 비교해 가면서 보면 코드를 이해하는데 큰 어려움은 없으리라 생각한다. 실행결과는 3-10 그림에 나와있다. Search 버튼을 누르면 dynamicContent.xml 의 내용을 테이블로 표시한다. Search 버튼을 누르면 기존에 존재하는 테이블 row 를 동적으로 제거한 후에 동적으로 다시 그린다. 이번 예제는 이 부분이 핵심으므로 가장 중요한 코드만을 설명하겠다.

 

function createCellWithText(text) {
    var cell = document.createElement("td");
    var textNode = document.createTextNode(text);
    cell.appendChild(textNode);
   
    return cell;
}

 

위 메소드는 테이블 컬럼(<td></td>)에 해당하는 정보를 생성하는 메소드이다. 3-10 그림을 보면 하나의 row 에는 3개의 컬럼 요소가 있으며 동적으로 하나의 행을 생성하기 위해서는 address, price, comments 에 해당하는 td 요소를 각각 생성해야 한다.

 

function addTableRow(address, price, comments) {
    var row = document.createElement("tr");
    var cell = createCellWithText(address);
    row.appendChild(cell);
   
    cell = createCellWithText(price);
    row.appendChild(cell);
   
    cell = createCellWithText(comments);
    row.appendChild(cell);
   
    document.getElementById("resultsBody").appendChild(row);
}

 

위 메소드는 테이블 행(<tr></tr>)에 해당하는 정보를 생성해서 테이블에 추가하는 메소드이다. 바로 위에서 언급했듯이 행을 구성하고있는 3개의 컬럼 요소를 각각 만들어서 row 변수에 추가한 후, 이 변수를 다시 tbody 속성에 추가하면 그림 3-10과 같은 화면이 완성되는 것이다.

 

 

 

3.4 요청 파라미터를 서버로 보내기

 

지금까지는 ajax를 이용하여 요청을 서버로 보내는 방법과 서버로부터 받은 결과 정보를 파싱해서 처리하는 여러 특징들에 대해서 살펴보았다. 하지만 이것만으로는 부족하다. xml 의 고정된 정보를 다루는 것이 실증나지 않는가? 요청을 보낼때 특정 파라미터를 실어서 서버에 보내고, 서버는 요청정보를 바탕으로 특화된 응답정보를 보내야만 쓸만해 진다.

 

XMLHttpRequest(XHR) 은 고전적 웹의 GET/POST 방식과 흡사하게 동작한다. GET 방식은 name=value 쌍의 파라마터를 url 에 실어서 서버로 전송한다. 물론 name=value 쌍은 리소스 url 의 끝을 의미하는 ? 이후에 구분자(&) 를 사이사이에 끼고 주욱 붙는다.

 

POST 방식은 GET 방식과 마찬가지로 name=value 쌍의 형태로 데이터를 전달한다. 물론 같은 구분자 (&)를 사용한다. 하지만 POST 방식은 폼 요소의 데이터를 인코딩하여 Http Request 객체의 body 에 저장해서 보낸다.

 

또 다른 차이점이 있다면 서버로 보낼 수 있는 요청정보의 크기인데, GET 방식으로는 name1=value1&name2=value2&name3=value3... 이런 문자열의 길이가, 브라우저마다 차이가 있지만, 대략 2000 byte 이상이면 불가능하다. 불가능하다는 의미가 무엇이냐 하면 브라우저는 요청정보를 보내려고 시도는 하지만 처리가 안되기 때문에 프러세스는 중단되고 만다. 따라서 서버로 보내는 파라미터가 많을 때는 POST 방식을 사용해야 한다. 일반적으로 데이터를 fetch(검색) 할때는 GET 방식을 사용하고 그 이외의 작업(추가, 수정, 삭제)에는 POST 방식을 주로 사용한다. 예를 들어 클릭해서 현재 이 글을 읽고 있는 경우는 GET 방식이 사용되었을 것이고, 이 글을 수정할때는 POST 방식이 사용될 것이다.

 

Ajax와 관련된 차이점이라면 GET 방식은 파라미터가 인코딩되어 url 에 붙어가기 때문에 해당 url 을 통째로 재사용(bookmark) 가능하지만 ajax 특성상 이런 북마킹 기능은 불가능 하다. HTML 폼 요소에는 method 속성이 있는데 개발자는 GET 또는 POST 방식을 선택할 수 있다. 요청 데이터들은 서버로 submit 될때 method속성에 알맞도록 자동으로 인코딩되지만 XHR 객체는 이런 내장 알고리즘이 없기 때문에 개발자가 쿼리 스트링을 작성해야 한다. 쿼리 스트링을 작성하는 방법은 GET 또는 POST 방식에 상관없이 동일하다. 유일한 차이점이 있다면 GET 방식의 쿼리 스트링은 요청 url 에 붙어서 서버로 전송되지만 POST 방식의 쿼리 스트링은 XHR 객체의 send(쿼리 스트링) 메소드가 호출될때 파라미터로 전송된다. 샘플을 살펴보면서 ajax 에서 GET 및 POST 방식을 어떻게 사용하는지 알아보자. 이번에는 결과화면을 먼저 소개한다.


 

 
<3-11 GET/POST 방식 샘플 화면>
 
 
위 그림 3-11 은 브라우저에서 First name, Middle name, Birthday 를 입력한 후 Send parameters using GET 버튼 혹은 Send parameters using POST 버튼을 눌렀을 때의 결과가 바로 아래 부분에 표시되는 형태의 단순한 예제이다. 위 샘플을 실행시켜 보기 위해서는 getAndPostExample.html 과 서버 프로그램인 GetAndPostExample.java 가 필요하다. 하지만 이번 예제의 주요 핵심은 서버 프로그램이 아니다. XHR 객체가 GET/POST 방식을 어떻게 사용하는지를 이해하는 것이 중요하다. 클라이언트 및 서버 프로그램 코드를 기술한 후 XHR 의 주요코드에 대해서 설명을 하겠다.
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Sending Request Data Using GET and POST</title>
<script type="text/javascript">
var xmlHttp;
function createXMLHttpRequest() {
    if (window.ActiveXObject) {
        xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else if (window.XMLHttpRequest) {
        xmlHttp = new XMLHttpRequest();
    }
}
   
function createQueryString() {
    var firstName = document.getElementById("firstName").value;
    var middleName = document.getElementById("middleName").value;
    var birthday = document.getElementById("birthday").value;
   
    var queryString = "firstName=" + firstName + "&middleName=" + middleName
        + "&birthday=" + birthday;
   
    return queryString;
}
function doRequestUsingGET() {
    createXMLHttpRequest();
   
    var queryString = "GetAndPostExample?";
    queryString = queryString + createQueryString()
        + "&timeStamp=" + new Date().getTime();
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.open("GET", queryString, true);
    xmlHttp.send(null);
}
function doRequestUsingPOST() {
    createXMLHttpRequest();
   
    var url = "GetAndPostExample?timeStamp=" + new Date().getTime();
    var queryString = createQueryString();
   
    xmlHttp.open("POST", url, true);
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");   
    xmlHttp.send(queryString);
}
   
function handleStateChange() {
    if(xmlHttp.readyState == 4) {
        if(xmlHttp.status == 200) {
            parseResults();
        }
    }
}
function parseResults() {
    var responseDiv = document.getElementById("serverResponse");
    if(responseDiv.hasChildNodes()) {
        responseDiv.removeChild(responseDiv.childNodes[0]);
    }
   
    var responseText = document.createTextNode(xmlHttp.responseText);
    responseDiv.appendChild(responseText);
}
</script>
</head>
<body>
  <h1>Enter your first name, middle name, and birthday:</h1>
 
  <table>
    <tbody>
        <tr>
            <td>First name:</td>
            <td><input type="text" id="firstName"/>
        </tr>
        <tr>
            <td>Middle name:</td>
            <td><input type="text" id="middleName"/>
        </tr>
        <tr>
            <td>Birthday:</td>
            <td><input type="text" id="birthday"/>
        </tr>
    </tbody>
 
  </table>
 
  <form action="#">
    <input type="button" value="Send parameters using GET" onclick="doRequestUsingGET();"/>   
   
    <br/><br/>
    <input type="button" value="Send parameters using POST" onclick="doRequestUsingPOST();"/>   
  </form>
  <br/>
  <h2>Server Response:</h2>
  <div id="serverResponse"></div>
</body>
</html>
<3-12  getAndPostExample.html 의 전체 소스 코드>
 
 
 
package ajaxbook.chap3;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class GetAndPostExample extends HttpServlet {
    /**
  *
  */
 private static final long serialVersionUID = 1L;
 protected void processRequest(HttpServletRequest request,
            HttpServletResponse response, String method)
    throws ServletException, IOException {
        //Set content type of the response to text/xml
        response.setContentType("text/xml");
        //Get the user's input
        String firstName = request.getParameter("firstName");
        String middleName = request.getParameter("middleName");
        String birthday = request.getParameter("birthday");
        //Create the response text
        String responseText = "Hello " + firstName + " " + middleName
                + ". Your birthday is " + birthday + "."
                + " [Method: " + method + "]";
        //Write the response back to the browser
        PrintWriter out = response.getWriter();
        out.println(responseText);
        //Close the writer
        out.close();
    }
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        //Process the request in method processRequest
        processRequest(request, response, "GET");
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        //Process the request in method processRequest
        processRequest(request, response, "POST");
    }
}

<3-13 GetAndPostExample.java 의 전체 소스 코드>
 

 

3-12 의 샘플 코드는 이전 예제와 비교해 봤을때 크게 어려운 부분은 없을 것이다. 3-13 의 프로그램은 단순한 에코성 문자열을 생성해서 다시 클라이언트로 보내는 것이므로 특별한 설명은 필요하지 않을 듯 싶다. 이번 예제의 핵심은 ajax 에서 GET 및 POST 방식으로 서버에 파라미터를 보내는 방법이다.

 

function createQueryString() {
    var firstName = document.getElementById("firstName").value;
    var middleName = document.getElementById("middleName").value;
    var birthday = document.getElementById("birthday").value;
   
    var queryString = "firstName=" + firstName + "&middleName=" + middleName
        + "&birthday=" + birthday;
   
    return queryString;
}

 

먼저 위 코드는 GET 및 POST 방식에 있어서 쿼리 스트링을 만들어 공통으로 사용하기 위한 메소드이다. 전에도 설명했듯이 name=value 쌍의 파라미터를 한줄의 String 데이터로 만들고 있다. 각 name=value 쌍은 & 로 구분지어야 한다. name=value 쌍의 순서는 상관없다.

 

function doRequestUsingGET() {
    createXMLHttpRequest();
   
    var queryString = "GetAndPostExample?";
    queryString = queryString + createQueryString()
        + "&timeStamp=" + new Date().getTime();
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.open("GET", queryString, true);
    xmlHttp.send(null);
}

 

위의 코드는 ajax 를 활용하여 GET 방식으로 파라미터를 서버에 보내는 코드이다. XHR 객체 생성후 쿼리 스트링을 생성한다. var queryString = "GetAndPostExample?"; 라인에서 GetAndPostExample 은 ajaxbook.chap3.GetAndPostExample 서브렛을 호출하기 위해서 서브렛 이름과 매핑된 url 이름이다. 이 부분은 해당 컨텍스트의 web.xml 에 다음과 같이 기술되어 있어야 한다.

<web-app>
  <servlet>
    <servlet-name>GetAndPostExample</servlet-name>
    <servlet-class>ajaxbook.chap3.GetAndPostExample</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>GetAndPostExample</servlet-name>
    <url-pattern>/GetAndPostExample</url-pattern>
  </servlet-mapping>

</web-app>

 

쿼리 스트링을 만들때 "&timeStamp=" + new Date().getTime(); 부분을 넣은 이유는 다음과 같다. 몇몇 브라우저들은 명확하지 않은 조건하에서 똑같은 url 로 XHR 멀티 요청을 보냈을때 서버의 결과를 캐싱하는 경향을 보인다. 비록 같은 url 이지만 응답이 다를경우엔 캐싱하는 경향이 오히려 예상치 못한 결과의 원인이 될 수도 있기때문에 되도록이면 unique 한 url을 생성하기 위한 방법을 택한것이다. 이렇게 생성한 쿼리 스트링을 open 메소드에 넣어주고 send 메소드에는 null 값을 설정한다. 다음은 POST 방식에 대해서 살펴보자.

 

function doRequestUsingPOST() {
    createXMLHttpRequest();
   
    var url = "GetAndPostExample?timeStamp=" + new Date().getTime();
    var queryString = createQueryString();
   
    xmlHttp.open("POST", url, true);
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");   
    xmlHttp.send(queryString);
}

 

POST 방식도 GET 방식과 마찬가지로 같은 쿼리 스트링을 사용한다. 하지만 용도는 다르다. GET 방식은 url 과 쿼리 스트링을 합쳐서 open 메소드에 넣어서 사용한 반면, POST 방식은 위 코드처럼 send 메소드에 파라미터로 넣어준다. 또 다른 차이점이라면 POST 방식은 HTTP Request 객체의 바디에 파라미터가 저장되는 것이므로 헤더에도 Content-Type 을 반드시 정의해 줘야 한다. 정의를 해주지 않으면 서버에서는 클라이언트에서 보낸 파라미터를 얻지 못한다.

 

 

 

 

3.5 XML 을 요청 파라미터로 사용하기

 

예제 3-13은 ajax를 이용하여 브라우저의 파라미터를 서버로 전송하고 그 결과를 처리하는 부분을 주로 살펴보았다. 하지만 파라미터를 name=value 쌍으로 보내는 것은 초보나 하는 짓으로 보일 수 있고 좀더 확정성과 유연성 그리고 가독성을 높이는 방향으로 노력을 해보자. 파라미터를 xml 로 변환해서 처리하도록 하는 것이다. 흥미진진하지 않은가? 그렇다고 이 방식이 완벽한 해결책이 되는 것은 아니다. 이번 예제도 물론 실험적이고 기초적인 코드를 가지고 그 활용도를 모색할 것이다.

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Sending an XML Request</title>

<script type="text/javascript">

var xmlHttp;

function createXMLHttpRequest() {
    if (window.ActiveXObject) {
        xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else if (window.XMLHttpRequest) {
        xmlHttp = new XMLHttpRequest();
    }
}
   
function createXML() {
    var xml = "<pets>";
   
    var options = document.getElementById("petTypes").childNodes;
    var option = null;
    for(var i = 0; i < options.length; i++) {
        option = options[i];
        if(option.selected) {
            xml = xml + "<type>" + option.value + "<\/type>";
        }
    }
   
    xml = xml + "<\/pets>";
    return xml;
}

function sendPetTypes() {
    createXMLHttpRequest();
   
    var xml = createXML();
    var url = "PostingXMLExample?timeStamp=" + new Date().getTime();
   
    xmlHttp.open("POST", url, true);
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");   
    xmlHttp.send(xml);
}
   
function handleStateChange() {
    if(xmlHttp.readyState == 4) {
        if(xmlHttp.status == 200) {
            parseResults();
        }
    }
}

function parseResults() {
    var responseDiv = document.getElementById("serverResponse");
    if(responseDiv.hasChildNodes()) {
        responseDiv.removeChild(responseDiv.childNodes[0]);
    }
   
    var responseText = document.createTextNode(xmlHttp.responseText);
    responseDiv.appendChild(responseText);
}


</script>
</head>

<body>
  <h1>Select the types of pets in your home:</h1>
 
  <form action="#">
    <select id="petTypes" size="6" multiple="true">
        <option value="cats">Cats</option>
        <option value="dogs">Dogs</option>
        <option value="fish">Fish</option>
        <option value="birds">Birds</option>
        <option value="hamsters">Hamsters</option>
        <option value="rabbits">Rabbits</option>
    </select>
   
    <br/><br/>
    <input type="button" value="Submit Pets" onclick="sendPetTypes();"/>
  </form>

  <h2>Server Response:</h2>

  <div id="serverResponse"></div>

</body>
</html>

<3-14 postingXML.html 의 전체 소스 코드>

 

 

 

package ajaxbook.chap3;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class PostingXMLExample extends HttpServlet {

    /**
  *
  */
 private static final long serialVersionUID = 1L;

 protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {

        String xml = readXMLFromRequestBody(request);
        Document xmlDoc = null;
        try {
            xmlDoc =
                    DocumentBuilderFactory.newInstance().newDocumentBuilder()
                    .parse(new ByteArrayInputStream(xml.getBytes()));
        }
        catch(ParserConfigurationException e) {
            System.out.println("ParserConfigurationException: " + e);
        }
        catch(SAXException e) {
            System.out.println("SAXException: " + e);
        }

        /* Note how the Java implementation of the W3C DOM has the same methods
         * as the JavaScript implementation, such as getElementsByTagName and
         * getNodeValue.
         */
        NodeList selectedPetTypes = xmlDoc.getElementsByTagName("type");
        String type = null;
        String responseText = "Selected Pets: ";
        for(int i = 0; i < selectedPetTypes.getLength(); i++) {
           type = selectedPetTypes.item(i).getFirstChild().getNodeValue();
           responseText = responseText + " " + type;
        }

        response.setContentType("text/xml");
        response.getWriter().print(responseText);
    }

    private String readXMLFromRequestBody(HttpServletRequest request){
        StringBuffer xml = new StringBuffer();
        String line = null;
        try {
            BufferedReader reader = request.getReader();
            while((line = reader.readLine()) != null) {
                xml.append(line);
            }
        }
        catch(Exception e) {
            System.out.println("Error reading XML: " + e.toString());
        }
        return xml.toString();
    }
}

 

<3-15 PostingXMLExample.java 의 전체 소스 코드>

 

3-14는 완전 html 날코딩이라 실망할 수도 있겠다. 파라미터를 담고 있는 xml 을 스트링 조합으로 생성해서 서버로 전송한다. 전송방식은 샘플 3-12 의 POST 방식과 동일하다. 3-15 는 서브릿으로써 브라우저에서 request 객체의 body 에 실어보낸 xml 을 파싱해서 데이터를 추출한 후 일정한 형식의 문자열로 변환하여 다시 클라이언트로 보내주는 방식이다. 이번 서버 프로그램역시 xml 을 파싱하고 정보를 추출하는 부분에 대해서는 특별한 설명을 하지는 않겠다. 하지만 서블릿에서 한가지 짚고 넘어가자면 클라이언트로부터 받은 xML 을 파싱할때 Document 인터페이스를 사용하는데, 이 는 W3C 가 구체화한 것으로 DOM 객체에 존재하는 같은 기능의 메소드인 getElementsByTagName("type") 을 사용하고 있다는 점이다. 나의 주요 논점은 어디까지나 클라이언트이다. 핵심 코드는 아래와 같다.

 

function createXML() {
    var xml = "<pets>";
   
    var options = document.getElementById("petTypes").childNodes;
    var option = null;
    for(var i = 0; i < options.length; i++) {
        option = options[i];
        if(option.selected) {
            xml = xml + "<type>" + option.value + "<\/type>";
        }
    }
   
    xml = xml + "<\/pets>";
    return xml;
}

 

핵심 로직이지만 너무 간단해서 설명할 것도 없을 것 같다. 위 부분은 xml을 String 형식의 쿼리 스티링을 생성하는 부분이다. petTypes 는 select box 에서 option 엘리먼트 값을 추출하여 문자열을 만드는 부분이다.

 

function sendPetTypes() {
    createXMLHttpRequest();
   
    var xml = createXML();
    var url = "PostingXMLExample?timeStamp=" + new Date().getTime();
   
    xmlHttp.open("POST", url, true);
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");   
    xmlHttp.send(xml);
}

 

위 코드 역시 이전 샘플에서 다루었기 때문에 특별하게 설명한 부분은 없다. 하지만 한가지 짚고 넘어갈 부분이 있는데, send() 메소드의 파라미터에는 문자열 및 DOM 객체를 설정하는 것이 가능하다. 하지만 왜 이번 예제에서는 DOM 객체 대신에 문자열을 넣었을까? 아쉽게도 지금까지 브라우저간 DOM 객체를 생성해서 공통적으로 사용할 수 있는 방법이 없기 때문이다. IE 의 경우는 ActiveXObject 컨트롤을 통해서, 모질라는 native 자바스립트를 통해서 제공하며, 심지어 이런 방법조차 지원하지 않는 브라우저도 존재한다. 따라서 어쩔수 없이 스트링값을 설정한 것이다.

다음은 위 프로그램을 실행한 결과 화면이다.

 


 

 

<3-16 postingXML.html 의 실행 결과 화면>

 

 

 

 

3.6 JSON 을 활용하여 데이터를 서버로 보내기

 

바로 이전의 3-14 샘플은 브라우저의 파라미터를 XML 형식으로 변환하여 서버로 보내는 방법을 제시했었다. 데이타 포맷의 표준이 되서버린 XML을 이용한다는 전체적인 맥락은 그럴싸했지만 사실 XML을 만드는 과정이 복잡한 쿼리 스트링 조합작업이라면 누가 하려하겠는가 말이다. 나 자신도 이런 작업은 정말 싫어한다.

 

XML 을 생성하기 위한 javascript 날코딩의 대안으로 JSON(Javascript Object Notation, www.json.org) 을 소개한다. JSON 은 텍스트 포맷기반의 경량 데이터 변환 포맷이다. 프로그래밍 언어에 독립적며, C 언어계열에 익숙한 데이터 구조 형식을 취하고 있다. JSON은 두가지 텍스트 포맷을 가지고 있는데, 첫번째는 name/value 쌍의 컬렉션 데이터 구조로 프로그래밍 언어로 따지면 object, record, struct 쯤 되겠다. 두번째는 정렬된 value 의 리스트형태로써 프로그래밍 언어로 비유하자면 배열이라고 보면 될 것이다.

 

JSON 의 데이터 구조는 많은 프로그램 언어에 의해서 지원되고 있기때문에 XML 보다는 이기종 시스템간의 이상적인 선택이 될 것이다. 추가적으로 JSON 은 표준 자바스크립트의 부류이므로 모든 웹 브라우저간에도 양립할 수 있는 것이다.

 


 

 
<3-17 JSON 오브젝트구조도(출처 : www.json.org)>
 
 
위 그림은 JSON의 데이터 구조를 나타내고 있다.
 
Object는 {} 으로 표시한다. 오브젝트에는 name/value 쌍이 콜론(:)  혹은 콤마(,) 로 구분되어져 있으며 순서는 없다.
 
Array 는 [] 으로 표시한다. 배열은 정렬된 value 가 콤마(,) 에 의해서 구분되어져 있으며, value 값은 스트링("" 으로 둘러싸야 함), 숫자, true or false, null, object , array 가 올수 있으므로 배열의 구조는 계층적이라고 할 수 있다.
스트링은 유니코드 조합 및 백래쉬 이스케이프(\)를 사용할 수 있으며 '' 을 사용하여 character 를 표현할 수 있다.
스트링과 숫자는 C언어 혹은 자바의 스트링과 거의 흡사하지만 8진수 및 16진수 포맷은 지원하지 않는다. 공백을 name/value 쌍 사이사이에 사용할 수 있다.
 
하나의 예를 들어보자. Employ 라는 클래스(멤버로 firstName, lastName, employeeNumber, title)의 인스턴스를 JSON 을 이용해서 아래와 같이 표현해 볼 수 있다.
 
var employee = {
 "firstName"    : John,
 "lastName"     : Doe,
 "employNumber" : 123,
 "title"        : "Manager"
}
 
그러면 위 표현을 오브젝트 속성을 이용해서 아래와 같이 다룰 수 있다.
var lastName = employee.lastName;//lastName 에 접근
var title = employee.title;//title 에 접근
employee.emplyeeNumber = 456;//employeeNumber 를 456 으로 수정
 

JSON 의 인코딩은 확실히 XML 인코딩보다 가볍다. 따라서 네트웍을 통해서 큰 데이터가 오고가는 상황에서는 많은 퍼포먼스의 차이가 발생할 것이다. JSON 싸이트에 가보면 적어도 14개 이상의 서버쪽 어플리케이션을 다루는 프로그래밍언어에서 JSON 을 사용할 수 있게끔 준비가 되어 있다.

 

이제 3장의 마지막 주제인 JSON 을 이용한 간단한 샘플을 살펴보자.

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>JSON Example</title>

<script type="text/javascript" src="json.js"></script>
<script type="text/javascript">

var xmlHttp;

function createXMLHttpRequest() {
    if (window.ActiveXObject) {
        xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else if (window.XMLHttpRequest) {
        xmlHttp = new XMLHttpRequest();
    }
}
   
function doJSON() {
    var car = getCarObject();
   
    //Use the JSON JavaScript library to stringify the Car object
    var carAsJSON = JSON.stringify(car);
    alert("Car object as JSON:\n " + carAsJSON);
   
    var url = "JSONExample?timeStamp=" + new Date().getTime();
   
    createXMLHttpRequest();
    xmlHttp.open("POST", url, true);
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");   
    xmlHttp.send(carAsJSON);
}
   
function handleStateChange() {
    if(xmlHttp.readyState == 4) {
        if(xmlHttp.status == 200) {
            parseResults();
        }
    }
}

function parseResults() {
    var responseDiv = document.getElementById("serverResponse");
    if(responseDiv.hasChildNodes()) {
        responseDiv.removeChild(responseDiv.childNodes[0]);
    }
   
    var responseText = document.createTextNode(xmlHttp.responseText);
    responseDiv.appendChild(responseText);
}

function getCarObject() {
    return new Car("Dodge", "Coronet R/T", 1968, "yellow");
}

function Car(make, model, year, color) {
    this.make = make;
    this.model = model;
    this.year = year;
    this.color = color;
}

</script>
</head>

<body>

  <br/><br/>
  <form action="#">
      <input type="button" value="Click here to send JSON data to the server"
        onclick="doJSON();"/>
  </form>
 
  <h2>Server Response:</h2>

  <div id="serverResponse"></div>

</body>
</html>

<3-18 jsonExample.htm 의 전체 소스 코드>

 

package ajaxbook.chap3;

import java.io.*;
import java.net.*;
import java.text.ParseException;
import javax.servlet.*;
import javax.servlet.http.*;
import org.json.JSONObject;

public class JSONExample extends HttpServlet {

    /**
  *
  */
 private static final long serialVersionUID = 1L;

 protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        String json = readJSONStringFromRequestBody(request);

        //Use the JSON-Java binding library to create a JSON object in Java
        JSONObject jsonObject = null;
        try {
            jsonObject = new JSONObject(json);
        }
        catch(ParseException pe) {
            System.out.println("ParseException: " + pe.toString());
        }

        String responseText = "You have a " + jsonObject.getInt("year") + " "
            + jsonObject.getString("make") + " " + jsonObject.getString("model")
            + " " + " that is " + jsonObject.getString("color") + " in color.";

        response.setContentType("text/xml");
        response.getWriter().print(responseText);
    }

    private String readJSONStringFromRequestBody(HttpServletRequest request){
        StringBuffer json = new StringBuffer();
        String line = null;
        try {
            BufferedReader reader = request.getReader();
            while((line = reader.readLine()) != null) {
                json.append(line);
            }
        }
        catch(Exception e) {
            System.out.println("Error reading JSON string: " + e.toString());
        }
        return json.toString();
    }
}

<3-19 JSONExample.java 의 전체 소스 코드>

 

마지막 예제에서 중요한 부분을 굵게 표시하였다. 이번 예제를 실행해 보기 위해서는 json.js 와 자바관련 json 라이브러리가 필요하다. 관련 파일들은 json 웹싸이트에서 다운받으면 된다. 우선 자바스크립트쪽 핵심코드를 먼저 살펴보자.

 

function getCarObject() {
    return new Car("Dodge", "Coronet R/T", 1968, "yellow");
}

function Car(make, model, year, color) {
    this.make = make;
    this.model = model;
    this.year = year;
    this.color = color;
}

 

위 코드는 설명이 별로 필요치 않을 것 같다. Car 객체를 만들어 주는 메소드이다.

 

function doJSON() {
    var car = getCarObject();
   
    //Use the JSON JavaScript library to stringify the Car object
    var carAsJSON = JSON.stringify(car);
    alert("Car object as JSON:\n " + carAsJSON);
   
    var url = "JSONExample?timeStamp=" + new Date().getTime();
   
    createXMLHttpRequest();
    xmlHttp.open("POST", url, true);
    xmlHttp.onreadystatechange = handleStateChange;
    xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");   
    xmlHttp.send(carAsJSON);
}

위 코드를 보면 자바스크립트 car 객체를 생성한 후 JSON 자바스크립트 라이브러이의 stringify 를 사용해서 JSON 객체로 변환하고 있다. 나머지 로직은 POST 방식을 구현한 것이고 send(() 메소드에 JSON 객체를 넣어준다.

 

이번엔 서버쪽 프로그램을 확인해 보자.

 

JSONObject jsonObject = null;
try {
    jsonObject = new JSONObject(json);
}
catch(ParseException pe) {
    System.out.println("ParseException: " + pe.toString());
}

String responseText = "You have a " + jsonObject.getInt("year") + " "
 + jsonObject.getString("make") + " " + jsonObject.getString("model")
 + " " + " that is " + jsonObject.getString("color") + " in color.";

 

우선 서버 프로그램은 Http request 객체에서 JSON 문자열을 추출한다. 이렇게 추출된 문자열을 JSON 자바 라이브러리의 JSONObject 클래스를 생성할때 생성자의 파라미터로 입력된다. JSONObject 는 자동으로 JSON 문자열을 파싱하고 getXxx 메소드를 이용해서 여러 타입의 데이터를 추출할 수 있는 것이다. 정말 간단하지 않은가?

 

다음은 3-19 샘플을 실행한 결과 화면이다.

 
Click here to send JSON data to the server 버튼을 클릭하면 alert 창으로 JSON object 데이터 구조를 확인할 수 있다. 그리고 서버에서 처리된 결과문자열인 You have a 1968 Dodge Coronet R/T that is yellow in color. 을 확인 할 수 있을 것이다.

Ajax이용하여 xml 데이터 수신

 

XML데이터를 수신하여 화면에 뿌려주는 간단한 예제

 

한글도 깨짐없이 잘 처리 되는군요 ㅎㅎ

 

ajaxXml.jsp

--------------------------------------------------------------------------------

<%@page contentType="text/html;charset=euc-kr" %>
<script>

/**
 * XmlHttpRequest 생성
 **/
function cfGetXmlHttpRequest() {
  var reqHttp;  
  if (window.ActiveXObject) {  // IE인 경우
       try {
           reqHttp = new ActiveXObject("Msxml2.XMLHTTP");
       } catch (e) {
           try {
               reqHttp =  new ActiveXObject("Microsoft.XMLHTTP");
           } catch (e1) {              
               reqHttp =  null;
           }
       }
  } else if (window.XMLHttpRequest){  // IE 이외의 브라우져
       try {
           reqHttp =  new XMLHttpRequest();
       } catch (e) {
           reqHttp =  null;
       }
  }
  if (reqHttp == null) cfFail();   //XMLHttpRequest 생성 실패
  return reqHttp;
}

/**
 * XmlHttpRequest 생성 실패 인 경우
 **/
function cfFail() {              
  alert("지원되지 않는 브라우져입니다.");
}

/**
 * 서버로 xml데이터 요청 및 call back 함수.
 **/
function cfXmlHttpStatus(sendType, url, commType, parameters) {
 var request = cfGetXmlHttpRequest();             
 
 request.open(sendType, url, commType);  // 송신방법,URL,통신방법

 /** Call back 함수 **/
 request.onreadystatechange = function() {   // 처리상태 변경 발생시 수행되는 이벤트 call back함수
  if (request.readyState == 4) {      // 서버 처리 완료
   if (request.status == 200) {               
    cfParseXml(request);       // XML데이터를 수신후 그 내용을 파싱하여 출력.
   } else {
    cfHttpException(request);      // 예외 처리
   }
       }
 }
 request.send(parameters);        // 서버로 요청
}


/**
 * 예외 처리 (status != 200)
 **/
function cfHttpException(xmlHttp) {
  var exceptShow = "상태 코드: " + xmlHttp.status;
  exceptShow += ",  비정상으로 종료되었습니다.";
  alert(exceptShow);
}

</script>
<script language="javascript" type="text/javascript">

/**
 * onload 시에 수행되는 코드 init
 **/
window.onload = function() {
  var btn = document.getElementById("oBtn");
  btn.attachEvent("onclick", fcRequestData);
}

/**
 * 서버로 XML데이터 요청.
 **/
function fcRequestData() {
  cfXmlHttpStatus("GET", "/test.xml.jsp", true, "");
}

// 메인 처리
function cfParseXml(xmlHttp) {
  var xml = xmlHttp.responseXML;     //XML 파일 데이터

  var result = "<br><table border='1' cellspacing='1'><tr><td colspan=2><b>Result</b></td></tr>";

  var p_list = xml.getElementsByTagName("list");    // Parent Node

  var p_nameNode = xml.getElementsByTagName("name");   
  var p_descNode = xml.getElementsByTagName("desc"); 

  var rowCnt = p_list.length;    // 레코드 건수

  for (i=0; i<rowCnt; i++) {  
  var p_name = p_nameNode[i].childNodes[0].nodeValue;  
  var p_desc = p_descNode[i].childNodes[0].nodeValue;  
     
       result += "<tr>";
       result += "<td>" + p_name + "</td>";      //약어명
       result += "<td>" + p_desc + "</td>";     //정식명칭
       result += "</tr>";
  }
  result += "</table>";
  document.getElementById("oResult").innerHTML = result;
}

</script>

</head>

<body>

 <h1>Ajax :: XML데이터 수신 샘플</h1>

 <input type="button" id="oBtn" value="Get XML" />

 <div id="oResult"></div>


</body>
</html>


 

 

test.xml.jsp

--------------------------------------------------------------------------------

<?xml version="1.0" encoding="utf-8"?>
<%@ page language="java" contentType="text/xml;charset=utf-8" %>
<root>
 <list>
  <name>홍길동</name>
  <desc><![CDATA[서울시 성동구]]></desc>
 </list>
 <list>
  <name>김길동</name>
  <desc><![CDATA[서울시 용산구]]></desc>
 </list>

</root>

 

AJAX에서 즐겨찾기와 뒤로가기를 다루는 방법

 

여기서는 AJAX 응용 프로그램에 즐겨찾기와 뒤로 이동을 지원하는 오픈 소스 자바스크립트 라이브러리를 소개할 것이다. 이 글을 마지막까지 보게된다면 구글맵스나 지메일과 같은 곳들조차 제대로 지원하지 못했던 웹 사이트에서의 즐겨찾기, 앞으로 이동, 뒤로 이동과 같은 AJAX의 문제들을 해결할 수 있게 될 것이다.

"AJAX: 즐겨찾기와 뒤로 이동을 다루는 방법"에서는 [1]에이잭스(AJAX) 응용 프로그램들이 현재 당면하고 있는 즐겨찾기, 뒤로 이동과 같은 중요한 문제들을 설명하고, 이 문제를 해결하기 위한 오픈 소스 프레임워크인 RSH(Really Simple History) 라이브러리를 소개하고, 실제 예제들을 몇 가지 소개할 것이다.

([1]역주: 영어사전에서 AJAX는 에이잭스로 소개되며, 그리스의 신화의 아이아스나 오딧세이의 아이아스를 의미한다. sys-con의 AJAX & Rich Internet Applications 프레젠테이션에서도 그 발음을 확인할 수 있다. 독일 축구팀의 이름을 딴 아약스, 또는 아작스 등으로 불리기도 한다. 한편, 세제 제조회사인 Ajax Industries는 "에이잭스社"로 옮긴다.)

여기서 소개할 프레임워크의 주요 부분은 두 개로 나뉘어진다. 첫째는 클라이언트의 정보를 갖고 있는 임시 세션 캐시를 사용하기 위해 숨겨진 HTML 폼을 사용한다. 이 캐시는 어떤 지점으로 이동하거나 페이지를 떠나는 경우에도 사용할 수 있다. 둘째는 A 태그와 숨겨진 iframe을 사용해서 브라우저 히스토리를 가로챈 다음 브라우저 히스토리 이벤트를 기록하고, 뒤로 이동과 앞으로 이동 버튼에 연결한다. 이 두 가지 기능은 모두 개발시에 쉽게 이용할 수 있도록 간단한 자바스크립트 라이브러리로 되어있다.

문제

즐겨찾기와 뒤로 이동은 여러 페이지로 구성된 전형적인 웹 응용 프로그램에서 매우 유용하다. 사용자가 웹 사이트를 항해할 때, 브라우저의 주소 창은 새로운 URL로 업데이트되며, 나중에 이 주소를 다시 방문하기 위해 이메일에 붙여넣거나 즐겨찾기에 추가할 수 있다. 뒤로 이동, 앞으로 이동 버튼들도 올바르게 동작하며, 사용자는 방문했던 페이지들을 자유로이 이동할 수 있다.

그러나, 에이잭스 응용 프로그램은 웹 페이지 하나에서 동작하는 복잡한 프로그램이기 때문에 예외적인 응용 프로그램이다. 브라우저는 이런 기능을 위해 만들어지지 않았다. 웹 응용 프로그램들이 마우스 클릭을 할 때마다 완전히 새로운 페이지를 가져올 수 있게 되었어도 브라우저는 과거에 사로잡혀 있다.

지메일과 같은 AJAX 소프트웨어에서 사용자가 기능을 선택하고, 응용 프로그램의 상태를 바꾸어도 브라우저의 주소창은 항상 같은 위치만 갖고 있기 때문에 응용 프로그램의 특정 화면에 대해 즐겨찾기를 축가하는 것은 불가능하다. 뿐만 아니라, 사용자가 이전 작업을 원상태로 되돌리기위해 뒤로 이동 버튼을 클릭하면 브라우저가 응용 프로그램의 웹 페이지를 떠나기 때문에 사용자들은 놀라게 된다.

해결책

오픈소스 RSH(Really Simple History) 프레임워크는 이러한 문제를 해결하고, 에이잭스 응용 프로그램에서 즐겨찾기, 뒤로 이동, 앞으로 이동을 제어할 수 있게 해준다. RSH는 현재 베타 버전이며, 파이어폭스 1.0, 넷스케이프 7 이상, 인터넷 익스플로러 6 이상의 버전에서 동작하며, 사파리는 현재 지원되지 않는다.(이에 대한 설명은 나의 웹 로그에서 Coding in Paradise: Safari: No DHTML History Possible를 참고하기 바란다.)

몇몇 에이잭스 프레임워크는 현재 즐겨찾기와 히스토리 문제를 해결하려 하고 있지만 이들 프레임워크는 구현 방식 때문에 몇가지 중요한 버그들을 해결하지 못하고 있다.(보다 자세한 사항은 "Coding in Paradise: AJAX History Libraries"를 참고한다) 게다가, 많은 에이잭스 [2]히스토리 프레임워크는 Backbase나 Dojo 같은 보다 큰 라이브러리안에 통합된 형태로 되어 있다. 이들 프레임워크는 AJAX 응용 프로그램에 대해서 완전히 다른 프로그래밍 모델을 제공하고 있으며, 개발자가 히스토리 기능을 사용하기 위해서는 완전히 새로운 접근방법을 사용할 것을 강요받게 된다.

([2]역주: 히스토리(History)는 "기록" 또는 "방문기록"으로 옮기지만 대부분이 히스토리에 익숙하기 때문에 번역하지 않았다)

반면에, RSH는 기존 AJAX 시스템에 포함시킬 수 있는 간단한 모듈로 되어 있다. 뿐만 아니라, RSH 라이브러리는 다른 히스토리 프레임워크들이 갖고 있는 버그들을 피하기 위한 기술들을 사용하고 있다.

RSH 히스토리 프레임워크는 자바스크립트 클래스 DhtmlHistory, HistoryStorage로 구성되어있다.


DhtmlHistory 클래스는 에이잭스 응용 프로그램을 위한 히스토리 추상화를 제공한다. 즉, 에이잭스 페이지는 브라우저에 새 위치와 그에 대한 히스토리 데이터를 지정한 히스토리 이벤트를 추가하는 add()를 호출한다. DhtmlHistory 클래스는 A 태그에 #new-location과 같은 해쉬를 사용해서 브라우저의 현재 URL을 업데이트하고, 히스토리 데이터와 새 URL을 연결한다. 에이잭스 응용 프로그램 자체를 히스토리 리스너(listener)로 등록하고, 사용자가 뒤로 이동, 앞으로 이동 버튼을 사용해서 이동할 때 마다 히스토리 이벤트는 add() 호출로 저장했던 히스토리 데이터와 브라우저의 위치를 제공한다.

개발자는 HistoryStorage 클래스를 사용해서 어떤 크기의 히스토리 데이터라도 저장할 수 있다. 일반 페이지에서 사용자가 새로운 웹 사이트로 이동할 때 브라우저는 이전 웹 페이지의 모든 응용 프로그램을 제거하고, 응용 프로그램과 자바스크립트의 상태를 정리한다. 따라서, 사용자가 뒤로 이동 버튼을 사용하여 이전 페이지로 돌아가면 모든 데이터가 사라진다. HistoryStorage 클래스는 put(), get(), hasKey()와 같은 간단한 해쉬 테이블 메서드를 제공하는 API를 통해서 이 문제를 해결한다. 사용자가 웹 페이지를 떠난 다음에도 개발자는 이들 메서드를 사용해서 데이터를 저장할 수 있다. 사용자가 뒤로 이동 버튼을 사용해서 돌아오면 HistoryStorage 클래스를 사용해서 데이터에 접근할 수 있다. 이 기능은 사용자가 웹 페이지를 떠난 후에도 브라우저가 폼 양식에 있는 값들을 자동으로 저장한다는 사실을 이용한 것으로, 숨겨진 폼 필드를 사용해서 구현됐다.



예제

이제 바로 간단한 예제로 살펴보자.

먼저, RSH 프레임워크를 사용할 페이지는 dhtmlHistory.js 스크립트를 반드시 인클루드해야 한다.

<!-- Load the Really Simple
       History framework -->
<script type="text/javascript"
            src="../../framework/dhtmlHistory.js">
</script>

DHTML 히스토리 응용 프로그램은 에이잭스 웹 페이지가 있는 디렉터리에 blank.html 파일을 포함시켜야 한다. 이 파일은 RSH 프레임워크에 함께 들어있으며, 인터넷 익스플로러에서 사용한다. 여담이지만, 인터넷 익스플로러는 히스토리 변경 사항을 추적하고 추가하기 위해 "숨겨진 iframe"을 사용한다. 히스토리 기능이 제대로 동작하기 위해서는 실제 위치를 가리키는 것이 필요한데, 이를 위해 비어있는 iframe을 사용한다. 그래서 이름도 blank.html이다.

RSH 프레임워크는 브라우저 히스토리를 조작하기 위한 진입점으로 dhtmlHistory라는 전역 객체를 생성한다. dhtmlHistory로 작업하는 첫번째 단계는 페이지 로딩이 끝난 후에 이 객체를 초기화하는 것이다.

window.onload = initialize;
     
function initialize() {
   // initialize the DHTML History
   // framework
   dhtmlHistory.initialize();

다음으로, 개발자는 히스토리 변경 이벤트를 [3]구독하기 위해 dhtmlHistory.addListener()를 사용해야 한다. 이 메서드는 DHTML 히스토리가 변경될 때, 페이지의 새로운 위치, 히스토리 변경 이벤트와 관련된 히스토리 데이터를 인자로 받는 콜백 함수를 인자로 받는다.

([3]역주: 이벤트 구독보다는 이벤트 감시가 더 적절한 표현이지만 원문이 subscribe이므로 구독으로 옮김)

window.onload = initialize;
     
function initialize() {
// DHTML 히스토리 프레임워크를 초기화한다
   dhtmlHistory.initialize();
   
// HTML 히스토리 변경 이벤트를 구독한다
   dhtmlHistory.addListener(historyChange);

historyChange() 메서드는 사용자가 새 위치로 이동한 후의 newLocation과 히스토리 변경 이벤트와 관계된 historyData를 인자로 받는 함수로 이해하기 쉽다.

/** 히스토리 변경 이벤트를 받기 위한 콜백 */
function historyChange(newLocation,
                                  historyData) {
   debug("A history change has occurred: "
            + "newLocation="+newLocation
            + ", historyData="+historyData,
            true);
}

위에서 사용한 debug() 메서드는 예제 소스 파일에 정의된 유틸리티 함수로 전체 예제 안에 포함되어 있다. debug()는 웹 페이지안에 메시지를 출력한다. 두번째 인자는 불리언(boolean) 타입으로 위 인자에서는 true를 사용했다. true를 사용하면 앞의 메시지를 모두 정리한 다음에 새로운 디버그 메시지를 출력한다.

개발자는 add() 메서드를 사용해서 히스토리 이벤트를 추가한다. 히스토리 이벤트를 추가하는 것은 edit:SomePage와 같이 히스토리 변경 이벤트와 함께 저장할 historyData 값을 제공하기 위한 새 위치를 지정하는 것이다.

window.onload = initialize;
     
function initialize() {
   // initialize the DHTML History
   // framework
   dhtmlHistory.initialize();
   
   // subscribe to DHTML history change
   // events
   dhtmlHistory.addListener(historyChange);
         
   // if this is the first time we have
   // loaded the page...
   if (dhtmlHistory.isFirstLoad()) {
      debug("Adding values to browser "
               + "history", false);
      // start adding history
      dhtmlHistory.add("helloworld",
                               "Hello World Data");
      dhtmlHistory.add("foobar", 33);
      dhtmlHistory.add("boobah", true);
         
      var complexObject = new Object();
      complexObject.value1 =
                           "This is the first value";
      complexObject.value2 =
                           "This is the second data";
      complexObject.value3 = new Array();
      complexObject.value3[0] = "array 1";
      complexObject.value3[1] = "array 2";
         
      dhtmlHistory.add("complexObject",
                               complexObject);

add()를 호출하는 즉시 사용자 브라우저의 URL 툴바에 새로운 위치가 표시된다. 예를 들어, http://codinginparadise.org/my_ajax_app 주소의 에이잭스 웹 페이지에서 dhtmlHistory.add( "helloworld", "Hello World Data" )를 호출하면 사용자는 브라우저의 URL 툴바에서 다음 주소를 보게 된다.

http://codinginparadise.org/my_ajax_app#helloworld

사용자는 이 페이지를 북마크할 수 있다. 사용자가 이 북마크를 이용하면 에이잭스 응용 프로그램은 #helloworld 값을 읽어서 웹 페이지를 초기화할 수 있다. 해시 # 다음의 위치 정보는 RSH 프레임워크에 의해 투명하게 인코딩과 디코딩이 수행된다.

historyData는 URL을 사용하는 것 보다 쉽게 에이잭스 위치 변화에 따른 복잡한 상태들을 저장할 수 있게 해준다. historyData는 숫자, 문자열, 객체와 같은 자바스크립트 타입으로 사용할 수 있는 값이다. 이를 사용하는 예로는 사용자가 DHTML 텍스트 에디터에서 다른 페이지로 이동하는 경우에 에디터의 내용을 모두 저장하는 것이다. 사용자가 뒤로 이동 버튼을 클릭해서 다시 DHTML 페이지로 돌아오면 브라우저는 히스토리 변경 리스너에게 객체를 돌려주게 된다.

개발자는 중첩된 객체나 복잡한 상태를 표현하는 배열과 같은 자바스크립트 객체 전체를 historyData에 사용할 수 있다. 히스토리 데이터에는 간단한 데이터 타입이나 NULL 타입 뿐만 아니라 JSON(자바스크립트 객체 표기법)으로 표현할 수 있는 무엇이든 될 수 있다. 그러나, DOM 객체에 대한 참조나 XMLHttpRequest와 같은 스크립트 가능한 브라우저 객체는 저장되지 않는다. historyData는 북마크와 함께 보존되는 데이터가 아니며, 브라우저를 종료하거나, 브라우저 캐시를 제거하거나, 사용자가 히스토리를 정리하면 사라지는 데이터이다.

dhtmlHistory를 사용하는 마지막 단계는 isFirstLoad() 메서드이다. 어떤 브라우저에서는 웹 페이지를 항해할 때 다른 페이지로 이동했다가 뒤로 이동 버튼을 눌러서 처음 사이트로 돌아오면 첫번째 페이지가 완전히 재로딩되고 onload 이벤트가 발생한다. 페이지를 반복적으로 재로딩하는 경우가 아니라 처음 로드될 때만 특별한 방법으로 초기화하길 원하는 코드를 망쳐버릴 수 있다. isFirstLoad() 메서드는 웹 페이지를 처음 로드한 경우와 사용자가 브라우저 히스토리에 저장된 웹 페이지에서 뒤로 이동한 경우를 구별해준다.

예제 코드에서는 페이지가 처음 로드되었을 때 히스토리 이벤트를 추가하길 원한다. 페이지가 로드된 다음에 사용자가 페이지로 돌아가기 위해 뒤로 이동 버튼을 누른 경우에는 히스토리 이벤트를 추가하지 않는다.

window.onload = initialize;
     
function initialize() {
   // initialize the DHTML History
   // framework
   dhtmlHistory.initialize();
   
   // subscribe to DHTML history change
   // events
   dhtmlHistory.addListener(historyChange);
         
   // if this is the first time we have
   // loaded the page...
   if (dhtmlHistory.isFirstLoad()) {
      debug("Adding values to browser "
               + "history", false);
      // start adding history
      dhtmlHistory.add("helloworld",
                               "Hello World Data");
      dhtmlHistory.add("foobar", 33);
      dhtmlHistory.add("boobah", true);
         
      var complexObject = new Object();
      complexObject.value1 =
                           "This is the first value";
      complexObject.value2 =
                           "This is the second data";
      complexObject.value3 = new Array();
      complexObject.value3[0] = "array 1";
      complexObject.value3[1] = "array 2";
         
      dhtmlHistory.add("complexObject",
                               complexObject);

이제 historyStorage 클래스를 살펴보자. dhtmlHistory, historyStorage와 마찬가지로 historyStorage라는 전역 객체 하나를 통해서 모든 기능을 제공한다. 이 객체는 해시 테이블을 시뮬레이션 하기 위해 put(keyName, keyValue0, get(keyName), hasKey(keyName)과 같은 메서드를 제공한다. 키 이름은 반드시 문자열이어야하며, 키 값은 XML로 된 문자열이나 자바스크립트 객체와 같이 복잡한 것도 사용할 수 있다. 예제에서는 페이지가 처음 로드될 때 historyStorage안에 간단한 XML을 저장하기 위해 put()을 사용하고 있다.

window.onload = initialize;
     
function initialize() {
   // initialize the DHTML History
   // framework
   dhtmlHistory.initialize();
   
   // subscribe to DHTML history change
   // events
   dhtmlHistory.addListener(historyChange);
         
   // if this is the first time we have
   // loaded the page...
   if (dhtmlHistory.isFirstLoad()) {
      debug("Adding values to browser "
               + "history", false);
      // start adding history
      dhtmlHistory.add("helloworld",
                               "Hello World Data");
      dhtmlHistory.add("foobar", 33);
      dhtmlHistory.add("boobah", true);
         
      var complexObject = new Object();
      complexObject.value1 =
                           "This is the first value";
      complexObject.value2 =
                           "This is the second data";
      complexObject.value3 = new Array();
      complexObject.value3[0] = "array 1";
      complexObject.value3[1] = "array 2";
         
      dhtmlHistory.add("complexObject",
                               complexObject);
                              
      // cache some values in the history
      // storage
      debug("Storing key 'fakeXML' into "
               + "history storage", false);
      var fakeXML =
         '<?xml version="1.0" '
         +         'encoding="ISO-8859-1"?>'
         +         '<foobar>'
         +             '<foo-entry/>'
         +         '</foobar>';
      historyStorage.put("fakeXML", fakeXML);
   }

다음으로, 사용자가 다른 페이지로 이동한 다음에 뒤로 이동 버튼을 통해 돌아온 경우에 get() 메서드를 사용해서 저장된 값을 가져올 수 있고, hasKey()를 사용해서 키가 있는지 확인할 수 있다.

window.onload = initialize;
     
function initialize() {
   // initialize the DHTML History
   // framework
   dhtmlHistory.initialize();
   
   // subscribe to DHTML history change
   // events
   dhtmlHistory.addListener(historyChange);
         
   // if this is the first time we have
   // loaded the page...
   if (dhtmlHistory.isFirstLoad()) {
      debug("Adding values to browser "
               + "history", false);
      // start adding history
      dhtmlHistory.add("helloworld",
                               "Hello World Data");
      dhtmlHistory.add("foobar", 33);
      dhtmlHistory.add("boobah", true);
         
      var complexObject = new Object();
      complexObject.value1 =
                           "This is the first value";
      complexObject.value2 =
                           "This is the second data";
      complexObject.value3 = new Array();
      complexObject.value3[0] = "array 1";
      complexObject.value3[1] = "array 2";
         
      dhtmlHistory.add("complexObject",
                               complexObject);
                              
      // cache some values in the history
      // storage
      debug("Storing key 'fakeXML' into "
               + "history storage", false);
      var fakeXML =
         '<?xml version="1.0" '
         +         'encoding="ISO-8859-1"?>'
         +         '<foobar>'
         +             '<foo-entry/>'
         +         '</foobar>';
      historyStorage.put("fakeXML", fakeXML);
   }
   
   // retrieve our values from the history
   // storage
   var savedXML =
                     historyStorage.get("fakeXML");
   savedXML = prettyPrintXml(savedXML);
   var hasKey =
                historyStorage.hasKey("fakeXML");
   var message =
      "historyStorage.hasKey('fakeXML')="
      + hasKey + "<br>"
      + "historyStorage.get('fakeXML')=<br>"
      + savedXML;
   debug(message, false);
}

preetyPrintXml()은 전체 예제에 포함된 유틸리티 메서드로 디버깅을 위해 웹 페이지에 XML을 출력한다.

데이터는 오직 현재 페이지의 히스토리에서만 유지된다는 점에 주의해야 한다. 즉, 브라우저를 종료하거나 사용자가 새로운 창을 열고 에이잭스 응용프로그램의 주소를 다시 입력하는 경우에 히스토리 데이터는 이용할 수 없다. 히스토리 데이터는 오직 뒤로, 앞으로 이동 버튼에 한해서만 유지된다. 사용자가 브라우저를 종료하거나 캐시를 정리하면 사라진다. 실제로, 오랜시간 유지할 수 있는 방법을 원한다면 에이잭스 대용량 저장 시스템(AMAXX, Ajax Massive Storage System)을 보기 바란다.

예제는 이것으로 끝났으며, 데모를 체험해보거나 전체 소스 코드를 다운로드하기 바란다.

예제2: 오라일리 메일

두번째 예제는 Gmail과 유사한 오라일리 메일이라는 가짜 AJAX 이메일 응용프로그램이다. 오라일리 메일을 통해 dhtmlHistory 클래스를 사용해서 브라우저 히스토리를 제어하는 방법, historyStorage 객체를 사용해서 히스토리 데이터를 캐시하는 방법에 대해 설명할 것이다.

오라일리 메일 사용자 인터페이스는 두 부분으로 구성되어 있다. 페이지의 왼쪽 사이즈는 받은 편지(Inbox), 임시 보관(Draft) 같은 다양한 이메일 폴더와 옵션으로 구성된 메뉴다. 사용자가 받은 편지 같은 메뉴를 선택하면 페이지의 오른쪽에 내용을 업데이트한다. 실제 응용프로그램에서는 선택된 메일박스의 내용을 가져와서 표시해야하지만 오라일리 메일에서는 선택한 옵션만 간단하게 보여준다.

오라일리 메일은 브라우저 히스토리에 메뉴 변경사항을 추가하고, 위치 바를 업데이트하는데 RSH 프레임워크를 사용하기 때문에 사용자가 응용프로그램을 북마크하고 브라우저의 뒤로, 앞으로 이동 버튼을 사용해서 메뉴 변경 이전으로 이동할 수 있다.

historyStorage 사용법을 설명하기 위해 Address Book(주소록)이라는 메뉴를 추가했다. 주소록은 이름과 이메일 주소를 자바스크립트 배열로 저장하고 있지만 실제 응용프로그램이라면 원격 서버에서 배열 내용을 가져와야 할 것이다. 그러나, 오라일리 메일에서는 배열을 직접 생성하고, 이름과 이메일 주소를 몇 개 추가한 다음에 historyStorage 객체에 저장한다. 사용자가 웹 페이지를 떠난후에 돌아오면 오라일리 메일 응용프로그램은 원격 서버에서 접속하는 대신 캐시에서 주소록을 가져온다.

주소록은 initialize() 메서드에서 저장하고 가져올 수 있다.

/** 페이지가 로딩을 끝낸 후에 초기화를 수행하는 함수 */
function initialize() {
    // DHTML 히스토리 프레임워크를 초기화한다
    dhtmlHistory.initialize();
   
    // DHTML 히스토리 리스너에 등록한다
    dhtmlHistory.addListener(handleHistoryChange);

    // 주소록을 가져올 수 없으면 직접 추가한 주소록을
    // 히스토리 저장소에서 캐싱한다.
   if (window.addressBook == undefined) {
         // 주소록을 전역 객체로 저장한다
       // 실제 응용프로그램에서는 백그라운드에서 서버로부터
       // 주소록을 가져와야한다
         window.addressBook =
             ["Brad Neuberg 'bkn3@columbia.edu'",
               "John Doe 'johndoe@example.com'",
               "Deanna Neuberg 'mom@mom.com'"];
               
         // 주소록이 있으면 이를 캐시에 보관한다
       // 사용자가 페이지를 떠난 후에 뒤로 이동으로 돌아온 경우에도 사용한다
       historyStorage.put("addressBook",
                                     addressBook);
    }
    else {
         // 히스토리 저장소에서 캐시된 주소록을 가져온다
         window.addressBook =
                      historyStorage.get("addressBook");
    }

히스토리 변경을 다루는 코드는 이해하기 쉽다. 아래 코드에서처럼 사용자가 뒤로 또는 앞으로 이동 버튼을 클릭할 때 handleHistoryChange가 호출된다. 그러면 newLocation을 갖게 된다. 이를 이용해서 사용자 인터페이스를 올바른 상태로 업데이트할 수 있다.

/** 히스토리 변경 이벤트를 처리한다 */
function handleHistoryChange(newLocation,
                                           historyData) {
    // 위치가 없으면 수신함의 기본 위치를 보여준다
    if (newLocation == "") {
         newLocation = "section:inbox";
    }
   
    // 위치 변화가 있으면 표시할 섹션을 추출한다.
    // newLocation은 "section:"으로 시작한다
    newLocation =
             newLocation.replace(/section\:/, "");
   
    // DHTML 히스토리 변경에 따라 브라우저를 업데이트한다
    displayLocation(newLocation, historyData);
}

/** 오른쪽 컨텐트 영역에 주어진 위치를 표시한다*/
function displayLocation(newLocation,
                                     sectionData) {
    // 선택한 메뉴 항목을 가져온다
    var selectedElement =
                  document.getElementById(newLocation);
                 
    // 이전에 선택된 메뉴 항목을 제거한다
    var menu = document.getElementById("menu");
    for (var i = 0; i < menu.childNodes.length;
                                                               i++) {
         var currentElement = menu.childNodes[i];
         // DOM 요소 노드인지 확인한다
         if (currentElement.nodeType == 1) {
             //모든 클래스 이름을 제거한다
             currentElement.className = "";
         }                                                         
    }
   
    // UI에서 다르게 나타나는 새로 선택된 메뉴 항목
    selectedElement.className = "selected";
   
    // 화면 오른쪽에 새로운 섹션을 표시한다.
    // sectionData에 따라 섹션을 결정한다

    // 앞서 캐시된 로컬 주소 데이터를 사용해서 주소록을 보여준다
    if (newLocation == "addressbook") {
         // 주소록을 보여준다
         sectionData = "<p>Your addressbook:</p>";
         sectionData += "<ul>";
         
         // 주소록이 아직 없다면 캐시에서 주소록을 가져온다
         if (window.addressBook == undefined) {
             window.addressBook =
                      historyStorage.get("addressBook");
         }
         
         // 주소록을 표시한다
         for (var i = 0;
                      i < window.addressBook.length;
                               i++) {
             sectionData += "<li>"
                                    + window.addressBook[i]
                                    + "</li>";                          
         }
         
         sectionData += "</ul>";
    }
   
    // sectionData가 없다면 원격으로 가져온다. 이 예제에서는 주소록을 제외한
    // 모든 데이터는 가짜를 이용하고 있다
    if (sectionData == null) {
         // 실제 응용프로그램에서는 섹션의 내용을 원격으로 가져와야한다
         sectionData = "<p>This is section: "
             + selectedElement.innerHTML + "</p>";  
    }
   
    // 제목과 내용을 업데이트한다
    var contentTitle =
             document.getElementById("content-title");
    var contentValue =
             document.getElementById("content-value");
    contentTitle.innerHTML =
                                    selectedElement.innerHTML;
    contentValue.innerHTML = sectionData;
}

오라일리 메일 데모를 체험하거나 소스 코드를 다운로드할 수 있다.

결론

RSH(Really Simple History) API를 사용해서 에이잭스 응용프로그램에서 즐겨찾기와 뒤로, 앞으로 이동 버튼을 사용하기 위한 방법을 보았으며 직접 에이잭스 응용프로그램을 작성하는데 발판이 될 수 있는 예제 코드들을 살펴보았다. 나는 즐겨찾기와 히스토리를 완전히 지원하는 여러분의 에이잭스 발명품들을 볼 수 있기를 기대한다.

감사의 글

이 글을 검토해준 모든 사람과 RSH 프레임워크를 제작한 모두에게 감사드린다: Michael Eakes, Jeremy Sevareid, David Barrett, Brendon Wilson, Dylan Parker, Erik Arvidsson, Alex Russell, Adam Fisk, Alex Lynch, Joseph Hoang Do, Richard MacManus, Garret Wilson, Ray Baxter, Chris Messina, and David Weekly.

참고자료

  • 이 기사의 모든 소스코드 다운로드
  • RSH 프레임워크 다운로드
  • 오라일리 메일 데모와 오라일리 메일 소스 코드 다운로드. 전체 예제 다운로드에는 시험할 수 있는 보다 많은 예제들이 있다
  • Coding in Paradise(코딩천국): AJAX, DHTML, 자바 기술, 위키위키와 같은 협업 기술 등을 다루는 저자의 웹로그
  • "Coding in Paradise: AJAX: Bookmarks and Back Buttons, Advanced Example"
  • "Coding in Paradise: Safari: No DHTML History Possible"
  • "Coding in Paradise: AJAX Tutorial: Saving Session Across Page Loads Without Cookies, On The Client Side"
  • "Coding in Paradise: AJAX History Libraries"

Bard Neuberg는 모질라, JXTA, 자카르타 피드 파서 등에 코드를 공헌하고 있으며, 오픈소스 커뮤니티에서 폭넓은 공헌을 하고 있다.

 

 

출처 : http://network.hanbitbook.co.kr/view.php?bi_id=1162