rubyonrails.org에서 더 보기:

Rails 플러그인 개발의 기초

Rails 플러그인은 코어 프레임워크를 확장하거나 수정하는 것입니다. 플러그인은 다음을 제공합니다:

  • 개발자들이 안정적인 코드베이스에 영향을 주지 않고 최신 아이디어를 공유할 수 있는 방법
  • 코드 단위를 자체 릴리스 일정에 따라 수정하거나 업데이트할 수 있는 세분화된 아키텍처
  • 코어 개발자들이 모든 멋진 새로운 기능을 포함하지 않아도 되는 배출구

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

  • 처음부터 플러그인을 만드는 방법
  • 플러그인에 대한 테스트를 작성하고 실행하는 방법

이 가이드는 다음과 같은 테스트 주도 플러그인을 만드는 방법을 설명합니다:

  • Hash와 String 같은 Ruby 코어 클래스 확장
  • acts_as 플러그인의 전통을 따라 ApplicationRecord에 메서드 추가
  • 플러그인에서 generator를 어디에 두어야 하는지에 대한 정보 제공

이 가이드의 목적을 위해 잠시 당신이 열렬한 조류 관찰가라고 가정해봅시다. 당신이 가장 좋아하는 새는 Yaffle이고, 다른 개발자들도 Yaffle의 좋은 점을 공유할 수 있게 해주는 플러그인을 만들고 싶습니다.

1 설정

현재 Rails 플러그인은 gem으로 만들어지며, 이를 gemified plugins라고 합니다. RubyGems와 Bundler를 사용하면 원하는 경우 서로 다른 Rails 애플리케이션에서 공유할 수 있습니다.

1.1 Gemified Plugin 생성하기

Rails는 rails plugin new 명령어를 제공합니다. 이 명령어는 dummy Rails 애플리케이션을 사용하여 통합 테스트를 실행할 수 있는 모든 종류의 Rails extension을 개발하기 위한 뼈대를 생성합니다. 다음 명령어로 plugin을 생성할 수 있습니다:

$ rails plugin new yaffle

help를 입력해서 사용법과 옵션을 확인해보세요:

$ rails plugin new --help
Usage:
  rails plugin new APP_PATH [options]

Options:
  -r, [--ruby=PATH]                                      # Ruby 바이너리를 위한 경로
                                                        # Default: /Users/fxn/.rvm/rubies/ruby-2.7.2/bin/ruby
      [--skip-bundle], [--no-skip-bundle]              # bundle install 실행을 건너뜀
  -B, [--skip-bundle]                                  # bundle install 실행을 건너뜀
      [--skip-gemfile], [--no-skip-gemfile]           # Gemfile 생성을 건너뜀
  -G, [--skip-gemfile]                                # Gemfile 생성을 건너뜀
  -O, [--skip-active-record]                          # Active Record 파일을 건너뜀
      [--skip-keeps], [--no-skip-keeps]               # .keep 파일 생성을 건너뜀
      [--skip-action-mailer], [--no-skip-action-mailer]  # Action Mailer 파일을 건너뜀
  -M, [--skip-action-mailer]                          # Action Mailer 파일을 건너뜀
      [--skip-active-storage], [--no-skip-active-storage]  # Active Storage 파일을 건너뜀
      [--skip-puma], [--no-skip-puma]                # Puma 관련 파일을 건너뜀
      [--skip-action-cable], [--no-skip-action-cable]  # Action Cable 파일을 건너뜀
      [--skip-sprockets], [--no-skip-sprockets]        # Sprockets 파일을 건너뜀
      [--skip-spring], [--no-skip-spring]            # Spring 적용을 건너뜀
      [--skip-listen], [--no-skip-listen]            # Listen gem을 건너뜀
      [--skip-javascript], [--no-skip-javascript]    # JavaScript 파일을 건너뜀
  -J, [--skip-javascript]                            # JavaScript 파일을 건너뜀
      [--skip-turbolinks], [--no-skip-turbolinks]    # Turbolinks를 건너뜀
      [--skip-test], [--no-skip-test]                # 테스트 파일을 건너뜀
  -T, [--skip-test]                                  # 테스트 파일을 건너뜀
      [--skip-system-test], [--no-skip-system-test]  # System test 파일을 건너뜀
      [--skip-bootsnap], [--no-skip-bootsnap]        # Bootsnap gem을 건너뜀
      [--dev], [--no-dev]                            # 개발 의존성을 설정
      [--edge], [--no-edge]                          # master branch에서 Rails를 설정
      [--master], [--no-master]                      # master branch에서 Rails를 설정 (--edge의 별칭)
      [--rc=RC]                                      # rails rc 파일 경로
      [--no-rc], [--no-no-rc]                        # rails rc 파일 실행을 건너뜀
      [--database=DATABASE]                          # 사용할 데이터베이스를 설정
                                                    # Default: sqlite3
  -d, [--database=DATABASE]                          # 지정된 데이터베이스를 선택(mysql/postgresql/sqlite3)  
      [--skip-namespace], [--no-skip-namespace]      # 모듈 네임스페이스를 건너뜀
      [--skip-collision-check], [--no-skip-collision-check]  # 충돌 검사를 건너뜀
      [--mountable], [--no-mountable]                # 마운트 가능한 엔진 생성
      [--full], [--no-full]                          # 전체 엔진 생성
      [--dummy-path=DUMMY_PATH]                      # 더미 앱 경로
                                                    # Default: test/dummy
      [--help], [--no-help]                          # 도움말 표시
      [--quiet], [--no-quiet]                        # 진행상황 메시지 감추기

Runtime options:
  -f, [--force]    # 기존 파일을 덮어씀
  -p, [--pretend]  # 실행할 작업을 나열하지만 실제로는 아무것도 하지 않음
  -q, [--quiet]    # 진행상황 메시지 표시 억제
  -s, [--skip]     # 이미 존재하는 파일 건너뜀

Rails options:
  -h, [--help]     # 도움말 메시지 표시
  -v, [--version]  # 버전 표시

Description:
    플러그인 생성기는 Rails 플러그인을 위한 골격 구조를 생성합니다. 
    이를 통해 자신만의 Rails 확장 기능을 패키징할 수 있습니다.

Example:
    rails plugin new ~/Code/Ruby/blog

    이는 ~/Code/Ruby/blog 디렉토리에 Rails 플러그인을 생성합니다.

2 새로 생성한 Plugin 테스트하기

plugin이 포함된 디렉토리로 이동하여 yaffle.gemspec 파일을 편집하고 TODO 값이 있는 모든 줄을 교체하세요:

spec.homepage    = "http://example.com"
spec.summary     = "Yaffle의 요약"
spec.description = "Yaffle의 설명"

...

spec.metadata["source_code_uri"] = "http://example.com"
spec.metadata["changelog_uri"] = "http://example.com"

그런 다음 bundle install 명령어를 실행하세요.

이제 bin/test 명령어를 사용하여 테스트를 실행할 수 있으며, 다음과 같이 표시됩니다:

$ bin/test
...
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

모든 것이 적절하게 생성되었다는 것을 알려주며, 이제 기능을 추가할 준비가 되었습니다.

3 Extending Core Classes

이 섹션에서는 Rails 애플리케이션 어디에서나 사용할 수 있는 메소드를 String에 추가하는 방법을 설명합니다.

이 예제에서는 to_squawk라는 메소드를 String에 추가할 것입니다. 먼저, 몇 가지 assertion이 포함된 새로운 테스트 파일을 생성하세요:

# yaffle/test/core_ext_test.rb

require "test_helper"

class CoreExtTest < ActiveSupport::TestCase
  def test_to_squawk_prepends_the_word_squawk
    assert_equal "squawk! Hello World", "Hello World".to_squawk
  end
end

bin/test를 실행하여 테스트를 실행하세요. to_squawk 메서드를 아직 구현하지 않았기 때문에 이 테스트는 실패해야 합니다:

$ bin/test
E

Error:
CoreExtTest#test_to_squawk_prepends_the_word_squawk:
NoMethodError: "Hello World":String에 대해 정의되지 않은 메서드 `to_squawk'입니다


bin/test /path/to/yaffle/test/core_ext_test.rb:4

.

0.003358초 만에 완료, 595.6483 실행/초, 297.8242 검증/초.
2 실행, 1 검증, 0 실패, 1 에러, 0 스킵

좋습니다 - 이제 개발을 시작할 준비가 되었습니다.

lib/yaffle.rb에서 require "yaffle/core_ext"를 추가하세요:

# yaffle/lib/yaffle.rb

require "yaffle/version"
require "yaffle/railtie" 
require "yaffle/core_ext"

module Yaffle
  # 여기에 코드를 작성하세요...
end

마지막으로 core_ext.rb 파일을 생성하고 to_squawk 메서드를 추가합니다:

# yaffle/lib/yaffle/core_ext.rb

class String
  def to_squawk
    "squawk! #{self}".strip
  end
end

작성한 메서드가 의도한대로 동작하는지 확인하려면 plugin 디렉토리에서 bin/test를 실행하여 unit test를 수행하세요.

$ bin/test
...
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

test/dummy 디렉토리로 이동하여 bin/rails console을 시작하고 squawking을 시작해보세요:

irb> "Hello World".to_squawk
=> "squawk! Hello World"

4 acts_as 메서드를 Active Record에 추가하기

plugin에서 흔히 볼 수 있는 패턴은 model에 acts_as_something이라는 메서드를 추가하는 것입니다. 이 경우에는 Active Record model에 squawk 메서드를 추가하는 acts_as_yaffle이라는 메서드를 작성하려고 합니다.

시작하기 위해 다음과 같이 파일을 설정하세요:

# yaffle/test/acts_as_yaffle_test.rb

require "test_helper"

class ActsAsYaffleTest < ActiveSupport::TestCase
end
# yaffle/lib/yaffle.rb

require "yaffle/version"
require "yaffle/railtie"
require "yaffle/core_ext" 
require "yaffle/acts_as_yaffle"

module Yaffle
  # 여기에 코드를 작성하세요...
end
# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
  end
end

4.1 Class Method 추가하기

이 플러그인은 모델에 last_squawk라는 이름의 메소드를 추가했다고 가정합니다. 하지만 플러그인 사용자들이 이미 자신의 모델에 다른 용도로 쓰이는 last_squawk라는 이름의 메소드를 정의했을 수 있습니다. 이 플러그인은 yaffle_text_field라는 이름의 class method를 추가하여 이름을 변경할 수 있도록 합니다.

시작하기 위해, 원하는 동작을 보여주는 실패하는 테스트를 작성해봅시다:

# yaffle/test/acts_as_yaffle_test.rb

require "test_helper"

class ActsAsYaffleTest < ActiveSupport::TestCase
  def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
    assert_equal "last_squawk", Hickwall.yaffle_text_field
  end

  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet 
    assert_equal "last_tweet", Wickwall.yaffle_text_field
  end
end

bin/test를 실행하면 다음과 같이 표시됩니다:

$ bin/test
# Running:

..E

에러:
ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
NameError: 초기화되지 않은 상수 ActsAsYaffleTest::Wickwall


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8

E

에러:
ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
NameError: 초기화되지 않은 상수 ActsAsYaffleTest::Hickwall


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4



0.004812초 만에 완료됨, 831.2949 실행/초, 415.6475 검증/초.
4 실행, 2 검증, 0 실패, 2 에러, 0 건너뜀

이는 우리가 테스트하려는 필요한 모델(Hickwall과 Wickwall)이 없다는 것을 알려줍니다. test/dummy 디렉토리에서 다음 명령어를 실행하여 우리의 "dummy" Rails 애플리케이션에서 이러한 모델들을 쉽게 생성할 수 있습니다:

$ cd test/dummy
$ bin/rails generate model Hickwall last_squawk:string
$ bin/rails generate model Wickwall last_squawk:string last_tweet:string

이제 dummy app으로 이동하여 testing database에 필요한 database table들을 migration하여 생성할 수 있습니다. 먼저 다음을 실행하세요:

$ cd test/dummy
$ bin/rails db:migrate

여기에서 Hickwall과 Wickwall 모델이 yaffles처럼 동작해야 한다는 것을 알 수 있도록 수정하세요.

# test/dummy/app/models/hickwall.rb

class Hickwall < ApplicationRecord
  acts_as_yaffle
end
# test/dummy/app/models/wickwall.rb

class Wickwall < ApplicationRecord
  acts_as_yaffle yaffle_text_field: :last_tweet
end

acts_as_yaffle 메서드를 정의하는 코드도 추가할 것입니다.

# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
    extend ActiveSupport::Concern

    class_methods do 
      def acts_as_yaffle(options = {})
      end
    end
  end
end
# test/dummy/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  include Yaffle::ActsAsYaffle

  self.abstract_class = true
end

그런 다음 plugin의 루트 디렉토리로 돌아가서(cd ../..) bin/test를 사용하여 test를 다시 실행할 수 있습니다.

$ bin/test
# 실행 중:

.E

오류:
ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
NoMethodError: #<Class:0x0055974ebbe9d8>에 대해 정의되지 않은 메서드 'yaffle_text_field'


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4

E

오류:
ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
NoMethodError: #<Class:0x0055974eb8cfc8>에 대해 정의되지 않은 메서드 'yaffle_text_field'


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8

.

0.008263초 만에 완료, 484.0999 실행/초, 242.0500 검증/초.
4회 실행, 2개 검증, 0개 실패, 2개 오류, 0개 건너뜀

더 가까워졌습니다... 이제 테스트를 통과하기 위해 acts_as_yaffle 메서드의 코드를 구현할 것입니다.

# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
    extend ActiveSupport::Concern

    class_methods do
      def acts_as_yaffle(options = {})
        cattr_accessor :yaffle_text_field, default: (options[:yaffle_text_field] || :last_squawk).to_s
      end
    end
  end
end
# test/dummy/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  include Yaffle::ActsAsYaffle

  self.abstract_class = true
end

bin/test를 실행하면 모든 테스트가 통과하는 것을 볼 수 있습니다:

$ bin/test
...
4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

4.2 인스턴스 메서드 추가하기

이 플러그인은 acts_as_yaffle을 호출하는 모든 Active Record 객체에 'squawk'라는 메서드를 추가합니다. 'squawk' 메서드는 데이터베이스의 필드 중 하나의 값을 단순히 설정합니다.

시작하기 위해, 원하는 동작을 보여주는 실패하는 테스트를 작성하세요:

# yaffle/test/acts_as_yaffle_test.rb
require "test_helper"

class ActsAsYaffleTest < ActiveSupport::TestCase
  def test_a_hickwalls_yaffle_text_field_should_be_last_squawk 
    assert_equal "last_squawk", Hickwall.yaffle_text_field
  end

  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
    assert_equal "last_tweet", Wickwall.yaffle_text_field
  end

  def test_hickwalls_squawk_should_populate_last_squawk
    hickwall = Hickwall.new
    hickwall.squawk("Hello World") 
    assert_equal "squawk! Hello World", hickwall.last_squawk
  end

  def test_wickwalls_squawk_should_populate_last_tweet
    wickwall = Wickwall.new
    wickwall.squawk("Hello World")
    assert_equal "squawk! Hello World", wickwall.last_tweet
  end
end

마지막 두 개의 테스트가 "NoMethodError: undefined method `squawk'"라는 에러와 함께 실패하는지 확인하기 위해 테스트를 실행하고, acts_as_yaffle.rb를 다음과 같이 업데이트하세요:

# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
    extend ActiveSupport::Concern

    included do
      def squawk(string)
        write_attribute(self.class.yaffle_text_field, string.to_squawk) 
      end
    end

    class_methods do
      def acts_as_yaffle(options = {})
        cattr_accessor :yaffle_text_field, default: (options[:yaffle_text_field] || :last_squawk).to_s
      end
    end
  end
end
# test/dummy/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  include Yaffle::ActsAsYaffle

  self.abstract_class = true
end

bin/test를 마지막으로 한 번 더 실행하면 다음과 같이 표시됩니다:

$ bin/test
...
6회 실행, 6개 assertion, 0건 실패, 0건 오류, 0건 건너뜀

모델의 필드에 쓰기 위해 write_attribute를 사용하는 것은 plugin이 모델과 상호 작용하는 방법의 한 예시일 뿐이며, 항상 사용할 수 있는 올바른 메서드는 아닙니다. 예를 들어, 다음과 같이 사용할 수도 있습니다:

send("#{self.class.yaffle_text_field}=", string.to_squawk)

5 Generators

Generator들은 플러그인의 lib/generators 디렉토리에 생성하기만 하면 gem에 포함시킬 수 있습니다. Generator 생성에 대한 자세한 정보는 Generators 가이드에서 확인할 수 있습니다.

6 Publishing Your Gem

개발 중인 gem 플러그인은 Git 저장소에서 쉽게 공유할 수 있습니다. Yaffle gem을 다른 사람들과 공유하려면 코드를 Git 저장소(GitHub 등)에 커밋하고 해당 애플리케이션의 Gemfile에 한 줄을 추가하기만 하면 됩니다:

gem "yaffle", git: "https://github.com/rails/yaffle.git"

bundle install을 실행한 후에는 gem 기능을 애플리케이션에서 사용할 수 있게 됩니다.

gem이 정식 릴리스로 공유될 준비가 되면 RubyGems에 퍼블리시할 수 있습니다.

또는 Bundler의 Rake task들을 활용할 수도 있습니다. 다음과 같이 전체 목록을 확인할 수 있습니다:

$ bundle exec rake -T

$ bundle exec rake build
# pkg 디렉토리에 yaffle-0.1.0.gem 파일을 빌드합니다

$ bundle exec rake install
# yaffle-0.1.0.gem을 빌드하고 시스템 gems에 설치합니다 

$ bundle exec rake release
# v0.1.0 태그를 생성하고 yaffle-0.1.0.gem을 빌드하여 Rubygems에 푸시합니다

RubyGems에 gem을 배포하는 것에 대한 자세한 내용은 다음을 참조하세요: gem 배포하기.

7 RDoc 문서화

플러그인이 안정화되고 배포할 준비가 되면, 다른 사람들을 위해 문서화를 해주세요! 다행히도 플러그인 문서화는 쉽습니다.

첫 번째 단계는 플러그인 사용 방법에 대한 자세한 정보로 README 파일을 업데이트하는 것입니다. 포함해야 할 몇 가지 핵심 사항은 다음과 같습니다:

  • 당신의 이름
  • 설치 방법
  • 앱에 기능을 추가하는 방법(일반적인 사용 사례의 여러 예시)
  • 사용자들에게 도움이 되고 시간을 절약할 수 있는 경고, 주의사항 또는 팁

README가 완성되면, 개발자들이 사용할 모든 메서드에 RDoc 주석을 추가하세요. 또한 공개 API에 포함되지 않는 코드 부분에는 # :nodoc: 주석을 추가하는 것이 관례입니다.

주석 작성이 완료되면 플러그인 디렉토리로 이동하여 다음을 실행하세요:

$ bundle exec rake rdoc

7.1 참조



맨 위로