2015년 10월 19일 월요일

Devise Authentication in Depth

이 문서는 "Authentication with Rails" 시리즈의 두번째 기사이다. 우리는 Devise에 대해 논의할 것이다. Devise는 Platformatec에 의해 개발된 인기있는 완전히 독립된 인증 솔루션이다.

Sorcery와 비교해서(지난번에 살펴본), Devise는 당신을 위해 많은 다양한 관점을 다루는 고수준의 솔루션이다. Devise는 controllers, views, mailers 그리고 routes를 제공한다. 실행하는 몇몇 커맨드를 입력하는 것은 쉬운반면 매우 세밀하게 커스터마이징할 수 있다. Devise는 매우 완전한 문서와 많은 유용한 확장기능을 만들어내는 큰 커뮤니티를 가지고 있다. Devise가 매우 인기가 많은 것은 경탄할만한 것은 아니다.

Devise는 필요한 것들만 고를 수 있는 편리한 모듈들을 가지고 있다. password recovery, e-mail confirmation account lock out, 그리고 매우 많은 다른 모듈들을 지원한다.

이 문서에서는 다음의 것들을 다룰 것이다:


  • 데모 앱에 Devise 통합하기
  • Devise를 셋업하고 특정 모듈을 enable 시키기
  • Devise를 커스터마이징하기
  • 특정 페이지에 대한 액세스를 제한하기
  • 비동기 e-mail delivery를 셋팅하기
  • password strength를 평가하는 Devise 확장기능을 통합하기
  • password strength 평가를 client-side에 추가하기
좋아보이는가? 시작해보자!

데모 애플리케이션은 https://sitepoint-devise.herokuapp.com/ 에 있다.
소스 코드는 Github에 있다.

Ground Work

시리즈의 이전 문서에서 보았듯이 우리는 인증외에는 아무런 기능을 제공하지 않는 앱을 생성할 것이다(몇몇 관련된 특성들을 가진다). 나는 지금 어떤 화려한 이름도 생각해낼 수 없다. 그래서 그냥 "Devise Demo"라고 부르겠다.


$ rails new DeviseDemo -T

Rails 4.20이 사용되었다. 그러나 Devise는 Rails 3과도 잘 호환된다.

몇몇 gem들을 설치하자:

Gemfile

[...]
gem 'devise', '3.4.1'
gem 'bootstrap-sass'
[...]

bootstrap-sass는 튜토리얼과 연관된 것은 아니지만 스타일링때문에 좋아한다.

다음을 실행한다.

$ bundle install

Bootstrap 스타일을 연결한다.

application.scss

@import "bootstrap-sprockets";
@import "bootstrap";
@import 'bootstrap/theme';

그리고 레이아웃을 변경한다:

views/layout/application.html.erb

[...]
<div class="container">
  <% flash.each do |key, value| %>
    <div class="alert alert-<%= key %>">
      <%= value %>
    </div>
  <% end %>
</div>

<%= yield :top_content %>

<div class="container">
    <h1><%= yield :header %></h1>
  <%= yield %>
</div>
[...]

어떤 방식으로든 flash 렌더링 기능을 추가해야 하는 것을 주의해라. Devise는 다양한 메시지를 표시하기 위해 flash에 의존한다. Devise와 관련된 bundle을 처음 실행시켰을 때 그것과 관련된 메시지를 보게될 확률이 크다.

보시다시피, top-contentheader라는 추가적인 컨텐츠를 위치시키기 위해 yield를 사용하고 있다. 또한 헤더를 제공하기 위해 헬퍼 메소드를 추가했다.

application_helper.rb

[...]
def header(text)
  content_for(:header) { text.to_s }
end
[...]

또한 우리의 홈페이지를 셋업하기 위해 PagesController를 생성하자.

pages_controller.rb

class PagesController < ApplicationController
end

그리고 나서 view는:

views/pages/index.html.erb

<% content_for :top_content do %>
  <div class="jumbotron">
    <div class="container">
      <h1>Welcome!</h1>
      <p>Register to get started.</p>
      <p>
        <%= link_to 'Register', new_user_registration_path, class: 'btn btn-primary btn-lg' %>
      </p>
    </div>
  </div>
<% end %>

그리고 이들을 모두 묶기 위해 route를 수정한다.

config/routes.rb

[...]
root to: 'pages#index'
[...]

마지막으로, 개발을 위해 기본 URL 옵션을 설정하자.

config/environments/development.rb

[...]
config.action_mailer.default_url_options = { host: '127.0.0.1', port: 3000 }
[...]

이는 e-mail 뷰 내부에 적절한 링크를 생성하기 위해 필요하다.

이제, Devise를 통합할 눈부신 시간이다.

Integrating Devise

우선, Devise의 configuration 파일과 transaction을 생성하기 위해 다음 명령을 실행하자:

$ rails generate devise:install

config/initializers/devise.rb 는 문서화가 잘된 많은 다양한 설정 옵션들을 포함하고 있다. 우리는 잠깐 이 파일을 수정할 것이다. config/locales/devise.en.yml 은 영어로 Device의 특정 번역본을 제공한다. 여기에 많은 다른 언어를 위한 번역본이 있다.

다음은 Device가 필요로 하는 추가적인 컬럼을 가진 모델을 생성한다.

$ rails generate devise User

당신은 User를 다른 어떤 이름으로로도 변경할 수 있다. 이 명령은 user.rb 모델 파일을 생성하고 모든 필요한 필드를 추가하는 마이그레이션을 생성한다. 만약 User 모델이 이미 존재하면, 업데이트 될 것이다.

모델 파일이 무엇을 포함하고 있는지 열어보자. 가장 중요한 라인은:

models/user.rb

[...]
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable, :validatable
[...]

이것은 이 모듈을 위해 활성화된 Divise 모듈의 리스트이다.

  • datebase_authenticatable - 사용자들은 데이터베이스에 저장된 로긴과 패스워드로 인증할 수 있을 것이다. (password is stored in a form of digest).
  • registerable - 사용자들은 그들의 프로파일을 등록하고 업데이트하고 제거할 수 있다.
  • recoverable - 잃어버린 패스워드를 리셋하는 메커니즘을 제공한다.
  • rememberable - 쿠키와 연관된 "remember me" 기능을 사용가능하다.
  • trackable - count, timestamps, IP 주소를 추적한다.
  • validatable - e-mail 과 password를 인증한다(custom validator도 사용가능하다)
보이는가? Devise는 당신을 위해 매우 다양한 측면들을 다룬다. - 단지 필요한 모듈을 골기만 하면 된다.

이 데모를 위해 두개의 추가적인 모듈을 사용해보자:
  • confirmable - 로그인이 허가되기 전에 등록 후 사용자의 e-mail 승인을 필요로 한다.
  • lockable - 사용자의 계정은 몇번의 인증 시도 실패후 잠길 것이다.
모델을 아래와 같이 수정해보자.

user.rb

[...]
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable
[...]

당신은 또한 마이그레이션 파일을 수정해야 한다. 파일을 열어서 다음의 라인을 주석해제하자.

db/migrate/xxx_devise_create_users.rb

[...]
t.string   :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string   :unconfirmed_email
t.integer  :failed_attempts, default: 0, null: false
t.string   :unlock_token
t.datetime :locked_at
add_index :users, :confirmation_token,   unique: true
add_index :users, :unlock_token,         unique: true
[...]

이들 필드들은 Confirmable 과 Lockable 모듈들이 정확하게 동작하게 하는데 필요하다. 또한 사용자가 그들의 이름을 제공하도록 하는 것도 좋은 아이디어이며 그래서 한 라인을 더 추가한다:

db/migrate/xxx_devise_create_users.rb

[...]
t.string :name
[...]

이제, 마이그레이션을 실행해보자:

$ rake db:migrate

users 테이블이 이제 생성되었고 Devise가 up 되고 실행되고 있다. 그러나 여전히 몇몇 해야할 일이 남아있다.

Setting Up Devise

이전에 언급하였듯이, Devise 셋팅들은 devise.rb initializer 파일 내에 제시되어 있다. 파일을 열어서 "Configuation for :lockable" 섹션을 찾아보자. 여기 대부분의 셋팅들은 코멘트 처리되어 있을 것이다. 그래서 그것들을 코멘트 해제하고 다음 값들을 제공하자:


  • config.lock_strategy = :failed_attempts - 이것은 몇번의 실패한 로긴 시도 이후에 계정이 잠길 것이라는 것을 의미한다. 실제로, 이는 유일한 사용가능한 전략이다. 그러나 당신은 이 셋팅을 none으로 셋팅하고 locking 메커니즘을 스스로 처리할 수 있다.
  • config.unlock_strategy = :both - 계정은 e-mail(Devise가 보낸 이메일에서 제공한 링크를 방문함으로써)을 통해서나 특정 시간을 기다림으로써 잠금해제가 될 수 있을 것이다. 이 두 방법 중 오직 하나만 사용가능하게 할려면 :email 이나 :time을 제공해라. 잠금해제를 스스로 처리하고자 하면 :none을 제공하여라.
  • config.maximum_attempts = 20 - 사용자가 계정이 잠금처리되기 전에 부정확한 패스워드를 입력할 수 있는 연속적인 횟수
  • config.unlock_in = 1.hour - 계정은 얼마 뒤에 잠금해제될 수 있는가. 이 셋팅은 오직 :time 이나 :both 잠금해제 전략을 사용한다면 제공한다.
  • config.last_attempt_warning = true - 사용자가 한번 로긴 시도를 남겼을 때 경고를 보여준다. 이는 경고가 flash 메시지 형태로 보여질 것이다.

좋다. 이제 "Configuration for : confirmable" 섹션을 살펴보고 이들 세팅을 잠금해제 하자.

  • config.confirm_within = 3.days - 사용자가 e-mail에 보내진 링크를 통해 계정을 활성화해야하는 시간이다(기본적으로 이는 Devise에 의해 생성된 활성화 토큰이 더이상 유효하지 않다는 것을 의미한다). 만약 계정이 활성화되지 않으면, 새로운 토큰이 요청될 수 있다. 만약 활성화 토큰이 기간만료되는 것을 원하지 않으면 이 값을 nil로 셋팅해라.
  • config.reconfirmable = true - 사용자는 프로파일 업데이트를 통해 e-mail을 변경할 때 재확인을 해야 한다. 이 절차는 등록 이후 확인 절차와 동일하다. 이 새로운, 미인증 e-mail은 사용자가 활성화 링크를 방문하기 전까지는 unconfirmed_email 필드에 저장될 것이다. 이 기간동안 로긴하는데 이전 e-mail이 사용된다.

만약 당신이 실제 애플리케이션을 만들고 있다면, 이들 셋팅값을 조정하는 것을 잊지마라.

  • config.mailer_sender - "From" 필드에 삽입될 e-mail을 여기에 제공하라.
  • config.secret_key - 다양한 토큰을 생성할 Secret key. 이 값을 변경하는 것은 이전에 생성된 모든 토큰을 무효화할 것이다.
이 파일에는 당신이 변경하기 원하는 더 많은 셋팅이 존재한다. 예를 들어, config.password_length는 최소, 최대 패스워드 길이를 셋팅한다.(기본으로 8에서 128이다).

Customizing Devise

Generating Views

이제 Devise는 우리가 좋아하는 방식으로 셋업되었다. 서버를 부팅하고 메인페이지를 네비게이팅한다. 그리고 "Register" 링크를 클릭한다. 당신에게 기본 폼이 제공될 것이다. 그러나 주의해야할 두가지가 있다:
  • 이 폼은 당신에게 이름을 입력할 폼을 제공하지 않는다. 우리가 마이그레이션 파일에 추가하였음에도 불구하고
  • 만약 당신이 스타일링을 위해 Bootstrap을 사용한다면, 폼은 좋게 보이지 않을 것이다(좋다. 이것은 큰 문제가 아니다. 그러나 이를 지속적으로 고려해보자.
다행히도, Devise에 의해 제공되는 모든 뷰들을 커스터마이징 할 수 있는 방법이 있다. 다음을 실행해보자.

$ rails generate devise:views

이 명령은 기본 Devise 뷰들을 바로 당신의 애플리케이션 폴더로 복사한다. devise 라고 불리는 새로운 폴더는 views 디렉토리에 생성될 것이다. 간략히 내부의 모든 폴더를 살펴보도록 하자.

  • confirmations - 이 디렉토리는 사용자가 confirmation e-mail을 다시 보내도록 요청할 때 그려지는 하나의 new.html.erb 뷰를 가지고 있다.
  • mailer - 이메일을 위한 모든 템플릿이 여기 저장되어 있다.
  • passwords - 패스워드를 요청하기 위한 폼을 가진 뷰를 가진다. 이메일을 리셋하고 실제로 패스워드를 변경한다.
  • registrations - 사이트에서 사용자가 등록할 때 new.html.erb 뷰가 렌더링된다.  edit.html.erb는 프로파일을 업데이트하는 폼을 포함한다.
  • sessions - 사이트를 위한 로긴 폼인 하나의 뷰를 가진다.
  • shared - 하나의 partial만이 제공된다. 이는 각 Devise의 페이지상에서 표시될 링크들을 포함한다("Forgot your password?" "Re-send confirmation email" 등과 같은)
  • unlocks - unlock link를 가진 email을 요청하는 폼을 가진 하나의 뷰를 가진다.
만약 특정 모듈만을 위한 뷰를 커스터마이징하고자 한다면(예를 들어, registerable 과 confirmable) 이 명령을 실행해라:

$ rails generate devise:views -v registrations confirmations

심지어 다른 모델들을 위한 분리된 뷰를 생성할 수 있다(만약 Devise를 갖춘 당신의 앱에서 다수의 모델이 있다면)

$ rails generate devise:views users

이부분을 더 읽어라

Registration Form

이제 하나 이상의 필드를 추가하고 스타일림함으로써 등록 폼을 제공하는 뷰를 변경하자.

views/devise/registrations/new.html.erb

<% header "Sign Up" %>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>

  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name, class: 'form-control' %>
  </div>

  <div class="form-group">
    <%= f.label :email %>
    <%= f.email_field :email, class: 'form-control' %>
  </div>

  <div class="form-group">
    <%= f.label :password %>
    <%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
    <% if @validatable %>
      &ltspan class="help-block"><%= @minimum_password_length %> characters minimum</span>
    <% end %>
  </div>

  <div class="form-group">
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
  </div>

  <%= f.submit "Sign up", class: 'btn btn-primary' %>
<% end %>

<%= render "devise/shared/links" %>


header 는 페이지의 헤더를 디스플레이하는 helper 메소드이다. divise_error_messages! 는 레코드를 저장하는 동안 발생하는 에러들을 렌더링한다.

서버를 재시작하고 새로운 사용자를 등록하자. 이제 사용자 이름을 제공할 수 있을 것이다.

Main Menu and Flash Messages

main page로 리다이렉트된 후, 몇몇 이슈들을 확인하게 될 것이다.

  • 현재 어떤 사용자가 로그인했는지 확인할 방법이 없다.
  • 로그아웃할 방법이 없다.
  • welcome flash message는 정확하게 스타일되지 않았다(만약 Bootstrap이 함께 한다면)
첫 두 이슈는 아래와 같이 메인 메뉴를 추가함으로써 쉽게 해결된다.

views/layouts/application.html.erb

<nav class="navbar navbar-inverse">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Devise Demo', root_path, class: 'navbar-brand' %>
    </div>
    <div id="navbar">
      <ul class="nav navbar-nav">
        <li><%= link_to 'Home', root_path %></li>
      </ul>
      <ul class="nav navbar-nav pull-right">
        <% if user_signed_in? %>
          <li class="dropdown">
            <a class="dropdown-toggle" data-toggle="dropdown" href="#">
              <%= current_user.name %>
              <span class="caret"></span>
            </a>
            <ul class="dropdown-menu" role="menu">
              <li><%= link_to 'Profile', edit_user_registration_path %></li>
              <li><%= link_to 'Log out', destroy_user_session_path, method: :delete %></li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to 'Log In', new_user_session_path %></li>
          <li><%= link_to 'Sign Up', new_user_registration_path %></li>
        <% end %>
      </ul>
    </div>
  </div>
</nav>

user_signed_in? 은 사용자가 로그인했는지 여부를 알려주는 Devise의 헬퍼 메소드이다. current_user는 user record를 리턴하거나 사용자가 로그인하지 않았다면 nil을 리턴한다. 만약 Devise 마이그레이션을 생성하는 동안 모델명을 다르게 했다면 이들 헬퍼 메소드들 또한 다른 이름을 가지고 있을 것이다. Admin 모델의 경우, admin_signed_in? 그리고 current_admin이 될 것이다. 메타프로그래밍은 좋다.

모든 routes(물론 root_path와 더불어)들은 Devise에 의해 잘 제공된다. destroy_user_session_path (log out)은 기본으로 DELETE HTTP 메소드를 필요로한다는 것에 주의해라. 만약 그것이 싫다면, devise.rb initializer 안에 있는 config.sign_out_via 를 변경하라.

스타일링 목적으로 Bootstrap's 의 dropdown 메뉴를 사용할 것이다. 만약 그것을 잘 사용하려면, 몇몇 action들이 더 필요할 것이다. Dropdown은 JavaScript 코드에 의지하고 Turbolinks를 사용한다. 페이지 사이를 이동할 때 turbolinks가 정확하게 동작하지 않을 거서이다. jquery_turbolinks를 사용해 그것을 수정해라.

Gemfile

[...]
gem 'jquery-turbolinks'
[...]

다음을 실행하는 것을 잊지마라.

$ bundle install

이제 application.js 파일을 수정해보자.

application.js

[...]
//= require jquery
//= require jquery.turbolinks
//= require jquery_ujs
//= require bootstrap/dropdown
//= require turbolinks
[...]

Devise 의 특정 flash message를 스타일링하기 위해 Sass @extend 메소드를 사용하자(이 메소드를 남용하지 말자. 이 지시자는 몇몇 결점이 있다):

application.scss

[...]
.alert-alert {
  @extend .alert-warning;
}

.alert-notice {
  @extend .alert-info;
}
[...]

서버를 다시 읽어들이고 drop down 메뉴를 살펴보자. 왜 사용자이름이 표시되지 않는가?

String_params and Edit Profile Page

만약 string_params를 가진 Rails를 사용하고 있다면(기본으로 Rails 4에서는 enable되어 있다), 하나의 스텝이 더 필요하다:
:name 속성이 화이트 리스팅 되어야한다.

application_controller.rb

[...]
before_action :configure_permitted_parameters, if: :devise_controller?

protected

def configure_permitted_parameters
  devise_parameter_sanitizer.for(:sign_up) << :name
  devise_parameter_sanitizer.for(:account_update) << :name
end
[...]

우리는 모든 Devise controllers에 sign up과 account update 액션을 위한 :name 속성을 화이트리스팅하는 before_action을 추가하였다. 만약 중첩된 해쉬 또는 배열을 가진 약간 더 복잡한 시나리오를 가지고 있다면 문서의 이 부분을 참고하여라.

당신의 이름을 변경하기 위한 프로파일 페이지를 이끌어가기 전에 대응되는 뷰가 업데이트 되어야 한다:

views/devise/registrations/edit.html.erb

<% header "Edit #{resource_name.to_s.humanize}" %>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= devise_error_messages! %>

  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name, class: 'form-control' %>
  </div>

  <div class="form-group">
    <%= f.label :email %>
    <%= f.email_field :email, class: 'form-control' %>
    <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
      <span class="label label-info">Currently waiting confirmation for: <%= resource.unconfirmed_email %></span>
    <% end %>
  </div>

  <div class="form-group">
    <%= f.label :password %>
    <%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
    <span class="help-block">leave blank if you don't want to change it</span>
  </div>

  <div class="form-group">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
  </div>

  <div class="form-group">
    <%= f.label :current_password %>
    <%= f.password_field :current_password, autocomplete: "off", class: 'form-control' %>
    <span class="help-block">we need your current password to confirm your changes</span>
  </div>

  <%= f.submit "Update", class: 'btn btn-primary' %>
<% end %>

<h3>Cancel my account</h3>

<p>Unhappy?
  <%= button_to "Cancel my account", registration_path(resource_name), class: 'btn btn-danger',
                data: { confirm: "Are you sure?" }, method: :delete %>
</p>

<%= link_to "Back", :back, class: 'btn btn-default btn-sm' %>


사용자는 또한 그들의 이메일 주소, 비밀번호 또한 변경하길 원할 것이다(우리가 이전에 논의했듯이 확인이 필요하다) 그리고 심지어 프로파일을 완전히 삭제하길 원할 것이다.

다른 Devise의 뷰들은 어떻게 스타일링하는지는 보여주지 않을 거서이다. 관심이 있다면 GitHub repo 를 확인하라.

Sending E-mail and DelayedJob

이제 간단하게 이메일을 보내는 절차에 대해 논의해보자. 먼저 개발 환경에서 이메일은 기본으로 보내지지 않는다는 것을 기억해야 한다. 여전히 콘솔을 통해 코멘트처리 되어 있는 것을 볼 수 있을 것이다. enable 시켜보자.

config/environments/development.rb

[...]
config.action_mailer.perform_deliveries = true
[...]

그리고 ActionMailer를 설정하자(여기 예제가 있다)

다음으로 production을 위해 유사한 설정을 추가해야할 것이다. 여기 데모 앱을 위한 설정이 있다.

config/environments/production.rb

[...]
config.action_mailer.delivery_method = :smtp
config.action_mailer.default_url_options = { host: 'sitepoint-devise.herokuapp.com' }

ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.sendgrid.net',
    :port           => '587',
    :authentication => :plain,
    :user_name      => ENV['SENDGRID_USERNAME'],
    :password       => ENV['SENDGRID_PASSWORD'],
    :domain         => 'heroku.com',
    :enable_starttls_auto => true
}
[...]

이 앱은 Heroku에서 실행되기 때문에 나는 그것의 메일 전송 add-ons 중 하나를 사용해야 했다(당신은 이메일을 곧바로 보낼 수 없을 것이다). 나의 설정에서 Sendgrid 가 사용된다. 이것은 free plan 상에서 실행되기 때문에 메일이 전송되는데 시간이 걸릴 것이다.

어째든, 당신은 커스텀 메일을 사용하기 원한다면, devise.rb에 있는 config.mailer를 수정하여라.

그리고 마지막으로 이메일을 보내기를 백그라운드로 실행하는 것이 좋은 아이디어이다. 반면에 사용자들은 새로운 페이지로 리다이렉트 되기 전에 메일이 보내질때까지 기다려야 할 것이다. Devise에 Delayedjob을 예제와 같이 통합해 보자.

새로운 gem을 추가하자:

Gemfile

[...]
gem 'delayed_job_active_record'
[...]

그리고 실행하자.

$ bundle install
$ rails generate delayed_job:active_record
$ rake db:migrate

gem 을 설치하고 필요한 마이그레이션을 생성하고 적용하기(생성될 scheduled task들을 저장할 새로운 테이블)

Rails4.2의 경우 queueing backend를 셋팅하기 위해 application.rb 파일을 변경해라

config/application.rb

[...]
config.active_job.queue_adapter = :delayed_job
[...]

이제, 당신 모델의 Devise의 메소드를 오버라이딩하자.

models/user.rb

[...]
def send_devise_notification(notification, *args)
  devise_mailer.send(notification, self, *args).deliver_later
end
[...]

deliver_later 는 전송이 큐에 담길 것을 의미한다.

만약 앱을 Heroku에 배포하고자한다면, 당신 프로젝트 루트에 Profile을 생성하고 다음 라인을 추가하라.

Profile

worker: rake jobs:work

job을 처리하기 위해 적어도 하나의 worker 프로세스가 job을 처리하도록 해야할 것이고 그것은 한달에 $35의 경비가 소요될 것이다.

$ heroku ps:scale worker=1

Heroku에서 Delayedjob을 사용하는 것에 대한 매우 이해하기 쉬운 가이드가 있다. 더 자세한 사항은 그것을 참고하라.

데모 앱에서는 background sending을 enable 하지 않을 것이다. 그러나 관련 코드는 GitHub repo에 있다.

Resticting Access

여기까지 잘 따라왔다. 특정 페이지에 대해 접근을 제한하는 것은 어떻게 할까? 어떻게 구현할 수 있을까?

Devise는 그것 또한 잘 다루는 것으로 보인다: 당신은 대응되는 메소드를 before_action  처럼사용하기만 하면 된다. 먼저 우리는 제한될 특별한 페이지가 필요하다. 그것을 간단히 "Secret" 이라고 부르자.

config/routes.rb

[...]
get '/secret', to: 'pages#secret', as: :secret
[...]

의미없는 간단한 뷰는 :

views/pages/secret.html.erb

<% header "Secret!" %>

당신은 심지어 secret 메소드를 PagesController에 추가할 필요조차 없다 - Rails는 그것을
암암리에 정의할 것이다.

메뉴를 약간 수정해보자:

views/layouts/application.html.erb

[...]
<ul class="nav navbar-nav">
  <li><%= link_to 'Home', root_path %></li>
  <% if user_signed_in? %>
    <li><%= link_to 'Secret', secret_path %></li>
  <% end %>
</ul>
[...]

그리고 controller를 수정해보자.

[...]
before_action :authenticate_user!, only: [:secret]
[...]

before_action은 secret 메소드를 호출하기 전에 사용자가 인증된 사용자인지 체크할 것이다. 미인증 사용자는 "Please authenticate" flash 메시지를 가진 페이지로 리다이렉트될 것이다. 이름이 다른 모델의 경우 이 메소드는 다른 이름을 가질 것이라는 것을 다시 한번 상기하자(Admin 모델의 경우 authenticate_admin!이다).

Using Devise Extensions

큰 Devise 커뮤니티는 더 많은 기능들을 추가하기 위해 많은 좋은 extensions 를 개발했다. Bit Zesty가 개발한 Devise Zxcvbn 을 시험삼아 통합해보자.

이 extension은 페스워드 길이를 측정하고 약한 패스워드는 거부한다. 그것은 zxcvbn-ruby에 의존하는데 Dropbox에 의해 만들어진 zxcvbn.js의 루비 포팅버전이다. 어떻게 이 솔루션이 동작하는지 알고 싶으면 이 공식 블로그 포스트를 읽어라.

새 gem을 추가하자:

Gemfile

[...]
gem 'devise_zxcvbn'
[...]

그리고 다음을 실행한다.

$ bundle install

그리고 당신의 모델을 위한 새로운 모듈을 등록하자.

models/user.rb

[...]
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable, :zxcvbnable
[...]

이제 User는 zxcvbnable 하다(그냥 발음해보아라). 이 extension을 위한 하나의 셋팅만 존재한다.

config/initializers/devise.rb

[...]
config.min_password_score = 0
[...]

이는 기본적으로 패스워드가 얼마나 강력해야하는가를 의미한다(높은 수가 더 강력하다). Strength는 cracking time을 측정한것으로 이해하면 된다.

  • 0 - estimated cracking time is less then 10**2 seconds.
  • 1 - 10 ** 4 seconds.
  • 2 - 10 ** 6 seconds.
  • 3 - 10 ** 9 seconds.
  • 4 - inifinity(글쎄, 사실은 이경우 몇 세기 정도로 생각할 수 있다)

 사용자는 복잡한 패스워드를 생각할 필요가 없을 것이므로 여기서는 0으로 셋팅했다. 실제 앱에서는 그러한 약한 패스워드를 허락하는 것은 추천하지 않는다.

마지막으로 에러 메시지를 커스터마이징하자:

config/locales/devise.en.yml

[...]
en:
  errors:
    messages:
      weak_password: "is not strong enough. Consider adding a number, symbols or more letters to make it stronger."
[...]

다양한 패스워드를 시도해보고 얼마다 그들이 강력한지 체크해보자.

Measuring Passwords' Strength on Client-Side

마지막으로, zxcvbn을 클라이언트 사이드에 어떻게 적용하는지 보여주겠다. 이 파일을 다운로드해서 당신의 프로젝트에 연결해보자. 다음 컨텐츠를 가진 새로운 CoffeeScript 파일을 생성해보자.

global.coffee

jQuery ->
  displayHint = (strength, crack_time) ->
    msg = 'Password is ' + strength + ' (time to break it: ' + crack_time + ')'

    estimate_message = this.next('.estimate-message')
    if estimate_message.length < 0
      estimate_message.text msg
    else
      this.after '<span class="help-block estimate-message">' + msg + '</span>'

  $('form').on 'keyup', '.estimate-password', ->
    $this = $(this)
    estimation = zxcvbn($this.val())
    crack_time = estimation.crack_time_display

    switch estimation.score
      when 0 then displayHint.call($this, "very weak", crack_time)
      when 1 then displayHint.call($this, "weak", crack_time)
      when 2 then displayHint.call($this, "okay", crack_time)
      when 3 then displayHint.call($this, "strong", crack_time)
      when 4 then displayHint.call($this, "very strong", crack_time)

  return

그리고 연결해보자.

application.js

[...]
//= require global
[...]

score 는 0에서 4까지의 수를 리턴 한다 - 우리는 조금 전에 그 값들에 대해 얘기했었다 - 그리고 그것은 strength를 측정하는 가장 편리한 방법으로 보인다. crack_time_display는 친근한 방식으로 패스워드를 크랙하는데 필요한 시간 측정값을 리턴한다("3days", "4days", "centuries" 등등).https://github.com/plataformatec/devise/wiki/How-Tos

이제 estimate-password 클래스를 원하는 아무 패스워드 필드에 할당해보자. 예를 들어

views/devise/registrations/new.html.erb

[...]
<div class="form-group">
  <%= f.label :password %>
  <%= f.password_field :password, autocomplete: "off", class: 'form-control estimate-password' %>
  <% if @validatable %>
    <span class="help-block"<>%= @minimum_password_length %> characters minimum</span>
  <% end %>
</div>

<div class="form-group">
  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control estimate-password' %>
</div>
[...]

물론, 이것은 가장 간단한 솔루션이다. 그러므로 이것을 자유롭게 확장해라.

Conclusion

이제 이 문서의 끝에 도달했다. 우리는 Devise를 앱에 통합했다. 그것의 모듈과 셋팅을 살펴보았고 extension을 추가했다. 사용가능한 더 많은 특성과 extension 들이 있다. 그러므로 나는 official Wiki 를 살펴볼 것을 장려한다. 특별히 How-to's 유심히 보아라 - 거기는 90개 이상의 작지만 유용한 튜토리얼이 있다.

난 Rails 시리즈와 함께한 Authentication 의 두번째 파트를 당신들이 즐겼기를 바란다. 다음 문서에서 우리는 Oauth2와 함께한 Authentication에 대해 논의할 것이다.

출처 http://www.sitepoint.com/devise-authentication-in-depth/

2015년 10월 18일 일요일

HTML5 개발자들을 위한 온라인 툴 17가지

HTML5 개발자들을 위한 최고의 온라인 툴 톱17

이미지

HTML5는 HTML의 최신 버젼으로 웹사이트 디자인에 많은 기능을 추가시켜줬죠. 특히 요즘 웹사이트들은 모바일환경에 적합해야되서 웹개발자들은 HTML5를 자유자재로 사용해 최대한 모바일과 호환성이 있는 기능들을 갖추게 하는것이 매우 중요면서 웹사이트의 유저익스피리언스 (UX)와 유저빌리티에 상당한 영향을 미칩니다. 그럼,이것을 제대로 이용하려면 여러 자원들이 있는데요, 프레임워크, 툴, 테크닉, 라이브러리, 그리고 플러그인들이 있습니다. 이러한 도구들을 웹디자인에 사용하게 되면 훨씬 더 강력해지고 빨라지며 안전성이 있고 인터액티브한 사이트가 될 수 있습니다. 그렇다면 정확히 HTML5의 웹디자인이 어떠한 장점을 가져다 줄 수 있는지, 그리고 가장 유용한 온라인 툴 17가지를 소개해 보겠습니다.

웹디자인의 장점
- 접근성 향상
- 오디오와 동영상 지원
- 깔끔하고 체계화된 코딩
- 높은 인터액션
- 여러 브라우저 호환성
- 모바일 반응형 개발 디자인


HTML5 유용한 온라인 툴 & 테크닉

1.  jQuery를 이용한 파일 업로드
http://tutorialzine.com/2011/09/html5-file-upload-jquery-php/
업로드 센터는 컴퓨터 하드드라이브에서 드래그엔 드롭으로 사진 파일들을 업로드 시킬수 있습니다. 새로운 API와 AJAX 그리고 파일 리더 API가 브라우저 윈도우를 통해 업로드를 가능하게 만들었습니다.

2.  Front Gragr: 프론트 테스팅 툴
http://fontdragr.com/
이 툴은 커스텀 디자인된 글꼴들을 테스트 하기에 좋습니다. 아무 코딩이 필요없는데요, 그냥 드래그 엔 드롭으로 테스팅을 하시면 됩니다. Front Gragr는 HTML이나 CSS코드를 바꾸지 않아도 글꼴을 쉽게 테스트 할 수 있는 기능을 주죠.

3.  jQuery HTML 업로더
http://www.igloolab.com/jquery-html5-uploader/
간단한 jQuery 플러그인으로 웹 어플리케이션에 업로드 시스템을 장착 시켜 줍니다. 사용자는 드롭박스 요소를 만들어 주기만 하면 나머지 작업은 jQuery가 업로더를 통해 처리합니다.

4.  D3 태그 틀라우드
http://jsfiddle.net/adiioo7/rutpj/light/
D3 태그 클라우드는 워드클라우드(WordClouds)의 오픈소스를 작용시켰습니다. 아웃풋을 그려내는데 캔버스를 사용하면서 좋은 성능을 보여줍니다.
이미지


5.  Audio5js
http://zohararad.github.io/audio5js/
HTML5의 오디오 지원은 꽤 뛰어난 편이지만 아직 예전 웹브라우저들과 호환성 문제점들을 지니고 있습니다. Audio5js는 본래 지원이 안되는 모든 코덱을 지원해줍니다.

6.  SceneGraph
http://gwennaelbuchet.github.io/SceneGraph.js/
SceneGraph는 자바스크립트에 기반한 애니메이션 프레임워크입니다. 이 프레임워크는 객체지향인데 쉬운 사용을 위해 캔버스를 바탕으로 만들어졌습니다.

7.  Fabric.js
http://fabricjs.com/
Fabric.js는 캔버스 라이브러리의 강력하고도 심플한 자바스크립트 입니다. 객체들을 자유롭게 만들고 수정할 수 있도록 여러 방법들이 제공됩니다.

8.  Howler.js
http://goldfirestudios.com/blog/104/howler.js-Modern-Web-Audio-Javascript-Library
웹오디오(Web Audio) API와 호환되어 작동할 수 있는 자바스크립트 라이브러리입니다. 만약 웹오디오 API를 못찾는다면 HTML5 오디오로 넘어가게 되죠. 이 자바스크립트는 여러 웹브라우저 호환성과 캐싱, 멀티플 재생, 사운드 루핑 등의 기능들을 가지고 있습니다.

이미지


9.  WYSI
http://xing.github.io/wysihtml5/
오픈소스 형태의 리치 텍스트 에디터 입니다. 꽤 가벼운데 어떠한 자바스크립트 프레임워크에 얽매이지 않아서 굉장히 빠르게 로딩이 됩니다.

10.  RazorFlow
https://razorflow.com/
RazorFlow는 PHP 프레임워크로 반응형 대시보드를 만들 수 있습니다. 이 대시보드의 장점은 모든 브라우저든과 디바이스 플랫폼에 호환이 가능하고 MySQL, SQLite 또는 PostgreSQL 데이터베이스들과 같이 일을 할 수 있습니다.


11.  WorkLess
http://workless.ikreativ.com/
WorkLess는 HTML5와 CSS3 프레임워크로 시간과 노력을 최소화 해서 여러 웹브라우저 및 웹사이트 호환을 시켜줍니다. 반복되는 작업을 감소해주면서 웹사이트 개발에 들어가는 시간을 확실히 단축시켜 주죠.

12.  Percentage Loader
http://widgets.better2web.com/loader/
퍼센테지 로더는 캔버스를 사용해서 아무 이미지 없이도 멋진 로더(loader)를 만들어 줍니다. 드래그 엔 드롭 기능을 통해서 한 스크린에 여러 로더를 보여 줄 수도 있죠.

13.  Zebra
http://www.zebkit.com/
Zebra는 자바스크립트 라이브러리로 유저들이 데스크탑과 같은 레이아웃을 만들 수 있도록 해줍니다. Zebra의 UI 요소들이 풍부하고 HTML 캔버스와 CSS3 요소들로 만들어졌습니다. Zebra는 모든 장면들이 웬만한 현대 웹브라우저들에 다 동일하게 보여지도록 렌더링을 해줍니다.

이미지



14.  Junior
http://justspamjustin.github.io/junior/#home
주니어는 HTML5 프레임워크로서 개발자들이 모바일 어플과 같이 만들 수 있도록 도와주는 프론트엔드입니다. 프레임워크와 함께 미리 제공되는 도큐먼트와 샘플들이 주니어를 사용하는데 매우 쉽게 해주죠. 부드러운 성능을 위해서 CSS3 전환과 여러 UI요소들을 사용합니다.

15.  Radi
http://radiapp.com/
라디는 정말 좋은 시각 디자인 어플리케이션입니다. 라디는 웹을 위한 애니메이션과 그래픽 및 동영상을 만드는데 최적화 된 어플인데, HTML과 자바스크립트 코딩을 이용해 사용할 수 있습니다. 추가로 캔버스 아웃풋을 통한 애니메이션도 만들 수 있죠.

16.  Literally Canvas
http://literallycanvas.com/
Literally Canvas는 그림판과 같은 오픈소스 위젯입니다. 이 위젯은 온라인에서 그림을 그려서 아무 HTML 페이지에 적용시킬 수 있죠. Literally Canvas에 딸려오는 툴로는 그리기, 색상 고르기, 지우기, 뒤로 돌리기 등의 기능들이 있습니다.

17.  HTML Sortable
http://farhadi.ir/projects/html5sortable/
HTML Sortable은 가벼운 플러그인으로서 여러 리스트 사이로 아이템들을 움직이고 연결시켜 줄 수 있습니다. 또한 모든 메이저 웹브라우저들과 호환이 가능한데 HTML5의 기존 API와 연동해서 드래그 엔 드롭을 통해 그리드(grid)나 리스트들을 분류할 수 있습니다. 


2015년 10월 15일 목요일

레일스에서 무한 스크롤링 : 실전편

이전 기본 문서에서 우리는 데모 포스트를 설정하고 간단한 pagination 대신에 무한 스크롤을 구현하였다. 이를 달성하기 위해 will_paginate와 javascript를 사용하였다. 

동작하는 데모는 Heroku에서 확인할 수 있다.

그 소스 코드는 GiHub에서 확인할 수 있다.

오늘은 무한 스크롤 대신에 "Load more" 를 구현해 보자. 이 솔루션은 footer 내부에 링크가 존재하고 스크롤링이 모든 레코드가 로딩되는 동안 그 링크를 없애 버리는 경우 편리하다.

이 것이 동작되는 원리를 설명하기 위해 PostsController 다음 변경사항들을 추가하자.

posts_controller.rb

def index
    get_and_show_posts
end

def index_with_button
    get_and_show_posts
end

private

def get_and_show_posts
    @posts = Post.paginate(page: params[:page], per_page: 15).order('created_at DESC')
    respond_to do |format|
        format.html
        format.js
    end
end

route를 추가하자:

config/routes.rb

get '/posts_with_button', to: 'posts#index_with_button', as: 'posts_with_button'

이제 두 컨셉을 설명하는 독립적인 두 페이지가 있다.

index_with_button.html.erb

<div class="page-header">
    <h1>My posts</h1>
</div>

<div id="my-posts">
    <%= render @posts %>
</div>

<div id="with-button">
    <%= will_paginate %>
</div>

<% if @posts.next_page %>
    <div id="load_more_posts" class="btn btn-primary btn-lg">More posts</div>
<% end %>

대부분의 경우, 뷰는 동일하다. 단지 pagination wrapper의 지시자만 변경했고(나중에 그 지시자를 적절한 상태를 쓰기 위해 사용할 것이다) Bootstrap 클래스의 도움을 받아 버튼으로 표시될 #load_more_posts 블록을 추가했다. 우리는 이 버튼이 더 사용가능한 페이지가 있을 때만 보이기 원한다. 오직 블로그에 하나의 포스트만 있는 상황을 상상해보아라.- 왜 "Load more" 버튼을 그릴 필요가 있을까?

이 버튼은 처음에는 보여서는 안된다 - 우리는 자바스크립트로 버튼을 보여줄 것이다. 이 방식은 JS가 disabled 되었있을 때 기본 행위에 대한 대체시스템이다.

application.css.scss

#load_more_posts {
    display: none;
    margin-bottom: 10px; /* Some margin to separate it from the footer */
}

이제 클라이언트 사이드 코드를 수정할 차례이다:

pagination.js.coffee

if $('#with-button').size() > 0
    $('.pagination').hide()
    loading_posts = false

    $('#load_more_posts').show().click ->
      unless loading_posts
        loading_posts = true
        more_posts_url = $('.pagination .next_page a').attr('href')
        $this = $(this)
        $this.html('<img src="/assets/ajax-loader.gif" alt="Loading..." title="Loading..." />').addClass('disabled')
        $.getScript more_posts_url, ->
          $this.text('More posts').removeClass('disabled') if $this
          loading_posts = false
      return


여기서 우리는 "Load more" 버튼을 보여주는 대신에 pagination 블록을 숨기고, click 이벤트 핸들러를 버튼과과 바인딩한다. loading_posts 플래그는 사용자가 버튼을 한번에 여러번 클릭시 다수의 요청을 보내는 것을 방지하는데 사용된다.

이벤트 핸들러 내부에서, 우리는 이전과 동일한 개념을 사용하고 있다: 다음 페이지 URL을 가져온다. "loading" 이미지를 추가하고 버튼을 disable 시킨다. 그리고 AJAX 요청을 서버로 제출하다. 또한 응답이 수신되었을 때 실행될 콜백을 추가하였다. 이 콜백은 버튼을 원래 상태로 복구하고 플래그를 false 상태로 셋팅한다.

그리고 이제 뷰는 다음과 같다:

index_with_button.js.erb

$('#my-posts').append('<%= j render @posts %>');
<% if @posts.next_page %>
    $('.pagination').replaceWith('<%= j will_paginate @posts %>');
    $('.pagination').hide();
<% else %>
    $('.pagination, #load_more_posts').remove();
<% end %>

다시 페이지에 새로운 포스트들을 추가한다. 만약 포스트가 더 존재한다면, 새로운 pagination이 그려지고 숨겨진다. 존재하지 않으면 pagination 버튼은 제거된다.

Link to a Paricular Page

이제 전통적인 pagination 대신에 무한 스크롤이나 "Load more" 버튼을 어떻게 만드는지 알게 되었다. 아마도 당신이 고려해야 하는 한가지는 어떻게 사용자가 특정 페이지에 대한 링크를 공유할 수 있느냐 이다. 현재로서는, 이를 수행할 방법이 없다. 왜냐하면 우리는 새로운 페이지를 로드할 때 URL을 변경하지 않기 때문이다.

이제 URL에 있는 search 부분을 자바스크립트를 사용해 변경함으로써 이를 달성해보자.(? 심볼로 시작하는)

window.location.search = 'page' + page_number

불행히도, 이는 즉각적으로 페이지를 릴로드한다. 이는 우리가 원하는 것이 아니다. 우리의 두번째 시도에서, hash 부분을 변경해보자(#심볼로 시작하는 hash). 실제로 이는 잘 동작한다. 이 페이지는 다시 릴로드되지 않는다. 그러나 세번째 더욱 우아한 솔루션이 있다 - History API. 이 API를 가지고  우리는 브라우저의 히스토리를 바로 조작할 수 있다.

이 특정 케이스에서, 우리는 pushState 메소드를 이용해 히스토리에 몇몇 엔트리를 추가하기 원한다.

먼저, Benjamin Arthur Lupton이 작성한 HTML5 History/State API를 위한 cross-browser를 지원을 제공하는 History.js 라이브러리를 다운로드 받자. jQuery를 위해 아마도 scripts/bundled/html4+html5/jquery.history.js 를 사용하기 원할 것이다.

이제, $.getScript가 리소스 로딩을 완료한 이후에 실행될 간단한 함수를 작성해보자.

pagination.js.coffee

page_regexp = /\d+$/

pushPage = (page) ->
    History.pushState null, "InfiniteScrolling | Page " + page, "?page=" + page
    return

$.getScript more_posts_url, ->
    # ...
    pushPage(more_posts_url.match(page_regexp)[0])

more_posts_url 은 다음 페이지에 대한 링크를 포함하고 있다는 것을 잊지말자. 거기서 페이지 넘버를 가져온다. pushPage 함수 내에서 우리는 브라우저의 히스토리를 조작하기 위해 History.js 를 사용하고 기본적으로 URL을 변경한다(마지막 파라미터를 가지고). 두번째 파라미터는 윈도우의 타이틀을 변경한다. 필요하면 첫 파라미터(null)는 몇몇 추가적인 데이터를 저장하는데 사용될 수 있다. URL이 변경된 뒤에는 사용자는 이전 페이지로 이동하기 위해 브라우저에 있는 "Back" 버튼을 클릭할 수 있다. 메우 좋다.

마지막으로 걱정할 것은 legacy 브라우저이다: IE 9과 소수의 특정 브라우저들, 그것들은 History API를 지원하지 않는다. 이들 고대의 가축들에는, 결과 URL이 다음과 같이 보일 것이다:
http://example.com?page=2 대신에 http://example.com#http://example.com?page=2. 그래서 우리는 이 경우를 위한 지원을 추가해야 한다.

pagination.js.coffee

[...]

hash = window.location.hash
  if hash.match(/page=\d+/i)
    window.location.hash = '' # Otherwise the hash will remain after the page reload
    window.location.search = '?page=' + hash.match(/page=(\d+)/i)[1]

[...]

이 코드 블록은 페이지에 적제된 상태에서 실행된다. 여기서 page= 를 위한 url hash를 스캔한다. 만약 URL의 search 부분이 대응하는 페이지 번호로 업데이트 되면 그 페이지가 리로드된다.

뷰를 가볍게 수정해서 pagination이 다음페이지가 사용가능할 때에만 보여주는 것은 좋은 아이디어이다("Load more" 버튼을 가지고 했던 것처럼). 반면에 사용자가 마지막 페이지로 직접 이동하기 위해 URL을 입력하였을 때 pagination은 여전히 디스플레이되고 javascript 이벤트 핸들러는 여전히 발견될 것이다.

index.html.erb

<% if @posts.next_page %>
    <div id="infinite-scrolling">
<%= will_paginate %>
    </div>
<% end %>

그러나 이 솔루션은 사용자가 이전 포스트들을 로딩할 수 없는 상황에서 문제점을 야기한다. 당신은 "Load previous" 버튼을 가지고 더 복잡한 솔루션을 구현하거나 "Go to the first page" 링크를 표시할 수 있다.

다른 방법은 기본 pagination을 조합하는 것이다. 무한 스크롤링과 함께 페이지의 제일 위에 표시한다. 이는 다른 문제점을 해결해준다: 방문자가 마지막 페이지로 이동하고자 한다면? 즉 31페이지로 이동하고자 하면 스크롤을 내리고 내려야(또는 "Load more"버튼을 30번)하는데 매우 짜증나는 일이다. 우리는 원하는 페이지로 점프하거나 몇몇 필터를 구현하는 방법으로 제공할 수 있다.(by date, category, view count 등)

Pagination and Infinite Scrolling

"조합된" 솔루션을 구현해보자. 무한 스크롤과 기본적인 pagination을 조합한다. 이는 또한 javascript가 disable된 곳에서도 잘 동작할 것이다. 사용자는 단지 두 곳에서 pagination을 볼 것이다. 이것은 나쁘지 않다.

먼저, view 블록에 다른 pagination 블록을 추가한다(다음 절에서, 우리는 static-pagination wrapper를 사용할 것이다)

index.html.erb 그리고 index_with_button.html.erb

<div class="page-header">
  <h1>My posts</h1>
</div>
<div id="static-pagination">
  <%= will_paginate %>
</div>
[...]

이후, 우리는 오직 하나의 pagination block 만 참조되도록 스크립트를 약간 수정해야 한다(수정된 라인 근처에 코맨트를 달아놓았다):

pagination.js.coffee

[...]

if $('#infinite-scrolling').size() > 0
    $(window).bindWithDelay 'scroll', ->
      more_posts_url = $('#infinite-scrolling .next_page a').attr('href') # <--------
      if more_posts_url && $(window).scrollTop() > $(document).height() - $(window).height() - 60
        $('#infinite-scrolling .pagination').html( # <--------
          '<img src="/assets/ajax-loader.gif" alt="Loading..." title="Loading..." />') # <--------
        $.getScript more_posts_url, ->
          window.location.hash = more_posts_url.match(page_regexp)[0]
      return
    , 100

  if $('#with-button').size() < 0
    # Replace pagination
    $('#with-button .pagination').hide() # <--------
    loading_posts = false

    $('#load_more_posts').show().click -<
      unless loading_posts
        loading_posts = true
        more_posts_url = $('#with-button .next_page a').attr('href') # <--------
        if more_posts_url
          $this = $(this)
          $this.html('<img src="/assets/ajax-loader.gif" alt="Loading..." title="Loading..." />').addClass('disabled')
          $.getScript more_posts_url, ->
            $this.text('More posts').removeClass('disabled') if $this
            window.location.hash = more_posts_url.match(page_regexp)[0]
            loading_posts = false
      return

[...]

index.js.erb

$('#my-posts').append('<%= j render @posts %>');
$('.pagination').replaceWith('<%= j will_paginate @posts %>');
<% unless @posts.next_page %>
    $(window).unbind('scroll');
    $('#infinite-scrolling .pagination').remove(); // <--------
<% end %>

index.js.erb 내에서 두 곳 모두에서 pagination이 업데이트 되길 원하므로 2번째 라인은 수정하지 않았다.

index_with_button.js.erb

$('#my-posts').append('<%= j render @posts %>');
$('.pagination').replaceWith('<%= j will_paginate @posts %>');
<% if @posts.next_page %>
    $('#with-button .pagination').hide(); // &lt--------
<% else %>
    $('#with-button .pagination, #load_more_posts').remove(); // <--------
<% end %>

여기에 동일한 컨셉이 적용되었다. 또한 두 경우 모두 replaceWith 를 조건문 밖으로 이동시켰다는 것을 주목해라. 이 지점에서 우리는 우리의 pagination이 다음 페이지가 열릴때마다 재작성되기를 원할 것이다. 만약 우리가 이 변경사항을 적용하지 않으면 사용자가 마지막 페이지를 열었을때 꼭대기의 pagination은 교체되지 않을 것이다.  - 오직 아래 있는 pagination 만이 제거될 것이다.

Spy the Scrolling!

이제 우리는 마지막에 도달했다. 이 부분은 아마도 가장 교묘한 부분일 것이다. 이 부분에서 우리는 사용자가 스크롤을 내리고 더 많은 포스트가 로딩될 때 URL을 업데이트하고 현재 페이지를 하이라이트한다. 그러나 사용자가 스크롤 백(맨 위쪽으로)하기로 결정했다면? 물론 URL 과 pagination 도 업데이트되지 않을 것이고 다소 혼란스러울수 있을 것이다.

이는 스크롤 스파잉을 구현함으로써 해결될 수 있다. 우리의 계획은 다음과 같다: 다른 페이지로부터의 포스트들 사이에 구분자를 추가하자(이들 구분자들은 페이지 넘버를 포함할 것이다) 그리고 사용자가 이들 구분자 만큼 스크롤할 때마다 이벤트를 발생시킨다. 이벤트 내에서 그가 현재 어떤 페이지를 보고있는지 체크하고 그에 대응하여 URL과 pagination을 업데이트하자.

구분자부터 시작해보자.

index.html.erb 그리고 index_with_button.html.erb

[...]

<div id="my-posts">
<div class="page-delimiter first-page" data-page="<%= params[:page] || 1 %>">
</div>
<%= render @posts %>
</div>
[...]

여기 data-page는 실제 페이지 넘버를 포함하고 있다. 우리는 GET 파라미터로부터 그것을 가져오거나 페이지 넘버가 제공되지 않으면 1로 셋팅한다. 우리가 사용하는 first-page 클래스는 짧게 사용한다는 것을 주목하라.

우리는 또한 스크립트를 업데이트할 것이다.

index.js.erb 그리고 inde_with_button.js.erb

var delimiter = $('<div class="page-delimiter" data-page="<%= params[:page] %>"></div>');
$('#my-posts').append(delimiter);
$('#my-posts').append('<%= j render @posts %>');

[...]

당장 이들 구분자들은 사용자들에게 보이지 않을 것이다.

마지막으로, 실제 스크롤 스파잉을 구현해라. Caleb Troughton이 만든 jQuery를 위한 Waypoints 라이브러리를 사용할 수 있다. 유사한 기능을 제공하는 다른 라이브러리들이 있지만 Waypoints는 사용자가 스크롤 업 하든 다운하든 트래킹할 수 있으며 우리 케이스에 더 편리하다.

다음 함수는 구분자에 사용자가 스크롤할 때마다 실행될 이벤트 핸들러를 붙일 것이다. 불행히도 우리의 구분자들은 동적으로 추가되기 때문에 우리는 이 이벤트 핸들러들을 각각 붙여야 할것이다. 그렇지 않으면 동작하지 않을 것이다.

pagination.js.coffee

jQuery ->
  page_regexp = /\d+$/

  window.preparePagination = (el) ->
    el.waypoint (direction) ->
      $this = $(this)
      unless $this.hasClass('first-page') && direction is 'up'
        page = parseInt($this.data('page'), 10)
        page -= 1 if direction is 'up'
        page_el = $($('#static-pagination li').get(page))
        unless page_el.hasClass('active')
          $('#static-pagination .active').removeClass('active')
          pushPage(page)
          page_el.addClass('active')

    return

  [...]

이제 코드는 사용자가 스크롤 업하고 있고 첫 페이지에 도달하지 않았는지 체크한다. 그러면 코드는 data-page로부터 페이지 넘버를 받아와서 만약 방향이 업이면 1만큼 감소시킨다. 이는 우리의 구분자들이 대응되는 페이지로부터의 포스트 앞에 놓여있기 때문이다. 그래서 사용자가 스크롤 업하고 이 구분자만큼 이동할 때 실제로 이 페이지를 떠나고 이전 페이지로 간다.

#static-pagination 셀렉터는 기본 pagination을 가진 블록을 가리킨다. 셀렉터는 현재 페이지 넘버를 가진 li 앨리먼트를 리턴하고 그것에 active 클래스를 지정한다. $('#static-pagination li')의 페이지는 0부터 시작하는 반면에 페이지 숫자는 1부터 시작한다는 것에 주의해라. 아직 우리는 1 만큼 page를 감소시키지 않는다. 이는 pagination 블록에 있는 첫 li는 항상 "Previous page" 링크를 갖고 있기 때문이다. 그래서 우리는 그것을 건너 뛴다. 마지막으로 또한 우리는 URL에 있는 hash를 변경한다.

preparePagination 함수는 window에 붙는다는 것을 명심해라. 그래서 우리는 이 함수를 이 파일 내에서 뿐만아니라 우리의 *.js.erb 뷰에서도 잘 호출한다. CoffeeScript는 전체 스코프를 오염시키는 것을 방지하기 위해 self-invoing anonymous 함수로 각 파일 내에서 코드를 래핑한다(이는 실제로 좋은 것이다). 그럼에도 불구하고 이 경우에는 우리가 함수를 window에 붙이지 않으면, 함수는 외부에서 보이지 않을 것이다.

이제 우리는 실제 적용할 수 있다.

pagination.js.coffee

[...]

if $('#infinite-scrolling').size() > 0
    preparePagination($('.page-delimiter'))

[...]

if $('#with-button').size() > 0
    preparePagination($('.page-delimiter'))

[...]

index.js.erb 그리고 index_with_button.js.erb

var delimiter = $('<div class="page-delimiter" data-page="<%= params[:page] %>"></div>');
$('#my-posts').append(delimiter);
$('#my-posts').append('<%= j render @posts %>');
$('.pagination').replaceWith('<%= j will_paginate @posts %>');
preparePagination(delimiter);

[...]

마지막으로 중요한 것은 index.js.erb로부터 $(window).unbind('scroll'); 을 제거하는 것이다. 왜냐하면 Waypoints는 이 이벤트에 의지하며 우리는 항상 그것에 귀기울이고 있어야 하기 때문이다.

또한 사용자가 현재 페이지를 체크할 수 있도록 기본 pagination에 고정된 위치를 지정하고 싶을 것이다. 간단한 스타일을 적용해보자.

#static-pagination {
  position: fixed;
  top: 30px;
  opacity: 0.7;
  &:hover {
    opacity: 1;
  }
}

이제 pagination 블록은 항상 꼭대기에 디스플레이 될 것이고 약간 불투명할 것이다. 사용자가 이 앨리먼트에 마우스를 갖다대면 불투명도가 1로 셋팅될 것이다.

Conclustion

이제 이 문서의 끝에 도달했다. 나는 독자들이 이 문서를 읽으며 유용한 팁을 발견했기를 바란다. 제시된 솔루션은 이상적이지 않다. 그러나 어떻게 업무가 달성될 수 있는지 이해를 제공했을 것이다. 이전 포스트를 로딩하는 문제를 해결하는 방법과 함께 이 문서에 대한 당신의 생각을 당신의 웹사이트에 공유해주기 바란다.


출처 <a href=" http:="" infinite-scrolling-rails-practice="" www.sitepoint.com="">http://www.sitepoint.com/infinite-scrolling-rails-practice/

국가재난안전통신망 사업

제1사업(평창) : KT 컨소시엄(KT, 위니텍, 아이티센, 한국전파기지국)
응용시스템과 코어망 장비, 보안설비, 관제시스템을 포함한 운영센터 구축

제2사업(강릉,정선) : SK텔레콤 컨소시엄(SK텔레콤, 사이버텔브릿지, 설악이앤씨)

재난망 시범사업 개요(자료:안전처·업계종합)
<재난망 시범사업 개요(자료:안전처·업계종합)> 


2015년 10월 14일 수요일

재난망 사업의 성패, 자가망·상용망 활용 범위에 달렸다


[미디어잇 이진] 국민안전처가 진행 중인 국가재난안전통신망(이하 재난망) 사업이 본격 시작될 예정이지만, 시범사업 결과에 따라 정부 계획이 전면 재검토될 가능성도 배제할 수 없는 것으로 나타났다. 자가망 대신 통신사의 상용망을 이용하는 비율이 높아질 경우, 종전에 책정한 예산의 대폭 증액이 필요하기 때문이다.

정부, 재난망 제1·2사업자 선정 '막바지' 작업 중
정부는 지난 8일 재난망 시범사업으로 진행되는 제1·2사업 관련 우선협상대상자로 KT 컨소시엄과 SK텔레콤 컨소시엄을 각각 선정했다. 이들 컨소시엄은 정부와의 협상이 완료된 후 시범사업에 돌입한다.
분리 발주형인 재난망 사업은 강원도 평창 지역에서 추진되는 제1사업과 강원도 강릉, 정선 지역을 대상으로 하는 제2사업으로 구분된다. 제1사업은 운영센터 구축이 포함돼 있기 때문에 배정 예산이 337억9800만 원이며, 제2사업은 82억 1600만 원이 책정됐다.
국민안전처는 시범사업으로 총 7개월의 시간을 배정했으며 내년 4월 말경 사업이 종료된다. PS-LTE의 최종 표준은 릴리즈13인데, 표준을 관할하는 3GPP는 내년 3월경 주요 내용을 최종 확정할 예정이다.
시범사업자는 PS-LTE 관련 기술적 검증을 진행해야 하며, 주요 통신장비 업체의 솔루션과 단말기, 철도망과의 연동 등 다양한 테스트를 진행한다.
아직 표준화가 되지 않은 내용은 단말기간통신(D2D) 등은 대체할 수 있는 기술을 활용해 구현 가능 여부를 판단하며, 재난망의 핵심이라고 할 수 있는 통신망과 관련해서는 자가망과 상용망 비율을 결정해야 한다.
PS-LTE 표준 미비는 재난망 사업의 최대 걸림돌로 거론되고 있다. 일각에서는 재난망 사업 자체가 지나치게 부실한 것 아니냐는 지적도 있지만, 시범사업에서 검증하면 된다는 입장이 힘을 얻고 있다.

기지국 수 '과소평가' 한 것 아니냐?
논란의 핵심에는 ▲사업비(예산) ▲기지국 수량과 커버리지 문제 ▲자가망과 상용망 활용의 범위 ▲국제 표준 미비에 따른 단말 공급의 불안정성 등을 들 수 있다.
특히 논란의 중심에 선 것은 재난망 사업 관련 정보화전략계획(ISP)에서 기지국 수를 지나치게 적게 설계했다는 주장이다.
기지국 수는 총 사업비를 결정할 때 결정적 고려 요소다. 재난망 총 사업비는 구축비(단말기, 기지국, 주제어시스템, 용역비, 이용기관 지원비)와 운영비(기지국 임차, 유지보수비, 전기료, 인건비)로 구분된다.
만약 정부가 설계한 기지국 수가 시범사업 과정에서 지나치게 적다는 평가를 받는다면, 예산 문제를 고민하지 않을 수 없다. 예상보다 기지국을 더 많이 짓게 되면, 그만큼 자체 비용뿐 아니라 운영비(유지보수비, 회선임차료 등)의 증가를 가져와 결과적으로 사업비의 대폭 인상이 필요하다. 정부가 예산을 대폭 인상해야 하기 때문에 사업의 추진이 중단될 수 있다.
국민안전처는 재난망 관련 확산사업의 예산을 예비비로 편성, 전면 재검토의 가능성을 열어뒀다.
김사혁 정보통신정책연구원 부연구위원은 '재난안전통신망 구축 주요 논쟁이슈에 대한 소고' 자료를 통해 "기지국 수를 검증한 결과 기존 설계보다 2배 이상 증설이 필요거나 그 이상이라면, 납득할 만한 편익 수준에 도달하지 못하는 것"이라며 "PS-LTE 기반 재난망 사업은 당분간 비용상 큰 폭의 하락 요인이 발생하지 않는 한 사업을 중단해야 한다"고 밝혔다.

자가망과 상용망 비율 조정도 '숙제'
자가망과 상용망 활용 비율에 대한 논란도 큰 이슈 중 하나다.
정부는 주요 재난망을 자가망 기반으로 구축하되 상용망 시설을 일부 활용할 계획이다. 700MHz 대역 중 20MHz 폭을 재난망에 배정했으며, 음영지역 해소 등을 위해 상용망을 활용할 예정이다. 시범사업에서는 자가망과 상용망의 구체적 활용범위, 방법 등을 검증한다. 
다만, 시범사업 중 상용망에 대한 의존도가 지나치게 높아질 경우 사업의 전면 재검토가 불가피하다. 접속료 이슈 등 예산을 대폭 인상시킬 수 있는 요인 때문이다.
음영 구간에서의 사업자간 로밍도 논란이 여지가 있다. 예를 들어 A빌딩 지하에서 B통신사는 잘 터지지만 C통신사는 안된다고 가정할 대, B통신사가 C통신사의 망을 임차하는 대신 중계기 등을 자체 설비해야 한다. 양사간 로밍이 안되기 때문이다.
정부가 음영 지역에서 상용망을 활용한다고 해도, C통신사가 재난망 사업자일 경우 A빌딩 지하에서는 통신 불통을 겪을 수밖에 없다.
김사혁 부연구위원은 "통신3사는 인빌딩, 지하구간에서 사업자간 로밍을 하지 않으므로 법제도적 정비가 선행돼야 하는 문제가 있다"고 지적했다.
이진 기자 miffy@it.co.kr
 이진 (miffy@it.co.kr)
저작권 (c) 미디어잇

PTT(Push To Talk)

PTT(Push To Talk)는 휴대전화를 워키토키처럼 사용할 수 있는 일종의 무전기 서비스로 버튼 하나만 누르면 한 사람이 휴대전화에서 말하는 것을 여러 사람이 동시에 들을 수 있는 서비스이다. 통화를 원하는 사람을 미리 등록해놓고 버튼 한 개만 눌러 일대일 또는 그룹통화가 가능한 기능으로 요금이 저렴하고 통화가 간편하여 미국에서 큰 인기를 끌고 있다. 기존 휴대전화가 상대방이 수신이 가능한 지 알지 못한 채 전화를 걸어야 하지만 흔히 인스턴트메시징(IM)으로 지칭되는 PTTMSN메신저처럼 휴대전화 화면에 수신할 수 있는 전화번호를 보여줘 젊은 층을 중심으로 인기를 얻고 있는 것.

일반 휴대전화 통화에 비해 대기시간이 짧고 사용이 간편한 반면 음성망이 아닌 데이터 네트워크를 통해 서비스되기 때문에 상대적으로 요금이 싼 것도 특징이다. 미국의 넥스텔이 서비스를 개시한 이후 버라이존, 스프린트PCS, AT&T와이어리스, 싱귤러 등이 서비스를 하고 있으며, 삼성전자·노키아·교세라 등 주요 휴대전화 제조업체들이 이 기능이 내장된 휴대전화를 속속 내놓았다. 우리나라에서는 2006년 하반기 도입되었다. 그러나 주파수공용통신(TRS) 사업자들은 이동전화사업자들의 PTT 서비스가 TRS 서비스 내용과 중첩된다며 강력히 반발하고 있다.
출처  [네이버 지식백과] PTT (시사상식사전, 박문각)

D2D(device to device)

대표적인 예로는 블루투스가 있으며, 근거리에 있는 통신기기 간에 LTE로 통신을 할 수 있게 해주는 LTE D2D 기술도 급부상하고 있다. 통신을 위해 기기 간 활성화를 시키는 과정과 단말기 인증이 필요한 블루투스와는 달리, LTE D2D는 기지국 없이 75Mbps의 속도로 데이터를 주고받을 수 있다. 자동으로 연결되고 재난, 전쟁 등으로 기지국 가동이 멈춘 경우에도 통신을 유지할 수 있는 것이 장점이다.
출처 [네이버 지식백과] D2D [device to device] (시사상식사전, 박문각)

안드로이드 M 개발자 프리뷰 정리

M 개발자 프리뷰
SDK 툴
에뮬레이터
디바이스 이미지
개발문서
샘플코드 포함

developer.android.com/preview

올바른 버전의 SDK 툴을 미리 설치하지 않으면 특히 fast boot tool
시스템이미지를 올리는 도중에 오류가 발생할 수 있다.

개발자 프리뷰 SDK는
안드로이드 스튜디오 1.3 버전 이상에서 다운가능

테스트
developer.android.com/preview/behavior-changes.html

Doze Mode 테스트
$ adb shell dumpsys battery unplug
$ adb shell dumpsys deviceidle step
$ adb shell dumpsys deviceidle -h

App Standby Mode
$ adb shell dumpsys battery unplug
$ adb shell am set-idle <packageName> true
$ adb shell am set-idle <packageName> false
$ adb shell am get-idle <packageName>

지문인식 테스트
$ adb -e emu finger touch 1

What's Next?
안드로이드 M 개발자 프리뷰 (한글)
developer.android.com/preview

이슈 트래커
goo.gl/Blq1eJ

GDG Korea 커뮤니티 페이지
facebook.com/gdgkorea

2015년 10월 13일 화요일

레일스에서 무한 스크롤링 : 기본

Pagination은 매우 일반적이고 널리 사용되는 네비게이션 기술이다. 그리고 그만한 이유가 있다. 무엇보다도, 성능을 고려해보자. 하나의 쿼리에 모든 가능한 레코드를 로딩하는 것은 매우 큰 비용이 소모된다. 게다가 사용자는 가장 최근의 레코드들 중 몇개에만 흥미가 있을 수 있다. (즉 블로그에서 가장최근의 포스트들) 그리고 모든 레코드가 로딩되고 렌더링되는데까지 기다리길 원하지 않는다. 또한 Pagination은 컨텐츠로 페이지를 넘치게 하지 않음으로써 페이지를 읽기 쉽게 만든다.

근래에는 많은 웹사이트가 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.erbshow.html.erb) indexshow 메소드를 가진 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
end
will_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.erbshow.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/

2015년 10월 11일 일요일

Ruby on Rails 서비스 개발을 위한 Gem 정리

1. 기본적으로 사용되는 gem들
- devise (https://github.com/plataformatec/devise) : 로그인, 회원가입 기능 개발을 도와줍니다.

- omniauth (https://github.com/intridea/omniauth) : twitter, facebook, google, openid, oauth등의 널리 사용되는 서비스의 ID로 로그인을 가능하게 합니다.

- cancan ( https://github.com/ryanb/cancan ) : railscast를 하는 ryanb라는 분이 만드신 gem인데, 사용자 계정에 따른 페이지 접근권한 관리를 해 줍니다. 사용자 테이블이 user, admin으로 갈라져 있는 경우에도 응용해서 사용할 수 있도록 유연합니다.

- mongoid( https://github.com/mongoid/mongoid ) : MongoDB를 사용하신다면 mysql2 gem대신에 이 gem을 써야 합니다. 하나의 프로젝트에 mysql과 mongoid 둘 다 동시에 혼용해서 사용하실 수도 있습니다. 저희 회사에서는 일부 데이터의 저장에 mongodb를 사용하고 있습니다.

- kaminari( https://github.com/amatsuda/kaminari ) : 목록 데이터의 페이지네이션을 해줍니다. 예전에는 will_paginate를 사용했었는데, 최근 kaminari가 더 좋아보입니다.

- resque ( https://github.com/defunkt/resque ) : web page의 request에 의존하지 않는 배치작업 처리를 관리해 줍니다. 전에는 delayed_job이라는 것을 썼었는데, resque가 성능면에서도 더 좋은 것 같고, GUI webpage interface도 있어서 좋아보입니다.

- paperclip ( https://github.com/thoughtbot/paperclip ) : 이미지 파일 관리를 도와줍니다. 자동으로 원하는 사이즈로 resize해서 저장합니다.

- meta_search ( https://github.com/ernie/meta_search ) : 데이터 검색, 정렬등을 쉽게 만들수 있게 도와줍니다. 주로 관리툴의 데이터 핸들링을 개발할 때 사용합니다.

- web-app-theme ( https://github.com/pilu/web-app-theme/ ) : 관리도구의 GUI를 쉽게 만들 수 있게 도와줍니다. CSS, javascript, HTML template를 생성해 주며, 다양한 theme을 지원합니다. 그리고, scaffold로 생성한 view도 여기 theme에 맞는 형식으로 overwrite해줘서 생산성을 높여 줍니다.

- activo ( https://github.com/dmfrancisco/activo ) : 위에서 소개한 web-app-theme과 쌍으로 같이 쓰이는데, 장점은 동일한 CSS요소를 쓰면서 formtastic이라는 form GUI helper형식의 CSS도 함께 제공합니다. 그리고 주관적인 생각이지만 조금 더 이쁩니다.

- formtastic ( https://github.com/justinfrench/formtastic ) : web form ui를 작은 코드로 생성할 수 있게 도와줍니다. 저는 아직 학습이 미숙해서 admin ui에만 활용하고 있습니다. 잘 쓴다면 사용자페이지의 form에도 적용하면 좋을 것 같습니다.

2. 선택적으로 사용할 수 있는 gem들
- awesome_nested_set ( https://github.com/collectiveidea/awesome_nested_set ) : 트리구조의 데이터 생성을 도와줍니다. 예를 들어 카테고리와 같은 Tree 자료주조 형태가 필요하면 이 gem을 사용합니다.

- acts-as-taggable-on ( https://github.com/mbleigh/acts-as-taggable-on ) : 모든 model을 대상으로 tagging을 할 수 있게 해줍니다. tag cloud 도 생성해 주고요.

- vestal_versions ( https://github.com/laserlemon/vestal_versions ) : 위키와 같이 데이터 변화의 revision을 관리해 줍니다. 특정 model에 적용해 두면 그 모델의 모든 변화는 추적가능합니다. 즉, 모든 변화를 log형태로 기록을 남길 수 있습니다.

- nested_form ( https://github.com/ryanb/nested_form ) : has_many, has_one 형태의 model간의 relation이 있는 경우, 하나의 form에서 입력을 받아야 할 경우 사용합니다.

- acts_as_commentable ( https://github.com/jackdempsey/acts_as_commentable ) : 모든 model을 대상으로 comment를 달 수 있게 합니다. 확장팩인 댓글의 댓글이 가능하게 하는  acts_as_commentable_with_threading이라는 gem도 있는데, 저도 많이 써보지 않아서 추천할 수 있는지는 아직 의문입니다.

- geocoder ( https://github.com/alexreisner/geocoder ) : 위도, 경도, 주소에 따라 근처 object등을 query해 준다던지 등의 위도, 경도, 주소 관련 관리를 해 줍니다. 지역정보 서비스를 하는데 사용하고 있습니다.

- twitter ( http://twitter.rubyforge.org/ ) : 트위터 연동을 쉽게 해 줍니다.

- mini_fb ( https://github.com/appoxy/mini_fb ) : 페이스북 연동을 쉽게 해 줍니다.

- memcache-client ( http://rubygems.org/gems/memcache-client ) : Rails가 아니여도 널리 사용되고 있는 memory cache시스템입니다. 성능을 위해서 memcached 사용은 필수죠.

- apn_on_rails ( https://github.com/PRX/apn_on_rails ) : apple push notification을 쉽게 할 수 있게 도와줍니다.

3. 그 밖의 gem들
- rqrcode ( http://whomwah.github.com/rqrcode/ ) : QR코드의 생성을 도와줍니다.

- encryptor ( http://github.com/shuber/encryptor ) : 암호화가 필요한 경우.

- stringex ( https://github.com/rsl/stringex ) : 루비 스트링 클래스의 확장기능을 제공합니다. 저는 이 중에서  “경기도”.to_url => “gyounggido” 과 같이 한글 음성 발음 대로 영문 URL패턴으로 변경되는 기능을 사용합니다.


그 외로 Rails 3.1에 기본으로 포함된 sass, coffee-script, uglifier, jquery-rails 등이 있음.

2015년 10월 5일 월요일

Ruby on Rails image uploads with CarrierWave and Cloudinary

When we set to develop Cloudinary’s Rails integration Gem, it was obvious to us that we’ll base it on CarrierWave. Here’s why.
Photos are a major part of your website. Your eCommerce solution will have multiple snapshots uploaded for each product. Your users might want to upload their photo to be used as their personal profile photo. What’s entailed when developing such a photo management pipeline, end-to-end?

  • You’ll need an HTML file upload form.
  • The server will need to manage the reception and processing of the uploaded image files.
  • Uploaded images should be stored on a safe storage with access for multiple application servers.
  • Model entities should keep references to uploaded images.
  • Uploaded images will need to be resized and cropped into different dimensions matching the graphics design of your web site.
  • The server will need to find and deliver the resized images to visitors of your site when displaying a page with the relevant model entity (e.g., display a thumbnail of the profile picture in a user profile page, etc.).
  • Allow overriding uploaded images with new ones when needed.
Cloudinary allows you to overcome this complexity in its entirety, but how does it work?

Over the years, we’ve had the pleasure of using some of RoR’s many excellent file upload solutions: CarrierWavePaperclipDragonflyattachment_fu and others. All-in-all, CarrierWave often proved a better fit for our needs:
  • Simple Model entity integration. Adding a single string ‘image’ attribute for referencing the uploaded image.
  • "Magic" model methods for uploading and remotely fetching images.
  • HTML file upload integration using a standard file tag and another hidden tag for maintaining the already uploaded "cached" version.
  • Straight-forward interface for creating derived image versions with different dimensions and formats. Image processing tools are nicely hidden behind the scenes.
  • Model methods for getting the public URLs of the images and their resized versions for HTML embedding.
  • Many others - see CarrierWave documentation page.
What we liked most is the fact the CarrierWave is very modular. You can easily switch your storage engine between a local file system, Cloud-based AWS S3, and more. You can switch the image processing module between RMagickMiniMagick and other tools. You can also use local file system in your dev env and switch to S3 storage in the production system.
 
When we developed Cloudinary and decided to provide a Ruby GEM for simple Rails integration, it was obvious that we’ll want to build on CarrierWave. Our users can still enjoy all benefits of CarrierWave mentioned above, but also enjoy the additional benefits that Cloudinary provides:
  • The storage engine is Cloudinary. All images uploaded through CarrierWave model methods are directly uploaded and stored in the cloud. 
  • All resized versions and image transformations are done in the cloud by Cloudinary: 
    • No need to install any image processing tools or Ruby GEMs. 
    • You can create the resized versions eagerly while uploading or lazily when users accesses the actual images. Save processing time and storage.
    • Change your desired image versions at any time and Cloudinary will just create them on the fly, no need to batch update all your images when the graphics design of your site changes.
  • All public image URLs returned by CarrierWave are Cloudinary URLs. This means they are automatically delivered through a global CDN with smart caching. Seamlessly enhancing the performance of your web application.
Some code samples:

class PictureUploader < CarrierWave::Uploader::Base

  include Cloudinary::CarrierWave
  
  version :standard do
    process :resize_to_fill => [100, 150, :north]
  end
  
  version :thumbnail do
    process :resize_to_fit => [50, 50]
  end     
    
end
class Post < ActiveRecord::Base
  ...
  mount_uploader :picture, PictureUploader
  ...
end
= form_for(:post) do |post_form|
  = post_form.hidden_field(:picture_cache)
  = post_form.file_field(:picture)
= image_tag(post.picture_url, :alt => post.short_name)

= image_tag(post.picture_url(:thumbnail), :width => 50, :height => 50)
We believe that for Ruby on Rails developers, the combination of Cloudinary with its CarrierWave-based gem, delivers a complete image management solution, with excellent model binding.
More details about about our CarrierWave plugin are available in our documentation:http://cloudinary.com/documentation/rails_integration#carrierwave_upload
What do you think about our solution? any suggestions or improvement ideas?
UPDATE: We've published another post about additional advanced image transformations in the cloud with CarrierWave & Cloudinary.