💪

ユーザー向け機能開発チームで行っている施策駆動レガシーフロントエンド改善

2022/12/13に公開

はじめに

こんにちは!
crowdworks.jp[1]でユーザー向けの機能開発を行うチームでリーダーをやっているエンジニアの@d4te74です。

私のいるチームでは2022年はcrowdworks.jpのUI改善施策を行っていましたが、改善対象となったUIはレガシーな技術で構築されていたため、そもそも「どういった変更を加えるべきか」「変更による影響の範囲はどの程度か」「既存コードはどういう振る舞いをしているのか」などを理解するのが困難な不確実性の高い状態でした。

この記事では負債解消をミッションにしていないチームが行った、不確実性を消し、計画通りに機能開発を進めるために行った、施策駆動レガシーフロントエンド改善のプロセスについて説明していきたいと思います。

crowdworks.jpのフロントエンドの状況

crowdworks.jpのフロントエンドの技術スタックはRails ERB / SCSS / jQuery / Vue.jsとなっており、徐々にバックエンドとフロントエンドの分離やVue.js化が進んでいますが、既存画面の多くはActiveRecord駆動で実装されたRails ERBやjQueryで実装されています。

crowdworks.jpのフロントエンド開発でつらいこと

Rails ERB

  • .erbファイルに複雑なif分岐やビジネスロジックが直接記述されており、かつ同様の記述が複数ファイルに散らばっている
  • ActiveRecordへの依存が強く、マークアップを変更するとバックエンドのロジックが壊れることがある
  • テストがない

SCSS

  • 秩序なく数年上書きされ続けたCSSが無数に存在していて、意図したように実装できなかったり意図しないページのデザインが破綻してしまうことが頻繁にある
  • 変更による影響範囲の特定が困難、かつできたとしても調査に時間がかかる
  • スタイルの中には!importantで上書きされ続けているものも多数存在しており、新規のデザインを適用される場合やむをえず!importantを上書きするしかない場合がある

jQuery

  • 古の時代に書かれたjQueryのイベントハンドラが大量に張り巡らされており、変更を加えると意図しないページで破綻していることがある
  • 変更による影響範囲の特定が困難、調査に時間がかかる
  • テストがない
  • jQueryに触りたくない

また、このように変更が困難であることに加えてデザイナー・プロダクトオーナーなどエンジニア以外のメンバーもUIの仕様(表示・振る舞い)を知っている人がいないことが多い状態です。

UI改善施策とは?

やってきたUI改善施策の内容を分類すると以下のようになります

  • .erbファイルに実装された既存UIを別の画面に表示(移植)する
  • .erbファイルに実装された既存UIの振る舞いを変えずにデザインのみ変える

.erbファイルに実装された既存UIを別の画面に表示(移植)する

以下のお仕事サマリーというUIは、仕事を募集しているクライアントが「仕事に応募してきたワーカー」や「契約済みのワーカー」など、仕事を発注するクライアントが仕事を管理しやすくするためのUIで、Railsの部分テンプレート(partial)として実装され複数画面で利用されていました。

施策ではお仕事サマリーをある画面で表示したいという要件だったのですが、お仕事サマリーはActiveRecordと密結合であり、かつ画面ごとの特化したスタイルで微妙に拡張されており既に実装されているpartialを呼び出せば良いというものではありませんでした。先述の通り当然テストもありません。

.erbファイルに実装された既存UIの振る舞いを変えずにデザインのみ変える

以下はワーカー管理と呼ばれる、クライアントが過去に契約したワーカーやまたお仕事を依頼したワーカーを管理するための画面で使われている、ワーカーを表現するUIです。

施策ではこのUIを「メモの保存」や「お気に入りに追加」など既存の振る舞いを変えずに以下のデザインに一新したい、という要件でした。

ただこちらもActiveRecordに依存したRails ERBやjQueryを使ったAPIリクエスト処理やアニメーションが実装されていました。先述の通り当然テストもありません。

ワーカー管理画面の改善施策については合わせて以下もご覧ください。

https://blog.crowdworks.jp/archives/4783

施策駆動レガシーフロントエンド改善

このような手を出すことができなくなったUIの改修における不確実性をなくし、ユーザーにとって当たり前の品質でリリースするためには仕様を理解に加えて、バックエンドとフロントエンドを疎結合にし、UIをVue.jsで作り直すしか道はなく、結果的にそれが近道でした。
つまり施策駆動フロントエンド改善とはユーザー向け機能開発で以下を実践することでした。

  • UIの状態・振る舞いの調査し理解する
    • .erbファイルに実装された既存UIを別の画面に表示(移植)する場合は既存コードを調査する
    • .erbファイルに実装された既存UIの振る舞いを変えずにデザインのみ変える場合はデザイナーと認識を合わせる
  • バックエンドとフロントエンドを疎結合化しフロントエンドがRailsに依存しない状態にする
  • 改修対象のUIを設計し、責務ごとに実装(マークアップ・スタイル・振る舞い)し、StorybookやJestを利用して表示状態・振る舞いのテストを追加
  • 不要になったコード(Rails ERB・SCSS・jQuery)を削除する

※ 当然負債解消をミッションにしたチームではないので、不必要だと判断されたリファクタリングなどは行わない、またはリリース後のタスクで対応することにしています。

また、crowdworks.jpの技術的負債の返済をリードするジャンヌチームによってVue2 → Vue3アップデートやStorybook、VRT(Visual Regression Test)の導入がされており、それらの導入も私たちがやろうとしていた施策駆動フロントエンド改善の追い風でした。

ジャンヌチームの活動については以下も併せてご覧ください。

https://engineer.crowdworks.jp/entry/crowdworks_frontend_2022

https://engineer.crowdworks.jp/entry/migrate-vue3

https://engineer.crowdworks.jp/entry/vrt_with_regsuit

やったこと

具体的には以下4つを行いました。

  • UIの状態・振る舞いの調査/設計
    • .erbファイルに実装された既存UIをVue.js化する時
    • .erbファイルに実装された既存UIの振る舞いを変えずにデザインのみ変える
  • UIが必要とする値を返却するAPIの設計・実装
  • UIの実装
  • バックエンドとフロントエンドの繋ぎ込み

UIの状態・振る舞いの調査/設計

ここでは以下2つを調査し、APIやコンポーネントの実装に必要な値を洗い出します

  • バックエンドからフロントエンドに渡すべき値の洗い出し
  • UIが持っている表示状態の洗い出し

.erbファイルに実装された既存UIをVue.js化する時

簡単なものですが以下のようなサービス上で自分以外の他のユーザーをフォロー/フォロー解除をするためのUIをVue.js化する時を例に説明していきたいと思います。

<% if current_user.present? && !current_user?(user) && !current_user.blocked_by(user) %>
  <% if current_user.following?(user) %>
    <%= form_for(current_user.relationships.find_by(followed_id: user.id), remote: true) do |f| %>
      <%= button_tag(type: "submit") do %>
        フォロー解除
      <% end %>
    <% end %>
  <% else %>
    <%= form_for(Relationship.build(followed_id: user.id), remote: true) do |f| %>
      <%= button_tag(type: "submit") do %>
        フォローする
      <% end %>
    <% end %>
  <% end %>
<% end %>

コードから理解できることをまとめると以下になります。

  • current_user.present? && !current_user?(user) && !current_user.blocked_by(user)がtrueの時のみUIを表示している
  • current_user.following?(user)がtrueの時、フォロー解除ボタンを表示している
  • current_user.following?(user)がfalseの時、フォローボタンを表示している
  • フォロー解除/フォローの実行にはuser.idが必要
  • 各ボタン押下後の処理は非同期で行っていそう

それらをまとめるとAPIがフロントエンドに返すべき値は以下になります。

{
  toggle_follow_button_is_available: boolean; // current_user.present? && !current_user?(user) && !current_user.blocked_by(user) の結果が入る
  user: {
    id: number; // user.id
    followed: boolean; // current_user.following?(user)
  }
}

フォロー/フォロー解除のためのボタンコンポーネントは followedの状態でフォロー/フォロー解除を出し分けており、かつそれぞれの状態でボタンを押下した時にフォロー/フォロー解除という振る舞いを実行しているので以下のようなPropsとEmitsを定義できると考えます。

type Props = {
  followed: boolean;
}

type Emits = {
  (e: 'follow', userId: number): void;
  (e: 'unfollow', userId: number): void;
}

.erbファイルに実装された既存UIの振る舞いを変えずにデザインのみ変える

振る舞いを変えずにデザインのみ変えたい場合は、デザイナーと実装対象となるUIの表示パターン・表示したい値の意味などの認識をあわせ、フロントエンドに渡すべき値を定義します。

また、既存UIをVue.js化する場合と同じくAPIにリクエストを投げている場合はリクエストに必要な値も調査し、APIの返却値のインターフェースと各コンポーネントのインターフェースを明らかにしておきます。

UIが必要とする値を返却するAPIの実装

.erbファイルに実装された既存UIではバックエンドとフロントエンドが密結合であり、バックエンドに変更を加えるとフロントエンドの表示などが壊れるという課題がありました。

それを解決するため、以下のようなUIが必要とする参照用のData Transfer Objectを返すクラスを実装してテスト追加し、UIはControllerなどに定義されたインスタンス変数を参照させないようにしました。これによって疎結合になり、互いへの意図しない影響はテストで検知することができます。

class ToggleFollowContainerDtoGenerator
  def initialize(current_user:, target_user:)
    @current_user = current_user
    @target_user = target_user
  end

  def execute
    {
      # current_user.present? && !current_user?(user) && !current_user.blocked_by(user)の結果を返却するPolicy
      toggle_follow_button_is_available: ToggleFollowButtonPolicy.new(current_user: current_user, target_user: target_user).available?,
      user: {
        id: target_user.id,
        followed: current_user.following?(target_user),
      }
    }
  end

  private
  attr_reader :current_user, :target_user
end

UIの実装

UIはContainer/Presentationalパターンで実装しました。

Container/Presentationalパターンはアプリケーションの振る舞いを責務とするContainerとUIの表示を責務とするPresentationalを分けて実装することで関心を分離することを目的とした設計パターンです。

https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

Container

ContainerはAPI通信処理などアプリケーションの振る舞いに関心をもっています。マークアップやスタイルは持たず、Presentational Componentを表示することができます。

フォロー/フォロー解除を実行するボタンコンポーネントをコードで表現すると以下のようになると考えます。

<script setup lang="ts">
import { onMounted } from 'vue';
import ToggleFollowButton from '@/path/to/ToggleFollowButton.vue';
import { useToggleFollowButtonContainer } from '@/path/to/composables';

const {
  state,
  follow, // フォローのためのリクエストを投げる
  unfollow, // フォロー解除のためのリクエストを投げる
  getToggleFollowButtonContainerState, // UIに必要な値を取得するリクエストを投げる
} = useToggleFollowButtonContainer();

onMounted(() => {
  state.toggleFollowButtonDto = getToggleFollowButtonContainerDto();
});
</script>

<template>
  <ToggleFollowButton
    v-if="state.toggleFollowButtonDto"
    :followed="state.toggleFollowButtonDto.user.followed"
    :user-id="state.toggleFollowButtonDto.user.id"
    @follow="follow"
    @unfollow="unfollow"
  />
</template>

アプリケーションの振る舞いを隔離したことでUIの変更が影響しなくすることができました、またAPIのリクエスト処理などContainerに書いたロジックのテスト追加は必須にしました。

Presentational

Presentationalは表示に関心を持っています。API通信処理などは行わず、利用者となるコンポーネントから値を受け取り、その値を使ってUIを構築します。

今回のフォロー/フォロー解除を実行するボタンコンポーネントをコードで表現すると以下のようになると考えます。

<script setup lang="ts">
type Props = {
  followed: boolean;
  userId: number;
};
const props = defineProps<Props>();

type Emits = {
  (e: 'follow', userId: number): void;
  (e: 'unfollow', userId: number): void;
};
const emits = defineEmits<Emits>();
</script>

<template>
  <template v-if="props.followed">
    <button @click="emits('unfollow', props.userId)">フォロー解除</button>
  </template>

  <template v-else>
    <button @click="emits('follow', props.userId)">フォロー</button>
  </template>
</template>

erbで実装されていた時の課題で、施策の不確実性を高めていたのがUI表示の仕様を知っている人がいないことでしたが、その課題を解決するためにUIコンポーネントの実装と合わせてStorybookへの追加も行います。

これによって、UIが持つ状態ごとの表示を誰でも確認できるようになり、かつ先述の通りVRTの導入も導入されているため、意図せずUIの表示に変更を加えてしまった場合でもテストで気づくことができるようになります。

※ Storybookへの追加はcrowdworks.jpのフロントエンド開発時の必須ルールとなっています

バックエンドとフロントエンドの繋ぎ込み

最後はerbに実装したContainerを繋ぎ込みます。

- <% if current_user.present? && !current_user?(user) && !current_user.blocked_by(user) %>
-   <% if current_user.following?(user) %>
-     <%= form_for(current_user.relationships.find_by(followed_id: user.id), remote: true) do |f| %>
-       <%= button_tag(type: "submit") do %>
-         フォロー解除
-       <% end %>
-     <% end %>
-   <% else %>
-     <%= form_for(Relationship.build(followed_id: user.id), remote: true) do |f| %>
-       <%= button_tag(type: "submit") do %>
-         フォローする
-       <% end %>
-     <% end %>
-   <% end %>
- <% end %>
+ <div id="toggle-follow-button-container-vue"></div>

crowdworks.jpにおけるerbへのVueコンポーネントレンダリング方法については本筋とそれるので説明は以下の記事に任せますが、実装したContainerはdiv#toggle-follow-button-container-vue にレンダリングされます。
当然繋ぎ込み後は不要になった既存のマークアップを削除することができ、かつ他の.erbファイル等で利用されていなければ.scssファイルに記述されているスタイルやjQueryのロジックを削除します。

https://engineer.crowdworks.jp/entry/2019/05/28/114806

このプロセスをやってみてどうだったか

ユーザー向けの機能開発を行うチームという特性上、リリース計画についてや問題があった時は言語化してプロダクトオーナーなど非エンジニアにもコミュニケーションが必要となりますが、このプロセスを実践したことにより不確実性が最小限になり、エンジニアメンバーだけでなく非エンジニアメンバーも含めて全員が今どういう状態で、今後どうしていけばよいかなどの認識を合わせることができました。

また施策駆動でレガシーフロントエンドを改善していくことで「既存仕様が理解しやすい」「変更によって意図せず壊してしまっても気づくことができる」など、開発に心理的安全性が担保されるだけでなく、Vue.js化されたUIが増えれば増えるほど今後の実装では再利用することが可能になるなどいいことしかないと思っています。

おわりに

いかがでしたでしょうか。
まだまだユーザー向けの改善は続きますし、レガシーフロントエンドの改善もやるべきことはたくさんあります。
引き続きプロダクトだけでなく、自分達のプロセスがもっと良くなるように挑戦し続けていこうと思っております!

※ 2022年に取り組んだ施策について以下も併せてご覧ください。

https://blog.crowdworks.jp/archives/4649/

https://blog.crowdworks.jp/archives/4676/

https://blog.crowdworks.jp/archives/4783/

https://blog.crowdworks.jp/archives/4813/

https://blog.crowdworks.jp/archives/4949/

https://blog.crowdworks.jp/archives/5022/

We're hiring!

クラウドワークスでは、ユーザー向けに機能開発を行うチームはもちろんのこと、各種サービスや各種ポジションでエンジニアを募集しています!

https://crowdworks.co.jp/careers/

脚注
  1. 弊社で運営しているお仕事をインターネット上で発注・受注することができるお仕事マッチングサイトです。お仕事の発注者をクライアントと呼び、お仕事の受注者をワーカーと呼びます。 ↩︎

Discussion