#277 Mountable Engines
- Download:
- source codeProject Files in Zip (56.6 KB)
- mp4Full Size H.264 Video (19.6 MB)
- m4vSmaller H.264 Video (12.7 MB)
- webmFull Size VP8 Video (18.7 MB)
- ogvFull Size Theora Video (23.3 MB)
Rails 3.1 HackFest가 지난 주말에 있었고 참석했던 사람들이 고생한 덕분에 이제 Rails 3.1.rc5를 사용할 수 있게 되었습니다. 이번 릴리스버전에는 mountable engine(장착용 엔진)에 대한 몇가지 중요한 변경내용이 포함되어 있습니다. 장착용 엔진을 이용하면 특정 레일스 어플리케이션을 다른 어플리케이션내에서 작동하도록 할 수 있어서 이번 연제에서 다루고자 합니다.
아마도 104번 연제에서 다루었던 Exception Notification plugin을 기억할 겁니다. 이 플러그인을 어플리케이션에 설치하면 어플리케이션에서 발생하는 예외들을 데이터베이스에 저장하게 될 것입니다. 또한 예외내용을 볼 수 있도록 하는 사용자 화면도 제공해 줍니다. 이번 연제에서는 이 플러그인을 업그레이드해서 장착용 엔진으로써 사용할 수 있도록 할 것입니다.
시작하기
엔진을 작성하기 전에 로컬 시스템에 레일스 3.1.rc5 또는 더 최신의 버전이 설치되어 있는지 확인할 필요가 있습니다. 다음과 같은 명령을 실행하여 최신 버전을 설치할 수 있습니다.
$ gem install rails --pre
일단 레일스 버전이 제대로 설치되면 장착용 엔진을 만들 수 있습니다. 기존 레일스 어플리케이션 내에서 엔진을 만들 필요가 없습니다. 엔진을 생성하는 것은 rails new
명령으로 레일스 어플리케이션을 새로 생성하는 과정과 비슷합니다. 한가지 차이점이라면 대신에 rails plugin new
명령을 실행한다는 것입니다. 만들고자 하는 어플리케이션이 예외들을 처리하는 것이므로 이름을 uhoh
라고 부르도록 하겠습니다. 이 때 --mountable
옵션을 지정해서 생성하는 플러그인이 장착용 엔진으로 작동하게 해야 합니다.
$ rails plugin new uhoh --mountable
만들고자 하는 엔진의 디렉토리 구조는 일반적인 레일스 어플리케이션처럼 보이며 기본적으로 단지 다른 어플리케이션내에서 작동되도록 디자인할 뿐입니다. 그러나, 몇가지 차이점은 있습니다. 전체 어플리케이션에 걸쳐 네임스페이스를 사용하기 위한 디렉토리들이 존재하는데, 예를 들면, application_controller
파일이 /app/controllers/uhoh
디렉토리내에 있게 되고 assets
, helpers
, views
디렉토리내에 있는 파일들에게도 똑같이 적용됩니다. 이렇게 하면, 엔진용 코드들이 장착될 어플리케이션에서 기존 코드들과 잘 구분되도록 해 줍니다. assets
디렉토리를 가지게 되면, 엔진이 다른 어플리케이션에 장착될 때 엔진용 assets을 public
디렉토리로 복사하는 것을 더 이상 걱정할 필요가 없게 해줍니다. assets pipeline 덕분에 이 모든 것이 우리를 대신해서 처리되기 때문입니다.
assets도 또한 하나의 디렉토리로 네임스페이스되기 때문에 assets으로 연결하고자 할 때 그 디렉토리를 경유해야 할 필요가 있습니다. 또한 layouts 디렉토리에서도 마찬가지입니다. RC5 버전에 버그가 있어 application.html.erb
파일이 두개가 만들어지는데, uhoh
디렉토리 바깥쪽에 있는 것을 삭제하면 안전합니다. 남아 있는 파일의 내용을 보게 되면 assets을 불러들이는 링크 주소에 모두 uhoh
가 별도로 포함하게 된다는 것을 알게 될 것입니다. 이미지나 다른 assets을 링크할 때는 항상 이 네임스페이스를 포함해야할 필요가 있는 것입니다.
<!DOCTYPE html> <html> <head> <title>Uhoh</title> <%= stylesheet_link_tag "uhoh/application" %> <%= javascript_include_tag "uhoh/application" %> <%= csrf_meta_tags %> </head> <body> <%= yield %> </body> </html>
다음으로 엔진의 핵심 파일 중의 하나이고 /lib/uhoh
디렉토리에 있는 engine.rb
파일을 살펴보도록 하겠습니다.
module Uhoh class Engine < Rails::Engine isolate_namespace Uhoh end end
엔진은 Rails::Engine
로부터 상속받는 하나의 클래이며 사용자 설정을 위한 중심이 되는 곳이기도 합니다. 이 클래스내에서 isolate_namespace
를 이미 호출하였는데 이것은 이 엔진이 하나의 독립체로서 자신이 장착될 다른 어플리케이션에 대해서 걱정할 필요가 없게 해 줍니다.
여기서 살펴볼 마지막 부분은 /test
디렉토리 내부입니다. /test/dummy
디렉토리안에, 엔진이 특정 어플리케이션에 장착될 때 어떻게 작동하는지를 보여주게 되는 하나의 레일스 어플리케이션이 존재합니다. 이 어플리케이션의 config
디렉토리에는 routes.rb
파일이 있습니다. 여기서는 mount
를 호출하는데, 이 때 엔진의 메인 클래스의 경로를 지정해 주게 됩니다.
Rails.application.routes.draw do mount Uhoh::Engine => "/uhoh" end
이것은 엔진을 원하는 경로상에 장착하여 어플리케이션에 설치할 때 누군가가 하게될 작업입니다. 이것은 하나의 Rack 어플리케이션으로 하나의 요청이 /uhoh 로 들어올 경우 설치한 Engine
클래스로 념겨지게 될 것입니다. 이 부분을, 엔진의 README 파일내의 설치안내문에 추가해 두어, 사용자가 어플리케이션의 routes 파일내에서 엔진을 장착하는 방법을 알 수있게 하는 것도 좋은 생각입니다.
비록 이 dummy 앱이 /test
디렉토리 안에 있긴 해도 수작업으로 테스트하는데 매우 유용하게 쓰이게 됩니다. 엔진이 있는 디렉토리에서 rails s
를 실행하면 바로 이 dummy 앱이 시작될 것입니다. http://localhost:3000/uhoh/
로 접속하게 되면, 해당 경로에 장착되어 있는 엔진으로 안내될 것입니다. 해당 페이지를 방문하게 되면 아직 관련 컨트롤러를 작성하지 않았기 때문에 현재로서는 에러 메시지를 보게 될 것입니다.
이제 그 작업을 하게 될텐데, 우선 엔진에서 failures
컨트롤러를 작성할 것입니다. 이 때, 레일스 어플리케이션에서 하는 방식과 같이 레일스의 제너레이터를 이용할 수 있습니다. 엔진에서 작업을 하더라도 컨트롤러에 네임스페이스를 지정할 필요가 없는데 이 모든 것이 우리를 대신해서 처리되기 때문입니다.
$ rails g controller failures index
위와 같이 명령을 실행하게 되면 일반적인 컨트롤러에서 기대하는 것 과 같은 파일들이 생성되지만, 모든 파일들은 네임스페이싱되는 디렉토리에 위치하게 됩니다. 다음으로, 엔진의 라우트를 변경해서 방금전에 생성한 컨트롤러의 index 액션으로 root
라우트를 지정합니다. 이러한 작업은 /config/routes.rb
파일에서 하게 됩니다.
Uhoh::Engine.routes.draw do root :to => "failures#index" end
http://localhost:3000/uhoh/
을 방문하게 되면 이제 index 액션의 뷰를 보게 될 것입니다.
이 페이지에서 지금까지 발생한 예외들의 목록을 보고자 합니다. 여기서 이러한 정보를 저장할 모델이 필요하게 되며 message 필드 하나만으로 구성된 간단한 Failure
모델을 생성할 것입니다.
$ rails g model failure message:text
자 이제, 모델을 만들었는데, 다음은 어떻게 마이그레이션을 실행할까요? 엔진 자체 내에서도, 일반적으로 실행하는 것과 같이, rake db:migrate
를 실행할 수 있으며 이 때 모든 것이 기대하는 바와 같이 제대로 작동할 것입니다. 그러나, 이 엔진을 임의의 어플리케이션에 장착하려고 시도한다면 이 작업은 제대로 동작하지 않을 것입니다. 왜냐하면, rake
가 장착된 엔진내의 마이그레이션 작업을 제대로 인식하지 못하기 때문입니다. 따라서 엔진 사용자에게 rake uhoh:install:migrations
명령을 대신 사용할 것을 알려주어야 합니다. 이 명령은 엔진의 마이그레이션 작업을 어플리케이션으로 복사해 주게 되며 이 때 비로소 일반적인 방법과 같이 rake db:migrate
명령을 실행할 수 있게 됩니다. 엔진에 대한 설치안내문에 이러한 정보를 포함하는 것은 좋은 생각이 될 수 있습니다.
레일스 콘솔 역시 엔진에서 기대하는 바와 같이 동작하여 다음과 같이 하나의 예제 Failure
를 생성할 수 있을 것입니다.
Uhoh::Failure.create!(:message => "hello world!")
주목할 것은 하나의 클래스를 참조할 때마다 해당 네임스페이스를 포함할 필요가 있는 것입니다. 이제 하나의 Failure
레코드가 추가되었고 이것을 FailuresController
의 index
액션에서 보여 주게 될 것입니다.
module Uhoh class FailuresController < ApplicationController def index @failures = Failure.all end end end
그러나, 콘솔에서와는 달리 이미 Uhoh
모듈내에 있기 때문에 여기서는 네임스페이스를 명시할 필요가 없게 됩니다. 뷰에서 코드작업을 통해서 모든 failures를 불러와 하나의 리스트에 보여 주게 될 것입니다.
<h1>Failures</h1> <ul> <% for failure in @failures %> <li><%= failure.message %></li> <% end %> </ul>
이제 페이지를 다시 로드하면, 추가한 Failure
를 보게 될 것입니다.
예외를 잡아채기
이제 failures를 기록하는 메소드를 가지게 되었으므로, 엔진이 장착된 어플리케이션에서 예외가 발생할 때마다 하나의 failure을 생성할 필요가 있을 것입니다. 이것을 테스트하기 위해서 dummy 어플리케이션에서 예외를 시뮬레이션할 수 있는 방법이 필요할 것입니다. 이를 위해서, 엔진의 dummy 어플리케이션 디렉토리로 이동하여 simulate
라고 하는 컨트롤러와 failure
액션을 만들게 될 것입니다.
$ rails g controller simulate failure
이 액션에서 예외를 발생시키도록 할 것입니다.
class SimulateController < ApplicationController def failure raise "Simulating an exception" end end
브라우저에서 이 액션을 호출하면 기대하는 바와 같이 예외가 발생할 것입니다.
여기서 엔진을 변경해서 예외가 발생할 때 이를 가로채서 새로운 Failure
레코드를 생성하도록 할 필요가 있습니다. 여기서 구현하게 될 방법은 그렇게 효과적이지 못하지만 간단한 코드로 원하는 작동할 제대로 할 수 있을 것입니다. 이제, 엔진에서 initializer 하나를 만들어서 작업을 해 줄 것입니다. 엔진의 config 디렉토리에는 initializer
디렉토리가 없지만 이 디렉토리를 직접 만들어서 거기에 initializers를 둘 수 있습니다. 이 디렉토리에서 exception_handler.rb
파일을 만들 것입니다.
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, start, finish, id, payload| if payload[:exception] name, message = *payload[:exception] Uhoh::Failure.create!(:message => message) end end
이 파일에서, 하나의 액션이 처리되었음을 우리에게 통보하는 notification을 감시하기 위해 notification(249번 연제에서 notifications에 대해서 자세히 다룬 바 있습니다[watch, read])을 등록할 것입니다. 하나의 액션이 처리될 때, payload
가 예외를 포함하고 있는지를 확인할 수 있습니다. 이 경우 하나의 예외가 발생했음을 알 수 있고 새로운 Failure
에 예외 메시지를 저장할 수 있게 됩니다.
이 내용들을 테스트해 보기 위해 서버를 재시작할 것입니다. 다시 예외를 발생시키기 위해 http://localhost:3000/simulate/failure
를 방문할 수 있습니다. 예외상황이 발생했음을 알게 되면, http://localhost:3000/uhoh
를 방문하여 그 내용을 볼 수 있게 될 것입니다.
엔진에서 URL 다루기
엔진내에서 사용하는 모든 URL 헬퍼는 해당 엔진에 대한 URL을 만들어 줄 것입니다. 예를 들어, failures의 index
페이지로부터 root URL로의 링크를 만들게 되면 이 링크는 엔진이 장착된 어플리케이션이 아니라 해당 엔진 자체의 root URL을 가리키게 될 것입니다.
<p><%= link_to "Failures", root_url %></p>
이 링크는 엔진의 root URL인 http://localhost:3000/uhoh
를 가리키게 됩니다. 이것은 라우트에서 root URL이 FailureController
의 index
액션을 가리키도록 정의를 해 두었기 때문에 이 링크가 있는 페이지와 같은 것입니다. 따라서, 다음과 같이 main_app
에 대해서 URL 헬퍼를 호출하여 어플리케이션 자체로의 링크를 만들 수 있습니다:
<p><%= link_to "Failures", root_url %></p> <p><%= link_to "Simulate Failure", main_app.simulate_failure_path %></p>
이것은 어플리케이션에 있는 Simulate Failure 페이지로의 링크인 http://localhost:3000/simulate/failure
를 만들어 줍니다.
그렇다면, 어플리케이션으로부터 엔진으로 링크를 만들고자 한다면 어떻게 될까요? 가장 먼저해야 할 것은 :as
옵션을 이용해서, 어플리케이션의 라우트 파일에서 있는, 엔진을 장착하는 라인을 변경하는 것입니다.
Rails.application.routes.draw do get "simulate/failure" mount Uhoh::Engine => "/uhoh", :as => "uhoh_engine" end
이후에, uhoh_engine
메소드를 호출하면 엔진의 URL 헬퍼에 접근할 수 있습니다. 이것을 입증하기 위해서, failure 액션을 잠시 변경해서 예외를 발생시키는 대신 엔진의 root URL로 리디렉트하도록 할 것입니다.
class SimulateController < ApplicationController def failure redirect_to uhoh_engine.root_url end end <p><a href="http://localhost:3000/simulate/failure"><code>http://localhost:3000/simulate/failure</code></a>를 방문하게 되면, 엔진의 URL 헬퍼를 이용하여 특정 URL로 리디렉트할 것이기 때문에, <a href="http://localhost:3000/uhoh"><code>http://localhost:3000/uhoh</code></a>로 리디렉트하게 될 것입니다. 이 또한 엔진의 README에서 언급할 또 다른 특징이 되는 것입니다. </p> <p>이제 엔진의 기능이 완벽해졌지만, failures 목록을 보여주는 페이지가 매우 단조롭기 때문에 약간의 assets을 이용해서 이쁘게 보이게 할 것입니다. 먼저, 이미지를 하나 추가할 할 것입니다. 이미 사용할 이미지를 찾아서 <code>/app/assets/images/uhoh</code> 디렉토리에 추가해 놓았습니다. 페이지에 이것을 포함하기 위해서, 다른 이미지에서 작업할 때와 같이, <code>image_tag</code> 헬퍼를 이용할 수 있습니다.</p> ``` /app/views/uhoh/failures/index.html.erb <%= image_tag "uhoh/alert.png" %> <h1>Failures</h1> <ul> <% for failure in @failures %> <li><%= failure.message %></li> <% end %> </ul> <p><%= link_to "Failures", root_url %></p> <p><%= link_to "Simulate Failure", main_app.simulate_failure_path %></p>
몇가지 CSS를 포함할 것입니다. SASS와 Coffeescript는, 의존적으로 추가될 수 있지만, 디폴트 상태에서는 엔진에서 사용할 수 없습니다. failures.css
파일에 몇가지 CSS를 추가한다면 자동으로 포함될 것입니다.
html, body { background-color: #DDD; font-family: Verdana; } body { padding: 20px 200px; } img { display: block; margin: 0 auto; } a { color: #000; } ul { list-style: none; margin: 0; padding: 0; } li { background-color: #FFF; margin-bottom: 10px; padding: 5px 10px; }
자바스크립트에서도 마찬가지입니다. failures.js
파일에 코드를 작성하면 자동으로 포함될 것입니다.
$(function() { $("li").click(function() { $(this).slideUp(); }); });
이제 페이지를 다시 로드하면 더 이쁘게 보일 것이며 자바스크립트가 포함되었다면 특정 예외문을 클릭할 경우 사라질 것입니다.
이번 연제는 여기까지입니다. 장착용 엔진은 레일스 3.1부터 지원하는 대단한 새로운 기능이며 고려해 볼만한 충분한 가치가 있습니다.