AJAX를 이용한 Naver 키워드 검색 효과 구현

최근 개발 트렌드 중의 하나는 브라우저 또는 인터넷을 이용하여 기존의 C/S 환경과 같은 다양한 기능을 수행하는 클라이언트를 의미하는 Rich Client(Smart Client)에 대한 것이다.
지금까지 브라우저에서의 Rich 클라이언트는 디버깅 환경조차 제공되지 않는 환경에서의 혼신의 노력을 다한 개발자의 수고(?)에 의해
DHTML, JavaScript 등을 이용하여 스파게티처럼 얽혀 있는 구조하에서 사용자의 요구사항을 만족하는 클라이언트를 구현하였다.
하지만 복잡한 스크립트성 코드로 인해 브라우저에 영향을 많이 받게 되고 구현하는데도 한계가 있으며 개발에서 가장 어려운 부분이 EJB도 아니요, 복잡한 SQL도 아닌 자바스크립트라고 할 정도로 생산성을 저하시키는 복병중의 하나였다.
유지보수 단계에서도 이것은 계속되어 운영중인 시스템에서 자주 스크립트 오류가 발생하거나 다른 개발자가 작성한 코드는 너무 읽기 어려워 심지어는 새로 만드는 경우까지 발생하고 있다.
이런 상황에서 Rich Client에 대한 새로운 기술적 요구는 당연한 것이라 할 수 있다. 사용자의 요구 증대 및 개발자의 요구에 맞추어 Rich Client를 위해 기존에 사용하는 기술들을 확장하거나 새로운 기술들이 나타나고 있다.
대표적인 Rich Client 지원도구는 ActiveX, Applet, Java Web Start 등이지만 각각 MS, Java에 의존한다는 점과 각각 단점을 가지고 있다.
Applet, Java Web Start의 경우 JRE가 설치되어야 하는 문제가 있으며 지금까지 엔터프라이즈 애플리케이션에서 소외되어 왔다고 해도 과언이 아니다. ActiveX의 경우 Windows 환경에만 사용가능하며
이 또한 사용자가 반드시 동의를 해야만 설치되는 단점도 있다.
(필자의 경험상 많은 수의 회사 내부 사용자를 위해 ActiveX를 설치하게 하는 것은 반드시 필요한 경우가 아니라면
피하는 것이 좋다. 아직까지 이런 설치에 익숙하지 않는 사용자가 많기 때문이다.)
최근에는 플래쉬를 브라우저에 표시하고 플래쉬를 이용하여 Rich Client를 구현하게 하는 매크로미디어사에서 발표한 RIA(Rich Internet Application, http://www.macromedia.com/kr/resources/business/rich_internet_apps/)나 Laszlo(http://www.laszlosystems.com/)라는 것들도 나타나고 있다.

RIA 예제


Laszlo 예제 및 소스




플래쉬를 이용한 Rich Client의 경우 영화예매와 같이 비쥬얼한 요구가 많은 반면 데이터에 대한 처리가 많고 복잡한 비즈니스 처리가 필요한 기업의 인트라넷 환경에서는 아직 많이 사용되지 않고 있다.
하지만 필자가 운영하는 시스템 중 경영진이 보는 챠트, 분석된 데이터를 보여 주는 화면 등과 같이 화려한 그래픽을 요구하는 화면에서는 일부 사용되고 있다.
플래쉬를 이용한 Rich Client는 애플리케이션 전체에서 사용되기 보다는 이렇게 비쥬얼을 요구하거나 사용자의 액션에 따라 동적으로 화면의 처리를 요구하는 기능에서는 계속 확산될 것이라 예상된다.

이번 컬럼의 주제인 AJAX(Asynchronous JavaScript and XML)의 경우 단순히 스펙상으로는 Rich Client와 상관이 없을 것 같지만
어떻게 활용하느냐에 따라 Rich Client를 구현할 수 있기 때문에 최근에 주목을 받고 있다.
AJAX는 스크립트 상에서 서버와 비동기적으로 데이터를 주고 받기 위해 태어났다.
AJAX가 Rich Client에 사용되어 지는 것은 AJAX가 스크립트를 이용하여 브라우저의 화면을 Reload 시키지 않으면서 서버로 request로 전송을 한 다음 결과를 받아 처리할 수 있게 하는 특성 때문이다.
웹 기반 애플리케이션의 가장 큰 단점이 데이터 조회, 비즈니스 로직 처리 등과 같이 서버와의 연결이 필요할 때 마다 매번 화면이 reload되어야 한다는 점이었다.
화면이 reload 되는 것은 간단한 문제인 것 같지만 화면을 구성하는 프로그램에는 많은 구현상 제약이 있었다. 화면의 마지막 상태 정보를 가지고 있어 Reload 된 후에도 필요 정보만 변경되고 나머지 정보는 사용자가 설정한 그대로 남아있게 처리해야 하는 등 개발자에게는 여간 골치아픈 문제가 아닐수 없었다.
대표적인 사례가 게시판의 게시글 수정 기능인데 다음과 같은 순서로 사용자가 게시글을 수정하였다.

"테스트" 라는 제목으로 게시글 검색, 5페이지로 이동, 특정 게시글 선택하여 상세조회 → 수정 화면 이동 → 목록 버튼 클릭

여기서 목록 버튼을 클릭하거나 저장 버튼을 클릭한 다음에는 보통 목록 화면으로 이동하는데 이때의 목록 화면에는 "테스트" 제목의 검색 결과에서 5페이지가 나타나기를 사용자는 원할 것이다. 이러한 사용자의 요구사항을 위해 개발자는 상세조회화면, 수정화면, 수정처리 이런 각각의 액션에 이전에 사용자가 보았던 화면에 대해서 기억을 유지시키는 코드를 삽입해야만 한다.

이것을 해결하기 위해 지금까지는 눈에 보이지 않는 IFRAME을 많이 이용하였는데 AJAX를 이용하면 화면이 Reload 없이 전송이 가능하다. (물론 대부분의 Rich Client 솔루션들은 화면을 Reload 하지 않는다.)
이렇게 화면을 Relaod 하지 않기 때문에 화면 구성을 위한 HTML 코드는 전송하지 않고 요청된 처리 또는 처리 결과에 대한 데이터만 전송함으로써 네트워크 트래픽이 감소하는 효과도 볼 수 있다.
물론 사용자 응답 속도 측면에서도 기존의 웹 애플리케이션보다 훨씬 빠른 속도를 낼수 있다. 실제 애플리케이션 계층에서의 속도 변화는 거의 없지만 화면 Reload가 없어짐에 따라 사용자가 느끼는 체감속도가 빨라졌다는 측면이 있다.

AJAX의 사용성에 대해 실제 프로젝트에서 쉽고 유용하게 사용할 수 있는 Naver의 키워드 검색 기능을 구현해 봄으로써 AJAX가 어떻게 사용되어지는지에 대해 간단하게 살펴보자.



xmlhttp.js
function paramEscape(paramValue)
{
   return encodeURIComponent(paramValue);
}

function formData2QueryString(docForm)
{  
   var submitString = '';
   var formElement = '';
   var lastElementName = '';
  
   for(i = 0 ; i < docForm.elements.length ; i++)
   {
     formElement = docForm.elements[i];
     switch(formElement.type)
     {
        case 'text' :
        case 'select-one' :
        case 'hidden' :
        case 'password' :
        case 'textarea' :
           submitString += formElement.name + '=' + paramEscape(formElement.value) + '&';
           break;
        case 'radio' :  
           if(formElement.checked)
           {
             submitString += formElement.name + '=' + paramEscape(formElement.value) + '&';
           }
           break;
        case 'checkbox' :  
           if(formElement.checked)
           {
             if(formElement.name = lastElementName)
             {
                if(submitString.lastIndexOf('&') == submitString.length - 1)
                {
                   submitString = submitString.substring(0, submitString.length - 1);
                }
                submitString += ',' + paramEscape(formElement.value);
             }
             else
             {
                submitString += formElement.name + '=' + paramEscape(formElement.value);
             }
             submitString += '&';
             lastElementName = formElement.name;
           }
           break;
     }                                                                            
   }
   submitString = submitString.substring(0, submitString.length - 1);
   //document.all("result").value = submitString;
   return submitString;                              
}

function xmlHttpPost(actionUrl, submitParameter, resultFunction)
{
   var xmlHttpRequest = false;
  
   //IE인경우
   if(window.ActiveXObject)
   {
     xmlHttpRequest = new ActiveXObject('Microsoft.XMLHTTP');
   }
   else
   {
     xmlHttpReq = new XMLHttpRequest();
     xmlHttpReq.overrideMimeType('text/xml');
   }  
       
   xmlHttpRequest.open('POST', actionUrl, true);
   xmlHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
   xmlHttpRequest.onreadystatechange = function() {
     if(xmlHttpRequest.readyState == 4)
     {
        switch (xmlHttpRequest.status)
        {
           case 404:
             alert('오류: ' + actionUrl + '이 존재하지 않음');
             break;
          case 500:
             alert('오류: ' + xmlHttpRequest.responseText);
             break;
          default:
             eval(resultFunction + '(xmlHttpRequest.responseText);');
             break;    
        }       
     }
   }
  
   xmlHttpRequest.send(submitParameter);            
}                        


ajax_search.html
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html;" charset="euc-kr">
<SCRIPT type="text/javascript" src="http://jaso.co.kr/xmlhttp.js"></SCRIPT>

<SCRIPT Language="javascript">
<!--
     
function keywordKeyDown()
{
    var keyCode = window.event.keyCode;
   
    if(keyCode ==  9)   return;     //Tab 키
    if(keyCode == 13)   return;     //Enter 키
    if(keyCode == 16)   return;     //Shift 키
    if(keyCode == 16)   return;     //Ctrl 키
    if(keyCode == 18)   return;     //Alt 키
    if(keyCode == 45)   return;     //Ins 키
    if(keyCode == 46)   return;     //Del 키
    if(keyCode == 33)   return;     //PgUp 키
    if(keyCode == 34)   return;     //PgDn 키
    if(keyCode == 35)   return;     //End 키
    if(keyCode == 36)   return;     //Home 키
   
    if(keyCode >= 37 && keyCode <= 40)   return;     //방향키
   
    //Keydown 이벤트 발생 시점에는 아직 TextField에 사용자가 입력한 키 값이 설정되지 않았기 때문에
    //브라우저가 이벤트에 반응하여 값을 설정할때 까지 잠시 기다린다.
    setTimeout('submitSearchKeyword()', 250);    

}

function submitSearchKeyword()
{
    var url = 'http://jaso.co.kr/searchKeyword.jsp';
    var queryString = formData2QueryString(document.MAIN_FORM);
    var resultProcessMethod = 'viewSearchKeywordResult';
   
    xmlHttpPost(url, queryString, resultProcessMethod);
}
               
function viewSearchKeywordResult(result)
{
    if(result == "")
    {
        var searchKeywordDiv = document.all("searchKeyword");
        searchKeywordDiv.innerHTML = "";
        searchKeywordDiv.style.visibility = "hidden";
    }
    else
    {
        var resultList = result.split('|');
        var viewResult = '';
        for(i = 0 ; i < resultList.length; i++)
        {
            if(i == 0)  viewResult += '<B>' + resultList[i] + '</B> <A href="javascript:hiddenSearchKeywordResult();">[닫기]</A><BR>'
            else        viewResult += '<A href="javascript:setKeyword(\'' + resultList[i] + '\');">' + resultList[i] + '</A><BR>'
        }       
        var searchKeywordDiv = document.all("searchKeyword");
        searchKeywordDiv.innerHTML = viewResult;
        searchKeywordDiv.style.visibility = "visible";
    }
}
   
function hiddenSearchKeywordResult()
{
    var searchKeywordDiv = document.all("searchKeyword");
    searchKeywordDiv.innerHTML = "";
    searchKeywordDiv.style.visibility = "hidden";
}
   
function setKeyword(selectedKeyword)
{
    document.MAIN_FORM.keyword.value = selectedKeyword;    
}
               
//-->
</SCRIPT>

<STYLE type="text/css">
<!--
  .scroll_div { scrollbar-face-color:#FFFFFF;
                scrollbar-highlight-color: #aaaaaa;
                scrollbar-3dlight-color: #FFFFFF;    
                scrollbar-shadow-color: #aaaaaa;
                scrollbar-darkshadow-color: #FFFFFF;
                scrollbar-track-color: #FFFFFF;    
                scrollbar-arrow-color: #aaaaaa;}
-->
</STYLE>
</HEAD>
<BODY onLoad="MAIN_FORM.keyword.focus()">
<FORM name="MAIN_FORM">
"가", "강"을 입력 해보세요.</BR>
<INPUT type="text" name="keyword" onkeydown="keywordKeyDown()" style:width=150px" autocomplete="off"><A href="javascript:alert('검색처리');">검색</A>
<DIV id="searchKeyword" style="width:250px;height:100px;visibility:hidden;background-color:#D1EED2;overflow=auto;font-size:12px" class="scroll_div">
</DIV>
</FORM>
</BODY>
</HTML>


소스에는 복잡한 내용은 거의 없다. 검색어 입력 Text의 Keydown 이벤트에 대한 처리 부분과 결과를 받아 화면에 나타내는 부분이 대부분이다. AJAX로 서버에 대한 요청은 다음과 같은 순서로 처리한다.

1.XMLHttpRequest 객체 생성
IE의 경우 new ActiveXObject('Microsoft.XMLHTTP');와 같이 생성하고 IE가 아닌 경우 new XMLHttpRequest(); 로 생성한다.

2. XMLHttpRequest open
open() 메소드에는 3개의 파라미터가 있는데 첫번째는 호출방법인 GET, POST 중에 하나가 온다.
필자의 경우 GET 방식보다는 POST 방식을 선호하기 때문에 대부분의 Request는 POST로 전송하는데 AJAX에서도 당연히 POST를 선호한다.
두번째 파라미터는 처리하는 서버의 URL 정보이다.
세번째 파라미터는 비동기/동기 방식에 대한 선택인데 true인 경우 비동기 방식으로 처리한다.
비동기 방식의 경우 Request를 전송한 다음 서버로부터 응답이 없더라도 브라우저는 계속해서 다른 처리를 할 수 있다.
사용자로부터 입력을 받거나 다른 스크립트를 수행할 수도 있다.
반면 동기방식은 요청후 서버로부터 결과를 받을때까지 다른 처리는 할 수 없도록 하는 방식이다.
검색 입력 필드의 예제에서와 같은 경우는 비동기 방식으로 처리하는 것이 보통이지만 데이터의 수정, 삭제, 입력에 대한 처리의 경우에는 처리가 완료되었다는 서버로부터의 응답을 받은 후 다른 액션을 할 수 있도록 하는 것이 좋다.

3.request의 content type 설정
GET 방식인 경우 설정할 필요가 없지만 POST 방식인 경우에는 content type을 다음과 같이 설정한다.
 
xmlHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

4.서버로부터 처리결과 전송 후 수행해야 하는 기능에 대한 정의
XMLHttpRequest의 onreadystatechange 속성은 서버로부터의 처리 결과에 대한 상태코드가 변경되었을 때 수행해야 하는 스크립트 function을 지정한다.
여기서는 anonymous function을 사용하여 직접 정의 햐였다.
  
   xmlHttpRequest.onreadystatechange = function() {
     if(xmlHttpRequest.readyState == 4)
     {
        //서버로부터 받은 상태코드 및 데이터를 이용하여 처리로직 구현
     }
   }
  
서버로부터 받은 결과는 XMLHttpRequest의 responseText 속성에 저장되어 있으며 이것은 HttpServletResponse에 의해 전송된 문자열일수도 있고 웹서비스로 Request를 전송한 경우라면 SOAP 프로토콜로 전송된 XML 형식의 데이터일 것이다.
문자열인 경우 여기서의 예제와 같이 처리할 수 있지만 SOAP 프로토콜로 전송된 XML 데이터인 경우 각각의 브라우저에서 제공하는
XML 또는 SOAP을 지원한 객체를 이용하여 쉽게 핸들링할 수 있을 것이다.

5. 4번까지의 작업으로 Request가 전송되지 않는다. 전송을 위한 준비 작업과 처리결과 수신시 처리방법에 대한 정의만 하였을 뿐이다.
  실제 Request를 전송하기 위해서는 send()를 호출하여 전송한다.
  send()의 파라미터는 전송되는 Request의 파라미터 값이다. 물론 여기서도 일반적인 Request와 같이 '&', '=' 으로 구성된 문자열(예: keyword=test&page=2 )을 만들어서 전송하지만, XML 형태로 만들어서 전송하는 것도 가능하다
 
  xmlHttpRequest.send(submitParameter);            

searchKeyword.jsp
<%@ page contentType="text/html; charset=euc-kr" %>
<%@ page import="java.util.*" %>
<%
    HashMap keywordData = new HashMap();
    keywordData.put("가", "강타|강일|가을소나타|강주희|강은비|강력3반|강동원|가격비교|가방|강수지");
    keywordData.put("강", "강타|강일|강주희|강은비|강력3반|강동원|가방|강수지");
   
    request.setCharacterEncoding("UTF-8");

    String keyword = request.getParameter("keyword");
    //여기에서 데이터베이스로부터 해당 keyword로 시작하는 단어 검색
    //예제에서는 간단하게 하기 위해 Hash에서 가져오는 것으로 처리
    String result = (String)keywordData.get(keyword);
    if(result == null)      result = "키워드 없음";
   
    out.print(keyword + " 키워드 목록|" + result);
%>
  

AJAX의 Request에 대해 서버에서의 처리는 위의 소스에서 보는 것과 같이 비즈니스 기능에 대한 처리(여기서는 데이터조회)에 대해서는 기존과 동일하지만 처리결과를 브라우저로 전송할때 HTML 형태가 아닌 순수한 데이터형태만 제공하여 클라이언트에서 처리하도록 한다.
여기서는 데이터의 구분자를 '|' 문자로 구분하도록 처리하였다.
  
AJAX의 경우 JavaScript로 처리되기 때문에 인코딩에 대한 처리를 모두 UTF-8로 처리한다.
따라서 서버에서 Request를 받아 처리할 때에는 처리할 때에서 반드시 UTF-8로 디코딩하여 처리하여야 한다.
예제의 경우 searchKeyword.jsp에서 다음과 같이 request에 대해 처리하고 있다.

request.setCharacterEncoding("UTF-8");

지금까지 AJAX를 이용하여 간단한 기능을 구현함으로써 AJAX에 대해서 살펴보았다.
현재 AJAX의 위치는 위의 예제와 같이 애플리케이션의 특정 부분에 대해서만 주로 사용되어지고 있다.
앤터프라이즈 애플리케이션에서 전체를 AJAX 기반으로 구현하기에는 아직까지 경험과 사례가 많이 부족하고 AJAX가 Rich Client의 주류로 자리 잡을 것인지에 대해서는 아직까지는 미지수이다.

단점 : 복잡한 HTML에 대한 생성을 자바 스크립트와 같은 스크립트 언어로 처리 (View에 대한 처리가 기존의 Servlet에서 처럼 복잡하게 구현됨. 현재는 JSP 또는 Struts의 taglib를 이용하여 쉽게 처리하고 있지만 이것이 어렵다.)


연구해야할 사항 : 이런 단점을 극복할 수 있는 스크립트 처리에 대한 표준 마련 및 솔루션 echo2와 같이 이미 나와 있지만 좀 더 많은 연구 및 레퍼런스의 확보가 필요하다고 할 수 있다.
그리고 지금까지의 아키텍처는 프리젠테이션 - 컨트롤 - 비즈니스 - 데이터와 같은 형태의 레이어 구조만 있었지만 이제는 프리젠테이션 계층을 좀 더 세분화하여 프리젠테이션 내부에서의 View(Dynamic 화면 구성), 컨트롤(요청 및 응답에 대한 제어), 데이터(서버로부터 받은 또는 서버로 전송할)와 같은 세부적인 아키텍쳐에 대한 연구도 필요할 것 같다.


테스트 페이지 : ajax_search.html

소스다운로드 : source.zip

레퍼런스
http://www.onlamp.com/pub/a/onlamp/2005/05/19/xmlhttprequest.html
http://developer.apple.com/internet/webcontent/xmlhttpreq.html
http://www.onlamp.com/pub/a/onlamp/2005/05/19/xmlhttprequest.html
http://www.state26.com/download/formdata2querystring.txt
http://jania.pe.kr/wiki/jwiki/moin.cgi/JavaScriptTips

출처 : Tong - bassdot님의 AJAX통



티스토리 툴바