<Google Suggestion 의 ajax 를 활용한 자동완성 기능>

위 그림과 같은 자동완성 기능은 Google Suggest 에서 가장 먼저 구현하였다. 그 후 국내에는 네이버를 필두로 지금은 거의 모든 포탈업체의 검색창에는 마치 유행처럼 위와 같은 기능이 구현되어 있다.

 
 
그럼 ajax 를 이용하여 단순한 자동완성 기능 예제를 살펴보자.
 
 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>Ajax Auto Complete</title>
    <style type="text/css">
    .mouseOut {
    background: #708090;
    color: #FFFAFA;
    }
    .mouseOver {
    background: #FFFAFA;
    color: #000000;
    }
    </style>
    <script type="text/javascript">
        var xmlHttp;
        var completeDiv;
        var inputField;
        var nameTable;
        var nameTableBody;
        function createXMLHttpRequest() {
            if (window.ActiveXObject) {
                xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
            }
            else if (window.XMLHttpRequest) {
                xmlHttp = new XMLHttpRequest();    
            }
        }
        function initVars() {
            inputField = document.getElementById("names");
            nameTable = document.getElementById("name_table");
            completeDiv = document.getElementById("popup");
            nameTableBody = document.getElementById("name_table_body");
        }
        function findNames() {
            initVars();
            if (inputField.value.length > 0) {
                createXMLHttpRequest();
                var url = "AutoCompleteServlet?names=" + escape(inputField.value);
                xmlHttp.open("GET", url, true);
                xmlHttp.onreadystatechange = callback;
                xmlHttp.send(null);
            } else {
                clearNames();
            }
        }
        function callback() {
            if (xmlHttp.readyState == 4) {
                if (xmlHttp.status == 200) {
                    setNames(xmlHttp.responseXML.getElementsByTagName("name"));
                } else if (xmlHttp.status == 204){//데이터가 존재하지 않을 경우
                    clearNames();
                }
            }
        }
       
        function setNames(the_names) {
            clearNames();
            var size = the_names.length;
            setOffsets();
            var row, cell, txtNode;
            for (var i = 0; i < size; i++) {
                var nextNode = the_names[i].firstChild.data;
                row = document.createElement("tr");
                cell = document.createElement("td");
               
                cell.onmouseout = function() {this.className='mouseOver';};
                cell.onmouseover = function() {this.className='mouseOut';};
                cell.setAttribute("bgcolor", "#FFFAFA");
                cell.setAttribute("border", "0");
                cell.onclick = function() { populateName(this);};                    
                txtNode = document.createTextNode(nextNode);
                cell.appendChild(txtNode);
                row.appendChild(cell);
                nameTableBody.appendChild(row);
            }
        }
        function setOffsets() {
            var end = inputField.offsetWidth;
            var left = calculateOffsetLeft(inputField);
            var top = calculateOffsetTop(inputField) + inputField.offsetHeight;
            completeDiv.style.border = "black 1px solid";
            completeDiv.style.left = left + "px";
            completeDiv.style.top = top + "px";
            nameTable.style.width = end + "px";
        }
       
        function calculateOffsetLeft(field) {
          return calculateOffset(field, "offsetLeft");
        }
        function calculateOffsetTop(field) {
          return calculateOffset(field, "offsetTop");
        }
        function calculateOffset(field, attr) {
          var offset = 0;
          while(field) {
            offset += field[attr];
            field = field.offsetParent;
          }
          return offset;
        }
        function populateName(cell) {
            inputField.value = cell.firstChild.nodeValue;
            clearNames();
        }
        function clearNames() {
            var ind = nameTableBody.childNodes.length;
            for (var i = ind - 1; i >= 0 ; i--) {
                 nameTableBody.removeChild(nameTableBody.childNodes[i]);
            }
            completeDiv.style.border = "none";
        }
    </script>
  </head>
  <body>
    <h1>Ajax Auto Complete Example</h1>
    Names: <input type="text" size="20" id="names" onkeyup="findNames();" style="height:20;"/>
    <div style="position:absolute;" id="popup">
        <table id="name_table" bgcolor="#FFFAFA" border="0" cellspacing="0" cellpadding="0"/>           
            <tbody id="name_table_body"></tbody>
        </table>
    </div>
  </body>
</html>
<autoComplete.html 의 전체 소스 코드>
 
 
package ajaxbook.chap4;
import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.servlet.*;
import javax.servlet.http.*;
/**
 *
 * @author nate
 * @version
 */
public class AutoCompleteServlet extends HttpServlet {
    private List names = new ArrayList();
    public void init(ServletConfig config) throws ServletException {
        names.add("Abe");
        names.add("Abel");
        names.add("Abigail");
        names.add("Abner");
        names.add("Abraham");
        names.add("Marcus");
        names.add("Marcy");
        names.add("Marge");
        names.add("Marie");
    }
   
    /** Handles the HTTP <code>GET</code> method.
     * @param request servlet request
     * @param response servlet response
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        String prefix = request.getParameter("names");
        NameService service = NameService.getInstance(names);
        List matching = service.findNames(prefix);
        if (matching.size() > 0) {
            PrintWriter out = response.getWriter();
            response.setContentType("text/xml");
            response.setHeader("Cache-Control", "no-cache");
            out.println("<response>");
            Iterator iter = matching.iterator();
            while(iter.hasNext()) {
                String name = (String) iter.next();
                out.println("<name>" + name + "</name>");
            }
            out.println("</response>");
            matching = null;
            service = null;
            out.close();
        } else {
            response.setStatus(HttpServletResponse.SC_NO_CONTENT);
            //response.flushBuffer();
        }
    }
   
    /** Handles the HTTP <code>POST</code> method.
     * @param request servlet request
     * @param response servlet response
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        doGet(request, response);
    }
       
    /** Returns a short description of the servlet.
     */
    public String getServletInfo() {
        return "Short description";
    }
}
 
<AutoCompleteServlet.java 의 전체 소스 코드>
 
 
 
package ajaxbook.chap4;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
 *
 * @author nate
 */
public class NameService {
    private List names;
   
    /** Creates a new instance of NameService */
    private NameService(List list_of_names) {
        this.names = list_of_names;
    }
   
    public static NameService getInstance(List list_of_names) {
        return new NameService(list_of_names);
    }
   
    public List findNames(String prefix) {
        String prefix_upper = prefix.toUpperCase();
        List matches = new ArrayList();
        Iterator iter = names.iterator();
        while(iter.hasNext()) {
            String name = (String) iter.next();
            String name_upper_case = name.toUpperCase();
            if(name_upper_case.startsWith(prefix_upper)){       
                boolean result = matches.add(name);
            }
        }
        return matches;
    }
   
}
 
<NameService.java 의 전체 소스 코드>


우선 실행결과 화면을 보고 설명을 이어가겠다.



위 그림의 검색어 입력창에 a 라는 문자를 입력하면 매칭되는 문자열의 리스트를 보여주는 그림이다.


입력창에 키워드를 입력하면 이벤트가 발생하고 XHR 객체가 문자를 GET/비동기 방식으로 서버로 보낸다. 서버에서는 요청한 검색어와 매칭되는 결과를 XML 로 만들어서 클라이언트로 보내고, XHR 객체가 응답 XML 을 파싱해서 결과를 화면에 보여주는 흐름이다.


        function callback() {
            if (xmlHttp.readyState == 4) {
                if (xmlHttp.status == 200) {
                    setNames(xmlHttp.responseXML.getElementsByTagName("name"));
                } else if (xmlHttp.status == 204){//데이터가 존재하지 않을 경우
                    clearNames();
                }
            }
        }


위 메소드에는 XHR 객체의 status 코드가 204 일 경우를 처리하고 있다. 즉, 검색 결과가 없을 경우에 결과창을 지우기 위한 것이다.

위 예제에서 눈여겨 봐야 할 부분은 XML 에서 데이터를 분석해서 동적으로 결과 화면을 만드는 부분일 것이다. DOM 객체의 속성 및 메소드를 많이 접해 봐야 한다.


       function setNames(the_names) {
            clearNames();
            var size = the_names.length;
            setOffsets();

            var row, cell, txtNode;
            for (var i = 0; i < size; i++) {
                var nextNode = the_names[i].firstChild.data;
                row = document.createElement("tr");
                cell = document.createElement("td");
               
                cell.onmouseout = function() {this.className='mouseOver';};
                cell.onmouseover = function() {this.className='mouseOut';};
                cell.setAttribute("bgcolor", "#FFFAFA");
                cell.setAttribute("border", "0");
                cell.onclick = function() { populateName(this);};                    

                txtNode = document.createTextNode(nextNode);
                cell.appendChild(txtNode);
                row.appendChild(cell);
                nameTableBody.appendChild(row);
            }
        }


위 메소드는 검색결과의 한 행을 만들어 테이블의 <tr></tr> 요소를 동적으로 생성해 주는 부분이다. cell.onmouseout 및 onclick 메소드 생성할때 setAttribute() 를 사용하지 않은 이유는 IE 가 지원하지 않기 때문이다. cross-browser, 즉 모든 브라우저에서 위 샘플 실행을 보장 받기 위해서는 코딩을 위처럼 해 줘야 한다.


이것으로 4장의 강의는 모두 마친다.



-------------------------- 한글 패치 추가(2006.02.23) ---------------------


위 예제는 한글 테스트가 불가능하기 때문에 이 기회에 Ajax 의 한글문제에 대해서 생각해 보자. GET 방식이든 POST 방식이든 파라미터가 서버로 전달될때는 해당 페이지에 설정된 charset 으로 인코딩 된다. 영문이나 숫자는 문제가 안되지만 한글과 같은 유니코드 문자는 적절한 charset 으로 디코딩해주지 않으면 ??? 혹은 이상한 문자로 출력될 것이다.


Ajax 의 경우 한글 인코딩 문제는 다음과 같이 두 부분으로 나누어서 생각해 볼 수 있다.


첫번째는 브라우저의 XHR 객체에서 한글 파라미터를 인코딩하고 서버에서 디코딩해서 처리하는 경우이다. 물론 같은 charset 으로 처리 해야 된다. 이부분은 개발자가 충분히 charset을 변경할 수 있으므로 유연하게 대처할 수 있다.


두번째는 서버에서 인코딩하고 브라우저의 XHR 객체에서 한글 데이터를 디코딩하는 경우이다. 서버에서 인코딩하는 경우는 개발자가 얼마든지 바꿀 수 있지만 브라우저에서 Ajax 가 데이터를 디코딩할 경우의 charset 은 UTF-8 로 정해져 있는것 같다. 다른 charset 으로 변경할 수 있는 방법이 존재하는지는 확실하지 않지만 여러방면으로 테스트해 본 결과 UTF-8로 지정되어 있는것 같다. 따라서 서버에서 인코딩하는 부분 뿐만아니라 첫번째의 경우도 모두 charset 을 UTF-8 으로 인코딩/디코딩하는 방법이 제일 간단할 것이다.


1. 브라우저에서 charset 을 UTF-8 으로 설정하기

먼저 autoComplete.html 의 소스에서는 charset 을 UTF-8 로 인코딩 해주는 부분만 수정해주면 될 것이다.

var url = "AutoCompleteServlet?names=" + escape(inputField.value);


위 코드에서 escape 메소드는 파라미터를 유니코드방식으로 인코딩하므로 이 메소드 대신에 UTF-8 방식으로 인코딩해주는 자바스크립트 메소드인 encodeURI 혹은 encodeURIComponent 으로 아래와 같이 수정해 준다.


var url = "AutoCompleteServlet?names=" + encodeURI(inputField.value);


위 예제는 GET 방식으로 작성되었으나 POST 방식의 경우도 같은 방법으로 charset 을 설정해주면 된다. 아래 코드는 POST 방식의 findNames 메소드이다.

function findNames() {
        initVars();
        if (inputField.value.length > 0) {
                createXMLHttpRequest();
                var url = "AutoCompleteServlet";
                xmlHttp.open("POST", url, true);
                xmlHttp.onreadystatechange = callback;
                xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                xmlHttp.send("names=" + encodeURI(inputField.value));
                //encodeURI 대신에 encodeURIComponent 를 사용해도 결과는 동일하다.
        } else {
                clearNames();
        }
 }


2. 서버에서 charset 을 UTF-8 로 설정하기

다음은 AutoCompleteServlet 서브릿 소스에서 수정할 부분을 살펴보자.

protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        String prefix = request.getParameter("names");

.

.

.


위의 doGet 메소드에 charset 을 UTF-8 으로 설정해주는 코드를 추가해준다. 그러면 브라우저에 UTF-8 방식으로 인코딩된 한글 파라미터는 같은 charset 으로 디코딩 될 것이다.


protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        String prefix = request.getParameter("names");

.

.

.


다음은 서버에서 브라우저로 응답을 보내기 전에 charset 을 UTF-8 로 바꿔줘야 한다.

response.setContentType("text/xml");


위 소스 코드를 아래와 같이 수정해 준다.

response.setContentType("text/xml;charset=UTF-8");


마지막으로 DB 조회결과를 가상으로 꾸미기 위하여 한글 데이터를 추가해 준다.

public void init(ServletConfig config) throws ServletException {
        names.add("Abe");
        names.add("Abel");
        names.add("Abigail");
        names.add("Abner");
        names.add("Abraham");
        names.add("Marcus");
        names.add("Marcy");
        names.add("Marge");
        names.add("Marie");
        names.add("스타크");
        names.add("스타크래프트");
        names.add("스타크래프트 치트키");
        names.add("스타크래프트 다운로드");
    }



+ Recent posts