Techouse Developers Blog

テックハウス開発者ブログ|マルチプロダクト型スタートアップ|エンジニアによる技術情報を発信|SaaS、求人プラットフォーム、DX推進

Ruby on RailsでUIコンポーネント構築を効率化、ユーザ体験の仮説検証ループを爆速で回しちゃうぞ!

ogp

はじめに

こんにちは、2023 年からジョブハウスで業務委託のバックエンドエンジニアをしている jxmtst です。

今日は、ジョブハウスで使用している Ruby on RailsViewComponent を用いて UI コンポーネントを実装する際に利用しているライブラリを紹介します。

ViewComponent(UI コンポーネント)× Lookbook(プレビュー)× rspec-snapshot(スナップショットテスト)

という、フロントエンドエンジニアには馴染みのあるようなエコシステムを、Ruby on Rails 上で実現しています。

ViewComponent とは

ViewComponent は、もともと GitHub のエンジニアによって開発されました。 大規模な Rails アプリケーションを扱う中で、ビューに関するコードの複雑化やメンテナンス性の低下といった問題に直面し、より堅牢で再利用可能なコンポーネントの構築を求められたことが背景にあります。

ビューの複雑化と再利用性の向上

Rails の従来のテンプレートシステム(ERB など)では、ロジックとマークアップが混在しがちです。特に大規模なアプリケーションではビューのコードが膨大になりやすい問題がありました。ViewComponent は、ビューを小さなコンポーネントに分割することで、各コンポーネントが独立した役割を持ち、再利用やメンテナンスが容易になるよう設計されています。

コンポーネントベースの UI フレームワーク

ViewComponent の設計思想は、React や Vue.js などのコンポーネントベースの UI フレームワークに大きく影響を受けています。明確なインターフェースと単方向データフローの概念は、次のようなメリットを生み出します。

1. コードの可読性と理解の向上

コンポーネントの受け取る入力(引数やプロパティ)がはっきりしているため、どのデータがどの部分で使われるのかが明確になります。これにより、他の開発者がコードを読んだ際に、各コンポーネントの役割や動作をすぐに把握しやすくなります。

2. テストの効率化

明確なインターフェースにより、各コンポーネントを独立してテストしやすくなります。入力データと期待される出力が明確なため、ユニットテストを組み立てる際にシンプルかつ効果的なテストケースを作成でき、コンポーネント単位での検証が容易になります。

3. 再利用性の向上

コンポーネントが独立した役割と明確なデータ依存性を持つため、他の部分でもそのコンポーネントを安心して再利用できます。これにより、コードの重複を避け、開発効率が向上します。

4. 保守性の向上

データの流れが一方向であるため、どこでデータ変更されるかが明確になり、実装の影響範囲を把握しやすくなります。結果として、コードのメンテナンスが容易になり、長期的なプロジェクト運営においても安定した状態を保ちやすくなります。

これらのメリットにより、全体の開発効率や品質向上に大きく寄与します。

パフォーマンスの向上

また、ViewComponent はアプリケーションの起動時にテンプレートをあらかじめコンパイルすることで、partial による実装よりも最大 10 倍近くの高速化を実現しています。

サンプルを実装する

実際に動くプログラムで紹介するために、サンプルコードを用意しました。実装環境は下記の通りです。

Ruby 3.4.1

gem:
  rails (8.0.1)
  view_component (3.21.0)
  lookbook (2.3.4)
  rspec-snapshot (2.0.3)

ViewComponent を実装する

Ruby on Rails アプリケーションの Gemfileview_component を追加して、bundle install を実行します。

雛形を作るためには Generator が便利です。

rails generate component DialogComponent message type

rails generate コマンドを実行すると、雛形を作成できました。 type 引数によってアイコンの表示とメッセージボックスのカラーが変わる様なコンポーネントを実装します。

# app/components/dialog_component.rb

class DialogComponent < ApplicationComponent
  def initialize(message:, type:)
    @message = message
    @type = type
  end

  def icon_class
    case @type
    when :success then "fa-check-circle"
    when :warning then "fa-exclamation-triangle"
    when :error then "fa-times-circle"
    else
      "fa-info-circle"
    end
  end
end
# app/components/dialog_component.html.erb <%= turbo_frame_tag "dialog" do %>
<div class="alert alert-<%= @type %> rounded shadow-lg flex flex-nowrap">
  <i class="fas <%= icon_class %>"></i>
  <p><%= @message %></p>
</div>
<% end %>

turbo_frame_tag は、Ruby on Rails でよく利用される Turbo (Hotwire) のタグです。ここでは詳細は割愛します。

ボタンとメッセージボックス

ボタンを並べて、クリックすると対応するメッセージボックスを表示する様にしました。 メッセージボックス用には app/views 以下のファイルは用意しておらず、ViewComponent のインスタンスを render に渡しています。

# app/controllers/home_controller.rb
def show
  render DialogComponent.new(message:, type:)
end

テンプレートは .rb ファイルにインラインで書くこともできますが、 app/components 以下にコンポーネントクラスと同名の .erb.slim のテンプレートを使うこともできます。

Lookbook でプレビューを実装する

Lookbook は、UI コンポーネントやデザインシステムの構築・共有を目的としたツールです。 開発者とデザイナーが同じプラットフォーム上でコンポーネントのプレビューやドキュメント作成、フィードバックのやり取りを行える環境を提供します。

プレビューを表示するために、Lookbook::Preview を継承したプレビュークラスを実装します。

class DialogComponentPreview < Lookbook::Preview
  # @param dialog_type select { choices: ['info', 'success', 'warning', 'error'] }
  def switch(dialog_type: 'type')
    render DialogComponent.new(message: 'This is an info message.', type: dialog_type.to_sym)
  end

  # @!group List

  def info
    render DialogComponent.new(message: 'This is an info message.', type: 'info')
  end

  def success
    render DialogComponent.new(message: 'This is a success message.', type: 'success')
  end

  def warning
    render DialogComponent.new(message: 'This is an warning message.', type: 'warning')
  end

  def error
    render DialogComponent.new(message: 'This is an error message.', type: 'error')
  end

  # @!endgroup
end

引数によってプレビューを切り替えたり、複数のプレビューをグループ化して一覧表示が可能です。

dialog_type を選択してプレビューを変えられる

複数のプレビューを同時に見る事ができる

Lookbook はプレビューメソッドに処理を書くこともできるので、コンテキストにあった UI コンポーネントのプレビューを柔軟に実装できます。 また、実際に出力される HTML も確認できるので、実際のマークアップに問題がないかを確認可能です。

rspec-snapshot でスナップショットテストを実装する

自動テストにおけるスナップショットテストは、コンポーネントや UI の状態をその時点で記録し、以降の変更と比較する手法です。スナップショットは出力をそのまま保存するため、複雑なテストコードを書く必要がなく、変更の差分を気軽に把握できます。

rspec-snapshotを使うことで、RSpec でスナップショットを取得できます。また、ViewComponent と組み合わせると、必要な UI コンポーネントのみのスナップショットを取得が可能です。

また、spec/support に下記の様なコードを配置することで、RSpec ファイルの記述を簡素化できます。

# spec/support/snapshot_view_component.rb

require 'view_component/test_helpers'
require 'view_component/system_test_helpers'

RSpec.configure do |config|
  config.include ViewComponent::TestHelpers, type: :component
  config.include ViewComponent::SystemTestHelpers, type: :component

  config.after(:each, :snapshot, type: :component) do |example|
    raise 'component snapshot has no content' if rendered_content.blank?

    klass = example.metadata[:described_class].name
    testing = example.metadata[:full_description].gsub(klass, '').strip.gsub(' ', '_')
    snapshot = '%s/%s' % [ klass.underscore, testing ]

    expect(rendered_content).to match_snapshot(snapshot)
  end
end

type: :component の RSpec ファイル内で、it メソッドに :snapshot を第二引数で渡します。

初回は自動的にスナップショットを保存し、2 回目以降は保存したスナップショットとの比較をしてくれます。

RSpec.describe DialogComponent, type: :component do
  let(:message) { 'Hello, world!' }

  describe 'snapshot' do
    it('info', :snapshot) { render_inline(described_class.new(message:, type: :info)) }

    context 'success' do
      it('is', :snapshot) { render_inline(described_class.new(message:, type: :success)) }
    end

    context 'warning' do
      it('is', :snapshot) { render_inline(described_class.new(message:, type: :warning)) }
    end

    context 'error' do
      it('is', :snapshot) { render_inline(described_class.new(message:, type: :error)) }
    end
  end
end

実際に出力されたスナップショットはこちらのディレクトリで確認ができます。

スナップショットを更新したい場合は UPDATE_SNAPSHOTS の環境変数を渡すとスナップショットが更新されます。

UPDATE_SNAPSHOTS=true bundle exec rspec spec/path/to_spec.rb

スナップショットは気軽に現在の表示を保存しておけるので、差分の検出を把握するのにはとても便利です。 しかし、マークアップの内容をきちんと確認しておかないと、誤っているにもかかわらずテストが通ってしまうという、偽陰性を招いてしまう可能性が高いテストです。 注意して利用するようにしてください。

今後の課題

Storybook における(deprecated になってしまいましたが)Storyshots ではプレビューの結果をそのままキャプチャしてスナップショットにできます。 Lookbook と rspec-snapshot では、まだプレビューのスナップショット化は実現できていません。

UI のプレビューがそのままテストケースになると、プレビュー自体が最新のビジュアル状態を反映しているため、コードの変更やリファクタリング後に意図しない見た目の崩れや不整合がないかをすぐに検証できます。

また、プレビューそのものをテストケースとして利用できるため、テストの記述や更新の手間が省けます。UI の変更があった際も、スナップショットを更新するだけで済むため、メンテナンスが容易になります。

ViewComponent にデフォルトで付いているプレビュー機能ではスナップショットを取得できました。しかし、プレビュー用のレイアウトまでスナップショットに保存されてしまったり、Lookbook の方がプレビューを気軽に実装できるため採用には至っていません。

今後も ViewComponent のエコシステムまわりを充実させて UI コンポーネント実装の効率化を促進していくつもりです。

さいごに

ViewComponent × Lookbook × rspec-snapshot による、Ruby on Rails の UI コンポーネント実装のエコシステムについてお話しさせていただきました。

デザイン思考やアジャイルの文脈では、ユーザ視点のフィードバックを取り入れつつ、いかにユーザ体験を向上させるかという仮説検証ループを何度も重ねる必要があります。

Ruby on Rails では View のテンプレートがカオスになりがちです。ViewComponent を部分的にでも取り入れて、品質を保ちやすい UI 開発体験を試してみてはいかがでしょうか。

References

Techouse では、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。

jp.techouse.com