주요글: 도커 시작하기
반응형

트위터, 페이스북, 유스트림 등은 페이지 이동 없이 한 페이지에서 사용자의 요청을 처리하는 1 페이지로 구성된 웹 어플리케이션이다. 크롬이나 파이어폭스에서 이들 서비스를 사용해보면  웹 브라우저의 주소는 변경되는 것 처럼 보이더라도 실제로는 페이지 이동 없이 Ajax 요청을 이용해서 웹 페이지의 특정 부분만 변경하는 방식으로 구현되어 있다. 인터넷 익스플로러로 실행할 경우 해시(#) 기호를 이용해서 전체 화면 변경 없이 필요한 부분만 교체하도록 구현하고 있다.


최근에 개인적으로 개발중인 웹 어플리케이션도  일부 화면들을 1 페이지 웹 어플리케이션으로 구현하고 있는데, HTML5의 History API 지원 브라우저와 비지원 브라우저 모두에서 동일한 코드를 사용하기 위해 History.js 사용하였다. History.js는 History/State API와 유사한 API를 제공하고 있으며, History/State API를 지원하는 브라우저(최신 버전의 크롬이나 파폭)에서는 이 API를 사용하며 미지원 브라우저(인터넷 익스플로러)에서는 해시(#)를 사용해서 기능을 구현하고 있다. 따라서, History.js의 사용자는 브라우저의 기능 지원 여부에 상관없이 동일한 코드로 1 페이지 웹 어플리케이션을 구현할 수 있다.


History/State API나 History.js 자체에 대한 설명은 https://github.com/browserstate/history.js 사이트와 HTML5 관련 서적/글을 참고하기 바라며, 본 글에서는 실제로 History.js를 이용해서 1 페이지 웹 어플리케이션을 구현하는데 사용하는 코드를 정리하였다.


1페이지 웹 어플리케이션의 구현 방식


1 페이지 웹 어플리케이션은 일반적으로 다음과 같은 방식으로 구현된다. (아래 표는 시간 순서대로 정리한 것이다. 그림으로 그리고 싶었지만, 게으름에 표로 대신한다.)


 순서

클라이언트 (웹브라우저) 

웹 서버 

 1최초에 웹 브라우저에서 주소를 입력
-> 웹 서버에 요청 전송
 
 2 

웹 서버의 단일 프로그램이 요청 받음

-> 1 페이지 웹 어플리케이션을 위한 HTML 응답 제공

 3

 응답 화면 출력

 
 4

<a> 태그 등의 링크 클릭/또는 웹 브라우저 주소 변경

-> 상태 변경 이벤트 캐치

-> 새로운 주소로 이동인 경우, 이벤트 전파 중지

-> Ajax로 웹 서버에 이동할 주소에 해당하는 데이터 요청

 
 5 

Ajax 요청과 매핑되는 프로그램이 요청 받음

-> 화면 구성에 필요한 데이터 응답

 6

 Ajax 응답 도착

-> Ajax 응답 데이터를 이용해서 화면 갱신

 


위 흐름을 보면 웹 브라우저의 조소가 변경될 때 다음의 두 가지 다른 상황이 있음을 알 수 있다.

  • 사이트에 최초로 들어오는 모든 요청은 서버의 단일 프로그램(스프링의 경우 컨트롤러)이 처리한다.
  • 이후 동일 사이트 내에서의 주소 변경(<a> 태그 클릭이나 브라우저 주소 변경)은 Ajax 요청으로 처리된다.
사이트에 대한 최초 진입은 URL에 상관없이 단일 컨트롤러가 처리한다. 이때 단일 컨트롤러는 1 페이지 웹 어플리케이션 HTML 응답을 제공한다. 이후, 뒤에서 살펴보겠지만, 페이지 이동 페이지 이동 요청이 발생할 때 마다 Ajax를 이용해서 서버에서 새로운 내용을 가져와 화면에 보여주며, 실제 페이지 이동은 발생하지 않는다. (페이지 이동 없이 한 개의 HTML 페이지에서 모든 기능이 제공되기 때문에, 1 페이지 웹 어플리케이션이라고 표현한다.)

위 방식을 처리하려면 요청 주소가 다음과 같이 두 가지로 구분된다.
  • 서버의 단일 프로그램으로 들어가는 요청 주소
  • Ajax 요청 주소
이 두 가지를 다른 방식으로 처리하도록 구성해주면 된다. 예를 들어, 단일 프로그램으로 들어가는 요청은 "/app/"으로 시작하고, ajax 요청 주소는 "/rest/"로 시작하는 식으로 규칙을 정하면 좋다.

History.js를 이용한 1 페이지 웹 어플리케이션 구현


이제 위 과정을 History.js가 제공하는 기능을 이용해서 실제로 구현해 보자. 먼저 최초 진입을 처리할 서버 프로그램이 필요하다. 이 서버 프로그램은 웹 브라우저에 1 페이지 웹 어플리케이션을 실행해주는 HTML을 제공하는 역할을 맡는다. 예를 들면, 다음과 같은 스프링 컨트롤러가 될 수 있다.


@Controller

public class AppController {


    @RequestMapping("/app/**")

    public String app() {

        return "app/app";

    }

}


이 AppController의 결과로 생성되는 뷰는 다음과 같이 1 페이지 웹 어플리케이션을 구동해주는 HTML 코드를 응답으로 제공하면 된다. 참고로, 아래 코드는 HTML5와 HTML4 브라우저 모두를 지원하고 jQuery를 지원하는 history.js를 사용하였다.


<%@ page contentType="text/html; charset=UTF-8" %>

<!DOCTYPE HTML >

<html>

<head>

    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

    <title>홈</title>

    <script src="/js/jquery/jquery-1.9.1.min.js"></script>

    <script src="/js/history.js/bundled/html4+html5/jquery.history.js"></script>

    <script type="text/javascript">


    $(document).ready(function(){

        (function(window){

            var History = window.History;

            if ( !History.enabled ) {

                return false;

            }

            History.Adapter.bind(window,'statechange',function(){

                var State = History.getState();

                load(History.getState().url+".data", "#contentArea");

            });

            

        })(window);


        load(History.getState().url+".data", "#contentArea");

    }

    

    function load(url, target) {

        $.ajax({

            url: url,

            success: function(data) {

                $(target).html(data);

                $('a.state').click(function(evt) {

                    evt.preventDefault();

                    History.pushState(null, null, $(this).attr('href'));

                });

            }

        });

    }

    </script>

</head>

<body>

    <div id="contentArea"></div>

</body>

</html>


위 코드는 크게 두 부분을 구성된다.

  • 문서 로딩이 완료될 때 History API를 이용해서, 윈도우 상태 변경시 Ajax를 호출하는 부분
  • Ajax 호출해서 결과를 화면에 보여주는 부분

먼저 문서 로딩이 완료되면 History API를 이용해서 윈도우에 상태변경 이벤트 핸들러를 등록한다.


History.Adapter.bind(window,'statechange',function(){

var State = History.getState();

load(History.getState().url+".data", "#contentArea");

});


History.getState() 메서드는 변경된 최종 상태 값을 구하는데, 이 상태의 url 속성을 변경될 주소 값을 의미한다. 따라서, 위 코드는 window에서 statechage 이벤트가 발생하면, 변경될 주소를 구한 뒤 그 주소뒤에 ".data"를 load() 메서드를 호출한다. 예를 들어, 변경할 주소가 "/deal/detail" 이라면, load() 메서드에서 전달되는 값은 "/deal/detail.data"이다.


위 코드에서 statechange 이벤트는 History.js가 제공하는 API를 이용해서 상태를 변경했을 때 발생한다. 실제로 이 이벤트와 관련된 처리를 하는 코드는 load() 메서드에 있다.


function load(url, target) {

    $.ajax({

        url: url,

        success: function(data) {

            $(target).html(data);

            $('a.state').click(function(evt) {

                evt.preventDefault();

                History.pushState(null, null, $(this).attr('href'));

            });

        }

    });

}


load() 메서드는 파라미터로 전달받은 url에 ajax 요청을 날리고, 그 결과를 target의 HTML로 설정한다. 즉, target이 가리키는 HTML 태그의 내용을 Ajax 응답으로 교체한다. 그리고, css 클래스가 state인 <a> 태그에 클릭 이벤트를 적용한다. 이 이벤트 핸들러는 이벤트 전파를 중단하고, History.pushState()를 이용해서 a 태그의 href 속성 값을 새로운 상태로 넣어준다.


History.pushState()를 실행하면 statechage 이벤트가 발생하게 된다. 따라서, css 클래스가 state인 <a> 태그를 클릭하면, 앞서 작성한 windowstate 이벤트 핸들러가 실행되고, 따라서 <a> 태그의 href 값을 이용해서 load() 함수가 다시 호출된다.


또한, ready()에 전달한 함수를 보면 마지막에 load(History.getState().url+".data", "#contentArea"); 코드를 실행하는데, 이는 현재 웹 브라우저의 URL + ".data"를 이용해서 load() 메서드를 실행하게 된다. 따라서, 최초에 웹 브라우저로 사이트에 접속하면 해당 URL 뒤에 .data를 붙인 Ajax 요청을 한 뒤에 그 결과를 contentArea 영역에 출력하게 된다. 


전체 흐름 정리


위 내용을 실행 순서대로 정리해보면 다음과 같다.


내용 

관련 코드 또는 함수

최초에 웹 페이지가 로딩되어 ready()에 전달한 함수가 실행된다.

최초 주소를 "/home" 이라고 가정하자.

 

window.statechage 이벤트에 핸들러를 등록한다.

ready 관련 코드:

History.Adapter.bind(window,'statechange', ... );

load("/home.data", "#contentArea") 메서드를 실행한다.

ready 관련 코드의 끝

ajax를 이용해서 "/home.data"를 요청한다.

load() 함수 

ajax 응답이 도착하면 #contentArea에 응답 결과를 보여준다.

응답 결과 중 다음 코드가 있다고 가정하자.


<a href="/list" class="state">list</a>

load() 함수 내부:

$(tager).html(data);

css 클래스가 state인 a 태그에 클릭 이벤트 핸들러를 등록한다.

load() 함수 내부:
$('a.state').click(function(evt) { ... });

사용자가 페이지 내용 확인

 

사용자가 /list인 a 태그를 클릭한다.

 

앞서 등록한 a 태그 클릭 이벤트 핸들러가 실행된다.
- 이벤트 전파를 취소해서 웹 브라우저가 페이지 이동을 하지 않도록 한다.

- History.pushState를 이용해서 a 태그의 링크 값('/list")을 추가한다.

- History.pushState() 메서드가 실행되면, statechange 이벤트가 발생한다.

load() 함수 내부:
evt.preventDefault();
History.pushState(null, null, $(this).attr('href'));

statechange 이벤트 핸들러가 실행된다.

상태로부터 url 값을 읽어와 load()를 실행한다. 앞서 발생한 url 값이 "/list" 였으므로, 다음의 load() 코드가 실행된다.


load("/list.data", "#contentArea");

History.Adapter.bind(window,'statechange',
    function(){
        var State = History.getState();
        load(History.getState().url+".data",

             "#contentArea");

    }

);

앞과 동일한 과정을 거쳐, contentArea 영역에 "/list.data"의 ajax 응답 결과가 표시된다.

 


실제 1페이지 웹 어플리케이션을 만들려면 이 보다 더 복잡한 과정을 거치겠지만, 기본적인 골격은 앞서 살펴본 것과 같다. (예를 들어, 경우에 따라 일부 영역만 변경하거나 전체를 변경하거나 하는 식의 차이가 있거나, statechange 이벤트를 처리하는 코드가 조건에 따라 다른 기능을 실행하도록 바뀔 수 있을 것이다.)


1페이지 웹 어플리케이션의 장점


1페이지 웹 어플리케이션의 장점은 웹 브라우저가 페이지 이동을 하지 않기 때문에, 웹 페이지와 관련된 연결을 유지할 수 있다는 데에 있다. 예를 들어, 유스트림의 경우 현재 시청중인 방송 시청을 계속하면서 다른 컨텐츠 목록을 탐색할 수 있으며, 페이스북의 경우 클릭을 통해 화면 이동을 하면서 동시에 끊김없이 채팅을 진행할 수 있다.


참고 자료

  • History.js : https://github.com/browserstate/history.js


+ Recent posts