クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

【Vue.js】負債を返却しながら機能追加しなければならない状況で実践したフロントエンドのコンポーネント設計

はじめに

こんにちは! 社会人2年目を頑張っております、エンジニアの@b0ntenmaruです。

今年2月までリファクタリング専門チームにてcrowdworks.jpの技術的負債を返却するために奮闘しておりましたが、そこから現在まではユーザーの皆様に安心安全なサービスを提供するためにクラウドワークス 安心安全宣言のための施策を行っています。

リファクタリング専門チームについては以下をご覧ください。

engineer.crowdworks.jp

qiita.com

施策による機能開発を行う際に直面した課題

施策では主にフロントエンドの機能追加をすることになったのですが、技術的負債によりスピードを維持しながら開発を続けることは困難な状態でした。 crowdworks.jpを取り巻くフロントエンドの技術スタックはざっくり書くと下記3つに分類できます。それぞれで発生している問題を簡潔にまとめます。

ERB

  • html.erbファイルにif分岐や業務ロジックが直接記述されており、かつ同様の記述が複数ファイルに散らばっている

※ ERBはcrowdworks.jpで採用されているRuby on Railsのテンプレートエンジン

CSS

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

jQuery

  • 古の時代に書かれたjQueryイベントハンドラが大量に張り巡らされており、変更を加えると意図しないページで破綻していることがある
  • そもそも読むのに膨大なエネルギーを消費する
  • 変更による影響範囲の特定が困難、かつできたとしても調査に時間がかかる

このような課題があり、スピードを維持して開発が続けられるよう機能追加だけでなく負債の返却作業をする必要もありました そこで、関心事を集約して複雑なUIを確実かつ堅牢に組み上げる手段であるコンポーネントベースの思想を取り入れ、かつ今後の負債化を防ぎ、持続的に開発していくためのコンポーネント設計を考えました

※ crowdworks.jpではVue.jsを採用

この記事ではhtml.erbからVue.js置き換えなど負債を返却しながら機能追加していく時に実践したフロントエンドのコンポーネント設計について書いていこうと思います。

アプリケーションロジックとUIを分離する

まず、バックエンドAPIなどアプリケーションロジックに関わる処理とUIを分離するために、Presentational ComponentとContainer Componentの考え方を取り入れました。 これはReact.js + Reduxの文脈で登場するアプリケーションの動作の関心事とUIなど見た目の関心事を分離するための思想です。

まずReact.js + ReduxにおけるPresentational ComponentとContainer Componentをまとめると以下のようになります。

Presentational Component

  • 見た目に関する責務を負う
  • 子要素には Presentational Component と Container Componentを持つことが可能
  • DOMマークアップとスタイルを持つ
  • this.props.childrenを受け取る
  • Fluxアクションなどアプリケーションロジックに依存しない
  • データ更新などの操作は行なわない
  • データやコールバックは親コンポーネントからpropsとして受け取る
  • 基本的にStateは持たず、持つとしてもUIの状態に関する物(Modalの開閉状態など)
  • パフォーマンス最適化の必要がない限り、Functional Componentとして記述する

Container Component

  • APIやFluxアクションなどのアプリケーションの動作に関する責務を負う
  • 子要素には Presentational Component と Container Componentを持つことが可能ですが、通常wrapper用のdivを除いて独自のDOMマークアップを持たず、スタイルもない
  • Fluxアクションの呼び出しを行い、Presentational Componentにコールバックとして渡す
  • Stateを持って良い
  • redux.connect()などを使用して生成される

(参考: Presentational and Container Components | by Dan Abramov

crowdworks.jpにとって役立つ部分のみ取り入れた

crowdworks.jpではそもそもVue.jsを使っており、ReduxのようなUIの状態を管理するフレームワーク(Vue.jsではVuexが該当)も現状使っていないので、役に立つ部分のみ取り入れてみました。

実践したPresentational ComponentとContainer Componentをまとめると以下のようになります。

実践したPresentational Component

  • 見た目に関する責務を負う
  • 子要素には Presentational Component と Container Componentを持つことが可能
  • DOMマークアップとスタイルを持つ
  • データやコールバックは親コンポーネントからpropsとして受け取る
  • バックエンドAPIなどアプリケーションロジックに依存しない
  • データ更新などの操作は行わず、親コンポーネントにイベントを通知する
  • 基本的にdataは持たず、持つとしてもUIの状態に関する物(Modalの開閉状態など)

実践したContainer Component

Presentational ComponentとContainer Componentの考え方を取り入れて良かった点

UIの関心事とアプリケーションの関心事など分離して疎結合にできたことによって、それぞれの変更が互いに影響することがなくなり、変更しやすくなっただけでなく互いの実装を待たずに並行して開発を進めることが可能になりました。

UIの設計にはAtomic Designを採用した

UI(Presentational Component)の設計ですが、crowdworks.jpでは先述の通り下記3つの要因が複雑に絡み合うことで機能開発や保守が困難になっています。

  • 情報(ERB)
  • スタイル(CSS
  • 状態管理・UIの動作(jQuery

また、チーム開発をするにあたってUIコンポーネントの粒度についての共通認識がないまま実装してしまうとコードの複雑化や実装の手戻りが容易に発生するという課題もありました。

そのため複雑さの要因となっている「情報・スタイル・状態管理・UIの動作」を関心事単位でまとまった形でカプセル化して分割統治し、かつチーム内でコンポーネントの粒度についての共通認識をつくるためにAtomic Designの考え方を取り入れました。

Atomic Designはページを構成する要素をAtoms(原子)・Molecules(分子)・Organisms(有機体)・Templates(テンプレート)・Pages(ページ)の5つのレイヤーにUIパーツを分割して考えるコンポーネント設計のためのデザインフレームワークです。

モバイルネイティブアプリ「Instagram」に適用されたアトミックデザイン。

(引用: Atomic Design Methodology | Atomic Design by Brad Frost

Atoms(原子)

Molecules(分子)

  • 2つ以上のAtomsで構成されるもの

Organisms(有機体)

  • MoleculesやAtomsで構成されるもの

Templates(テンプレート)

  • ページの雛形で、Organisms・Molecules・Atomsを適切に配置するもの
  • 実際に表示されるべきコンテンツがないワイヤーフレーム

Pages(ページ)

MoleculesとOrganismsの違い

MoleculesとOrganismsの説明では具体的にどのように違うかイメージし辛く、実装時も頻繁に迷うことがありました。 書籍 Atomic Design ~堅牢で使いやすいUIを効率良く設計するには以下のように定義されています。

Molecules: 独立して存在できるコンポーネントではなく、他のコンポーネントの機能を助けるヘルパーとしての存在意義が高いコンポーネント

Organisms: 独立して存在できるスタンドアローンコンポーネント

(引用: Atomic Design ~堅牢で使いやすいUIを効率良く設計する

Molecules

クライアントへの評価コンポーネント

上記のコンポーネントはcrowdworks.jpで使われているクライアントの評価を表示するためのコンポーネントですが、これ単体では何の評価しているか理解できず、クライアントの情報を表示するコンポーネントに含まれて初めてクライアントの評価を表示するコンポーネントであることが理解できるため、 独立して存在できるコンポーネントではなく、他のコンポーネントの機能を助けるヘルパーとしての存在意義が高いコンポーネント であるMoleculesであると判断できます。

Organisms

クライアント情報を表示するコンポーネント"

一方crowdworks.jpのお仕事詳細画面下部に表示されているクライアント情報を表示する上記のコンポーネントは、それ単体でクライアントの情報を表示していることが明確であり、独立して存在できるスタンドアローンなコンポーネント であるOrganismsと判断できます。

スタイル実装時の注意すること

コンポーネントには配置に関するCSSを書かず、子コンポーネントをどのように配置するかは親コンポーネントに書くこと。

ボタンAとボタンBが横並びになっている図

例えば横並びになっているボタンが2つあるとします。ボタン間の余白は10pxです。 ここで言う10pxの余白はデザイン用件を満たすためにどのようにボタンを配置するかということに関心を持つCSSです。 もしこの10pxの余白をボタンコンポーネント内にカプセル化してしまった場合、再利用性が失われることになります。

ボタンAとボタンBが縦並びになっている図

例えば、他のページで「ボタンを縦に並べたい」という用件が発生した場合、必要ではない10pxの余白が入ってしまいます。

上記は簡単な例ではありますが、配置するためのCSSを利用者である親コンポーネントに書かなければ再利用性が失われるだけでなく、メンテナンス性も失われることになるなどコンポーネント化による恩恵を受けることができなくなります。

事実crowdworks.jp内でもこのような問題が発生しており、チームでVue.jsのコンポーネントを実装する際は特に気をつけました。

「なんか、こう、『え、なんでこんな汎用的なクラスに margin-bottom: 5px; だけ入ってるの?』みたいなことが頻発してとてもつらいですね今……。」というSlack上での発言

Atomic Designの考え方を取り入れて良かったこと

メンテナンスしやすくなった

先述の通り、情報/スタイル/状態管理・UIの動作の記述が凝集しておらず、変更による影響範囲を見通すことが非常に困難かつ変更しにくいという当初の課題を解決し、変更容易性・理解容易性が向上しました。

チーム開発においての生産性が向上した

粒度の認識違いによる複雑化や手戻りがありませんでした。 また、「ページ」ではなく「コンポーネント」に実装タスクのスコープが縮まりました。 このため、クライアント情報コンポーネントにあるクライアントの「評価」「募集実績」「プロジェクト完了率」などのコンポーネントは並行開発することができ、生産性が向上しました。

オンボーディングしやすかった

施策を進めていくにあたって、自分以外のチームメンバーはフロントエンド開発が未経験の状態で、HTML/CSS/JavaScript(Vue.js)についてはほぼ1から学んでいく必要があったのですが、Atomic Designの考え方を取り入れ、UIコンポーネントを適切な粒度に分割したことによって解決したい課題が分割され、それによって実装が単純化して難易度を下げることができました。

実装が単純化したことによって入門したてでも手を動かしやすく、学びながら実践することが容易になり、1月経つ頃には当初未経験だったメンバーも一人で実装していけるようになっているなど、設計を考えたことによりコードの品質が上がっただけでなくエンジニアとして成長する機会も作り出すことができました。

Container ComponentとPresentational Component(Atomic Design)を取り入れた最終的な構成

ここまで説明した概念を組み合わせた概念の関係を図に表すと以下のようになります。

Backend APIとContainer層、Presentational層の関係図

crowdworks.jpでは部分的にVue.jsを使用しており、未適用の部分は段階を踏んでVue.jsに置き換えていくフェーズです。そのためContainer Componentの下にOrganismsやMoleculesが来ることも許容しております。また、図では表現されておりませんが当然下位レイヤーのコンポーネントを複数持つことは可能です。

html.erbからVue.jsへの置き換えなど負債を返却しながら機能追加を行ったわけですが、リリースまで期限がある施策チームであるため、リソースの関係上厳密にコンポーネント化するのはMoleculesまでにして、Atomsに関しては複数ページで利用され、かつ他チームでも頻繁に利用される可能性があるUIのみコンポーネント化しました。(そのため図ではAtomsを点線囲っております)

おわりに

以上、負債を返却しながら機能追加しなければならない状況で実践したフロントエンドのコンポーネント設計でした。

実践した内容としてはシンプルなことでしたが、これだけで負債返却・機能追加へ大きな効果があっただけでなく、チーム開発での生産性にも貢献しました。
また、自分で戦略を考えてチームに推進したことによってより一層設計の重要性を学ぶことができて良い経験でした。

ここまで読んでいただきありがとうございました。

We're hiring!

クラウドワークスでは、働き方の変革に挑戦するエンジニアを募集しています!

www.wantedly.com

© 2016 CrowdWorks, Inc., All rights reserved.