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.rb
에 my_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_unit
이 test_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을 제공합니다.