Rails 생성기 및 템플릿 만들기와 사용자 정의

Rails 생성기는 워크플로우를 개선하는 필수적인 도구입니다. 이 가이드를 통해 생성기를 만들고 기존 생성기를 사용자 정의하는 방법을 배울 수 있습니다.

이 가이드를 읽고 나면 다음을 알 수 있습니다:

  • 애플리케이션에서 사용 가능한 생성기를 확인하는 방법.
  • 템플릿을 사용하여 생성기를 만드는 방법.
  • Rails가 생성기를 호출하기 전에 어떻게 생성기를 검색하는지.
  • 생성기 템플릿을 재정의하여 스캐폴드를 사용자 정의하는 방법.
  • 생성기를 재정의하여 스캐폴드를 사용자 정의하는 방법.
  • 많은 생성기를 덮어쓰지 않도록 대체 방법을 사용하는 방법.
  • 애플리케이션 템플릿을 만드는 방법.

첫 번째 접촉

rails 명령을 사용하여 애플리케이션을 만들면 실제로 Rails 생성기를 사용하고 있습니다. 그 후에 bin/rails generate를 실행하여 사용 가능한 모든 생성기 목록을 확인할 수 있습니다.

$ rails new myapp
$ cd myapp
$ bin/rails generate

참고: Rails 애플리케이션을 만들려면 rails 전역 명령을 사용하는데, 이는 gem install rails로 설치된 Rails 버전을 사용합니다. 애플리케이션 디렉토리 내에서는 bin/rails 명령을 사용하는데, 이는 애플리케이션에 번들로 포함된 Rails 버전을 사용합니다.

Rails와 함께 제공되는 모든 생성기 목록이 표시됩니다. 특정 생성기에 대한 자세한 설명을 보려면 --help 옵션과 함께 생성기를 실행하세요. 예:

$ bin/rails generate scaffold --help

첫 번째 생성기 만들기

생성기는 Thor를 기반으로 구축되어 있으며, 구문 분석을 위한 강력한 옵션과 파일 조작을 위한 훌륭한 API를 제공합니다.

config/initializers 디렉토리에 initializer.rb라는 이름의 초기화기 파일을 만드는 생성기를 만들어 보겠습니다. 첫 번째 단계는 lib/generators/initializer_generator.rb에 다음 내용으로 파일을 만드는 것입니다:

class InitializerGenerator < Rails::Generators::Base
  def create_initializer_file
    create_file "config/initializers/initializer.rb", <<~RUBY
      # Add initialization content here
    RUBY
  end
end

새로운 생성기는 매우 간단합니다: Rails::Generators::Base를 상속받고 메서드 정의가 하나 있습니다. 생성기가 호출되면 정의된 순서대로 생성기의 모든 public 메서드가 순차적으로 실행됩니다. 우리의 메서드는 create_file을 호출하여 주어진 대상 위치에 주어진 내용으로 파일을 만듭니다.

새로운 생성기를 실행하려면 다음과 같이 실행하면 됩니다:

$ bin/rails generate initializer

계속하기 전에 새로운 생성기의 설명을 확인해 보겠습니다:

$ bin/rails generate initializer --help

Rails는 일반적으로 생성기가 네임스페이스화되어 있으면 좋은 설명을 유추할 수 있지만, 이 경우에는 그렇지 않습니다. 이 문제를 두 가지 방법으로 해결할 수 있습니다. 첫 번째 방법은 생성기 내부에서 desc를 호출하여 설명을 추가하는 것입니다:

class InitializerGenerator < Rails::Generators::Base
  desc "This generator creates an initializer file at config/initializers"
  def create_initializer_file
    create_file "config/initializers/initializer.rb", <<~RUBY
      # Add initialization content here
    RUBY
  end
end

이제 새로운 생성기에 --help를 실행하면 새로운 설명을 볼 수 있습니다.

두 번째 방법은 생성기와 같은 디렉토리에 USAGE 파일을 만드는 것입니다. 다음 단계에서 이 방법을 사용해 보겠습니다.

생성기로 생성기 만들기

생성기 자체에도 생성기가 있습니다. 우리의 InitializerGenerator를 제거하고 bin/rails generate generator를 사용하여 새로운 생성기를 만들어 보겠습니다:

$ rm lib/generators/initializer_generator.rb

$ bin/rails generate generator initializer
      create  lib/generators/initializer
      create  lib/generators/initializer/initializer_generator.rb
      create  lib/generators/initializer/USAGE
      create  lib/generators/initializer/templates
      invoke  test_unit
      create    test/lib/generators/initializer_generator_test.rb

이것이 방금 생성된 생성기입니다:

class InitializerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)
end

먼저 생성기가 Rails::Generators::Base 대신 Rails::Generators::NamedBase를 상속받는다는 점에 주목하세요. 이는 생성기에 최소 하나의 인수가 필요하며, 이 인수는 초기화기의 이름이 되고 name을 통해 코드에서 사용할 수 있습니다.

새로운 생성기의 설명을 확인해 보면 이를 알 수 있습니다:

$ bin/rails generate initializer --help
Usage:
  bin/rails generate initializer NAME [options]

또한 생성기에 source_root 클래스 메서드가 있다는 것을 알 수 있습니다. 이 메서드는 템플릿이 있는 경우 그 위치를 가리킵니다. 기본적으로 방금 생성된 lib/generators/initializer/templates 디렉토리를 가리킵니다.

생성기 템플릿이 어떻게 작동하는지 이해하기 위해 lib/generators/initializer/templates/initializer.rb 파일을 다음 내용으로 만들어 보겠습니다:

# Add initialization content here

그리고 생성기를 호출할 때 이 템플릿을 복사하도록 생성기를 변경해 보겠습니다:

class InitializerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)

  def copy_initializer_file
    copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
  end
end

이제 생성기를 실행해 보겠습니다:

$ bin/rails generate initializer core_extensions
      create  config/initializers/core_extensions.rb

$ cat config/initializers/core_extensions.rb
# Add initialization content here

copy_file이 우리의 템플릿 내용으로 config/initializers/core_extensions.rb를 만들었습니다. (file_name 메서드는 Rails::Generators::NamedBase에서 상속받은 것입니다.)

생성기 명령줄 옵션

생성기는 class_option을 사용하여 명령줄 옵션을 지원할 수 있습니다. 예:

class InitializerGenerator < Rails::Generators::NamedBase
  class_option :scope, type: :string, default: "app"
end

이제 우리의 생성기를 --scope 옵션과 함께 실행할 수 있습니다:

$ bin/rails generate initializer theme --scope dashboard

옵션 값은 options를 통해 생성기 메서드에서 접근할 수 있습니다:

def copy_initializer_file
  @scope = options["scope"]
end

생성기 해결

생성기 이름을 해결할 때 Rails는 여러 파일 이름을 사용하여 생성기를 찾습니다. 예를 들어 bin/rails generate initializer core_extensions를 실행하면 Rails는 다음 파일을 순서대로 로드하려고 시도합니다:

  • rails/generators/initializer/initializer_generator.rb
  • generators/initializer/initializer_generator.rb
  • rails/generators/initializer_generator.rb
  • generators/initializer_generator.rb

이 중 하나도 찾지 못하면 오류가 발생합니다.

우리는 생성기를 애플리케이션의 lib/ 디렉토리에 두었는데, 이 디렉토리는 $LOAD_PATH에 있기 때문에 Rails가 파일을 찾아 로드할 수 있습니다.

Rails 생성기 템플릿 재정의

Rails는 생성기 템플릿 파일을 해결할 때도 여러 위치를 확인합니다. 그 중 하나는 애플리케이션의 lib/templates/ 디렉토리입니다. 이 동작을 통해 Rails의 기본 제공 생성기가 사용하는 템플릿을 재정의할 수 있습니다. 예를 들어 스캐폴드 컨트롤러 템플릿이나 스캐폴드 뷰 템플릿을 재정의할 수 있습니다.

이를 실제로 확인해 보겠습니다. lib/templates/erb/scaffold/index.html.erb.tt 파일을 다음 내용으로 만들어 보겠습니다:

<%% @<%= plural_table_name %>.count %> <%= human_name.pluralize %>

주목할 점은 템플릿이 다른 ERB 템플릿을 렌더링하는 ERB 템플릿이라는 것입니다. 따라서 결과 템플릿에 나타나야 할 <%생성기 템플릿에서 <%%로 이스케이프되어야 합니다.

이제 Rails의 기본 제공 스캐폴드 생성기를 실행해 보겠습니다:

$ bin/rails generate scaffold Post title:string
      ...
      create      app/views/posts/index.html.erb
      ...

app/views/posts/index.html.erb의 내용은 다음과 같습니다:

<% @posts.count %> Posts

Rails 생성기 재정의

Rails의 기본 제공 생성기는 config.generators를 통해 구성할 수 있으며, 일부 생성기를 완전히 재정의할 수도 있습니다.

먼저 스캐폴드 생성기가 어떻게 작동하는지 자세히 살펴보겠습니다.

$ bin/rails generate scaffold User name:string
      invoke  active_record
      create    db/migrate/20230518000000_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app계속해서 Rails 생성기 재정의에 대한 내용을 번역하겠습니다.

Rails의 기본 제공 생성기는 [`config.generators`][]를 통해 구성할 수 있으며, 일부 생성기를 완전히 재정의할 수도 있습니다.

먼저 스캐폴드 생성기가 어떻게 작동하는지 자세히 살펴보겠습니다.

```bash
$ bin/rails generate scaffold User name:string
      invoke  active_record
      create    db/migrate/20230518000000_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      create      app/views/users/_user.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      create      test/system/users_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder

출력 결과를 보면 스캐폴드 생성기가 다른 생성기들을 호출하고 있음을 알 수 있습니다. 예를 들어 scaffold_controller 생성기가 호출되고, 그 생성기 또한 다른 생성기들을 호출합니다. 특히 scaffold_controller 생성기는 helper 생성기를 호출합니다.

이제 기본 제공 helper 생성기를 새로운 my_helper 생성기로 재정의해 보겠습니다.

$ bin/rails generate generator rails/my_helper
      create  lib/generators/rails/my_helper
      create  lib/generators/rails/my_helper/my_helper_generator.rb
      create  lib/generators/rails/my_helper/USAGE
      create  lib/generators/rails/my_helper/templates
      invoke  test_unit
      create    test/lib/generators/rails/my_helper_generator_test.rb

그리고 lib/generators/rails/my_helper/my_helper_generator.rb에서 생성기를 다음과 같이 정의합니다:

class Rails::MyHelperGenerator < Rails::Generators::NamedBase
  def create_helper_file
    create_file "app/helpers/#{file_name}_helper.rb", <<~RUBY
      module #{class_name}Helper
        # I'm helping!
      end
    RUBY
  end
end

마지막으로 Rails가 기본 제공 helper 생성기 대신 my_helper 생성기를 사용하도록 config.generators에 설정해야 합니다. config/application.rb에 다음을 추가합니다:

config.generators do |g|
  g.helper :my_helper
end

이제 다시 스캐폴드 생성기를 실행하면 my_helper 생성기가 사용되는 것을 확인할 수 있습니다:

$ bin/rails generate scaffold Article body:text
      ...
      invoke  scaffold_controller
      ...
      invoke    my_helper
      create      app/helpers/articles_helper.rb
      ...

참고: 기본 제공 helper 생성기의 출력에는 “invoke testunit"이 포함되지만, `myhelper의 출력에는 포함되지 않습니다.helper생성기는 기본적으로 테스트를 생성하지 않지만, [hookfor][]를 사용하여 테스트 생성을 위한 훅을 제공합니다.MyHelperGenerator클래스에hookfor :testframework, as: :helper를 포함하여 동일한 기능을 구현할 수 있습니다.hookfor` 문서를 참고하세요.

생성기 대체 방법

특정 생성기를 재정의하는 또 다른 방법은 대체 방법(fallbacks)을 사용하는 것입니다. 대체 방법을 통해 생성기 네임스페이스를 다른 생성기 네임스페이스에 위임할 수 있습니다.

예를 들어 test_unit:model 생성기를 my_test_unit:model 생성기로 재정의하고 싶지만, test_unit:* 생성기 전체를 대체하고 싶지는 않다고 가정해 보겠습니다.

먼저 lib/generators/my_test_unit/model/model_generator.rbmy_test_unit:model 생성기를 만듭니다:

module MyTestUnit
  class ModelGenerator < Rails::Generators::NamedBase
    source_root File.expand_path("templates", __dir__)

    def do_different_stuff
      say "Doing different stuff..."
    end
  end
end

다음으로 config.generators를 사용하여 test_framework 생성기를 my_test_unit으로 구성하고, my_test_unit:* 생성기가 해결되지 않으면 test_unit:*으로 대체되도록 설정합니다:

config.generators do |g|
  g.test_framework :my_test_unit, fixture: false
  g.fallbacks[:my_test_unit] = :test_unit
end

이제 스캐폴드 생성기를 실행하면 my_test_unittest_unit을 대체했지만, 모델 테스트만 영향을 받았음을 확인할 수 있습니다:

$ bin/rails generate scaffold Comment body:text
      invoke  active_record
      create    db/migrate/20230518000000_create_comments.rb
      create    app/models/comment.rb
      invoke    my_test_unit
    Doing different stuff...
      invoke  resource_route
       route    resources :comments
      invoke  scaffold_controller
      create    app/controllers/comments_controller.rb
      invoke    erb
      create      app/views/comments
      create      app/views/comments/index.html.erb
      create      app/views/comments/edit.html.erb
      create      app/views/comments/show.html.erb
      create      app/views/comments/new.html.erb
      create      app/views/comments/_form.html.erb
      create      app/views/comments/_comment.html.erb
      invoke    resource_route
      invoke    my_test_unit
      create      test/controllers/comments_controller_test.rb
      create      test/system/comments_test.rb
      invoke    helper
      create      app/helpers/comments_helper.rb
      invoke      my_test_unit
      invoke    jbuilder
      create      app/views/comments/index.json.jbuilder
      create      app/views/comments/show.json.jbuilder

애플리케이션 템플릿

애플리케이션 템플릿은 특별한 종류의 생성기입니다. 생성기 헬퍼 메서드를 모두 사용할 수 있지만, Ruby 클래스 대신 Ruby 스크립트로 작성됩니다. 다음은 예시입니다:

# template.rb

if yes?("Would you like to install Devise?")
  gem "devise"
  devise_model = ask("What would you like the user model to be called?", default: "User")
end

after_bundle do
  if devise_model
    generate "devise:install"
    generate "devise", devise_model
    rails_command "db:migrate"
  end

  git add: ".", commit: %(-m 'Initial commit')
end

먼저 템플릿은 사용자에게 Devise를 설치할지 묻습니다. 사용자가 "yes”(또는 “y”)로 답하면 Devise를 Gemfile에 추가하고 Devise 사용자 모델의 이름을 묻습니다(기본값은 User). 이후 bundle install이 실행된 후에는 Devise 모델이 지정된 경우 Devise 생성기와 rails db:migrate를 실행합니다. 마지막으로 템플릿은 전체 앱 디렉토리를 git add하고 git commit합니다.

우리의 템플릿은 새 Rails 애플리케이션을 생성할 때 -m 옵션을 사용하여 실행할 수 있습니다:

$ rails new my_cool_app -m path/to/template.rb

또는 기존 애플리케이션 내에서 bin/rails app:template를 사용하여 실행할 수 있습니다:

$ bin/rails app:template LOCATION=path/to/template.rb

템플릿은 로컬에 저장할 필요가 없으며, URL을 지정할 수도 있습니다:

$ rails new my_cool_app -m http://example.com/template.rb
$ bin/rails app:template LOCATION=http://example.com/template.rb

생성기 헬퍼 메서드

Thor는 [Thor::Actions][]를 통해 많은 생성기 헬퍼 메서드를 제공합니다. 예:

이 외에도 Rails는 Rails::Generators::Actions를 통해 많은 헬퍼 메서드를 제공합니다. 예:

생성기 테스트

Rails는 Rails::Generators::Testing::Behaviour를 통해 테스트 헬퍼 메서드를 제공합니다. 예:

생성기에 대한 테스트를 실행할 때는 디버깅 도구가 작동하도록 RAILS_LOG_TO_STDOUT=true를 설정해야 합니다.

RAILS_LOG_TO_STDOUT=true ./bin/test test/generators/actions_test.rb

이 외에도 Rails는 Rails::Generators::Testing::Assertions를 통해 추가적인 어서션을 제공합니다.