더욱 강력해진 Turbolinks 3

곧 정식으로 릴리스될 예정인 레일스 5의 여러가지 추가 기능 중에 두 가지가 주목을 받고 있다. 하나는 'ActionCable', 다른 하나는 'Turbolinks 3'다. 'ActionCable'에 대해서는 별도의 글을 준비 중이며, 여기서는 'Turbolinks 3'에 대해서 추가된 기능 중심으로 알아 보도록 하겠다.

참고동영상: New Turbolinks 3 Features With Ruby on Rails 특히 'Turbolinks 3'에서 추가된 기능 중 'Partial Replacement'에 대한 내용을 샘플 애플리케이션과 함께 알아 보도록 하자.


레일스 프로젝트를 생성하면 디폴트로

'Turbolinks'가 작동하게 된다. 따라서 페이지 내의 링크를 클릭할 때마다 해당 링크로 접속한 후 렌더링되는 전체 페이지 중 'body' 부분의 컨텐츠만 현재 페이지의 'body'로 업데이트된다. 따라서 자바스크립트와 'CSS'를 포함한 전체 페이지가 리로드될 때보다 속도가 빨라지는 효과를 볼 수 있다. 최근 버전 3로 업그레이드되면서 'pjax'와 비슷한 기능을 구현할 수 있게 되었는데, 페이지 내의 특정 'id' 값을 가지는 태그의 컨텐츠만을 업데이트할 수 있는 기능을 제공하게 되었다.

1. 샘플 애플리케이션의 생성 아래의 샘플 애플리케이션은 루비 v2.3.0, 레일스 v5.0.0.beta1 에서 작성하였다. 샘플 애플리케이션에서는

'Post' 모델을 'Comment' 모델과 일대다의 관계로 연결한다. 'posts#show' 액션 뷰 파일 하단에 'comments'라는 'id' 값을 가지는 'div' 태그를 생성한 후, 이 안에서 기작성된 'comment' 객체들을 보여주고 'comment'를 입력하는 폼을 위치시킨다. 이 폼에 글을 입력하고 'submit'하면 'comments#create' 액션이 'ajax'로 호출된 후 현재 페이지로 리디렉트 되면서 '{change: 'comments'}' 옵션으로 지정된 부분만 업데이트 된다. 이것이 'Turbolinks 3: Partial Replacement' 기능의 대략적인 로직이다.

$ rails new turbolinks3

2. Gem 추가

# Gemfile 
gem 'font-awesome-rails' 
gem 'bootstrap-sass' 
gem 'simple_form' 
gem 'turbolinks', github: 'rails/turbolinks' 

주의 : Rubygems.org 젬 목록에는 3버전이 아직 배포되지 않았기 때문에, 'github' 저장소 옵션을 추가해 주고 'bundle install'한다. 'bundle list' 명령을 실행하여 인스톨 버전이 'turbolinks (3.0.0 dd31f4b)' 인지 확인한다.

3. 번들 인스톨 및 젬 셋팅

$ bundle install 

'app/assets/stylesheets/application.css'파일의 확장명을 '.scss' 파일로 변경하여 'sassy CSS'로 작성할 수 있도록 한다.

// app/assets/stylesheets/application.scss 
@import "bootstrap-sprockets"; 
@import "bootstrap"; 
@import "bootstrap/theme"; 
@import "posts"; 

주의 : 레일스 5.0.0.beta1 버전에서 'scaffolding'으로 생성된 스타일시트 파일명의 확장자는 이전 버전과는 달리 '.css'로 지정되기 때문에, 'posts.css' 파일을 'posts.scss'로 변경한다. 자바스크립트 'manifest' 파일인 'app/assets/javascripts/application.js' 파일에는 'bootstrap-sprocket'를 추가해서 'Bootstrap'이 동작하도록 해 준다.

// app/assets/javascripts/application.js 
//= require jquery 
//= require jquery_ujs 
//= require bootstrap-sprockets 
//= require turbolinks 
//= require_tree . 

'simple_form' 젬이 동작하기 위해서 아래와 같은 설치 명령을 실행해야 한다. 이 때 'Bootstrap' 스타일을 적용하기 위해서 '--bootstrap' 옵션을 반드시 지정해 주어야 한다.

$ rails g simple_form:install --bootstrap

4. Post 리소스의 Scaffolding

'scaffolding' 제너레이터를 이용하여…

$ rails g scaffold Post title content:text

5. Comment 모델의 생성

'model' 제너레이터를 이용하여…

$ rails g model Comment post:references content:text

6. DB 마이그레이션 실제로 DB 테이블을 생성하기 위해서

'db:migrate' 작업을 해 준다.

$ rails db:migrate

7. Post 모델 클래스의 관계선언

'app/models/post.rb' 파일을 열고 'comments' 리소스와의 관계선언을 'has_many'로 지정해 준다. 또한 'title' 속성을 필수항목으로 지정해 준다.

class Post < ApplicationRecord 
  has_many :comments, dependent: :destroy 
  validates :title, presence: true 
end 

8. Comment 모델 클래스의 필수항목 지정

'app/models/comment.rb' 파일을 열고 'content' 속성을 필수항목으로 지정해 준다.

class Comment < ApplicationRecord 
  belongs_to :post 
  validates :content, presence: true 
end 

9. 중첩 라우트와 루트 라우트의 선언

이제 루트 라우트와 두 리소스의 중첩 라우팅을 추가해 준다.

root "posts#index" 
resources :posts do 
  resources :comments 
end 

10. 스타일 커스터마이징 약간의 스타링을 추가하기 위해 아래와 같이

'posts.scss'파일을 업데이트한다.

/* app/assets/stylesheets/posts.scss */ 
body { 
  padding-top: 60px; 
} 
.navbar-brand { 
  color : darkred !important; 
  font-weight: bold !important; 
} 
h1, h2, h3, h4, h5, h5 { 
  margin-bottom:1em !important; 
} 
fieldset { 
  border:1px solid #eaeaea !important; 
  border-radius: 5px; 
  padding-left: 1em !important; 
  padding-right: 1em !important; 
  margin-bottom: 1em; 
} 
legend { 
  display: inline !important; 
  background: #eaeaea !important; 
  padding-left:.5em !important; 
  padding-right: .5em !important; 
} 
#comments { 
  overflow: auto; 
  margin-bottom: 1em !important; 
  h2 { 
    margin-bottom: .5em !important; 
  } 
} 
.form-actions { 
  border:1px solid #eaeaea; 
  padding: 1em 1em 2em; 
  background-color: #eeeeee; 
}

11. 애플리케이션 레이아웃 파일의 변경

애플리케이션 레이아웃을 아래와 같이 변경한다. 9번째 코드라인은 스마트 디바이스에서 접속할 때 반응형으로 보이도록 하기 위한 것이다.

<!-- app/views/layouts/application.html.erb --> 
<!DOCTYPE html> 
<html> 
  <head>
    <title>FieldsForR5</title> 
    <%= csrf_meta_tags %>
    <%= action_cable_meta_tag %>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 
  </head>
  <body>
    <nav class="navbar navbar-default navbar-fixed-top"> 
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button> 
          <a class="navbar-brand" href="/">Turbolinks 3</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse"> 
          <ul class="nav navbar-nav">
            <li class="active"><a href="#">Home</a></li> 
            <li><a href="#about">About</a></li>
            <li><a href="#contact">Contact</a></li>
          </ul>
        </div><!--/.nav-collapse --> 
      </div> 
    </nav> 
    <div class="container"> 
      <div class="row"> 
        <div class="col-md-12"> 
          <div id="flash_message"> 
            <%= flash_messages %>
          </div> 
          <%= yield %> 
        </div> 
      </div> 
    </div> 
  </body> 
</html> 

41번 코드라인의 'flash_messages' 메소드는 레일스의 'flash' 메시지를 'Bootstrap' 스타일을 이용하여 보이도록 별도의 헬퍼 메소드를 작성하여 사용하였다.

12. Flash 메시지 헬퍼 메소드의 정의

# app/helpers/application_helper.rb 

module ApplicationHelper 
  def bootstrap_class_for flash_type 
    hash = HashWithIndifferentAccess.new({ success: "alert-success", error: "alert-danger", alert: "alert-warning", notice: "alert-info" }) 
    hash[flash_type] || flash_type.to_s 
  end 

  def flash_messages(opts = {}) 
    html_all = "" 
    flash.each do |msg_type, message| 
      html = <<-HTML 
<div class="alert #{bootstrap_class_for(msg_type)} alert-dismissable">
  <button type="button" class="close" data-dismiss="alert" aria-label="Close">
    <span aria-hidden="true">×</span>
  </button> 
  #{message} 
</div> 
HTML 
      html_all += html 
    end 
    html_all.html_safe 
  end 
end 

13. Posts#Index 뷰 파일의 변경

'Table' 태그에 'Bootstrap'용 클래스인 'table'을 추가해 준다(5번 코드라인). 26번 코드라인에서는 링크에 버튼 스타일을 클래스로 지정한다.

<!-- app/views/posts/index.html.erb --> 
<h1>Posts</h1> 
<table class="table"> 
  <thead> 
    <tr> 
      <th class="col-md-8">Title</th> 
      <th colspan="3"></th> 
    </tr> 
  </thead> 
  <tbody> 
    <% @posts.each do |post| %> 
      <tr> 
        <td><%= post.title %></td> 
        <td><%= link_to 'Show', post %></td> 
        <td><%= link_to 'Edit', edit_post_path(post) %></td> 
        <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td> 
      </tr> 
    <% end %> 
  </tbody> 
</table> 
<div class="form-actions"> 
  <%= link_to 'New Post', new_post_path, class:'btn btn-default' %> 
</div> 

14. Post 폼 파셜 파일 변경 8번 코드라인에서

'textarea' 행 수를 5개로 지정하고, 폼 관련 링크버튼을 일관성있게 보이도록 13, 14번 코드라인을 추가한다.

<!-- app/views/posts/\_form.html.erb --> 
<%= simple\_form_for(@post) do |f| %> 
  <%= f.error_notification %> 
  <div class="form-inputs"> 
    <%= f.input :title %> 
    <%= f.input :content, input_html: { rows: 5 } %> 
  </div>
  <div class="form-actions"> 
    <%= f.button :submit %> 
    <%= link_to 'Show', @post, class:'btn btn-default' if @post.persisted? %>
    <%= link_to 'Back', posts_path, class: 'btn btn-default' %> 
  </div> 
<% end %> 

15. Post#New 뷰 템플릿 파일의 변경 링크 버튼을 폼 파셜로 이동한다.

<!-- app/views/posts/new.html.erb --> 
<h1>New Post</h1> 
<%= render 'form', post: @post %> 

16. Post#Edit 뷰 템플릿 파일의 변경 링크 버튼을 폼 파셜로 이동한다.

<!-- app/views/posts/edit.html.erb --> 
<h1>Editing Post</h1> 
<%= render 'form', post: @post %>

17. Posts#Show 뷰 파일의 변경 23~31 코드라인에 걸쳐 이미 작성된

'comment' 객체들을 보여주고 새로운 'comment'를 입력할 수 있는 폼을 추가해 준다.

<!-- app/views/posts/show.html.erb --> 
<h2>Post 
  <small><%= action_name %></small>
</h2> 
<fieldset> 
  <legend>Post#<%= @post.id %></legend> 
  <p> 
    <strong>Title:</strong> <%= @post.title %> 
  </p> 
  <p>
    <strong>Content:</strong> <%= @post.content %>
  </p> 
</fieldset> 
<div class="text-right"> 
  <%= link_to 'Edit', edit_post_path(@post), class:'btn btn-default' %>
  <%= link_to 'Back', posts_path, class:'btn btn-default' %>
</div>
<div id="comments">
  <h2><%= pluralize @post.comments.count, "Comment" %></h2>
  <ul>
    <%= render @post.comments %>
  </ul>
  <%= render "comments/form", comment: @post.comments.build %>
</div>

18. Comments 폼 파셜의 생성

'app/views/comments' 폴더를 생성하고 '_form.html.erb' 파일을 추가한 후 아래와 같이 코드를 작성한다.

<!-- app/views/comments/\_form.html.erb --> 
<%= simple\_form_for [comment.post, comment], remote: true do |f| %>
  <%= f.input :content, label: 'Add a comment', input_html: { rows: 5 } %>
  <%= f.button :submit, class: 'pull-right' %>
<% end %> 

19. Comment 뷰 파셜의 생성 이미 작성된

'comment'들을 보여 주기 위한 '_comment.html.erb' 파셜 파일을 아래와 같이 작성한다.

<!-- app/views/comments/\_comment.html.erb --> 
<li> 
  <%= comment.content %>
  (<%= link\_to "x", [comment.post, comment], method: :delete, data:{confirm: "Are you sure?"}, remote: true %>)
</li>

20. Comments 컨트롤러의 생성

'controller' 제너레이터를 이용하여 'comments' 컨트롤러를 생성한다. 이 때 'create''destroy' 액션도 생성되도록 추가해 준다.

 $ rails g controller comments create destroy

각 액션에 아래와 같이 코드를 작성해 준다.

# app/controllers/comments_controller.rb 

class CommentsController < ApplicationController 
  def create 
    post = Post.find(params[:post_id]) 
    comment = post.comments.build(comment_params) 
    if comment.save 
      flash[:notice] = "Successfully saved!" 
    else 
      flash[:error] = "Validation error! : you should type at least a character in comment." 
    end 
    redirect_to post, { change: ["comments", "flash_message"] } 
  end 

  def destroy 
    post = Post.find(params[:post_id]) 
    comment = post.comments.find(params[:id]) 
    if comment.destroy 
      flash[:notice] = "Successfully deleted!" 
    else 
      flash[:error] = "Error! : it could not be deleted." 
    end 
    redirect_to post, { change: ["comments", "flash_message"] } 
  end 

  private 
  def comment_params 
    params.require(:comment).permit(:post_id, :content)
  end
end 

12, 23번 코드라인이 바로 'Turbolinks 3'에서 새로 도입한 'Partial Replacement'가 동작하게 해 준다. 'changes' 옵션에 지정하는 문자열은 태그의 'id' 값을 의미한다. 따라서 각 액션에 대한 'remote(ajax)' 호출이 있은 후 이 두개의 'id'를 가지는 'div' 태그의 컨텐츠를 업데이트해 주게 된다. 이상으로 20단계를 거쳐 'Turbolinks 3'의 데모를 구현할 수 있었다.


소스 :

https://github.com/luciuschoi/turbolinks3_sample 데모 : https://turbolinks3.herokuapp.com/

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.