rubyonrails.org에서 더 보기:

GitHub에서 이 파일을 읽지 마세요. 가이드는 https://guides.rubyonrails.org 에서 제공됩니다.

Rails Generator 생성 및 커스터마이징

Rails generator는 워크플로우를 개선하기 위한 필수적인 도구입니다. 이 가이드를 통해 generator를 생성하고 기존의 generator를 커스터마이징하는 방법을 배우게 됩니다.

이 가이드를 읽은 후에는 다음 내용을 알게 될 것입니다:

  • 애플리케이션에서 사용 가능한 generator를 확인하는 방법
  • 템플릿을 사용하여 generator를 생성하는 방법
  • Rails가 generator를 실행하기 전에 어떻게 검색하는지
  • generator 템플릿을 재정의하여 scaffold를 커스터마이징하는 방법
  • generator를 재정의하여 scaffold를 커스터마이징하는 방법
  • 대규모 generator 세트의 덮어쓰기를 피하기 위한 fallback 사용 방법
  • 애플리케이션 템플릿을 생성하는 방법

1 첫 접근

rails 명령어를 사용하여 애플리케이션을 생성할 때, 실제로는 Rails generator를 사용하고 있는 것입니다. 그 후에 bin/rails generate 명령어를 실행하여 사용 가능한 모든 generator의 목록을 볼 수 있습니다:

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

Rails 애플리케이션을 생성하기 위해서는 gem install rails를 통해 설치된 Rails 버전을 사용하는 rails 글로벌 명령어를 사용합니다. 애플리케이션 디렉토리 안에서는 애플리케이션에 번들링된 Rails 버전을 사용하는 bin/rails 명령어를 사용합니다.

Rails와 함께 제공되는 모든 generator의 목록을 확인할 수 있습니다. 특정 generator에 대한 자세한 설명을 보려면 --help 옵션과 함께 generator를 실행하세요. 예를 들어:

$ bin/rails generate scaffold --help

참고: 모든 generator와 마찬가지로 scaffold도 Ruby on Rails가 제공하는 --help 옵션을 지원합니다. --help 옵션을 통해 scaffold와 관련된 이용 가능한 모든 옵션을 확인할 수 있습니다.

2 Creating Your First Generator

Generator는 강력한 파싱 옵션과 파일을 다루기 위한 훌륭한 API를 제공하는 Thor 기반으로 만들어졌습니다.

config/initializers 안에 initializer.rb라는 initializer 파일을 생성하는 generator를 만들어봅시다. 첫 번째 단계는 lib/generators/initializer_generator.rb에 다음 내용으로 파일을 생성하는 것입니다:

class InitializerGenerator < Rails::Generators::Base
  def create_initializer_file
    create_file "config/initializers/initializer.rb", <<~RUBY
      # 여기에 초기화 내용을 추가하세요
    RUBY
  end
end

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

우리의 새로운 generator를 실행하기 위해서는 다음을 실행합니다:

$ bin/rails generate initializer

계속 진행하기 전에, 우리의 새로운 generator에 대한 설명을 살펴보겠습니다:

$ bin/rails generate initializer --help

Rails는 일반적으로 ActiveRecord::Generators::ModelGenerator와 같이 namespace가 있는 generator에 대해서는 적절한 description을 유추할 수 있지만, 이 경우에는 그렇지 않습니다. 이 문제는 두 가지 방법으로 해결할 수 있습니다. 첫 번째 방법은 우리의 generator 내부에서 desc를 호출하는 것입니다:

class InitializerGenerator < Rails::Generators::Base
  desc "이 generator는 config/initializers에 initializer 파일을 생성합니다"
  def create_initializer_file
    create_file "config/initializers/initializer.rb", <<~RUBY
      # 초기화 내용을 여기에 추가하세요
    RUBY
  end
end

--help를 새로운 generator에 실행하면 새로운 설명을 볼 수 있습니다.

설명을 추가하는 두 번째 방법은 generator와 같은 디렉토리에 USAGE라는 파일을 만드는 것입니다. 다음 단계에서 이 작업을 수행할 것입니다.

3 Generators로 Generators 만들기

Generator 자체도 generator를 가지고 있습니다. 우리의 InitializerGenerator를 제거하고 bin/rails generate generator를 사용하여 새로운 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

다음은 방금 생성된 generator입니다:

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

제너레이터가 Rails::Generators::Base 대신 Rails::Generators::NamedBase를 상속받는다는 점에 먼저 주목하세요. 이는 우리의 제너레이터가 최소 하나의 인자를 기대하며, 이 인자는 initializer의 이름이 될 것이고 name을 통해 우리의 코드에서 사용할 수 있다는 것을 의미합니다.

새로운 제너레이터의 설명을 확인해보면 이를 알 수 있습니다:

$ bin/rails generate initializer --help 
사용법:
  bin/rails generate initializer NAME [options]

또한 generator가 source_root라는 클래스 메서드를 가지고 있다는 것을 확인하세요. 이 메서드는 템플릿의 위치를 가리키며, 기본적으로 방금 생성된 lib/generators/initializer/templates 디렉토리를 가리킵니다.

generator 템플릿이 어떻게 작동하는지 이해하기 위해서, 다음 내용으로 lib/generators/initializer/templates/initializer.rb 파일을 생성해봅시다:

# 여기에 초기화 내용 추가

아래와 같이 생성자(generator)를 호출할 때 이 템플릿을 복사하도록 변경해봅시다:

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

  def copy_initializer_file
    # initializer 파일을 config/initializers 디렉터리에 복사합니다
    copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
  end
end

이제 generator를 실행해봅시다:

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

$ cat config/initializers/core_extensions.rb
# 여기에 초기화 내용을 추가하세요

copy_file이 우리의 템플릿 내용으로 config/initializers/core_extensions.rb를 생성한 것을 볼 수 있습니다. (목적지 경로에서 사용된 file_name 메서드는 Rails::Generators::NamedBase에서 상속받은 것입니다.)

4 Generator 커맨드 라인 옵션

Generator는 [class_option][]을 사용하여 커맨드 라인 옵션을 지원할 수 있습니다. 예를 들어:

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

이 예시에서 scope class_option은 "app" 기본값을 가진 string 타입으로 정의됩니다.

이제 우리의 generator는 --scope 옵션으로 호출될 수 있습니다:

$ bin/rails generate initializer theme --scope dashboard

생성기 메소드에서 [options][]을 통해 옵션값에 접근할 수 있습니다.

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

initializer 파일을 복사하고 options에서 "scope" 값을 @scope 인스턴스 변수에 할당합니다.

5 Generator Resolution

Rails는 generator의 이름을 해석할 때 여러 파일명을 사용하여 generator를 찾습니다. 예를 들어 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

이 중 어느 것도 찾지 못하면 에러가 발생합니다.

우리는 generator를 애플리케이션의 lib/ 디렉토리에 넣습니다. 이 디렉토리가 $LOAD_PATH에 있어서 Rails가 파일을 찾고 로드할 수 있기 때문입니다.

6 Rails Generator 템플릿 오버라이딩

Rails는 generator 템플릿 파일을 해석할 때도 여러 위치를 찾습니다. 그 중 하나가 애플리케이션의 lib/templates/ 디렉토리입니다. 이 동작으로 Rails의 내장 generator가 사용하는 템플릿을 오버라이드할 수 있습니다. 예를 들어 scaffold controller template이나 scaffold view templates을 오버라이드할 수 있습니다.

이것을 실제로 확인하기 위해, 다음 내용으로 lib/templates/erb/scaffold/index.html.erb.tt 파일을 만들어봅시다:

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

템플릿은 다른 ERB 템플릿을 렌더링하는 ERB 템플릿이라는 점에 주의하세요. 따라서 최종 템플릿에 나타나야 하는 <%generator 템플릿에서 <%%로 이스케이프되어야 합니다.

이제 Rails의 내장 scaffold generator를 실행해보겠습니다:

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

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

<% @posts.count %> 포스트

7 Overriding Rails Generators

Rails의 내장 generator들은 일부 generator를 완전히 덮어쓰는 것을 포함하여 config.generators를 통해 설정할 수 있습니다.

먼저, scaffold generator가 어떻게 동작하는지 자세히 살펴보겠습니다.

$ 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 제너레이터가 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에서 generator를 다음과 같이 정의합니다:

class Rails::MyHelperGenerator < Rails::Generators::NamedBase
  def create_helper_file
    create_file "app/helpers/#{file_name}_helper.rb", <<~RUBY
      module #{class_name}Helper
        # 도움을 주고 있습니다!
      end
    RUBY
  end
end

마지막으로, Rails에게 내장된 helper generator 대신 my_helper generator를 사용하도록 알려주어야 합니다. 이를 위해 config.generators를 사용합니다. config/application.rb에 다음을 추가합시다:

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

이제 scaffold generator를 다시 실행하면 my_helper generator가 실행되는 것을 볼 수 있습니다:

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

기본 제공되는 helper 제너레이터의 출력에는 "invoke test_unit"이 포함되어 있지만, my_helper의 출력에는 포함되어 있지 않음을 알 수 있습니다. helper 제너레이터는 기본적으로 테스트를 생성하지 않지만, hook_for를 사용하여 테스트를 생성할 수 있는 훅을 제공합니다. MyHelperGenerator 클래스에 hook_for :test_framework, as: :helper를 포함시켜 동일한 작업을 수행할 수 있습니다. 자세한 내용은 hook_for 문서를 참조하세요.

7.1 Generators Fallbacks

generator를 오버라이드하는 또 다른 방법은 fallbacks 를 사용하는 것입니다. fallback을 사용하면 generator namespace가 다른 generator namespace로 위임할 수 있습니다.

예를 들어, test_unit:model generator를 우리가 만든 my_test_unit:model generator로 오버라이드하고 싶지만, test_unit:controller와 같은 다른 test_unit:* generator는 교체하고 싶지 않다고 가정해봅시다.

먼저 lib/generators/my_test_unit/model/model_generator.rbmy_test_unit:model generator를 생성합니다:

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

    def do_different_stuff
      say "다른 작업을 수행하는 중..."
    end
  end
end

다음으로, config.generators를 사용하여 test_framework generator를 my_test_unit로 구성하지만, my_test_unit:* generator가 누락된 경우 test_unit:*로 해결되도록 fallback을 구성합니다:

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

:my_test_unit이 설치되지 않은 경우 :test_unit이 사용되도록 구성합니다.

scaffold generator를 실행하면 my_test_unittest_unit을 대체했지만 model test만 영향을 받은 것을 볼 수 있습니다:

$ 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
    여러가지 작업을 수행중...
      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

8 Application Templates

Application template은 특별한 종류의 generator입니다. generator helper methods를 모두 사용할 수 있지만, Ruby 클래스 대신 Ruby 스크립트로 작성됩니다. 다음은 예시입니다:

# template.rb

if yes?("Devise를 설치하시겠습니까?") 
  gem "devise"
  devise_model = ask("사용자 모델의 이름을 무엇으로 하시겠습니까?", 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")로 답하면, 템플릿은 Gemfile에 Devise를 추가하고, 사용자에게 Devise user 모델의 이름을 물어봅니다(기본값은 User입니다). 이후 bundle install이 실행된 다음, Devise 모델이 지정되었다면 템플릿은 Devise 생성기와 rails db:migrate를 실행합니다. 마지막으로, 템플릿은 전체 앱 디렉토리를 git add하고 git commit합니다.

우리는 rails new 명령어에 -m 옵션을 전달하여 새로운 Rails 애플리케이션을 생성할 때 템플릿을 실행할 수 있습니다:

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

또는 bin/rails app:template로 기존 애플리케이션 안에서 템플릿을 실행할 수 있습니다:

$ bin/rails app:template LOCATION=template.rb의/경로

템플릿은 로컬에 저장되지 않아도 됩니다 - 경로 대신 URL을 지정할 수 있습니다:

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

9 Generator Helper Methods

Thor는 Thor::Actions를 통해 다음과 같은 많은 generator helper method를 제공합니다:

이러한 메소드 외에도 Rails는 Rails::Generators::Actions를 통해 많은 helper method를 제공합니다:

10 Testing Generators

Rails는 Rails::Generators::Testing::Behaviour를 통해 테스트 helper method를 제공합니다:

generator에 대한 테스트를 실행하는 경우, 디버깅 도구가 작동하도록 하려면 RAILS_LOG_TO_STDOUT=true를 설정해야 합니다.

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

이들 외에도 Rails는 Rails::Generators::Testing::Assertions을 통해 추가적인 assertion을 제공합니다.



맨 위로