By Ryan Cook June 23rd, 2011 레일스3가 릴리스된 이후 개발자들은 루비젬으로 패키징할 수 있는 새롭고 깔끔한 방식으로 레일스 엔진을 작성해 왔습니다. 레일스 엔진이란 패키지 형태의 하나의 어플리케이으로 다른 어플리케이션 안에서 실행되거나 마운트될 수 있습니다. 하나의 엔진은 자체 모델, 뷰, 컨트롤러, 제너레이터, 그리고 웹으로 공개서비스되는 정적 페이지 파일을 가질 수 있습니다.
하나의 엔진을 만들면 반복해서 사용할 수 있기 때문에 많은 양의 코드 작성을 좋아하지 않을 경우 이것은 대단한 소식이 아닐 수 없습니다. 소규모 사업을 위한 웹사이트를 많이 만들어야 하는 경우를 생각해 봅시다. 일반적으로 필요로하는 것은 특정 회사의 모든 직원들에 대한 목록을 보여주고 각 직원에 대한 기본 정보를 보여주는 웹페이지입니다. 이러한 기능은 거의 변경되지 않아서 공통적으로 필요로 하는 항목군으로 모아 볼 수 있어서 레일스 엔진 젬으로 개발하기에 딱 좋은 개발 대상이 됩니다.
본 게시물에서, 우리는 직원들에 대한 목록을 보여주는 데이터베이스 연동 팀 페이지를 만들기 위해서 사용하게 될 레일스 엔진을 만드는 과정을 살펴 볼 것입니다.
[노트 : 프로 웹디자이너와 개발자들이 반드시 가져야 할 것: “Printed Smashing Books Bundle”에는 일상 코딩 작업에서 실질적인 내용들이 가득합니다. 지금 당장 이 bundle을 구해 보시기 바랍니다!]
Enginex 엔진X
레일스의 핵심 인물인 Jose Valim은 Enginex라고 하는 툴을 개발했는데, 이것은 레일스3 호환 엔진젬을 만들 수 있는 골격을 만들어 줍니다. 이 툴은 엔진젬 개발자들이 직면하게 되는 많은 예기치 않았던 문제점들을 해결해 줍니다. 이 툴은 테스트용 레일스 어플리케이션을 포함해서 시작점을 위한 기본적인 설정을 대신해 줍니다.
엔진젬을 만들기 위해서 먼저, 일반적인 프로젝트 디렉토리로 이동하여 다음의 명령행을 실행합니다:
$ gem install enginex
$ enginex team_page
이렇게 해서 표준 엔진X 골격을 가지는 team_page 디렉토리에 하나의 프로젝트를 가지게 될 것입니다.
Set-Up 설치
젬을 셋업하기 위해서 우리는 몇가지 파일을 변경할 것입니다. 먼저, team_age.gemsepc과 Gemfile 파일 두가지는 약간만 변경하면 됩니다.
# CURRENT FILE :: team_page.gemspec
require File.expand_path("../lib/team_page/version", __FILE__)
# Provide a simple gemspec so that you can easily use your
# Enginex project in your Rails apps through Git.
Gem::Specification.new do |s|F
s.name = "team_page"
s.version = TeamPage::VERSION
s.platform = Gem::Platform::RUBY
s.authors = [ "Your Name" ]
s.email = [ "your@email.com" ]
s.homepage = "http://yourwebsite.com"
s.description = "A simple Rails 3 engine gem that adds a team page to any Rails 3 application."
s.summary = "team_page-#{s.version}"
s.rubyforge_project = "team_page"
s.required_rubygems_version = "> 1.3.6"
s.add_dependency "activesupport" , "~> 3.0.7"
s.add_dependency "rails" , "~> 3.0.7"
s.files = `git ls-files`.split("\n")
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
s.require_path = ‘lib’
end
# CURRENT FILE :: Gemfile
source "http://rubygems.org"
# Specify any dependencies in the gemspec
gemspec
이것은 gemspec 파일의 일부 설정을 변경해서 Git로 커밋한 파일과 나중에 추가하게 될 실행파일들을 자동으로 사용할 수 있게 해 주고 젬에 명시하게 될 VERSION 상수를 사용할 수 있게 해 줍니다.
또한, bundle install을 실행하게 되면, Gemfile에서 gemspec을 호출하여, team_page.gemspec으로부터 모든 연관 젬들을 로드하게 될 것이며 이것은 규칙입니다.
다음으로는, 젬을 레일스 엔진으로 변환하기 위해 세개의 파일을 추가하거나 변경하게 됩니다. 우선, 최상위 팀 페이지 파일은 다음과 같습니다:
# CURRENT FILE :: lib/team_page.rb
# Requires
require "active_support/dependencies"
module TeamPage
# Our host application root path
# We set this when the engine is initialized
mattr_accessor :app_root
# Yield self on setup for nice config blocks
def self.setup
yield
self
end
end
# Require our engine
require "team_page/engine"
mattr 함수를 사용하기 위해는 ActiveSupport가 필요합니다. 또한 self.setup메소드를 이용해서 젬을 설정할 수 있는 멋진 방법이 있습니다. 엔진이 파일의 마지막에서 필요하게 되는데 모든 관련된 젬들이 이전에 명시되어야 함을 확인해야 합니다.
두번째로, 버전 파일을 다음과 같습니다:
# CURRENT FILE :: lib/team_page/version.rb
module TeamPage
VERSION = "0.0.1
end
마지막으로, 엔진 파일은 다음과 같습니다:
# CURRENT FILE :: lib/team_page/engine.rb
module TeamPage
class Engine < Rails::Engine
initialize "team_page.load_app_instance_data" do |app|
TeamPage.setup do |config|
config.app_root = app.root
end
end
initialize "team_page.load_static_assets" do |app|
app.middleware.use ::ActionDispatch::Static, "#{root}/public"
end
end
end
이것은 두개의 레일스initialize 블럭을 정의하게 되는 이것은 호스트 레일스 어플리케이션의 루트 디렉토리로 연결하는 실마리를 제공해 줄 뿐만 아니라 젬의 루트public 디렉토리에 있는 파일을 제공하게 해 줍니다.
Data Model
젬에서 하나의 모델을 추가하기 위해서는 먼저 하나의 마이그레이션과 하나의 제너레이터 클래스를 호스트 레일스 어플리케이션으로 복사하도록 명시해 두어야 합니다. 이러한 과정은 레일스 3.1에서 훨씬 더 분명하게 처리될 것이지만, 현재로서는 몇가지 제너레이터 클래스를 만들어 줄 필요가 있습니다. 이러한 과정에 대한 훌륭한 리소스를 Nepal on Rails에서 찾을 수 있습니다.
먼저, 제너레이터 클래스를 추가해 보겠습니다:
# CURRENT FILE :: lib/generators/team_page/team_page_generator.rb
# Requires
require ‘rails/generators’
require ‘rails/generators/migration’
class TeamPageGenerator < Rails::Generators::Base
include Rails::Generators::Migration
def self.source_root
@source_root ||= File.join(File.dirname(__FILE__), ‘templates’)
end
def self.next_migration_number(dirname)
if ActiveRecord::Base.timestamped_migrations
Time.new.utc.strftime("%Y%m%d%H%M%S")
else
"%.3d" % (current_migration_number(dirname) + 1)
end
end
def create_migration_file
migration_template ‘migration.rb’, ‘db/migrate/create_team_members_table.rb’
end
end
이 제너레이터를 추개해 줌으로써 레일스 어플리케이션내에서 rails g team_page 명령행을 실행해서 필요로 하는 마이그레이션 파일을 생성토록하여 team page가 작동하도록 해 줄 것입니다.
다음으로, 샘플 마이그레이션 파일을 만들게 될 것입니다:
# CURRENT FILE :: lib/generators/team_page/templates/migration.rb
class CreateTeamMembers < ActiveRecord::Migration
def self.up
create_table :team_members do |t|
t.string :name
t.string :twitter_url
t.string :bio
t.string :image_url
t.timestamps
end
end
def self.down
drop_table :team_members
end
end
마지막으로, 샘플 모델을 만들어서 team page 젬으로 명칭을 구분할 수 있게 합니다.
# CURRENT FILE :: app/models/team_page/team_member.rb
module TeamPage
class TeamMember < ActiveRecord::Base
attr_accessible :name , :twitter_url , :bio , :image_url
end
end
지금까지 작업한 내용은 레일스 3 엔진젬을 시작하기 위한 단계를 따라해 보는 것이었습니다. 엔진으로 구성작업이 완료했고, 자체 마이그레이션 제너레이터를 만들었으며, 액티브레코드 모델도 만들었습니다.
이제, 젬이 호스트 레일스 어플리케이션이 사용하게 될 라우터, 컨트롤러, 뷰를 가지도록 셋업해 보겠습니다.
The Route 라우트
레일스 엔진젬은, 적절하게 세팅이 되면, 호스트 프로젝트의 config 와 app 디렉토리를 자동으로 로드합니다. 이것은 전체 레일스 어플리케이션과 똑같은 구조에서 코드를 작성할 수 있기 때문입니다.
따라서, 라우트를 설정하기 위해서 프로젝트의 config 디렉토리에 routes.rb 파일을 생성합니다. team
라우트를 생성하기 위해 다음과 같이 합니다:
# CURRENT FILE :: config/routes.rb
Rails.application.routes.draw do
get "team" => "team_page/team#index" , :as => :team_page
end
여기서 작업한 내용을 검토해서 봄으로써 우리는 약간의 고통을 필할 수 있습니다. 먼저, 호스트 레일스 어플리케이션으로 요청되는/team 라우트를 제대로 해당 컨트롤러와 액션으로 연결할 수 있게 될 것입니다.
두번째로, 레일스에게, 엔진에서 만들게 될 네임스페이스가 정해진 컨트롤러의index 라우트로 요청을 보내도록 지시를 해 놓았습니다.
마지막으로, 라우트에 이름을 붙여 놓아 어플리케이션의 어디에서든 링크 헬퍼 메소드를 이용할 수 있게 되었습니다.
The Controller
컨트롤러는, 우리가 익숙해 있듯이,app 디랙토리에 위치합니다. 한가지 문제는, 모델에서 했듯이, 이 컨트롤러들을 team_page 디렉토리로 옮겨야 한다는 것입니다. 이것은 단순히 페이지상에 보여줄 모든 팀 멤버를 로드하게 될 것입니다.
# CURRENT FILE :: app/controllers/team_page/team_controller.rb
module TeamPage
class TeamController < ::ApplicationController
def index
@team_members = TeamMember.all
end
end
end
알 수 있듯이, 호스트 레일스 어플리케이션에 있는 최상위 ::ApplicationController를 상속받았다는 것입니다.
The View 뷰
마무리하기 위해서, 뷰를 렌더링해야 할 필요가 있습니다. 컨트롤러에 별도로 사용하게 될 레이아웃을 명시하지 않았기 때문에, 디폴트로, 호스트 레일스 어플리케이션의 메인 어플리케이션 레이아웃을 이용하게 될 것입니다.
모델과 컨트롤러에서서 했듯이, team_page 디렉토리 상에 뷰가 위치하게 될 것입니다. 외부 의존을 최소한으로 줄이기를 원하기 때문에 우리는 여기서 HAML 대신에 ERB 형태로 뷰를 작성하게 될 것입니다.
<!– CURRENT FILE :: app/views/team_page/index.html.erb –>
<ul>
<% @team_members.each do |team_member| %>
<li>
<span>
<%= link_to @team_member.name , @team_member.twitter_url %>
</span>
<%= @team_member.bio %>
<%= image_tag @team_member.image_url , :class => "team-member-image" %>
</li>
<% end %>
</ul>
Getting Started With Tests 테스트 검증하기
분명한 것은, 작성한 젬에 대한 유닛 또는 전체 테스트를 아직 작성하지 않았습니다는 것입니다. 본 게시물의 내용을 완성하게 될면 레일스3 엔진젬을 잘 이해하게 될 것입니다. enginex 툴은 기본 레일스 어플리케이션 구조와 함께 자동으로test 디렉토리도 생성해 줍니다.
test_helper.rb 파일이 문제가 없다는 것을 확인한 상태에서 시작해 보겠습니다.
# CURRENT FILE :: test/test_helper.rb
# Configure Rails Environment
ENV["RAILS_ENV"] = "test"
ENV["RAILS_ROOT"] = File.expand_path("../dummy", __FILE__)
require File.expand_path("../dummy/config/environment.rb", __FILE__)
require "rails/test_help"
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.default_url_options[:host] = "test.com"
Rails.backtrace_cleaner.remove_silencers!
# Run any available migration
ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__)
# Load support files
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
한가지 주목해야 할 것은, 테스트 셋업 헬퍼 메소드에서 드문상황인데, 테스트 헬퍼 메소드에서 어디에도 로컬 젬 코드를 필요로 하지 않는다는 것입니다. Bundler를 이용할 것이기 때문에, 젬은 실제로 test/dummy/config/application.rb 에서 Gemfile을 통해서 더미 레일스 어플리케이션에서 필요로 합니다. 그 외에도 레일스 환경을 셋업하고 어플리케이션을 부팅하고 모든 마이그레이션을 실행하게 될 것입니다. 아래에서 샘플 유닛 테스트 코드를 볼 수 있습니다.
# CURRENT FILE :: test/team_page_test.rb
require ‘test_helper’
class TeamPageTest < ActiveSupport::TestCase
test "truth" do
assert_kind_of Module, TeamPage
end
test ‘setup block yields self’ do
TeamPage.setup do |config|
assert_equal TeamPage, config
end
end
end
레일스 어플리케이션에서 엔진을 테스트하고 통합하는 방법을 계속해서 알아보기 위해서는, test/dummy/ 레일스 어플리케이션으로 가서 서버를 부팅하거나 콘솔에서 작업을 해 보기 바랍니다.
Resources And Tips
여기에서는 제대로 따라하고 있다는 것을 확인할 수 있는 몇가지 방법을 제공해 줍니다. 아래에는 본 게시물에 있는 코드 예재를 따라한 후에 엔진젬이 가져야 하는 디렉토리를 구조를 보여 줍니다.
## DIRECTORY STRUCTURE
#
– team_page/
– app/
– controllers/
– team_page/
+ team_controller.rb
– models/
– team_page/
+ team_member.rb
– views/
– team_page/
+ index.html.erb
– config/
+ routes.rb
– lib/
– team_page.rb
– generators/
– team_page/
+ team_page_generator.rb
– templates/
+ migration.rb
– team_page/
+ engine.rb
+ version.rb
– test/
+ team_page_test.rb
+ test_helper.rb
+ team_page.gemspec
+ Gemfile
+ Gemfile.lock
아래에 간단한 스크립트를 통해서 데이터베이스에 팀 멤버 몇 사람을 로드할 수 있습니다.
## DATA SEED
#
# => Method 1
# Copy the code into your application’s db/seeds.rb file.
#
# => Method 2
# If you would like to run this code in the engine that you are
# developing, place it in the seeds file inside of the dummy app
# contained in your integration tests.
#
# With either of the above methods, be sure to run the following from
# your command line…
#
# rake db:seed
#
5.times do |i|
TeamPage::TeamMember.create!( {
:name => "Team Member #{i}",
:twitter_url => "http://twitter.com/team_member_#{i}",
:bio => "A really fancy team member!",
:image_url => "http://bit.ly/muSWki"
} )
end