Shim Won

December 31, 2014 1:22 pm

번역 빨리 알았으면 좋았을 뻔한 레일스 기술

시작하며

저는 rails를 만지기 시작한게 버전 1에서 버전2로 바뀔때였습니다만, 버전2가 나온 해를 돌아보니 무려 2007년 이었습니다. 새월이 참 빨리 흘러 놀라던 중에, 빨리 았았으면 좋았을뻔 했네 싶은 걸 곰곰히 생각해 보았습니다. “최근 rails 만지기 시작했어요!”라는 분에게 참고가 되었으면 합니다. 이번엔 편리한 gem 같은게 아니라 그냥 rails로도 할 수 있는 것을 정리했습니다. 참고로 버전은 이하의 환경입니다.

About your application's environment
Ruby version              2.1.3-p242 (x86_64-darwin14.0)
RubyGems version          2.2.2
Rack version              1.5
Rails version             4.1.8

app밑에 디랙토리를 추가해도 돼요.

rails new했을 때의 app밑의 디랙토리 구성은 아래와 같습니다.

- app/
 |+ assets/
 |+ controllers/
 |+ helpers/
 |+ mailers/
 |+ models/
 |+ views/

처음엔 이대로 만들지 않으면 안됀다고 생각했으나, 처음부터 준비된 틀에 맞지 않는 폴더는 추가해도 좋습니다.

예를들어 자작 벨리데이터를 정리해두는 validators라던가, 백그라운드에서 움직이는 워커가 들어있는 workers라던가가 있겠네요.

- app/                         
 |+ assets/
 |+ controllers/
 |+ decorators/
 |+ helpers/
 |+ jobs/
 |+ mailers/
 |+ models/
 |+ queries/
 |+ uploaders/
 |+ validators/
 |+ views/
 |+ workers/

아니면, app에 넣을 물건이 아닌데 싶은 범용적인 유틸리티는 lib에 넣는 방법도 있습니다. 좀 신경써야 할것이 로드패스. app 바로 밑의 디랙토리는 알아서 부르지만, 예를들어 app/workers/concerns처럼 디랙토리를 더 만들면 거기까지 읽어주지는 않습니다. 그러므로 config/application.rb으로 로드하도록 설정합시다. 밑에 예에서는 식을 전개하기 때문에 %w가아니라, %W를 사용하는 것에 주목하세요. 깊은 계층까지 한꺼번에 추가할 경우 “lib이하의 로드패스를 추가”에서 쓰는 방법을 쓰세요.

require File.expand_path('../boot', __FILE__)
require 'rails/all'
Bundler.require(*Rails.groups)
module SampleApp
  class Application < Rails::Application
    # app 以下の独自ディレクトリも読み込む
    config.autoload_paths += %W(#{config.root}/app/jobs/concerns #{config.root}/app/workers/concerns)
    # lib 以下もロードパスに追加
    config.autoload_paths += Dir["#{config.root}/lib/**/"]
    # 中略
  end
end

중첩된 리소스의 routes에 shallow를 사용해보세요.

여기서는 설명을 위해 샘플 어플을 만듭니다. User가 여러 Note를 가지고 있는 어플이 있다고 합시다

./bin/rails g scaffold user name:string
./bin/rails g scaffold note user:references title:string body:text

이제, 리소스가 중첩되므로 config/routes.rb에 그렇게 적습니다.

Rails.application.routes.draw do
  resources :users do
    resources :notes
  end
end

이렇게 적으면 라우팅은 아래처럼 됩니다.

        Prefix Verb   URI Pattern                              Controller#Action
    user_notes GET    /users/:user_id/notes(.:format)          notes#index
               POST   /users/:user_id/notes(.:format)          notes#create
 new_user_note GET    /users/:user_id/notes/new(.:format)      notes#new
edit_user_note GET    /users/:user_id/notes/:id/edit(.:format) notes#edit
     user_note GET    /users/:user_id/notes/:id(.:format)      notes#show
               PATCH  /users/:user_id/notes/:id(.:format)      notes#update
               PUT    /users/:user_id/notes/:id(.:format)      notes#update
               DELETE /users/:user_id/notes/:id(.:format)      notes#destroy
         users GET    /users(.:format)                         users#index
               POST   /users(.:format)                         users#create
      new_user GET    /users/new(.:format)                     users#new
     edit_user GET    /users/:id/edit(.:format)                users#edit
          user GET    /users/:id(.:format)                     users#show
               PATCH  /users/:id(.:format)                     users#update
               PUT    /users/:id(.:format)                     users#update
               DELETE /users/:id(.:format)                     users#destroy

이것으로 라우팅이 중첩된 리소스에 부합하는 모양이 되었습니다. 단지, 특정 노트를 참조하려 할때, 노트id만 알면 대상을 한번에 알수 있어야 하지만, 이 경우 유저id가 필요하게 되었습니다. 그래서 shallow: true를 추가하면…

Rails.application.routes.draw do
  resources :users, shallow: true do
    resources :notes
  end
end
       Prefix Verb   URI Pattern                         Controller#Action
   user_notes GET    /users/:user_id/notes(.:format)     notes#index
              POST   /users/:user_id/notes(.:format)     notes#create
new_user_note GET    /users/:user_id/notes/new(.:format) notes#new
    edit_note GET    /notes/:id/edit(.:format)           notes#edit
         note GET    /notes/:id(.:format)                notes#show
              PATCH  /notes/:id(.:format)                notes#update
              PUT    /notes/:id(.:format)                notes#update
              DELETE /notes/:id(.:format)                notes#destroy
        users GET    /users(.:format)                    users#index
              POST   /users(.:format)                    users#create
     new_user GET    /users/new(.:format)                users#new
    edit_user GET    /users/:id/edit(.:format)           users#edit
         user GET    /users/:id(.:format)                users#show
              PATCH  /users/:id(.:format)                users#update
              PUT    /users/:id(.:format)                users#update
              DELETE /users/:id(.:format)                users#destroy

이것으로 notes#show 할때 이중이었던 유저id가 필요없게 되었습니다. 조금 주의해야하는게 form_for에서 넘기는 패스입니다. 예를들어 notes#create할 때는 /users/:user_id/notes고, notes#update할 때는 /notes/:id로 유저id의 유무가 다릅니다. 이경우 form_for(@note) do만으로는 notes#new할 때 undefined method notes_path라는 에러가 납니다. (shallow 옵션을 사용하지 않는 경우에는 form_for([@user, @note]) 라고 쓰면 됩니다.) 여기서 form_for에 이렇게 설정합니다.

form_for(@note, url: polymorphic_path([@user, @note])) do |f|

덧붙여 컨트롤러의 new, edit도 이런 느낌입니다.

# GET /notes/new
def new
  @user = User.find params[:user_id]
  @note = @user.notes.build
end
# GET /notes/1/edit
def edit
end

이렇게 함으로써 새로만들때는 action="/users/1/notes”, 편집할 때는 action="/notes/1"로 잘 지정할 수 있습니다.

nil일지도 모를때는 ||, &&, presense, try가 편리해요.

예를들어, User 클래스에 name 라는 변수가 있을때, namenil 이면 값이 설정하고 싶을 때는,

user.name = 'anonymous' unless user.name

이렇게 하지 않고

user.name ||= 'anonymous'

이렇게 적으면 됩니다. 루비에서는 이것을 “nil가드”라고 부릅니다. 알기 쉽게 식을 전개하면 아래와 같은 의미가 됩니다.

user.name = (user.name || 'anonymous')

물론 쓰이는 곳은 nil가드뿐은 아닙니다. User클래스에 nickname도 있다고 하고

닉네임이 설정되어있으면 닉네임을, 설정되어있지않으면 (nil이면) 이름을 표시하고 싶을 때는 이렇게하면 됩니다.

user.nickname || user.name

비슷한 사용법으로 presence 를 사용하는 경우가 있습니다. presencepresent? 메소드가 참이면 self를 거짓이면 nil을 반환하는 메소드입니다. present?nil, false, 빈 배열, 빈 해쉬, 빈 문자열, 특정 문자열만을 false 판정해주는 메소드입니다.^1 여기서 특정 문자열은 정규표현 /\A[[:space:]]*\z/를 만족하는 것입니다.

user.name에서 nil 뿐만아니라 공백 문자열도 판정하고싶은 경우엔

user.name.present? ? user.name : 'anonymous'

presence를 사용하면

user.name.presence || 'anonymous'

라고 적을 수 있습니다. 다음으로, 약간 예를 바꿔서 UserNotehas_many로 가지고 있다고 합시다. 이런 느낌입니다.

class User < ActiveRecord::Base
  has_many :notes
end
class Note < ActiveRecord::Base
  belongs_to :user
end

여기서, Note에서 유저명을 표시해야하지만, 유저가 존재하는지 확실치 않을때

note.user.name if note.user

라던가

note.user && note.user.name

처럼 적을 수 있지만, 이런 때는 try를 쓰면 됩니다.

try는 인수로 메소드명을 넘겨 실행하며, 단 대상이 nil일 경우 실행하지 않고 nil을 돌려주는 것입니다. 구현은 간단합니다. ObjectNilClasstry가 있습니다.

class Object
  def try(*a, &b)
    if a.empty? && block_given?
      yield self
    else
      public_send(*a, &b) if respond_to?(a.first)
    end
  end
end
class NilClass
  def try(*args)
    nil
  end
end

try라면 메소드명을 잘못 입력하거나 하면, nil를 반환하지만, 존재하지 않는 메소드의 경우는 NoMethodError를 반환하는 try!도 있습니다.

delegate도 사용해보세요.

위의 예제에서는 note.user.name로 메소드 호출을 하고 있지만, 객체지향 프로그래밍에선 “테메트르의 법칙”에 반한다고 말해지고 있습니다. 그러면, 어떻게 해야할까요?

class Note < ActiveRecord::Base
  belongs_to :user
  def user_name
    user.try :name
  end
end

노트에서 유저이름을 뽑는 메소드를 정의해도 되지만, 수가 많아지면 힘들어지죠. 그럴때에는 delegate를 쓸 수 있습니다.

class Note < ActiveRecord::Base
  belongs_to :user
  delegate :name, to: :user, prefix: :author, allow_nil: true
end

이렇게 하면 note.author_name을 부를 수 있게 됩니다. delegate의 사용법은 위임할 메소드명, 위임할 곳(to) 말고도 메소드의 전치사(prefix), 그리고 nil을 허가할까 말까(allow_nil)를 옵션으로 넘길수 있습니다. prefix: false이라면 note.name라고 불려지게 되고 prefix: true라면 note.user_name가 됩니다. allow_nil: true라고 해두면 note.usernil일때 에러가 되므로 편리합니다. 또, 위임할 곳의 관련없는 정수, 클래스 변수, 인스턴스 변수라도 다단으로 delegate할 수도 있습니다.^2

class Foo
  CONSTANT
  @@class_val
  def initialize
    @instance_val
  end
  delegate :foo, to: :CONSTANT
  delegate :bar, to: :@@class_val
  delegate :baz, to: :@instance_val
end

참고로 보통 루비에도 위임을 위한 Forwardable 모듈이 이용되고 있고, 사용법도 비슷합니다. Forwardable은 메소드를 리네임할 수 있는 것이 장점이지만, ActiveSupportdelegate에 있는 prefixallow_nil이 쓰기 편해 rails에서는 ActiveSupportdelegate를 좀더 많이 사용합니다.

클래스 매크로를 직접 만들어 보세요

클래스 메크로는 예를 들면 attr_accessor같은 겁니다.

class User
  attr_accessor :name
end

이것은 준비되어있는 것을 쓰는것 뿐만아니라, 직접 만들 수도 있습니다. “메크로는 어려운 것“이 아니라, 그 실체는 그냥 클래스 메소드이므로 무섭지 않습니다.

class User
  attr_accessor :name
  def self.suffix attr, value
    define_method("#{attr}_with_suffix") {
      name = instance_variable_get "@#{attr}"
      "#{name} #{value}" if name.present?
    }
    alias_method_chain attr, :suffix
  end
end
class Boy < User
  suffix :name, 'くん'
end
class Girl < User
  suffix :name, 'さん'
end

덤으로 alias_method_chain의 이야기도 같이합시다. 클래스 메크로는 def self.suffix attr, value의 부분에, 이 정의에 의해 계승한 클래스에서 클래스 메크로를 사용할 수 있게 되었습니다.

> girl = Girl.new
=> #<Girl:0x007fb9314610f8>
> girl.name = 'はなこ'
=> "はなこ"
> girl.name_with_suffix
=> "はなこ さん"
> girl.name
=> "はなこ さん"
> girl.name_without_suffix
=> "はなこ"

실행해본 결과는 위에 나온 대로입니다. >가 입력 =>가 출력입니다. 클래스 메크로에 의해, 사용 시에 "#{attr}_with_suffix"라는 메소드가 작성되게 되어있습니다. 즉, Boy, Girl 클래스에서 suffix :name, 'さん'라고 사용함으로써, name_with_suffix 메소드가 메크로에 의해 정의되는것입니다. 더욱더 메크로에 의해 정의된 메소드의 내용은 “이름이 설정되어있으면, 뒤에 메크로에서 지정한 문자열을 추가한다”라는 내용이었으므로 girl.name_with_suffix에서는 “はなこ さん”이 반환된 것입니다. 이 예에서 실은 girl.name라고 불려지는 것만으로 girl.name_with_suffix과 같은 효과를 내는것도 가능 합니다. alias_method_chain으로, 원래 name 메소드가 name_without_suffix로 바뀌고, name_with_suffix라는 정의의 메소드가 name 으로 바뀌어 불리기 떄문입니다.^3

클래스 메크로의 정의 부분은 이번처럼 부모클래스나, ActiveSupport::Concern으로 모듈화 하는등 여러가지 방법이 있습니다.

super do 로 동작을 추가하세요.

밑에 보이는 것처럼 클래스를 만들어 보았습니다.

class User
  attr_accessor :age
  def initialize
    @age = 0
  end
  def birthday
    puts 'happy birthday!'
    @age += 1
  end
end

User#birthday 메소드는 “happy birthday!”라는 메세지를 표시하고, 나이에 1더한 수를 반환하는 메소드입니다.

class Boy < User
  def birthday
    super
    puts 'some process...'
  end
end

User클래스를 계승한 Boy에 의해 birthday 메소드가 오버라이드하려고 할때, super로 계승하려는 곳의 메소드를 부르는 것이 가능합니다. 하지만, “메세지의 표시”와 “나이의 증가”의 사이에 처리를 넣고싶은 경우는 어떻게 해야 할까요. 그럴 때에는 super를 사용하지 않고 오버라이드 해버리면, 메세지 정의가 중복되어 유지보수성이 떨어집니다. 그런 때에는 부모 클래스에 처리를 끼워놓을 곳을 만들어 두면 됩니다.

class User
  attr_accessor :age
  def initialize
    @age = 0
  end
  def birthday
    puts 'happy birthday!'
    yield if block_given?
    @age += 1
  end
end
class Boy < User
  def birthday
    super do
      puts 'some process...'
    end
  end
end

User클래스에 yield if block_given?를 준비했으므로, 자식 클래스에서 birthday 메소드를 오버라이드 했을때 블록으로 super를 불렀을 때, 처리를 추가하는것이 가능해집니다.^4

로그인 처리에 잘 사용되는 plataformatec/devise 에서는 예를 들면, Devise::SessionsController 등으로 “로그인 했을때 유저에게 뭔가 처리하게 하고 싶을 때”를 위해 처리사이에 끼워넣을 포인트를 준비해두고 있습니다.

class Devise::SessionsController < DeviseController
  # POST /resource/sign_in
  def create
    self.resource = warden.authenticate!(auth_options)
    set_flash_message(:notice, :signed_in) if is_flashing_format?
    sign_in(resource_name, resource)
    yield resource if block_given?
    respond_with resource, location: after_sign_in_path_for(resource)
  end
end

active model은 편리한 녀석

rails 어플을 만들고 있으면, DB에 저장하지는 않지만, ActiveRecord처럼 쓸수 있는 편리한 모델은 필요한데 싶을 때가 있습니다. 예를 들면 검색용 폼같은게 있습니다. view에서 form_for도 쓰고싶고, validate 도 똑같이 정의하고 싶으니까요.

그럴때가 ActiveModel 을 쓸 때입니다.

class UserSearchForm
  include ActiveModel::Model
  attr_accessor :name, :age
  validates :name, presence: true
end

사용법도 굉장히 단순해 include ActiveModel::Model할 뿐입니다. 이걸로 validates등이 쓸수 있게 됩니다.

끝으로

편리해보이는 Rails Tips를 정리해 보았습니다. 누군가에게 도움이 되었으면 합니다. 다른것도 생각나면 추가해 갈 생각이고, 이런것도 빨리 알았으면 좋았다 생각되는게 있으면 알려주세요.

1: 아마도 오기일것같아서 문맥 상 맞게 고쳐번역했습니다. 원문은 present? は nil, false, 空の配列, 空のハッシュ, 空の文字列, 特定文字列のみ文字列を判定してくれるメソッドです。 2: 하지만 다단으로 딜리게이트할 상황이 있다는 것 자체가 나쁜 설계라는 이야기입니다. 가능하면 설계를 고치세요. 3: 메크로를 잘쓰면 좋다는건 동의하지만, alias_method_chain같은건 디버그 테스트가 골치아파서 저는 왠만하면 사용하지 않는 편이에요.
4: yield의 용법을 보여주고 싶었던 것 같은데.. 사실 이 예제는 이렇게 처리하는게 의존성 흐름도 읽기 편하고 더 간단합니다.