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
라는 변수가 있을때, name
이 nil
이면 값이 설정하고 싶을 때는,
user.name = 'anonymous' unless user.name
이렇게 하지 않고
user.name ||= 'anonymous'
이렇게 적으면 됩니다. 루비에서는 이것을 “nil가드”라고 부릅니다. 알기 쉽게 식을 전개하면 아래와 같은 의미가 됩니다.
user.name = (user.name || 'anonymous')
물론 쓰이는 곳은 nil가드뿐은 아닙니다.
User
클래스에 nickname
도 있다고 하고
닉네임이 설정되어있으면 닉네임을, 설정되어있지않으면 (nil이면) 이름을 표시하고 싶을 때는 이렇게하면 됩니다.
user.nickname || user.name
비슷한 사용법으로 presence
를 사용하는 경우가 있습니다.
presence
는 present?
메소드가 참이면 self를 거짓이면 nil
을 반환하는 메소드입니다.
present?
는 nil
, false
, 빈 배열, 빈 해쉬, 빈 문자열, 특정 문자열만을 false 판정해주는 메소드입니다.^1
여기서 특정 문자열은 정규표현 /\A[[:space:]]*\z/
를 만족하는 것입니다.
user.name
에서 nil
뿐만아니라 공백 문자열도 판정하고싶은 경우엔
user.name.present? ? user.name : 'anonymous'
presence
를 사용하면
user.name.presence || 'anonymous'
라고 적을 수 있습니다.
다음으로, 약간 예를 바꿔서 User
는 Note
를 has_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
을 돌려주는 것입니다.
구현은 간단합니다. Object
와 NilClass
에 try
가 있습니다.
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.user
가 nil
일때 에러가 되므로 편리합니다.
또, 위임할 곳의 관련없는 정수, 클래스 변수, 인스턴스 변수라도 다단으로 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
은 메소드를 리네임할 수 있는 것이 장점이지만,
ActiveSupport
의 delegate
에 있는 prefix
나 allow_nil
이 쓰기 편해 rails에서는 ActiveSupport
의 delegate
를 좀더 많이 사용합니다.
클래스 매크로를 직접 만들어 보세요
클래스 메크로는 예를 들면 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의 용법을 보여주고 싶었던 것 같은데.. 사실 이 예제는 이렇게 처리하는게 의존성 흐름도 읽기 편하고 더 간단합니다.