이 글에서는 레일즈 어플리케이션과 앵귤러(Angular.js) 어플리케이션이 혼용되는 프레임웍 구조에 대해서 기술하고자 한다. 여기서 혼용이라는 말은 레일즈의 MVC로만 작성된 어플리케이션과 앵귤러 SPA(Single Page Application) 작성된 어플리케이션이 한 웹 어플리케이션에서 공존한다는 의미이다.
애샛 파이프라인과 템플릿
레일즈와 앵귤러의 통합이 생각보다 쉽지 않은 이유는 레일즈 애샛 파이프라인 프레임웍 내에서 앵귤러 템플릿이 동작해야 한다는데 있다. 앵귤러의 모든 컴포넌트는 자바스트립트 파일 형태이므로 app/assets/javascripts/ 아래 놓이는게 자연스럽다. 문제는 애샛 파이프라인이 동작하면서 앵귤러 컴포넌트의 파일명에 핑거프린트를 추가하면서 발생한다. 이렇게되면 앵귤러 템플릿 파일명이 변형되면서 다른 앵귤러 컴포넌트에서 이 템플릿을 참조하지를 못하는 문제가 발생한다. 한발 더 나아가면 템플릿 안에서 이미지나 비디오 등 다른 애샛을 사용할 때 이들을 어떻게 참조할 것인가라는 문제도 해결해야한다. 제시되는 대안으로는 앵귤러가 사용하는 모든 파일을 /public 아래에 배치한다거나, 레일즈의 애샛 컴파일 옵션을 조정해서 런타임 컴파일을 사용하자는 의견이 있다. 하지만 이 방식들은 레일즈 컨벤션에서 벗어나거나 퍼포먼스가 떨어지기는 문제를 동반하기 때문에 만족스러운 방법은 아니다. 최근 트렌드 중의 하나는 레일즈 애샛 파이프라인 대신 새로운 툴링(Tooling)을 구축하자는 움직임인데 아래에서 잠시 소개를 하고 나중에 별도의 글에서 다루도록 하겠다.
애샛 파이프라인을 다른 툴로 대치하는 방안
최근들어 홍수처럼 쏟아지고 있는 프론트엔드 프레임웍의 등장에 맞춰 개발과 관리, 배포를 편리하게 해주는 도구도 급격한 진화를 하고 있다. 특히 걸프(Gulp), 요맨(Yeoman), 바우어(Bower), 브라우저리파이(Browserify) 등은 지금까지의 프론트엔드 개발 관행을 바꿀 정도로 높은 호응을 받고 있는 도구들이다. 이와 같은 새로운 툴링(Tooling)을 잘 구축해 놓으면 서버와 프론트엔드의 개발을 완전히 분리할 수 있어서 개발 효율을 높혀줄 뿐 아니라 추후 서버 프레임웍 교체가 발생해도 독립적으로 대처가 가능하다는 장점이 존재한다. 레일즈 커뮤니티에서도 이러한 추세를 따라 애샛 파이프라인 대신 새로운 툴링(Tooling)을 구축해서 사용하려는 시도가 계속되고 있다. 특히 걸프의 파이프 구조는 레일즈 애샛 파이프라인보다 속도가 빠를뿐 아니라 유연한 구성이 가능하기 때문에 점차적으로 채택이 증가할 것으로 예측된다. 한편 새로운 툴링을 구축하는 것에 대한 반대급부도 고려해야 하는데 대략 다음과 같은 문제가 지적된다.
  • 새로운 툴을 습득하는데 소요되는 러닝 커브
  • 사용하는 젬이 자체 애샛이 있을때 애샛 파이프라인 없이 동작해야 하는 문제
  • 레일즈 5와의 호환성
참고 자료:
레일즈 프로젝트 생성
여기서는 터보링크를 사용하지 않는 것을 가정한다. 만약 터보링크가 이미 설치되어 있다면 Gemfile, application.js에서 turbolink가 사용된 곳을 수동으로 제거한다.
rails new RailsAngular --skip-turbolinks
부트랩(Bootstrap) 셋업

gem 'bootstrap-sass', '~> 3.3.3'
gem 'jquery-ui-rails'
gem 'compass-rails'
cd app/assets/stylesheets
mv application.css application.css.scss
touch _layout.css.scss
vi application.css.scss

// app/assets/stylesheets/application.css.scss
@import "compass";
@import "bootstrap-sprockets";
@import "bootstrap";
@import 'jquery-ui'; // 필요 시
@import "layout";
vi application.js

/* app/assets/javascripts/application.js */
//= require jquery
//= require jquery_ujs
//= require jquery-ui # 필요 시
//= require bootstrap-sprockets
//= require_tree .
레일즈 MVC 앱
앵귤러를 사용하지 않는 레일즈 앱은 평소대로 작성한다.
rails g scaffold blog name description
앵귤러 셋업
vi Gemfile

source 'https://rails-assets.org' do
  gem 'rails-assets-angular'
  gem 'rails-assets-angular-ui-router'
  # gem 'rails-assets-angular-animate'
  # gem 'rails-assets-angular-resource'
end
gem 'angular_rails_csrf'
gem 'angular-rails-templates'
vi application.html.erb

# app/views/layout/application.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>AngularDemo
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%#= javascript_include_tag 'application' %>
  <%= csrf_meta_tags %>
</head>
<body ng-app="app">
  <%= yield %>
  <%= javascript_include_tag(@angular_app || 'application') %>
</body>
</html>
마지막에 자바스트립트 매니페스트(Manifest) 파일을 지정하는 방법을 주목하자. 일반적인 레일즈 앱은 application.js 파일을 사용하고, 앵귤러 앱은 콘트롤러 단위로 지정 가능한 @angular_app값을 사용한다.
앵귤러 앱(e.g., dashboard) 생성
rails g controller dashboard index --skip-assets --skip-routes --skip-helper
이제 Dashboard 콘트롤러의 index 액션과 index.html.erb 뷰는 앵귤러 SPA의 엔트리포인트로서 사용된다. 이와 같은 방식으로 레일즈의 라우팅에 의해 구분되는 독립적인 SPA를 여러개 만들 수 있다.
vi dashboard_controller.rb

# app/controllers/dashboard_controller.rb
def index
  @angular_app = 'dashboard_app'
end
vi routes.rb

# config/routes.rb
get '/dashboard', to: 'dashboard#index'
vi index.html.erb

# app/views/dashboard/index.html.erb
<div class="<%= @angular_app %>" ui-view></div>
매니페스트 파일
mkdir -p app/assets/angulars/dashboard/templates
vi app/assets/angulars/dashboard_app.js

/* app/assets/angulars/dashboard_app.js */
//= require angular
//= require angular-ui-router
//= require angular-rails-templates
//= require_tree './dashboard'
이 파일은 dashboard 앱에 필요한 앵귤러 파일을 로드한다. require_tree './dashboard' 선언에 의해 app/assets/angulars/dashboard/에 있는 모든 자바스크립트 또는 커피스트립트 파일이 자동으로 로드된다. 마지막으로 이 파일이 프리컴파일이 되도록 아래와 같이 등록한다.
vi config/initializers/assets.rb

# config/initializers/assets.rb
Rails.application.config.assets.precompile += %w( dashboard_app.js )
앵귤러 컴포넌트 파일
app/assets/angulars/dashboard/에 특징 기반(Feature-based) 또는 MVC 기반으로 앵귤러 모듈을 배치한다.
vi app/assets/angulars/dashboard/app.coffee

app = angular.module('app', ['ui.router', 'templates'])

app.config(['$stateProvider', '$urlRouterProvider', ($stateProvider, $urlRouterProvider) ->
  $stateProvider.state 'home',
    url: '/'
    templateUrl: 'dashboard/templates/index.html'
    # template: '<h2 class='text-info'>Hello, Angular</h2>'

  $urlRouterProvider.otherwise('/')
])

app.controler(...)

app.directive(...)
템플릿 파일
제안된 구조에서 템플릿 파일들은 app/assets/angulars/dashboard/에 *.html.erb 형태로 존재한다. 콘트롤러, 디렉티브, 라우터 등 앵큘러 컴포넌트에서는 templateUrl 옵션을 사용해서 이곳의 템플릿을 포인팅한다. 이곳에 배치된 템플릿 파일들은 angular-rails-templates 젬에 의하여 앵귤러의 $templateCache로 컴파일되어 서비스된다. vi app/assets/angulars/dashboard/index.html.erb

<h2 class='text-info'>Hello, Angular</h2>
[옵션] Dependency Injection Minification 사용 중지
vi config/environments/production.rb

config.assets.js_compressor = Uglifier.new(mangle: false)
[옵션] 레일즈 뷰(View)에서 앵귤러 템플릿 구현
rails g controller templates tempate
vi templates_controller.rb

# app/controllers/templates_controller.rb
def template
  render "templates/#{params[:path]}", layout: nil
end
vi routes.rb

# config/routes.rb
get '/templates/:path.html', to: 'templates#template', constraints: { path: /.+/  }
mkdir -p app/views/templates/dashboard
vi app/views/templates/dashboard/index.html

# app/views/templates/dashboard/index.html
<h2 class='text-info'>Hello, Angular</h2>
위 셋업에 의해 앵귤러 코드 templateUrl: '/templates/dashboard/index.html'는 레일즈의 templates 콘트롤러의 template 액션으로 라우팅 되고, 레일즈 뷰 디렉토리로부터 템플릿 파일 index.html.erb를 Ajax로 가져와서 처리한다. 이 방식은 앵귤러 템플릿 파일을 애샛 디렉토리가 아닌 레일즈 뷰 디렉토리에서 서비스 하고자 할 때 사용할 수 있는 유용한 기법이다.