Rev. 2.73

HTML5의 History/State API를 이용하여 브라우저의 히스토리를 조작하는 방법에 대해 알아보겠습니다. 그동안 location.hash로 꾸리 하게 밖에 구현할 수 없었던 브라우저 히스토리를 대체할 수 있는 명세로서, Ajax로 호출한 컨텐츠가 드로잉 된 페이지를 조회한 후 브라우저에서 "뒤로 가기" 버튼을 눌러 다시금 이전 위치로 이동하고 해당 컨텐트를 조회할 수 있게 하는 목적으로 사용할 수 있습니다. 알다시피, 이와 같은 뒤치다꺼리를 하지 않으면 방문객은 우리의 웹사이트를 무심결에 떠나 버리는 불상사가 발생할 수 있지요. 얼마 전에 소개한 Customized JS Bin에 이 명세를 적극적(대부분의 요청에 사용)으로 반영하면서 알아낸 몇 가지 사실들에 대하여 정리해보겠습니다.

비동기(동적으)로 컨텐츠를 갱신할 때 매번 마음 한구석에 남는 응어리는 바로 시멘틱 웹입니다. Ajax로 불러들여 진 컨텐츠가 정적이어(검색되어)야 할 필요성 가장 먼저 판단해야 합니다. 왜냐하면, 이에 따른 추가작업이 필요하거나 구조가 달라지기 때문이죠. 만약 시멘티컬한 환경을 고려한다면 서버-사이드에서 하나의 URL을 각기다른 형식의 데이터로 응답할 필요가 있습니다. 비동기 요청에는 레이아웃을 제외한 갱신영역이나 내용만을 담은 작은 HTML 조각 혹은 JSON과 같은 응답형태를 가지고, 일반적인 요청에는 레이아웃을 포함한 완벽하게 파싱된 정적인 HTML 컨텐트를 응답하는 두 가지를 모두 준비하는 것이 좋습니다.

A 요소에 href 속성에는 페이지가 갱신될 주소를 반드시 기입해서 웹 크롤러들이 컨텐츠를 샅샅이 긁어갈 수 있게 하고 클릭을 자바스크립트로 제어하여 Ajax로 요청 및 갱신하는 것이 일반적인 패턴입니다. 하나의 주소에서 응답형태가 다른 컨텐트를 제공하는 것이 마음에 안들지도 모릅니다. 물론 개별적으로 응답형태에 따른 주소를 제공하거나 파라미터를 이용할 수도 있겠지만, 개인적으로 선호하지 않는 방법이기 때문에 개별 주소를 사용하는 방법에 대해서는 생략하겠습니다. 대부분의 서버-사이드 언어는 Ajax 요청인지를 구분할 수 있으며, PHP는 다음과 같은 방법으로 알아냅니다.

<?php
  $ajax = isset($_SERVER['HTTP_X_REQUESTED_WITH'])
      || (isset($_SERVER['HTTP_ORIGIN']) && $_SERVER['REQUEST_METHOD'] == 'GET');

  if ($ajax) {
    header("Content-Type: application/json; charset=utf-8");
?>
{
  "title": "Fire JS Bin",
  "url": "http://jsbin.firejune.com",
  "text": "Collaborative playground for web developers."
}
<?php
  } else {
    header('Content-type: text/html; charset=utf-8');
?>
<html>
  <head>
    <title>Fire JS Bin</title>
  </head>
  <body>
    <div id="content">Collaborative playground for web developers.</div>
  </body>
</html>
<?php
}
?>

JS Bin의 컨텐츠는 코드이기 때문에 검색으로 접근될 확률이 낮습니다만, 기본 설계가 정적인 컨텐츠를 제공하는 형태로 만들어져 있기 때문에 시멘틱한 모습으로 방향을 잡았습니다. History API를 지원하는 브라우저에서는 동일한 주소에서 JSON형식의 데이터만 수신하고 그렇지 않으면 주소창의 주소가 변경되면서 이동합니다. 이제 브라우저 히스토리를 조작해 봅시다.

  function drawHTML(data, state) {
    if (state) history.pushState(data, data.title, data.url);
    $('#content').html(data.text);
  }

  function requestError(req, state) {
    alert('WTF? ' + state);
    //location.href = this.url;
  }

  $('a').click(function() {
    if (history.pushState) {
      $.getJSON(this.href).success(drawHTML).error(requestError);
      return false;
    }
    return true;
  });

범국민 라이브러리인 jQuery를 이용한 간단한 예입니다. A 요소의 클릭을 감시하는 함수는 지원 여부를 구분하여 href에 명시된 주소로 JSON 요청을 날린후 이벤트 진행을 중단하며 그렇지 않은 경우 A 요소가 정상적으로 클릭 되어 일반적인 주소 이동이 발생하도록 내버려 둡니다. drawHTML 함수에는 패이지가 갱신될 코드와 history.pushState 메서드를 호출하는 코드로 구성되어 있습니다. 이 메서드는 3개의 인자를 받는데, 해당 페이지의 상태를 보존하기 위한 데이터와 문서 제목 그리고 해당 페이지의 URL입니다. drawHTML 함수의 두 번째 state 인자가 유효한 때에만 실행되며 호출되는 즉시 브라우저의 주소창이 바뀌고 히스토리 스택이 쌓입니다. 여기까지는 쌓기만 한 것입니다. 이제 히스토리가 변경되면 발생하는 이벤트인 window.onpopstate를 작성해 봅시다.

  $(window).bind("popstate", function(event) {
    var data = event.originalEvent.state;
    if (data) drawHTML(data, false);
  });

이때 보존해 두었던 데이터를 그대로 캐시처럼 활용함으로써 두 번 다시 서버를 힘들게 하지 않아도 되는 장점이 있습니다. 그리고 drawHTML의 두번째 인자를 false로 넘겨 주는 것은 히스토리를 더이상 쌓지 않게 하기 위함입니다. 이미 쌓여있는 녀석을 또 쌓을 필요는 없으니까요. 이외에도 history.replaceState라는 메서드가 있는데, 해당 주소의 컨텐트가 갱신된 경우에 사용하는 것입니다. 이게 전부에요. 간단하죠? 해시가 아닌 URL이 변경되는 레알 히스토리이면서 Ajax로 신규 컨텐트만 갱신하고 캐시효과도 누릴수 있기 때문에 매우 빠릅니다. 우리가 잘 알고있는 GitHub에서도 슬라이드 효과를 동반한 소스코드 살펴보기 그리고 개시물을 조회하는 곳곳에 이 API를 사용하고 있습니다.

음... History/State API를 지원하지 않는 브라우저에서는 종전과 같이 location.hash트릭으로 비스무리한 기능을 부가적으로 제공할 수 있습니다. HTML5 History/State API를 모든 브라우저에서 사용 가능하게 하는 History.js도 좋은 대안이 될 수 있을 것아서 적용했다가, JS Bin은 이미 이 해시를 이용하여 에디터의 뷰 활성 상태를 관리하기 때문에 충돌이 발생했고 또한 다소 무거운 느낌이 들어서 바로 제거했습니다. jsbin.firejune.com으로 이동해서 컨텐츠와 주소창이 변경되는 모습을 직접 확인해보세요. 참 그리고 커스터마이즈한 JS Bin도 많이 진척되었습니다. 관심 있으신 분들은 릴리즈 노트도 함께 봐주세요. 소개글 이후 자세한 변동내역이 모두 기록되어 있습니다.

Comments

Got something to add? You can just leave a comment.

Your Reaction Time!

captcha

avatar