HTML5 PushState()를 활용한 PJAX 구현




HTML5 PushState()를 활용한 PJAX 구현




기존 AJAX 사용 시 이용할 수 없었던 브라우저 뒤로 가기(히스토리 내역을 활용한 기능)기능을 보완하기 위한 방법으로 "location.hash" 를 활용한 Hashbang(hash: # 와 bang: !의 합성)기술이 등장 했습니다.



하지만 이 방식은 뒤로 가기 기능은 처리할 수 있지만, 일종의 URL Hack 방식으로, 검색 엔진들로 하여금 인덱싱되지 않는 단점(구글은 해시뱅에 대해 인덱싱이 가능하도록 escape 처리함)을 가지고 있습니다. 


또한, 방식 자체가 자바스크립트 기술(ajax)에 크게 의존하고 있기 때문에 스크립트 오류 시 깨지기 쉬운 사이트 구조를 가지게 됩니다.


이번 포스트에서 설명드릴 방식은 HTML5 명세의 PJAX(pushState + ajax)라는 기술이며, 이 기능을 통해 뒤로 가기 기능과 검색 인덱싱 처리를 동시에 보완할 수 있습니다.


하지만 HTML5 명세 기술이므로 모든 브라우저에서 지원하지 않는 단점도 가지고 있습니다.



아래는 PJAX(pushState + ajax)를 구현한(서버 + 클라이언트) 예제 코드이며, 코드에서처럼 전송 방식(ajax, normal)을 분기하여 접근 방식에 따라 사용자에게 다른 결과(컨텐츠)를 보여주게 됩니다.




- Ajax 분기 처리에 대한 더 자세한 설명은 아래 포스트 주소를 확인하시기 바랍니다.


http://mohwaproject.tistory.com/entry/Ajax-%EC%A0%84%EC%86%A1-%EA%B5%AC%EB%B6%84%ED%95%98%EA%B8%B0






1. 서버측(.NET MVC 기준) 코드:

public string Pjax1()
{
    // GET 요청 시 Jquery 및 Prototype과 같은 대표적인 라이브러리에서 ajax 요청을 판단하는 요청 값(X-Requested-With)을 추가하여 요청한다.
    if ((Request.Headers["X-Requested-With"] != null && Request.Headers["X-Requested-With"].ToLower() == "xmlhttprequest") || Request.Headers["ORIGIN"] != null)
    {
	return "Ajax Request History 1";
    }
    else
    {
	return "Normal Request History 1";
    }
}

public string Pjax2()
{
    // X-Requested-With Jquery 및 Prototype과 같은 대표적인 라이브러이에서 아래와 같은(X-Requested-With) 요청 헤더를 추가하여 요청한다.
    if ((Request.Headers["X-Requested-With"] != null && Request.Headers["X-Requested-With"].ToLower() == "xmlhttprequest") || Request.Headers["ORIGIN"] != null)
    {
	return "Ajax Request History 2";
    }
    else
    {
	return "Normal Request History 2";
    }
}


2. 클라이언트 측(자바스크립트) 코드: 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<script type="text/javascript" src="/sabo/contents/script/jquery.js"></script>
<title></title>
 
 
<script type="text/javascript">
/*
    
History.pjax(History.pjax);
History.push(data, title, url);
    
*/
 
var History = new (function () {
 
    var History = function () {
 
        return this;
    }
 
    History.fn = History.prototype = {
        pjax: function () {
 
            url = url || '';
            callback = callback || function () { ; };
 
            var xhr = new XMLHttpRequest();
            xhr.open("get", url, true);
 
            xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
 
            var that = this;
 
            xhr.onload = function () {
                callback.apply(that, [xhr.response]);
            };
 
            xhr.send(null);
 
        },
        push: function (data, title, url) {
 
            data = data || '';
            title = title || '';
            url = url || '';
 
            window.history.pushState && window.history.pushState(data, title, url);
 
            return this;
        }
    }
 
    return History;
 
} ())();
 
 
bind(window, 'load', function () {
 
    bind(document.getElementById('pjax1'), 'click', function (e) {
 
        History.pjax('http://kin.dragonflygame.co.kr/sabo/blog/board/Pjax1/', function (data) {
 
            var data = document.getElementById('containerHTML').value + data + ", " + new Date().getTime() + ' ';
 
            // pushState 추가
            this.push(data, '', window.location.href);
            show(data);
        });
 
        window.history.pushState && stopDefault(e);
 
    }, false);
 
    bind(document.getElementById('pjax2'), 'click', function (e) {
 
        History.pjax('http://kin.dragonflygame.co.kr/sabo/blog/board/Pjax2/', function (data) {
 
            var data = document.getElementById('containerHTML').value + data + ", " + new Date().getTime() + ' ';
 
            // pushState 추가
            this.push(data, '', window.location.href);
            show(data);
 
        });
 
        window.history.pushState && stopDefault(e);
 
    }, false);
 
 
    bind(window, 'popstate', function (e) {
        show(e.state);
    });
 
}, false);
 
function show(data) {
    if (data) document.getElementById('containerHTML').value = data;
}
 
 
function bind(elem, type, handler, capture) {
    type = typeof type === 'string' ? type : '';
    handler = typeof handler === 'function' ? handler : function () { ; };
    capture = capture || false;
 
    if (elem.addEventListener) {
        elem.addEventListener(type, handler, capture);
    }
    else if (elem.attachEvent) {
        elem.attachEvent('on' + type, handler);
    }
 
    return this;
}
 
function stopDefault(e){
    if (window.event) e.returnValue = false;
    else if (e.preventDefault) e.preventDefault();
};
 
 
//]]>
</script>
</head>
<body>
<textarea id="containerHTML" rows="" cols="" style="width:500px;height:500px"></textarea><br /><br />
<a id="pjax1" href="http://kin.dragonflygame.co.kr/sabo/blog/board/Pjax1/">Pjax1</a>
<a id="pjax2" href="http://kin.dragonflygame.co.kr/sabo/blog/board/Pjax2/">Pjax2</a><br /><br />
</body>
</html>



3. 실행 결과:


PJAX 를 지원 브라우저에서는 아래 결과와 같이 모든 Ajax 요청에 대한 히스토리(뒤로/앞으로 가기 지원)를 가지게 됩니다.





반면, 그렇지 않는 브라우저(IE8-)에서는 서버측 분기 처리로 인해 <a> 요소의 href 속성에 할당된 주소의 컨텐츠를 
가지게 됩니다.




일반 전송 방식에 의한 결과.





참고 사이트:


ajax 와 hashbang 그리고 pjax:

http://rkjun.wordpress.com/2012/05/29/ajax-%EC%99%80-hashbang-%EA%B7%B8%EB%A6%AC%EA%B3%A0-pjax/


HTML5 History/State API를 이용한 Ajax 히스토리 구현:

http://firejune.com/1743#0