Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.

기술 뉴스 #73 : 17-03-01

웹개발 관련

  • The Google Analytics Setup I Use on Every Site I Build : 글쓴이가 수없이 웹사이트에 GA를 설정해 보면서 얻는 경험으로 GA 스크립트의 로딩 자체를 최적화하는 방법과 커스텀 디멘션을 사용해서 유용한 추적설정을 한 내용을 모두 설명하고 있다. 설정 방법과 어떻게 보고 참고할 수 있는지까지 잘 나와 있어서 웬만한 사이트는 이 글의 내용을 그대로 따라 해도 될 정도고 여기 나온 내용을 바탕으로 사이트에 필요한 추적을 추가하기에도 좋다. 이 사람은 GA를 엄청나게 연구했는지 비슷한 작업을 매번 하다가 결국 autotrack 플러그인을 개발했고(이 플러그인만 사용해도 엄청 편하다) 이 글에서 설명한 코드를 참고할 수 있도록 analytics.js boilerplate를 만들어서 공유하고 있다.(영어)
  • This browser tweak saved 60% of requests to Facebook : 정적 리소스의 캐시 만료시간이 지나면 서버에 조건부 GET 요청을 보내서 확인하는데 페이스북은 hash로 URL을 만들므로 파일이 바뀔 일이 없다. 이런 경우 GET 요청이 필요가 없으므로 Chrome팀과 Firefox팀과 협의해서 이 부분을 개선했다. Chrome은 긴 캐시를 가진 파일에서는 GET 요청을 줄이고 Firefox는 cache-control: immutable 헤더를 추가해서 이 문제를 해결했다. 이를 통해 사용자는 페이지를 더 빨리 볼 수 있고 서버는 요청을 훨씬 적게 받을 수 있게 되었다. 정적 리소스 URL에 Hash를 사용하고 있다면 이 글의 내용을 참고해 볼 만하다. 서버에서 적용할 작업도 그다지 없다.(영어)
  • 구글 Test My Site를 통해 사이트 모바일 친화도를 측정해 보세요! : 구글이 웹사이트의 성능을 분석하는 Test My Site의 한글 보고서를 지원하기 시작했다. 사실 구글에서는 PageSpeed Insights모바일 친화성 테스트도 제공하고 있는데 각기 얼마나 다른 지까진 잘 모르겠다.(한국어)
  • Annotation is now a web standard : 웹사이트에 댓글을 달거나 메모를 했을 때 이를 표준으로 다른 서비스와 공유할 수 있도록 웹 어노테이션이 W3C의 표준이 되었다.(영어)
  • JavaScript Garden : JavaScript 언어에서 자주 겪는 실수나 미묘한 버그 등을 설명하는 javaScript Garden을 번역한 페이지.(한국어)

그 밖의 프로그래밍 관련

  • Google, SHA-1 해쉬함수 충돌쌍 공격 발표 : 더는 안전하지 않다고 판단되던 SHA-1의 충돌이 실제 가능하다는 것이 공개되었다. 이를 설명하는 shattered 사이트를 만들어서 공개했고 내용이 다른 두 개의 PDF가 같은 SHA-1 해시를 가져서 공격할 수 있음을 증명했다. 이 소스코드는 90일 뒤에 공개하기로 했다. 이미 SHA-1을 쓰면 안 된다고 얘기되고 있었지만 실제로 공격할 수 있음이 증명되었으므로 정말 SHA-256등으로 갈아타야 한다. SHA-1을 쓰는 git에는 암호화처럼 큰 영향은 없고 git도 sha-1을 벗어날 거라고 리누스 토발스가 글을 쓰기도 했다.
  • Docker Swarm을 이용한 쉽고 빠른 분산 서버 관리 : Docker Swarm으로 서버 오케스트레이션 하는 방법을 설명한 글이다. 현재 사용 가능한 오케스트레이션 도구들의 장단점도 정리되어 있고 Swarm이 제공하는 기능을 설명한 후 실제로 따라 해 보면서 테스트해볼 수 있게 글이 작성되어 있어서 오케스트레이션 도구를 검토하고 있다면 찬찬히 읽어봐야 할 글이다. 얼마 전에 Docker Swarm을 보고 간단하면서 기능이 강력해서 꽤 좋은 인상을 받았는데 정리된 글이 나와서 반갑다.(한국어)
  • Node 6 at Wikimedia: Stability and substantial memory savings : Wikimedia에서 Node.js v4를 사용하던 시스템은 Node.js v6으로 올린 과정과 결과를 정리한 글이다. 실제로 코드를 바꾼 것은 없고 생길지 모르는 문제에 대한 준비와 성능 테스트에 관한 얘기가 대부분이다. v6으로 올린 것 만으로도 프로덕션 서버의 메모리 사용이 전반으로 줄어든 것을 볼 수 있다.(영어)

볼만한 링크

  • A glimpse into GitHub's Bug Bounty workflow : GitHub이 버그 바운티 프로그램을 3년간 운영하면서 GitHub 내부에서 올라온 각 버그를 어떻게 처리하고 위험도 등급을 어떻게 나눈 뒤 엔지니어링팀과 애플리케이션 보안팀이 이슈를 처리하는지를 설명한 글이다.(영어)
  • 텐센트를 모바일 왕좌에 앉힌 주역 '쟝샤오룽' : 텐센트에서 위챗을 만든 쟝샤오룽에 대한 글이다. 이전에 폭스메일을 만들어서 매각하고 텐센트에서 QQ 메일을 만들면서 위챗을 만들어서 성공하기까지의 과정이 나와 있다. 중국 서비스의 흐름은 잘 모르는 터라 재미있게 읽었다.(한국어)

IT 업계 뉴스

  • Incident report on memory leak caused by Cloudflare parser bug : Cloudflare의 HTML 파서의 버그로 인해서 Cloudflare를 사용하는 사이트의 비밀번호, API 키, 토큰 등이 메모리를 통해서 유출되는 상황이 벌어졌다. 이 버그는 구글 등 검색엔진이 크롤링하면서 해당 비밀 정보를 크롤링하면서 Cloudflare에 보고되었고 해당 검색엔진의 캐시를 삭제하고 발표되었다. 버그의 흐름을 보면 최초는 작년 9월부터 메모리를 통한 유출이 시작된 것으로 보고 있다. sites-using-cloudflare에 Cloudflare를 사용하는 웹사이트들이 정리되어 있는데 여기 있는 사이트가 모두 유출된 것은 아니지만 유출된 사이트 목록을 따로 보고하지 않았기 때문에 Cloudflare를 이용하는 웹사이트를 사용하고 있다면 비밀번호 등을 바꾸기를 권고하고 있다. 이 사건은 Cloudbleed라고 불리는데 Cloudbleed Simple Checker에서 자신이 이용하는 사이트를 간단히 확인해 볼 수 있다.(영어)
  • Amazon AWS S3 outage is breaking things for a lot of websites and apps : 미국 태평양 타임 기준 2월 28일 오전 11시 반 정도부터 3시간 정도 Amazon의 스토리지 서비스 S3 버지니아 리전(US-EAST-1)에서 장애가 발생했다. S3에서 장애가 발생해서 US-EAST-1을 이용하는 다수의 서비스가 해당 시간에 영향을 받은 것으로 보고 있다.(영어)
  • Mozilla Acquires Pocket : Mozilla가 북마킹 서비스인 Pocket을 인수했다.(영어)

프로젝트

  • awless : Go로 작성된 AWS CLI.
  • autocannon : Node.js로 작성된 간단한 HTTP 벤치마크 도구로 autocannon-ci를 쓰면 빌드할 때마다 결과를 출력하게 할 수 있다.
  • trevor : travis.yml로 다양한 버전의 Node.js를 로컬에서 Docker로 테스트해볼 수 있게 하는 프로젝트.
  • Prophet : Facebook에서 오픈 소스로 공개한 시계열 데이터의 예측 도구로 R과 Python으로 작성되었다.
  • neutrino : Webpack을 이용해서 웹 애플리케이션이나 Node.js 프로젝트 설정을 구성하는 프로젝트.
  • AR.js : three.js와 jsartoolkit5를 이용해서 웹에서 증강현실을 구현한 프로젝트.
  • Upspin : 구글에서 공개한 Go로 작성한 실험 프로젝트로 파일이나 데이터를 안전하게 공유하는 프레임워크이다.

버전 업데이트

2017/03/01 19:31 2017/03/01 19:31

Ajax를 사용할 때 웹브라우저 "뒤로 가기"의 구현

전에 블로그 글 주제를 요청받기로 하고 올라온 이슈 중에 Ajax를 사용할 때 "뒤로 가기"에 대한 요청이 있었다. 난이도가 아주 높은 건 아니므로 재밌겠다는 생각이 들었다.

일단 최근에는 Ajax를 쓸 때 페이지 뒤로 가기를 직접 구현해 본 적이 없다. 나는 "문서로서의 웹"이라는 기본 개념을 중시하기 때문에 Ajax를 본문 중심으로 헤비하게 쓰지 않는 편이고 SPA(Single Page Application)을 많이 선호하진 않지만, SPA를 하더라도 Angular.js나 React, Backbone 같은 프레임워크에서 제공하는 라우팅을 썼기 때문에 직접 구현해 본 적은 없다. 이 글은 내가 아는 지식 선에서 Ajax를 사용할 때의 "뒤로 가기"에 대해서 정리한 글이지만 그사이에 뭔가 새로운 방법이나 접근이 있을 수도 있는데 그런 경우 알려주면 좋겠다.

글 목록을 보여주는 방식

해당 이슈를 보면 "'페이스북'처럼 목록이 로딩된 상태 (페이징 처리X) 에서 뒤로 가기 처리하는 방법"이라고 나와 있는데 내 지식 범위에서 목록에 대한 관리는 2가지로 나뉜다. 그래서 이해를 도우려면 이 2가지를 먼저 구분하고 설명하는 게 더 좋겠다.

전통적인 방식의 게시판 같은 것을 생각해 보자. 게시판의 글 목록이 화면에 나오고 다음 페이지로 이동하면 글 목록만 Ajax로 불러오게 된다. 이 경우 URL 등으로 정해진 각 페이지 번호에 따라 보여야 하는 글의 목록이 정해져 있다. 물론 새로운 글이 올라오면 목록이 달라지겠지만, 프로그램 관점에서는 불러와야 하는 글의 목록이 정해져 있다.

대신 페이스북이나 트위터 같은 경우의 목록을 생각하면 무한스크롤이다. 페이지별로 나오는 글 목록이 정해져 있는 것이 아니라 URL이 달라지지 않은 상태에서 무한 스크롤을 하게 된다. 그래서 목록의 URL을 복사해서 다른 사람에게 공유한다고 해서 같은 화면을 보지 않는다.(위에서 설명한 상황에서는 같은 주소는 같은 글 목록을 본다.) 이런건 웹사이트의 요구사항에 따라 달라지지만 보통은 URL에 따라서 보여주는 글의 목록을 정하지 않겠다는 의도이다. 그래서 페이스북이든 트위터든 글 목록에서 무한스크롤로 계속 로딩해서 보다가 특정 글을 클릭해서 이동했다가 뒤로 가기로 돌아오면 글 목록을 처음부터 다시 로딩한다.

후자의 경우는 기술적인 문제라기 보다는 서비스의 특성에 관한 것이다. 이어서 설명한 방법을 이용해서 이전에 어디까지 무한스크롤을 했는지 기억했다가 페이지를 로딩하고 사용자를 이 위치에 스크롤 시켜놓는 것은 가능하지만 나는 이 방식은 무한 스크롤과는 좀 맞지 않는다고 생각한다. 이런 상황이 있다면 구현하기 보다는 실제로 무한 스크롤이 정말 필요한가를 고민해 볼 필요가 있다고 생각한다. 그래도 요구하면 할수 없기는 하지만 여기서는 무한스크롤로 보여지는 목록이 아닌 페이징 처리가 된 상태에서 뒤로가기를 구현하는 방식에 대해서 설명하도록 하겠다. 이슈를 올려주신 분이 무한스크롤쪽을 원하는 것 같기도 한데 그렇다면 이슈를 새로 올려주시거나 추가 설명을 해주시면 좋겠다. 그리고 기술적으로 방식은 동일하기 때문에 무한스크롤에서 뒤로가기를 구현하더라도 이글에서 설명하는 방식을 먼저 이해해야 한다.

링크를 이용한 방식

링크 방식의 데모

위 방식을 보자. Ajax 등을 쓰지 않고 링크로 목록을 보여주고 페이지관리를 하는 방식이다. 이 방식이 웹의 기본 방식이고 특별한 이유가 없으면 링크로 모든 페이지를 연결하면 된다. 이렇게 하면 모든 페이지에 고유 URL이 있으므로 SEO나 소셜을 대응하는데도 아무런 문제가 없다. "다음"을 클릭하면 다음 10개의 목록을 가져오고 개별 URL을 가지므로 상세 목록에 들어갔다가 뒤로 가기를 해도 이전에 보던 페이지로 잘 돌아간다.

동작하는 예제는 여기에서 볼 수 있다.

여기서는 주소에 /link/page1처럼 되어 있으므로 1페이지에서 1~10의 글 목록을 보여주게 된다. 여기서 어떤 글 목록을 보여줄지는 모두 서버가 관리하고 있으므로 이 목록을 HTML로 만들어서 내려주고 다음 버튼에 대한 링크로 만들어서 내려준다. 브라우저는 다운받은 HTML을 그냥 보여주는 방식이고 "다음" 버튼을 눌러서 /link/page2로 이동하게 되면 서버가 11~20의 글 목록을 HTML로 만들어서 내려주게 된다. 이때는 링크방식이므로 클릭할 때마다 새로운 URL로 이동하게 되고 모든 페이지를 항상 새로 로딩한다. 항상 새로운 URL을 가지므로 "뒤로 가기"를 눌렀을 때도 아무런 문제 없이 동작한다. 이게 전통적인 방식이다.

그래서 /link/page1로 접속하면 다음과 같은 HTML 문서를 내려받게 된다.

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
  <h1>Ajax 뒤로 가기 예제</h1>
  <h3>게시글 목록</h3>
  <ul id="list">
    <li><a href="/detail/1">제목 1</a></li>
    <li><a href="/detail/2">제목 2</a></li>
    <li><a href="/detail/3">제목 3</a></li>
    <li><a href="/detail/4">제목 4</a></li>
    <li><a href="/detail/5">제목 5</a></li>
    <li><a href="/detail/6">제목 6</a></li>
    <li><a href="/detail/7">제목 7</a></li>
    <li><a href="/detail/8">제목 8</a></li>
    <li><a href="/detail/9">제목 9</a></li>
    <li><a href="/detail/10">제목 10</a></li>
  </ul>
  <a href="/link/page2" id="next">다음 </a>
</body>
</html>


Ajax를 이용한 방식

앞의 방식에서는 링크로 연결되어 있으므로 클릭할 때마다 항상 전체 페이지를 새로 로딩하게 된다. 이 데모 페이지는 목록 외에 스타일이나 다른 문서가 없으므로 문제가 될게 거의 없지만 복잡한 페이지의 경우 페이지마다 헤더나 사이드바, 푸터를 로딩하는 건 속도 저하의 영향을 준다. 그래서 등장한 게 Ajax(Asynchronous JavaScript and XML)다. Ajax는 이미 보편화한 기술이지만 약간 설명하지만 제시 제임스 가렛(Jesse James Garrett)이 만든 용어로 Ajax를 이용하면 웹 페이지에서 일부만 로딩해서 갈아치우는 것이 가능해졌고 이후 웹사이트 작성에 큰 영향을 주었다.

Ajax 방식의 데모

이 예제의 동작을 확인하려면 여기서 볼 수 있다.

이 주소는 /ajax 주소를 가지는데 다음과 같은 HTML을 내려주게 된다.

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
  <h1>Ajax 뒤로 가기 예제</h1>
  <h3>게시글 목록</h3>
  <ul id="list">
  </ul>
  <a href="/pages/2" id="next">다음 </a>
</body>
</html>

앞의 HTML과 크게 다른 점은 <ul id="list"> 부분에 내용이 안 채워져 있다는 것이다. 이는 빈 내용이 서버에서 내려오고 JavaScript로 데이터를 가져와서 채워 넣게 된다. 이 방식이 Ajax이다. 페이지가 로딩되면 자바스크립트가 실행되는데 여기서 서버에서 글 목록을 가져와서 HTML을 갈아치우게 된다. 게시글을 보여주는 부분이 <ul id="list"></ul>이고 다음 페이지를 보는 버튼이 <a href="/pages/2" id="next">다음</a>이다.

뒤에도 그렇지만 여기의 예제는 데모용으로 최적화된 코드이다. 코드의 품질보다는 동작 방식을 봐주길 바란다. 마지막 두 라인을 보면 위의 다음 버튼 #next에 링크된 주소에서 마지막 페이지 번호를 가져와서 currentPage 변수에 저장한다. 최초에 로딩되었을 때는 다음 페이지는 2페이지이지만 현재 페이지에서 보여줄 페이지는 1페이지이므로 -1을 해서 getList를 호출한다. 여기서 getList는 서버에서 목록을 가져오는 함수인데 서버에 /pages/페이지 번호로 GET 요청을 보내서 받아온 결과를 <li> 목록으로 만들어서 #list 안에 넣어주게 된다. 그리고 #next 페이지도 다음 페이지로 바꿔줘서 클릭 시 다음 목록을 다시 불러오도록 한다. $('#next').click() 부분은 클릭 이벤트를 받아서 버튼의 URL에서 페이지 번호로 다음 목록을 불러오도록 하는 것이다. #next에서 굳이 이렇게 할 필요는 없지만, JavaScript가 없는 경우 최소한의 동작을 보장하기 위해서이다. 물론 이 보장을 하기 위해서는 추가적인 코딩이 더 필요하다. 여기서는 글 목록을 내려주는(여기서는 JSON으로 내려준다.) /pages/페이지 번호의 URL이 하나 더 필요하다. /ajax에서 이 URL로 요청을 보내면 요청받은 글 목록을 JSON으로 내려주게 된다. 자바스크립트는 아래와 같다.

var getList = function(page) {
  $.getJSON('/pages/' + page, function(data) {
    var list = $.map(data.list, function(l) {
      return '<li><a href="/detail/' + l.id + '">' + l.title + '</a></li>'
    }).join('');
    $('#list').html(list);
    $('#next').attr('href', '/pages/' + (+page+1));
  });
};

$('#next').click(function(event) {
  event.preventDefault();
  var page = $(this).attr('href').slice(7);
  getList(page);
});

var currentPage = $('#next').attr('href').slice(7);
getList(--currentPage);

이 방식은 앞의 링크방식과 큰 차이점이 있다. 일단 페이지의 URL이 /ajax가 되고 이 페이지 내에서 목록만 계속 달라진다. 그래서 "다음"을 눌러서 몇 페이지로 가든지 글 제목을 클릭해서 상세페이지로 갔다가 "뒤로 가기"를 누르면 다시 1페이지로 돌아온다. 그 이유는 위 스크립트와 링크에서 보듯이 /ajax가 로딩되면 최초 1페이지를 불러오게 되어 있기 때문이다. 그래서 2, 3, 4, 5 페이지를 눌러서 봤다고 하더라도 페이지가 바뀌지 않았으므로 "뒤로 가기"를 누르면 이전 페이지로 가게 된다.

앵커를 이용한 방식

앞에서 설명한 대로 Ajax를 이용하면 전체 페이지를 매번 로딩할 필요 없이 웹페이지의 핵심 콘텐츠 부분만 가져와서 로딩할 수 있다. 그래서 속도가 빠르고 사용자 편의성도 증가하지만 웹 브라우저 입장에서는 페이지의 URL은 바뀌지 않았다. 그래서 "다음" 버튼을 눌러서 새로운 목록을 보더라도 "뒤로 가기"를 누르면 사용자의 기대와 다르게 이전 목록이 나오는 게 아니라 이전 페이지 여기서는 /ajax에 오기 전의 페이지로 이동하게 된다. 이 부분은 사용자의 기대와는 완전히 다른 것이므로 사용자 경험을 몹시 나쁘게 만든다.

그래서 이를 해결하는 방법으로 이용한 것이 앵커다. 예를 들어 babel 프로젝트의 README를 보자. 상단에 목차가 있는데 README 의 주소는 https://github.com/babel/babel/blob/7.0/README.md이지만 각 목차를 누르면 https://github.com/babel/babel/blob/7.0/README.md#faq, https://github.com/babel/babel/blob/7.0/README.md#team같은 식으로 뒤에 #faq, #team 등이 붙는다. 이를 앵커라고 하는데 문서에서 #뒤에 붙은 id를 가진 요소로 자동 이동하게 되는데 이는 서버에 요청을 보내는 것이 아니라 현재 페이지 내에서 이동만 하게 된다. 그리고 이 설명해서 중요한 부분은 브라우저가 이를 URL이 바뀐 것으로 인식하기 때문에 앵커를 이동할 때마다 "뒤로 가기"가 된다. 여러 번 앵커를 이동하더라도 "뒤로 가기"를 누르면 각 부분으로 알아서 이동하게 된다.

앵커방식의 데모

동작하는 예제는 여기서 볼 수 있고 이 페이지에 접속하면 다음과 같은 HTML을 내려주게 된다.

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
  <h1>Ajax 뒤로 가기 예제</h1>
  <h3>게시글 목록</h3>
  <ul id="list"></ul>
  <a href="/pages/2" id="next">다음 </a>
</body>
</html>

이전에 본 ajax와 거의 비슷하다. 서버가 목록을 내려주는 게 아니라 페이지가 로딩된 후 자바스크립트에서 서버에 목록을 받아와서 화면에 보여준다. 가장 크게 다른 점은 getList에서 목록을 보여준 뒤 window.location.hash = '#page' + page; 부분이다. 그래서 /hash로 접속하면 주소가 /hash#page1로 바뀌게 되고 "다음"을 누르면 /hash#page2로 주소가 달라진다. 새로운 페이지 목록을 불러올 때마다 앵커를 이용해서 주소를 바꾸게 되므로 웹 브라우저는 이를 기록하고 "뒤로 가기"가 가능하게 된다.

여기서 주의할 점은 앵커는 서버에 전달되는 값이 아니라는 것이다. /hash#page1로 접속하더라도 서버에는 /hash만 전달되고 뒤의 #page1는 브라우저가 처리하는 부분이고 뒤로 가기를 할 때도 서버에 새로 요청을 보내지 않는다. 그래서 히스토리는 남지만, 서버가 인식하는 주소는 모두 같기 때문에 각 앵커에 따라 적절한 글 목록을 보여주는 부분을 따로 구현해야 한다. 그 부분이 $(window).on('hashchange', function() {});이다. 주소에서 해시(앵커를 얘기한다)가 달라지는 이벤트를 받아서 해시가 달라시면 해시를 파싱해서 페이지 번호를 가져오고 새로운 목록을 가져오게 된다. 이렇게 하면 매번 글 목록은 요청하게 되지만 사용자로서는 "뒤로 가기"가 기대대로 동작한다. 자바스크립트는 아래와 같다.

var getList = function(page) {
  $.getJSON('/pages/' + page, function(data) {
    var list = $.map(data.list, function(l) {
      return '<li><a href="/detail/' + l.id + '">' + l.title + '</a></li>'
    }).join('');
    $('#list').html(list);
    $('#next').attr('href', '/pages/' + (+page+1));
    window.location.hash = '#page' + page;
  });
};

$('#next').click(function(event) {
  event.preventDefault();
  var page = $(this).attr('href').slice(7);
  getList(page);
});

$(window).on('hashchange', function() {
  var page = parseInt(location.hash.slice(5));
  if (!!page && currentPage !== page) {
    getList(page);
  }
});

var currentPage = $('#next').attr('href').slice(7);
if (location.hash) {
  currentPage = parseInt(location.hash.slice(5)) + 1;
}
getList(--currentPage);

동작 원리는 같지만 좀 더 주소처럼 보이려고 만든 것이 해시뱅이다. 해시뱅의 차이점은 /hash#page1를 쓰는 대신 /hash/#!/page1처럼 만들어서 좀 더 URL 주소처럼 만들었다는 것뿐이고 실제 동작은 위에 설명한 앵커방식과 같다. 예전의 트위터도 이 방식이었고 Backbone이나 Angular의 기본 라우팅(HTML5 모드가 아니라면)도 이 방식이었다. 이 방식을 사용하는 이유는 브라우저 지원 폭이 좋기 때문이다. 앵커 자체야 모든 브라우저에서 동작하지만 hashchange 이벤트를 이용하려면 IE 8 이상이어야 한다.

pushState를 이용한 방식

pushState는 ajax가 등장하고 이를 해결하기 위한 편법인 해시뱅이 유행하면서 "뒤로 가기"" 문제를 해결하기 위해 HTML5에 새로 추가된 History API의 새 메서드다. 자바스크립트를 약간 다뤄봤다면 폼 처리를 하면서 history.go(-1);이나 history.forward();같은 기능을 사용해 봤을 텐데 이 History API에 새롭게 추가된 메서드라고 보면 된다. 브라우저는 URL 혹은 앵커가 달라질 때마다 이를 리스트처럼 관리하고 있고 "뒤로 가기"나 "앞으로 가기"를 할 때 이 리스트를 기준으로 이동하게 된다. 그리고 앵커가 달라진 경우가 아니라면 URL이 바뀔 때마다 항상 서버에 요청을 보내게 된다.

하지만 pushState를 사용하면 URL을 바꾸면서 서버에 요청을 보내지 않을 수 있다.

pushState 방식의 데모

이 예제의 동작을 확인하려면 여기서 볼 수 있다. 이 페이지는 서버에서 다음과 같은 HTML을 내려준다.

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
  <h1>Ajax 뒤로 가기 예제</h1>
  <h3>게시글 목록</h3>
  <ul id="list">
    <li><a href="/detail/1">제목 1</a></li>
    <li><a href="/detail/2">제목 2</a></li>
    <li><a href="/detail/3">제목 3</a></li>
    <li><a href="/detail/4">제목 4</a></li>
    <li><a href="/detail/5">제목 5</a></li>
    <li><a href="/detail/6">제목 6</a></li>
    <li><a href="/detail/7">제목 7</a></li>
    <li><a href="/detail/8">제목 8</a></li>
    <li><a href="/detail/9">제목 9</a></li>
    <li><a href="/detail/10">제목 10</a></li>
  </ul>
  <a href="/pushstate/page2" id="next">다음 </a>
</body>
</html>

HTML을 자세히 보면 처음에 본 링크방식과 유사하게 <ul id="list"> 안에 글 목록이 내려온 것을 볼 수 있다. 이는 서버에서 받아온 HTML 이므로 글 목록은 서버에서 만들어 준 것이다. pushstate는 새롭게 추가된 API이지만 "뒤로 가기" 및 웹에 맞게 구현하려면 해야 할 작업이 좀 더 있다. "다음"을 눌렀을 때는 ajax와 똑같이 동작한다. "다음" 버튼에서 페이지 번호를 파싱해서 getList를 호출하고 서버에서 글 목록을 받아와서 <ul id="list"></ul>안에 새로운 목록을 채워주고 버튼도 다음 페이지를 가리키도록 바꿔준다. 자바스크립트는 아래와 같다.

var getList = function(page) {
  $.getJSON('/pushstate/page' + page, function(data) {
    var list = $.map(data.list, function(l) {
      return '<li><a href="/detail/' + l.id + '">' + l.title + '</a></li>'
    }).join('');
    $('#list').html(list);
    $('#next').attr('href', '/pushstate/page' + (+page+1));
    history.pushState({list: list, page: page}, 'Page '+ page, '/pushstate/page' + page);
  });
};

$('#next').click(function(event) {
  event.preventDefault();
  var page = $(this).attr('href').slice(15);
  getList(page);
});

$(window).on('popstate', function(event) {
  var data = event.originalEvent.state;
    $('#list').html(data.list);
    $('#next').attr('href', '/pushstate/page' + (+data.page+1));
});

마지막에 앵커를 바꿔주는 대신 history.pushState({list: list, page: page}, 'Page '+ page, '/pushstate/page' + page);가 있는걸 볼 수 있다. 여기서 다음 페이지가 2라면 주소는 '/pushstate/page' + page에서 '/pushstate/page2로 바뀌게 된다. 여기서 주소는 링크방식처럼 바뀌지만, 페이지를 새로 요청하지 않고 Ajax 요청만 발생한다. pushState에 전달되는 인자는 상태 객체(이는 나중에 뒤로 가기를 할 때 사용한다.), 페이지 제목, 페이지 주소가 된다. 브라우저가 관리하는 히스토리 리스트에 새로운 히스토리를 하나 추가하는 것이므로 주소와 페이지, 상태객체를 전달하게 된다.

그리고 $(window).on('popstate', function(event) {}) 부분을 통해 사용자가 "뒤로 가기"를 클릭했을 때 히스토리에서 상태가 나온 것을 탐지할 수 있다. 기본이라면 window.onpopstate로 이벤트를 받아서 event.state를 사용하지만 여기서는 jQuery를 사용했으므로 event.originalEvent.state로 앞에서 pushState에서 넣은 상태객체를 가져왔다. 앞에서 이 상태객체에 목록에 넣은 리스트와 "다음" 버튼에 넣을 페이지 번호를 넣었으므로 그대로 꺼내와서 다시 화면에 보여주도록 했다. 이렇게 하면 앵커를 이용한 방식과는 달리 "뒤로 가기"를 했을 때 서버에 Ajax 요청이 전혀 발생하지 않으면서도 예전 목록을 그대로 화면에 보여줄 수 있다.

추가로 앞의 예제와 크게 다른 점 중 하나가 해당 페이지의 주소가 /pushstate/page1인데 Ajax로 목록을 받아오는 주소도 /pushstate/page2로 완전히 같은 것을 볼 수 있다. 앞의 Ajax나 앵커를 이용한 방식이 화면이 뜬 후에 브라우저에서 목록을 가져온 방식인데 반해 pushState는 URL 자체를 바꿔버리므로 /pushstate/page1에서 "다음"을 눌러서 /pushstate/page2로 이동했다고 하더라도 브라우저의 동작을 생각하면 /pushstate/page2를 복사해서 새 창에 붙여넣으면 똑같은 목록이 나와야 한다.

그러므로 서버는 주소에 따라 해당 목록을 HTML로 내려줄 수 있어야 한다. 이는 서버에서 렌더링하는 방식과 브라우저에서 Ajax로 하는 방식이 섞여 있는 것이므로 서버에서 요청의 Content-Typetext/html이면 목록을 채운 HTML 페이지를 내려주고 application/json이면 해당 페이지의 목록만 JSON으로 내려주도록 구현한 것이다. pushState를 사용함으로써 할 일은 좀 더 복잡해졌지만 사용자가 느끼는 경험은 훨씬 자연스러워졌다. pushState는 IE 10 이상에서 사용할 수 있다.


Node.js에서 express와 jade를 이용해서 간단하게 작성한 예제이지만 전체 소스를 확인하고 싶다면 저장소를 참고하면 된다.

2017/02/28 10:02 2017/02/28 10:02