근래에는 많은 웹사이트가 infinite scrolling(또는 endless page)라고 불리는 약간씩 다른 기술을 사용한다. 기본적으로, 사용자가 페이지를 스크롤하면 AJAX를 이용해 비동기적으로 더 많은 레코드가 로딩된다. 이러한 방식으로 스크롤링은 사용자가 끊임없이 '다음 페이지'링크를 클릭하는 것보다 더 자연스럽고 쉬울 것이다.
이 글에서는 클래식 pagination 대신에 무한 스크롤을 어떻게 구현하는지 설명할 것이다.
먼저, will_paginate gem을 사용하는 기본 pagination을 구현하는 데모 프로젝트를 준비할 것이다. 이 튜토리얼을 통해 작업해나가면서 pagination은 무한 스크롤링이 되어갈 것이다. 이는 루비와 함게 몇몇 JavaScript(와 CoffeeScript)를 작성하는 것을 요구한다.
제공되는 솔루션은 사용자가 브라우저에 javascript를 disable 시켰다면 기본 pagination으로 후퇴시킬 것이다. 결국, 우리 뷰는 실질적으로 어떤 수정도 요구하지 않으므로 어떤 웹사이트에서도 쉽게 구현할 수 있다.
변환될 다른 아이템들은 다음과 같다:
- 스크롤링 대신에 "Load more" 버튼을 어떻게 구현할 것인가, SitePoint에서 사용되는 것과 매우 유사하게
- 몇몇 골치거리와 잠재적인 문제들, 특별히 History API 와 scroll spying이 우리를 도울 수 있다.
동작하는 데모는 http://sitepoint-infinite-scrolling.herokuapp.com 에서 볼 수 있다.
소스 코드는 GitHub에서 받을 수 있다.
좋아보이는가? 시작해보자.
프로젝트 준비하기
이 문서 작성시에는 Rails 3.2.16을 사용하였으나 Rails 4로 동일한 솔루션을 구현할 수 있다.
$ rails new infinite_scrolling -T
여기서 T는 test suite를 생성하는 것을 생락하고자 한다는 의미이다(필자는 RSpec을 선호하지만 이 플래그를 제거할 수 있다).
우리는 쓸모있는 몇몇 gem을 연결할 것이다.
Gemfile
gem 'will_paginate', '~> 3.0.5' gem 'betterlorem', '~> 0.1.2' gem 'bootstrap-sass', '~> 3.0.3.0' gem 'bootstrap-will_paginate', '~> 0.0.10'
will_paginate 는 레코드에 페이지를 잘 매길 것이다. 다음 섹션에서 이 gem에 대해 더 자세히 다룰 것이다. betterlorem 은 우리 레코드에 데모 텍스트를 생성한다. "Lorem lpsum" 텍스트를 생성하는 다른 유사한 gem들이 있지만 우리의 경우엔 이 gem이 가장 편리하다고 생각한다(우리는 view에서가 아니라 seeds.rb에서 그것을 사용할 것이다).
상을 받을 디자인을 만들어 내는 것은 아무 의미가 없다. 그래서 빠르고 쉬운 솔루션으로 우리는 Twitter Bootstrap 3를 사용할 것이다(무게면에서 보자면 가장 작은 것은 아니지만). bootstrap-sass gem을 우리 레일스 프로젝트에 추가했다. bootstrap-will_paginate 는 pagination 자체를 위한 몇몇 Bootstrap 스타일을 포함하고 있다.
다음을 실행하는 것을 잊지마라.
$ bundle install
다음을 application.js에 추가하고
// = require bootstrap
다음은 application.css.scss 에 모든 Bootstrap 스타일과 스크립트들을 포함하기 위해 추가하자.
@import "bootstrap";
물론, 실제 애플리케이션에서는 오직 필요한 컴포넌트만을 선택할 것이다.
The Model
하나의 테이블 :Post만 존재하는 상태이다. 간단한 구조이면 다음의 컬럼들을 포함하고 있다.- id (integer, primary key)
- title (string)
- body (text)
- created_at (datetime)
- updated_at (datetime)
실행하기
$ rails g model Post title:string body:text $ rake db:migrate
우리는 적절한 migration을 생성하고 그것을 데이터베이스에 적용할 것이다.
다음 과정은 몇몇 테스트 데이터를 생성하는 것이다. 가장 쉬운 바법은 seeds.rb를 사용하는 것이다.
seeds.rb
50.times { |i| Post.create(title: "Post #{i}", body: BetterLorem.p(5, false, false)) }이것은 BetterLorem에 의해 생성된 body를 가진 50개의 post를 생성한다. 각각의 생성된 컨텐츠의 집합은 5개의 paragraph들로 구성된다. 마지막 두개의 아규먼트는 BetterLorem에게 p 태그로 텍스트를 감싸고 trainling period를 포함하도록 지시한다.
실행하기
$ rake db:seed
이것은 데이터베이스에 몇몇 test 포스트들을 덧붙이다. 대단하다!
마지막은 대응하는 뷰들과 함께(index.html.erb와 show.html.erb) index와 show 메소드를 가진 PostController를 생성하는 것이다.
routes.rb
resources :posts, only: [:index, :show] root to: 'posts#index'
마지막에는 Rails 3를 사용한다면, public/index.html 파일을 제거해라.
The Controller
이제 재미있는 파트로 넘어갈 준비가 되었다. 먼저, 잘린 body를 가진 페이지가 매겨진 포스트들을 디스플레이해보자. 이를 위해, will_paginate를 사용할 것이다. - a simple yet convenient gem by Mislav Marohnić that works with Ruby on Rails, Sinatra, Merb, DataMapper and Sequel.이 솔루션에 대한 대안도 있다. - kaminari by Akira Matsuda that is more powerful and more sophisticated. You can also give it a try. Basically, it doesn’t matter which gem you use.
우리의 컨트롤러에서:
post_controller.rb
@posts = Post.paginate(page: params[:page], per_page: 15).order('created_at DESC')
paginate 메소드에 대한 호출은 page 옵션을 받아서 GET 파라미터가 요청한 페이지 번호를 받아오는데 사용하도록 지시한다. per_page 옵션은 페이지당 표시할 레코드의 수를 지정한다. per_page 옵션은 아래처럼 전체 모델에 대해 지정하거나 전체 프로젝트에 대해 지정할 수 있다.
post.rb
class Post self.per_page = 10 endwill_paginate.rb(in an initializer)
WillPaginate.per_page = 10
paginate 메소드는 ActiveRecord::Relation을 리턴하므로 우리가 order 메소드를 호출해서 묶을 수 있다.
The View
index.html.erb<div class="page-header"> <h1>My posts</h1> </div> <div id="my-posts"> <%= render @posts %> </div> <div id="infinite-scrolling"> <%= will_paginate %> </div>
page header는 Bootstrap class의 help로 지정되었다. 다음 블록, #my-posts는 우리의 페이지가 지정된 포스트를 포함한다. render @posts를 사용하여 _post.html.erb partial을 사용해 array로부터 각 포스트를 디스플레이한다. 마지막 블록 #infinite-scrolling은 pagination control들을 포함한다.
will_paginate는 @posts를 페이지 지정하기 원한다는 것을 이해할 만큼 영리하다는 것을 주목해라. 명시적으로 이렇게 지정할 수도 있다: will_paginate @posts.
여기 우리의 partial이 있다.
_post.html.erb
_post.html.erb
<div> <h2><%= link_to post.title, post_path(post) %></h2> <small><em><%= post.timestamp %></em></small> <p><%= truncate(strip_tags(post.body), length: 600) %></p> </div>우리는 모든 post를 div로 둘러싸고 있다. 그러면 전체 post를 읽기 위하여 링크처럼 동작하는 타이틀을 디스플레이할 수 있다. timestamp는 post가 생성된 때를 가리킨다. timestamp 함수는 아래와 같이 model 내에 정의되어 있는 함수이다.
post.rb
def timestamp created_at.strftime('%d %B %Y %H:%M:%S') end
마지막으로 우리는 post로부터 모든 tag들을 제거하기 위해 strip_tags 함수를 사용하였고 600개의 symbol 만을 남기기 위해 truncate 메소드를 사용한다. 이로써 view를 가지고 하는 작업은 끝이났다(layout.html.erb 와 show.html.erb를 위한 markup은 중요하지 않으므로 생략한다. GitHub repo에 있는 것을 참고하라.)
Infinite Scrolling
이제 무한 스크롤링을 위해 우리의 페이지 지정을 수정할 준비가 되었다. jQuery가 우리를 도와줄 것이다.javascripts 디렉토리 안에 pagination.js.coffee 파일을 새로 만들자.
pagination.js.coffee
jQuery -> if $('#infinite-scrolling').size() > 0 $(window).on 'scroll', -> more_posts_url = $('.pagination .next_page a').attr('href') if more_posts_url && $(window).scrollTop() > $(document).height() - $(window).height() - 60 $('.pagination').html('<img src="/assets/ajax-loader.gif" alt="Loading..." title="Loading..." />') $.getScript more_posts_url return return
만약 페이지 지정이 페이지에 제공된다면 scroll 이벤트가 여기서 윈도우에 바인딩 된다. 사용자가 스크롤 하면, 다음 페이지에 대한 링크를 가져온다 - 방문하는 것은 Rails가 페이지로부터 레코드를 읽어오게 한다(여전히 우리는 이 동작을 위해 컨트롤러를 수정할 일이 남아있다)
그러면, URL이 제공되고 있고 사용자가 페이지 아래 마이너스 60px까지 스크롤하는지 체크하자. 이는 더 많은 포스트를 읽어오기 원하는 시점이다. 60px은 임의의 값이고 아마 케이스마다 그 값을 변경할 수 있을 것이다.
만약 이들 상태가 true이면 우리의 pagination은 ajaxload.info에서 자유롭게 다운로드 될 수 있는 "loading" GIF 이비지로 교체될 것이다. 마지막으로 해야할 것은 이전에 가져온 URL을 이용하여 실제로 비동기 요청을 수행하는 것이다. $.getScript 는 서버로부터 JS script를 로딩하고 그것을 실행할 것이다.
두 return 명령을 주목해라. 기본으로 CoffeeScript는 마지막 표현을 리턴할 것이다(동일한 컨셉이 루비에도 적용된다) 그러나 여기서 우리는 무언가를 리턴하는 jQuery 함수나 이벤트 핸들러를 원하지 않으므로 "return nothing"을 의미하는 return을 지정하였다.
PostController#index 메소드는 HTML과 JavaScript에 반응해야 한다. 이를 달성하기 위해 우리는 respond_to 를 사용할 것이다.
posts_controller.rb
@posts = Post.paginate(page: params[:page], per_page: 15).order('created_at DESC') respond_to do |format| format.html format.js end
마지막으로 할일은 JS에 응답했을 때 표현될 뷰를 생성하는 것이다.
index.js.erb
$('#my-posts').append('<%= j render @posts %>');
<% if @posts.next_page %>
$('.pagination').replaceWith('<%= j will_paginate @posts %>');
<% else %>
$(window).off('scroll');
$('.pagination').remove();
<% end %>
우리는 #my-posts 블록에 더 많은 포스트들을 추가함으로써 더 많이 그리게 될 것이다. 그후, 페이지가 더 남아 있는지 체크하자. 만약 남아 있으면, 현재 pagination 블록(현 시점에는 "loading" 이미지를 포함하고 있다)을 새로운 pagination으로 교체한다. 남아 있지 않으면, 더이상 이벤트를 들을 지점이 없으므로 pagination 컨트롤을 제거하고 scroll 이벤트와 window를 바인딩 해제한다.
현재는, 무한 스크롤이 준비되었다. 사용자가 브라우저의 JavaScript를 disable 시켰더라도, bootstrap-will_paginate gem 덕분에 제공되는 몇몇 스타일을 가진 기본 pagination으로 표현될 것이다.
한가지 언급할 가치가 있는 것은 스크롤링이 scroll 이벤트의 작업량을 가중시킬 것이다. 만약 이 이벤트의 처리를 지연시키고자 한다면 Brian Grinstead가 작성한 BindWithDelay 오픈소스 라이브러리를 사용할 수 있다. 라이브러리를 사용하기 위해 단순히 소스를 다운로드 하고 그것을 프로젝트에 포함하기만 하면 된다. 그리고 나서, 소스에 다음과 같이 수정을 추가한다.
pagination.js.coffee
$(window).bindWithDelay 'scroll', ->
# the code
, 100
이 소스는 100ms 만큼 이벤트 발사를 딜레이한다. index.js.erb 안의 $(window).off('scroll'); 코드는 여전히 event를 바인딩 해제할 것이다. 그러므로 그곳에는 수정을 필요로 하지 않는다.
이로써 문서의 첫 파트가 끝이났다. 다음 파트에서는 "Load more" 버튼에 대해 이야기 할 것이고 무한 스크롤을 사용할 때 발생하는 몇몇 문제점에 대해 얘기할 것이다.
출처 http://www.sitepoint.com/infinite-scrolling-rails-basics/
댓글 없음:
댓글 쓰기