Skip to content

Guk0/rails-engine-example

Repository files navigation

Rails Engine

Engine이란?

레일즈에서 제공하는 모듈 generator로써 기본 어플리케이션 안에 서브 어플리케이션을 둘 수 있게 하는 기술입니다.

어떨 때 사용해야 하는가?

프로젝트의 일부 기능을 따로 떼내어 모듈화 혹은 서브 어플리케이션 사용하고자 할 때 주로 사용합니다. devise, activeadmin을 비롯한 다양한 gem들을 이 rails engine으로 구현합니다.

장점

가장 큰 장점은 각각의 어플리케이션을 모듈로 관리할 수 있다는 점입니다. 모듈별로 각각 controller, model, views, routing 등 독립적으로 관리할 수 있습니다.



세팅 및 주의사항


new plugin

rails plugin new engine_name --mountable

스크린샷 2021-12-05 오후 11 37 53

/content/content.gemspec 안의 TODO를 아래와 같이 변경

spec.email       = ["abcgy39@naver.com"]
spec.homepage    = "https://github.com/Guk0"
spec.summary     = "test for rails engine"
spec.description = "test for rails engine"

plugin들을 apps 로 옮기고 아래와 같이 젬파일에서 경로 설정

# root gemfile

gem 'content', path: 'apps/content'

subdomain 설정

반드시 root "home#index" 위에 선언해야함

# root routes

constraints :subdomain => 'content' do
  mount Content::Engine, :at => '/'
end

root "home#index"

partial render 할때는 앞에 모듈 이름을 붙여준다

# content 모듈에서 가져올때
<%= render "content/shared/header" %>

# root 모듈에서 가져올때
<%= render "shared/header" %>

link_to 에서는 path 앞에 모듈이름을 붙여준다.

# 루트 어플리케이션의 패스 가져올때

main_app.destroy_user_session_path

# content 모듈에서 패스 가져올때

content.destroy_user_session_path

# 같은 모듈 안에서는 앞에 namespace를 생략할 수 있다.

comments_path



model / migration

https://tanzu.vmware.com/content/blog/leave-your-migrations-in-your-rails-engines

해당 모듈 디렉토리에서 rails g model comment 하면 마이그레이션 파일이 해당 디렉토리의 db에서 생성된다. 마이그레이션은 각각의 모듈에서 관리하는게 좋을 것 같고 아래와 같이 각 엔진 설정 파일에서 root app의 Migrate 폴더 설정에 아래와 같이 패스를 추가해준다.

# apps/content/lib/engine.rb

...
initializer :append_migrations do |app|
  unless app.root.to_s.match root.to_s
    config.paths["db/migrate"].expanded.each do |expanded_path|
      app.config.paths["db/migrate"] << expanded_path
    end
  end
end
...

모델 generate시 migration 파일의 table 명은 content_comments 가 된다

What this isolation of the namespace means is that a model generated by a call to bin/rails generate model, such as bin/rails generate model article, won't be called Article, but instead be namespaced and called Blorgh::Article. In addition, the table for the model is namespaced, becoming blorgh_articles, rather than simply articles. Similar to the model namespacing, a controller called ArticlesController becomes Blorgh::ArticlesController and the views for that controller will not be at app/views/articles, but app/views/blorgh/articles instead. Mailers, jobs and helpers are namespaced as well.

하지만 table 명만 그렇게 생성되는거고 모델이나 뭐 그런것들은 앞에 namespace가 안붙고 생성되는 거라 크게 상관은 없을 듯 하다. migration 파일 생성할 때만 주의하면 될 것 같다.

add_column, remove_column, change_column 등에서 테이블명은 content_items와 같이 앞에 네임스페이스를 같이 적어준다,



rails console

irb> Content::Comment.find(1)
=> #<Content::Comment id: 1 ...>

앞에 모듈명을 꼭 적어줘야 한다.

Content::Comment.create(user: User.first, post_id: 1, title: 1, content: 1)



assets

main_app에서 파생되는 형태이면 sub_app의 layout을 main_app의 layout을 사용하는 것도 좋은 방법이다.(script, link 태그 중복 방지)

<!DOCTYPE html>
<html>
<head>
  <title>Content</title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>
  <%= stylesheet_link_tag "application", media: "all" %>  
  <%= stylesheet_link_tag "content/application", media: "all" %>
</head>
<body>

<%= yield %>

<%# javascript_include_tag('application') %>
<%= javascript_include_tag('content/application') %>

</body>
</html>

request_varient를 사용하는 경우 +web 같이 붙어있는 부분을 찾질 못함. 추가적인 content/application_controller.rb 컨트롤러 설정 필요

https://stackoverflow.com/questions/9590598/devise-rails-no-route-matches-get-users-sign-out/12023959

webpacker 인 경우

https://gist.github.com/pioz/805d0887ebfdde6322c4db4b46b0120f

sprockets 인 경우

https://stackoverflow.com/questions/64210874/how-do-i-tell-sprockets-4-to-compile-assets-for-a-vendored-gem

# application.html.erb

<%= stylesheet_link_tag "application", media: "all" %>  
<%= stylesheet_link_tag "content/application", media: "all" %>
...
<%= javascript_include_tag('application') %>
<%= javascript_include_tag('content/application') %>
# main_app's menifest.js
# content 모듈의 menifest를 가져옴

//= link content_manifest
# content menifest에 아래와 같이 정의
//= link_directory ../stylesheets/content .css
//= link_directory ../javascripts/content .js
//= link content/application.css
# application.scss

*= require_tree .
*= require_self
*= require content/comments
*= require content/items

application.js

//= require content/items



Application Controller

module Content
  class ApplicationController < ::ActionController::Base
    layout 'application'

    def current_content_user
      if params[:user_id].blank?
        User.find(current_user.id)
      else
        User.find(params[:user_id])
      end
    end
  end
end
  • ApplicationController를 완전히 분리하여도 된다. 다만 기존에 ApplicationController에 많은 variable이나 method가 들어가고 해당 요소들을 Engine에서도 사용할 것이라면 root 어플리케이션의 ApplicationController를 상속 받아 해당 Engine에서 필요한 설정들만 추가한다.
  • views의 layout도 root application의 layout을 그대로 사용한다.



devise

https://github.com/heartcombo/devise/wiki/How-To:-Use-devise-inside-a-mountable-engine#warning

heartcombo/devise#2827

devise는 engine 들에 여러번 인스톨이 불가능함. 또 하나의 모듈에서 정의한 뒤 다른 모듈에서 이를 가져다 쓰는 것이 바람직한 구현 방법일 것 같음.

하지만 각 엔진에서 User모델안에 관계가 추가될 때가 조금 애매함. 만약 엔진 내에서 종속을 갖는 관계를 루트 엔진의 User에 정의하는 것은 종속성을 온전히 나타내지 못하기 때문에 분리하는 것이 좋을 것으로 생각됨.

루트에 devise 인스톨 후 각 엔진에서 User에 대한 관계를 나누고 싶을 때

# content/user.rb

module Content
  class User < ::User
    has_many :items
  end
end

# content/application_controller
...
 def current_content_user
    if params[:user_id].blank?
      User.find(current_user.id)
    else
      User.find(params[:user_id])
    end
  end
...
  • current_user 같은 경우 다시 정의해서 써여한다. devise가 root에 정의되어 있고 current_user는 루트의 application controller에 정의되기 때문에 current_user는 루트의 User를 가져온다.
  • root 엔진의 User는 root 어플리케이션의 관계들만 가져오기 때문에 각 엔진에서 정의한 관계는 가져오지 못한다. 따라서 각 엔진에 정의된 User를 사용하기 위해 current_user를 재정의 해야한다.
  • current_user를 override하는 방법도 찾아보고 있긴한데 일단은 위와 같이 current_content_user를 정의하여 사용한다.



locale

  • model 명 앞에 engine 명 + / 을 달아준다.
content/items:
  title: 상품유형
  description: 설명
  created_at: 생성일
  price: 가격
  status: 상태
  • 위와 같이 적용 시 아래와 같이 label_tag에서 로케일을 반환한다.
f.label :title
  • I18n 클래스를 이용하여 직접 접근하고 싶을 때는 아래와 같이 모델명 앞에 content/ 을 붙인다.
I18n.t("enum.content/items.status.#{params[:status]}")



nested_attributes

  • nested_attributes 사용시 foreign_key를 인식하지 못하는 문제가 발생한다.
  • User - Item 관계에서 Item의 foreign_key를 user_id로 인식하기 때문
  • foreign_key: content_user_id 로 관계 제대로 명시



partial rendering

<%= render "#{request.subdomain}/shared/header" %>
  • subdomain 명을 engine명과 동일하게 설정하면 위와 같은 처리도 가능하다.
  • root application은 보통 서브도메인에 두진 않으니 request.subdomain은 ""이다.



has_many 관계

has_many :items, foreign_key: "content_user_id"

has_many에 있는 관계의 인스턴스들을 가져오려면 반드시 foreign_key를 설정해줘야 함.



const_get

params[:target_type].constantize 으로 하면 Content 모듈이 아닌 root 모듈의 모델을 가져옴.

아래 와 같이 const_get으로 사용

Content.const_get(params[:target_type])



resourec.class.name

모델 name을 얻고 싶을 때 일반적으로 target.class.name 를 많이 사용하지만 engine 내에서는 target.class.name을 사용하면 앞에 네임스페이스가 붙게됨(모듈명)

그래서 아래와 같이 model_name.human 사용

target.model_name.human



Polymorphic

target_type을 모듈명과 같이 넣어줘야함.

target_type: "Content::Item"

About

A project used to test Rails Engine

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published