Slide 1

Slide 1 text

SIROK, Inc. リリース8年目のサービスの 1800個のERBファイルを ViewComponentに移行した方法とその結果 Kaigi on Rails 2024 Naoyuki Kataoka (@katty0324) 1

Slide 2

Slide 2 text

SIROK, Inc. 自己紹介 片岡 直之 / Naoyuki Kataoka (  katty0324) 株式会社シロク CTO Excel VBAでプログラミングを始める 大学院生の頃に出会った仲間と株式会社シロクを立ち上げ アプリ、SaaS、広告事業などを経て、現在はブランド事業 昨年からコーチングに興味をもってトレーニング中 先月に2人目の子どもが生まれて、「技術×事業×育児」で奮闘中 2

Slide 3

Slide 3 text

SIROK, Inc. 私たちのサービスについて 2017年に、スキンケアブランドN organicを発売 N organicは、肌と心を満たすことで ありのままの自分を好きになるブランド スキンケア、ヘアケア、メイクアップなど100点以上の商品 自社商品を販売するシロクオンラインショップを開発・運営 Ruby on Railsで開発 3

Slide 4

Slide 4 text

SIROK, Inc. 課題 4

Slide 5

Slide 5 text

SIROK, Inc. 8年間のアップデートを経た課題 2017年のサービスローンチから8年目 10,000回以上のリリース 300以上のActiveRecordモデル、1,800以上のerbファイルまで規模化 開発メンバーの入れ替わり 実装の一貫性のなさや、意図の分からないビューも増加 5

Slide 6

Slide 6 text

SIROK, Inc. ビューレイヤーが抱えていた課題 各レイヤーに課題はあるが、特にビューレイヤーに関していうと ● パラメータ定義の曖昧さ ● 一貫性のないパラメータの渡し方 ● テンプレート内のロジックの多さ これらによって想定外のバグの発生 6

Slide 7

Slide 7 text

SIROK, Inc. パラメータ定義の曖昧さ あるビュー要素を使う時、どのようなパラメータを渡して使用するのかが不明瞭 <%= render 'elements/common/headline', var1: xxx, var2: xxx %> どんなパラメータを指定すべきか分からない 7 再利用できそうなビュー要素があるが...

Slide 8

Slide 8 text

SIROK, Inc. パラメータ定義の曖昧さ: どのように判別するか? 呼び出し時に必要なパラメータを把握するために、erbファイルを読む必要 # app/views/elements/common/headline.html.erb
<% if overlay %>
<% end %>

<%= title %>

<% if description %> <%= description %> <% end %>
<%= image_tag(image_url, alt: "#{title} アイキャッチ画像") %>
これらが渡すべき変数 8

Slide 9

Slide 9 text

SIROK, Inc. パラメータ定義の曖昧さ: 引き起こされる問題 ビュー要素の使い手側は、実装ではなく仕様に従って使うべき しかし、仕様がコードベースにない 実装を読み解かないと使用することができない手間 読み間違えて必要なパラメータを渡し忘れるとエラーになるリスク 既存のビュー要素が使いづらいので再利用されない 似たようなビュー要素が新たに作成される 9

Slide 10

Slide 10 text

SIROK, Inc. 一貫性のないパラメータの渡し方 Railsのerbは、インスタンス変数で渡すことも、ローカル変数で渡すこともできる 結果、インスタンス変数やローカル変数の使用が混在 <%= render 'store_skin_questions/step4', f: form %> # app/views/store_skin_questions/_step4.html.erb ...
<% @step4_options.each_with_index do |question, index| %>
<%= f.radio_button 'step4', index, class: 'form-radio__input', id: index %> <%= f.label index, question, class: 'form-radio__label' %>
<% end %>
... ローカル変数 インスタンス変数 10 呼び出し時に変数を渡している それとは別に、インスタンス変数も 使う実装になっている

Slide 11

Slide 11 text

SIROK, Inc. 一貫性のないパラメータの渡し方: 引き起こされる問題 インスタンス変数は暗黙的なので、パラメータの渡し忘れの発生 インスタンス変数を定義していない場面で使えず再利用性が低下 再利用されるビュー要素はローカル変数に限定するとよい しかし、開発当初からそういう規約がなかった 11

Slide 12

Slide 12 text

SIROK, Inc. テンプレート内のロジックの多さ 複雑なロジックがテンプレート内に埋め込まれており、可読性・保守性が低下 <% browser = ContextManager.get_browser acquisition = browser.latest_acquisition partner = acquisition.partner %> <% if partner.present? %> <% end %> ロジック 12 ビューの先頭で処理をしている例がある

Slide 13

Slide 13 text

SIROK, Inc. テンプレート内のロジックの多さ: 引き起こされる問題 その結果、特定の条件でエラーの発生 ビュー要素のロジックのテストをすることは容易ではない 全パターンを手動で検証したり、実行コストの高いE2Eテストで保証 <% browser = ContextManager.get_browser acquisition = browser.latest_acquisition partner = acquisition.partner %> <% if partner.present? %> <% end %> acquisition = nilでエラー 13

Slide 14

Slide 14 text

SIROK, Inc. 解決策 14

Slide 15

Slide 15 text

SIROK, Inc. ViewComponent Railsアプリケーションのビュー要素を作成するためのフレームワーク https://viewcomponent.org/ 15

Slide 16

Slide 16 text

SIROK, Inc. ViewComponent GitHubのエンジニアによって開発され、GitHubでも使われている Reactにインスパイアされた設計 ViewComponentは、明確なインタフェースを提供し、ビューのテストも容易に行え るため、コードの保守性が大幅に向上 16

Slide 17

Slide 17 text

SIROK, Inc. ViewComponentの使い方: ビュー要素の実装の方法 ビュー要素ごとにクラスとテンプレートを記述 # app/components/message_component.rb class MessageComponent < ViewComponent::Base def initialize(name:) @name = name end end # app/components/message_component.html.erb

Hello, <%= @name %>!

17 テンプレートだけでなく、クラス を定義する 必要なパラメータを初期化引数で渡す

Slide 18

Slide 18 text

SIROK, Inc. ViewComponentの使い方: レンダリングの方法 必要なパラメーターを渡して、ViewComponentインスタンスを初期化 renderメソッドで描画 使い方は、とても簡単 # app/views/demo/index.html.erb <%= render(MessageComponent.new(name: "World")) %>

Hello, World!

18

Slide 19

Slide 19 text

SIROK, Inc. ERB PartialsとViewComponentの比較 ViewComponentを使わず素のerbを使うことを「ERB Partials」と呼んで比較 ViewComponentのメリット ● ロジックの分離 ● テストの容易さ ● インタフェースの明示 ● パフォーマンスの向上 19

Slide 20

Slide 20 text

SIROK, Inc. ロジックの分離 (ERB Partials) ERB Partials: テンプレート内にロジックが混在 <% color_class = case color when 'red' 'border-red text-red' when 'black' 'border-black text-black' end %> <%= text %> ロジック 20 CSSのクラス属性を生成するロジックがビュー内に書かれている

Slide 21

Slide 21 text

SIROK, Inc. ロジックの分離 (ViewComponent) ViewComponent: クラスファイルにロジックを書き、テンプレート内はメソッドの 呼び出しのみ class Common::Label::Component < ViewComponent::Base def initialize(text:, color:) @text = text @color = color end def color_class case @color when 'norganic-red' 'border-red text-red' when 'black' 'border-black text-black' end end end <%= @text %> ロジック 21 テンプレートはメソッドや変数を呼び出すだけになる

Slide 22

Slide 22 text

SIROK, Inc. テストの容易さ (ERB Partials) ERB Partials: コンポーネント単位のテストは可能 describe 'common/_label.html.erb', type: :view do it do render locals: { color: 'red', text: 'hello world' } expect(rendered).to match /border-red text-red/ expect(rendered).to match /hello world/ end end 22 実は私は知らなくて、今回のセッションのために調べていて、ERBのテストを書けると知りました…。

Slide 23

Slide 23 text

SIROK, Inc. テストの容易さ (ViewComponent) ViewComponent: コンポーネント単位だけでなく、メソッド単位でのテストも可能 describe Common::Label::Component, type: :component do it do render_inline(described_class.new(color: 'red', text: 'hello world')) expect(page).to have_css '.border-red.text-red' expect(page).to have_text 'hello world' end end 23 describe Common::Label::Component, type: :component do it do color_class = described_class.new(color: 'red', text: 'hello world').color_class expect(color_class).to eq 'border-red text-red' end end ViewComponentは、Rubyオブジェクトなので、一般的な単体テストの手法が応用可能 ロジック

Slide 24

Slide 24 text

SIROK, Inc. インタフェースの明示 (ERB Partials) ERB Partials: どのような変数が必要かは実装を読まないと分からない インスタンス変数でのパラメータの受け渡しは暗黙的なので、下位層まで見る必要 必要なパラメータの正確な判別は困難 <% color_class = case color when 'red' 'border-red text-red' when 'black' 'border-black text-black' end %> <%= text %> 渡すべき変数 24

Slide 25

Slide 25 text

SIROK, Inc. インタフェースの明示 (ViewComponent) ViewComponent: コンストラクタ引数でインタフェースが明示される 初期化引数を見れば十分 必要なパラメータは、これ以上でもこれ以下でもない module Common module Label class Component < ViewComponent::Base def initialize(text:, color:) @text = text @color = color end end end end 25

Slide 26

Slide 26 text

SIROK, Inc. パフォーマンスの向上 (ERB Partialsの検証コード) ViewComponentのサイトには10倍高速という記述 大量の要素を表示する計測を行った 10,000個のビュー要素の呼び出し # app/views/common/_text.html.erb <%= text %> <%= render 'common/text', text: 'hello world' %> <%= render 'common/text', text: 'hello world' %> <%= render 'common/text', text: 'hello world' %> # ...10,000個の要素 26

Slide 27

Slide 27 text

SIROK, Inc. パフォーマンスの向上 (ViewComponentの検証コード) 同じ表示をViewComponentを使って構築 <%= render Common::Text::Component.new(text: 'hello world') %> <%= render Common::Text::Component.new(text: 'hello world') %> <%= render Common::Text::Component.new(text: 'hello world') %> # ...10,000個の要素 # app/components/common/text/component.html.erb <%= text %> # app/components/common/text/component.rb module Common module Text class Component < ViewComponent::Base def initialize(text:) @text = text end end end end 27

Slide 28

Slide 28 text

SIROK, Inc. パフォーマンスの向上: 検証の結果 大量のビュー要素を表示する計測を行った結果 ERB Partials: 401ms ViewComponent: 57ms ViewComponentの方がレンダリング時間が大幅に短縮された # ERB Partials Completed 200 OK in 401ms (Views: 401.1ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 61.2ms) # ViewComponent Completed 200 OK in 57ms (Views: 56.7ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 7.7ms) 28

Slide 29

Slide 29 text

SIROK, Inc. 既存のERB PartialsをどうやってViewComponentに移行するか? ViewComponentの利用はメリットが多いことは分かった しかし、ビューファイル数は膨大 既存のコードベースをどのように移行するか? 29 $ find app/views -name '*erb' | wc -l 1812

Slide 30

Slide 30 text

SIROK, Inc. 大量のファイル移行の懸念 移行期間中に他の開発を止めることはできない 移行中にも、ビューが変更されたり、新しいビューが追加される 移行が完了しない場合、中途半端に2種類の実装が混在する 認知負荷が上がり、技術負債となる 30

Slide 31

Slide 31 text

SIROK, Inc. 移行戦略として決断したこと すべてのViewComponentを、erbを元に自動生成する 移行期間中の追加・更新も、自動生成を再実行すれば反映できる状態にする すべて移行できると確認できた段階で、新しいビュー実装に切り替える ● 自動生成スクリプトの実装 ● 新しいビュー実装のリリース 31

Slide 32

Slide 32 text

SIROK, Inc. 実装 32

Slide 33

Slide 33 text

SIROK, Inc. 自動生成スクリプト 実際に構築した自動生成スクリプトの概要について紹介します 解析のフェーズ 生成のフェーズ erbの一覧化 erbのパース renderメソッドの探索 パラメータの認識 rubyファイルの生成 erbファイルの生成 rspecファイルの生成 テストデータの生成 33

Slide 34

Slide 34 text

SIROK, Inc. erbの一覧化 app/views 配下の .html.erb を探索すればよい partial_collection = PartialCollection.new Dir.glob(Rails.root.join('app/views/**{,/*/**}/*')).uniq.each do |f| next unless f.end_with?('.html.erb') partial_collection.add(Partial.new(f)) end 34 検索して見つかったerbファイルをコレクションクラスに追加してリスト化する

Slide 35

Slide 35 text

SIROK, Inc. erbの一覧化: ファイル名の正規化 一覧のファイル名と、renderの呼び出しは、必ずしも一致しない 相対パスで指定する場合や、ファイル名にアンダースコアがあるため そこで、erb名を正規化して管理する <%= render 'page/button' %> <%= render 'button' %> ファイル名 正規化後 app/views/page/index.html.erb page/index app/views/page/_button.html.erb page/button 呼び出し 正規化後 render 'page/button' page/button render 'button' page/button 35 ファイルや呼び出しのerbを正規化して扱う これによって、ファイルと呼び出しの同一性が取れる

Slide 36

Slide 36 text

SIROK, Inc. erbのパース erb内でrenderメソッドを呼ぶ位置を特定する 当初は正規表現でマッチさせていたが、ブロックをもつ場合などはマッチが困難 Ruby Parserを用いて解析 node = Parser::CurrentRuby.parse(code) (begin (lvasgn :color_class (case (send nil :color) (when (str "red") (str "border-red text-red")) (when (str "black") (str "border-black text-black")) nil)) (lvar :color_class) (send nil :text)) 36 抽象構文木を得られる

Slide 37

Slide 37 text

SIROK, Inc. erbのパース: パースするための前処理 erbそのものは、Rubyコードとしてパースできない 事前処理としてHTMLタグを除去することで、Rubyコード部分のみをの取り出す HTMLタグ部分はコメントに置換して、処理後に逆変換できるようにしておく #html_code:1 color_class = case color when 'red' 'border-red text-red' when 'black' 'border-black text-black' end #html_code:2 color_class #html_code:3 render 'common/text', text: text #html_code:4 <% color_class = case color when 'red' 'border-red text-red' when 'black' 'border-black text-black' end %> <%= render 'common/text', text: text %> 37 HTMLタグはコメントに

Slide 38

Slide 38 text

SIROK, Inc. renderメソッドの探索 抽象構文木から、renderの位置を特定 前処理したerbをRuby Parserで抽象構文木に変換する #html_code:1 color_class = case color when 'red' 'border-red text-red' when 'black' 'border-black text-black' end #html_code:2 color_class #html_code:3 render 'common/text', text: text #html_code:4 (begin (lvasgn :color_class (case (send nil :color) (when (str "red") (str "border-red text-red")) (when (str "black") (str "border-black text-black")) nil)) (lvar :color_class) (send nil :render (str "common/text") (hash (pair (sym :text) (send nil :text))))) renderを探索 38

Slide 39

Slide 39 text

SIROK, Inc. renderメソッドの探索: 具体的な探索のプログラム 抽象構文木のノードを再帰的に探索してrenderを特定 見つかった場合は、コード中の位置情報を返し、後の処理に利用する # renderメソッドの位置を探索するメソッド def self.search_render_node(node, parent_node = nil) if node.is_a?(Parser::AST::Node) if node.loc.is_a?(Parser::Source::Map::Send) && node.children[0].nil? && node.children[1] == :render return [ # renderの開始位置、メソッドの終了位置 (ブロックの開始位置 )、ブロックの終了位置、を配列にして返す [ node.loc.expression.begin_pos, node.loc.expression.end_pos, parent_node&.type == :block ? parent_node.loc.expression.end_pos : nil ] ] elsif node.respond_to?(:children) return node.children.flat_map.with_index { |child_node, i| search_render_node(child_node, i == 0 ? node : nil) } end end return [] end 39

Slide 40

Slide 40 text

SIROK, Inc. erbの依存関係の把握 ビューの依存関係や、どのようなパラメータを渡しているかを把握する必要 renderを呼び出す側、呼び出される側を把握してツリーを作る この際に、どこからも呼ばれていないerbが発見され削除 # あるビューのツリー構造 dashboard/products/index └ dashboard/elements/breadcrumbs/product │ └ dashboard/elements/breadcrumbs/base └ dashboard/dashboard_tags/search_box └ dashboard/elements/daterange └ dashboard/elements/pager └ dashboard/elements/reports/head 40 もっと巨大なツリーになるビューもある

Slide 41

Slide 41 text

SIROK, Inc. パラメータの認識 各ViewComponentの初期化引数に渡すパラメータを特定する必要 erbファイルで使用されているインスタンス変数や、renderメソッドで渡されている ローカル変数を把握 # パラメータを特定するメソッド def arguments (local_variables + instance_variables + child_variables).uniq end def child_variables @child_partials.flat_map { |child_partial| child_partial.instance_variables + child_partial.child_variables }.uniq end 41 インスタンス変数、ローカル変数に加えて、子要素で使うパラメータも必要になる

Slide 42

Slide 42 text

SIROK, Inc. rubyファイルの作成 解析結果を元に、必要なコードを生成 必要なパラメータをキーワード引数として持ち、インスタンス変数に代入するとい うメソッド # 初期化メソッドを生成するコード arguments_code = @partial.arguments.map do |argument| "#{check_reserved_parameter(argument)}: nil" end.join(", ") initialize_code = @partial.arguments.map do |argument| "@#{argument} = #{check_reserved_parameter(argument)}" end.join("\n") constructor_code = <<~EOT def initialize(#{arguments_code}) #{indent(initialize_code)} end EOT 42

Slide 43

Slide 43 text

SIROK, Inc. 一部のビューは追加の考慮が必要 ● ブロックを持つビュー要素の場合は、Slots(renders_one)の設定を追加 ● erb内でHelperのメソッドを用いている場合は、delegateを追加 ● ViewComponentはcontentという引数を予約しているので、contentというパ ラメータは改名が必要 rubyファイルの作成: 生成されたファイルの例 class Partials::Dashboard::Elements::Breadcrumbs::Product::Component < ViewComponent::Base renders_one :children def initialize(product: nil) @product = product end end 43

Slide 44

Slide 44 text

SIROK, Inc. erbファイルの作成 ほとんど元のerbをそのまま持ってくるだけ ● renderメソッドをViewComponentの呼び出しに書き換え ● ローカル変数をすべてインスタンス変数に書き換え 44

Slide 45

Slide 45 text

SIROK, Inc. erbファイルの作成: 変換されたerbの例 <%= render 'dashboard/elements/breadcrumbs/base' do %> <% if product&.persisted? %> <% end %> <%= yield %> <% end %> <%= render Partials::Dashboard::Elements::Breadcrumbs::Base::Component.new() do |c| c.with_children do %> <% if @product&.persisted? %> <% end %> <%= children %> <% end end %> renderの書き換え 変数の書き換え 45

Slide 46

Slide 46 text

SIROK, Inc. rspecファイルの作成 テストの内容は、エラーなくレンダリングができるかどうかのみ カバレッジは目指さない方針 生成元のerbは本番で動作中のものなので動作は保証されていると考える 初期化引数に、適切な値を渡す必要がある RSpec.describe Partials::Dashboard::Elements::Breadcrumbs::Product::Component, type: :component do before do # 前処理 end example '表示できる' do product = build(:product) component = described_class.new(product: product) render_inline(component) end end 46

Slide 47

Slide 47 text

SIROK, Inc. テストデータの生成 パラメータの変数名をもとに、どのようなオブジェクトを渡すべきか推測して渡す 特定のビュー要素だけで使うような特殊なテストデータの場合は、変数名だけでな くerbのファイル名も利用して分岐をする def test_data(variable_name) TestData.new(@partial.canonical_name, variable_name).code end def factory_code(name) case name # ... when 'product' return <<~EOT.chomp build(:product) EOT # ... end return "build(:#{name})" end 47 productという変数名に対するテストデータ のコードを生成

Slide 48

Slide 48 text

SIROK, Inc. 自動生成スクリプトとテストデータの微調整 ViewComponentのコードを生成をしてrspecを実行 失敗したら、自動生成スクリプトを修正するか、テストデータを修正 rspecがパスするまで繰り返す この微調整が、作業工数の8割…。 ● 記法の一貫性のなさのため、イレギュラーの対応が必要 ● ビュー内でActiveRecordを利用している箇所で、複雑なテストデータが必要 ● partialを前提としたgemの利用 個別対応が必要 48

Slide 49

Slide 49 text

SIROK, Inc. 自動生成と全テストのパス 生成されたすべてのViewComponentのrspecがパスすることを確認 20万行のコードが生成された 49 20万行…!

Slide 50

Slide 50 text

SIROK, Inc. リリース 50

Slide 51

Slide 51 text

SIROK, Inc. 新しいビュー実装の組み込み 生成したViewComponentを実際にプロダクションコードに組み込む 安全にリリースをするために、段階的にリリースする方法を検討 ● 別系統の生成 ● view_pathの分岐 ● フィーチャーフラッグによる検証 ● カナリアリリース 51

Slide 52

Slide 52 text

SIROK, Inc. 別系統の生成 app/views2というフォルダを作成 app/views app/views2 app/components/partial s spec/components/partia ls app/controllers 利用 テスト 自動生成 利用 52 どちらを使うかを切り替える 利用 既存のビュー実装 ViewComponentを使う ビュー実装

Slide 53

Slide 53 text

SIROK, Inc. 別系統の生成: app/views2の中身 app/views: ERB Partialsを利用する今までの実装 app/views2: ViewComponentを利用する新しい実装 # app/views/dashboard/products/index.html.erb <% content_for(:title) { '商品の一覧' } %>
# app/views2/dashboard/products/index.html.erb <% content_for(:title) { '商品の一覧' } %> <%= render Partials::Dashboard::Products::Index::Component.new(products: @products, product: @product, ...)%> 53

Slide 54

Slide 54 text

SIROK, Inc. view_pathとは app/viewsとapp/views2の使い分けをする前に、Railsのビューの解決について Railsがビューの解決をする場合、view_pathsを順に探索していく ApplicationController.view_paths.map{|path| path.to_s} => ["/path/to/app/views", "/path/to/vendor/bundle/ruby/3.2.0/gems/view_component-3.18.0/app/views", "/path/to/vendor/bundle/ruby/3.2.0/gems/turbo-rails-2.0.11/app/views", "/path/to/vendor/bundle/ruby/3.2.0/gems/actiontext-7.2.1.1/app/views", "/path/to/vendor/bundle/ruby/3.2.0/gems/actionmailbox-7.2.1.1/app/views"] 54 app/viewsが上位にいるので基本的にはapp/viewsのビューを使う

Slide 55

Slide 55 text

SIROK, Inc. view_pathの分岐 どのようにview_pathsを操作するか? append_view_path, prepend_view_pathメソッドを呼ぶ prepend_view_path で app/views2を追加することで、ViewComponent版の ビュー実装に切り替わる prepend_view_path 'app/views2' ApplicationController.view_paths.map{|path| path.to_s} => ["/path/to/app/views2", "/path/to/app/views", "/path/to/vendor/bundle/ruby/3.2.0/gems/view_component-3.18.0/app/views", "/path/to/vendor/bundle/ruby/3.2.0/gems/turbo-rails-2.0.11/app/views", "/path/to/vendor/bundle/ruby/3.2.0/gems/actiontext-7.2.1.1/app/views", "/path/to/vendor/bundle/ruby/3.2.0/gems/actionmailbox-7.2.1.1/app/views"] 追加される 55 app/views2が上位にくると、app/views2が使われるようになる

Slide 56

Slide 56 text

SIROK, Inc. フィーチャーフラッグによる検証 フィーチャーフラッグ: 特定の人のみに新機能をリリースすること 特定のCookieを持つ人に対して、app/views2を選択 まず社内で動作を確認 また、Cookieの有無でレスポンスを取得してdiffを確認 before_action -> { prepend_view_path 'app/views2' if cookies[:feature_flag] == 'view_component' } 56

Slide 57

Slide 57 text

SIROK, Inc. カナリアリリース カナリアリリース: 一部のユーザーのみに先にリリースすること 10%の確率で、app/views2を選択 1%から開始して、ログなどを確認しながら、比率を上げていく before_action -> { prepend_view_path 'app/views2' if rand < 0.1 } 57

Slide 58

Slide 58 text

SIROK, Inc. 結果 58

Slide 59

Slide 59 text

SIROK, Inc. ViewComponentの導入によって得られた効果 ViewComponentの導入により、以下の効果を得ることができました。 ● コンポーネント呼び出しのインタフェースが分かりやすくなった ● コードジャンプがしやすくなった ● ビューのテストが可能になり、実装ミスに気づきやすくなった 59

Slide 60

Slide 60 text

SIROK, Inc. コンポーネント呼び出しのインタフェースが分かりやすくなった Before: どのように呼び出すべきかはerbをすべて読まないと分からない After: コンストラクタを見れば分かる 複雑なビューであっても一目でパラメータを把握できる class Partials::Sirok::Media::Ads::Contents::Components::Component < ViewComponent::Base include OptimizeHelper include ViteHelper def initialize(components: nil, orders: nil, model: nil, extra: nil, replaced_link: nil, lazy_load: nil, is_content_editable: nil, survey_redirect_paths: nil, product_id: nil, type: nil, campaign: nil, campaign_user: nil, order: nil, is_preview: nil, is_lazy: nil, amazon_pay_redirect_path: nil, ad: nil, upsell_impression: nil, store_types: nil, stores: nil, coordinates: nil, marker_data: nil, direct_captureable: nil) # ...略... end end 60 あまり良い実装ではないが、とてつもなくパラメータの多い複雑なビュー…。

Slide 61

Slide 61 text

SIROK, Inc. コードジャンプがしやすくなった Before: (開発環境にもよると思いますが)erbのファイル名でコードジャンプでき ず、相対パスも混ざって検索性も低い After: クラスとしてのコードジャンプが可能 61

Slide 62

Slide 62 text

SIROK, Inc. ビューのテストが可能になり、実装ミスに気づきやすくなった Before: 重要なページのみにE2Eテストを導入してビュー動作を保証 After: 全ViewComponentのrenderのテストを可能 1800ファイルの描画確認だけであれば、20分程度 並列可能なので、分散させれば数分でテスト可能 62

Slide 63

Slide 63 text

SIROK, Inc. 移行にかかった工数・時間 期間: 1年半 実装時間: 120時間 サブミッションとして単独で取り組んでいたので期間は長かった 業務時間をフルに使えば1〜2ヶ月で完遂できたかもしれない 63

Slide 64

Slide 64 text

SIROK, Inc. 自動生成するという移行戦略はどうだったのか? 計算してみると、erbファイル1つあたり、4分程度 手で書き換えるよりも圧倒的に短時間 機能開発によってビューファイルが変更されても、再生成するだけで済んだ 手動移行では、変更されるたびに追従する作業が必要で、心が折れていた可能性大 自動生成する戦略は成功であった 64

Slide 65

Slide 65 text

SIROK, Inc. パフォーマンスはどうなったのか? 50%の確率で分岐させて、ABテスト 実計測での差はわずか app/views: 平均 93.4ms app/views2: 平均 93.1ms ビュー内でActiveRecordのクエリ呼び出しがあり、クエリ時間が支配的 65

Slide 66

Slide 66 text

SIROK, Inc. まとめ ViewComponentライブラリを紹介 私たちのサービスにおける移行の方法について解説 ViewComponentは、Railsで保守性の高いビューを実装するために有用 大規模なプロジェクトでは、既存のerbを元に自動生成することで工数を短縮可能 リファクタリング全般において自動生成は有効 66

Slide 67

Slide 67 text

SIROK, Inc. ありがとうございました! 67