ちょっと意識高そうなタイトルを付けてしまいましたが、皆さんどのような開発でお過ごしでしょうか。

Nagisa では現在フロントエンドエンジニアは1人です。長きに渡って1人で開発してきましたが、最近は iOS エンジニアやデザイナーも開発に参加することでスピードとクオリティを担保しています。
そこで今回はスキルや役割の違うメンバーで効率よく開発を進める。そしてコードをカオスに落とさず秩序を保つために行ったワークフローの改善や、取り入れたデザインパターンなどを紹介したいと思います。

複数人でチームを組んで開発をするのは、1人のときよりも難しいです。雰囲気で開発を進めていけばコードはカオスに陥り、スピードも品質も担保できなくなります。
他人のソースコードを引き継いだ時や3ヶ月前の自分 (殆ど他人) のコードを改修している時も似たようなことが起こります。
逆に、1人で開発して1人で継続的にメンテして引継ぎ等もない世界であれば、カオスを感じることも観測されることもないはずです。

チーム開発が上手く進まない、引き継ぎが上手くいかない等のケースでは プロジェクトやフレームワークに対して設計手法やデザインパーターンが画一されていない ことが多いです。
設計やルールが弱いため役割が曖昧になったり依存関係が強く、最悪の場合闇落ちします。

ここまでものすごく当たり前のことしか書けていませんが、フロントエンドのコードはとてもカオスに陥りやすく、秩序を保つのが難しいです。
なぜなら HTML、CSS、JS といった同じ技術の中で、ペライチのようなスモールな静的ページから、PWA と呼ばれるネイティブに近いアプリケーションまで様々な表現が求められるからです。
そのためものすごい数のデザインパターンや設計手法からプロジェクトに合わせて最適なものを選択し組み合わせるという事が必要になります。

ワークフローの再考

それぞれバラバラのスキル、役割を持つメンバーが同時に効率よくコミットできるようにするため、まずはじめにワークフローの再考を行いました。
開発メンバーの役割やスキルを整理すると

  • フロントエンド
    • 設計
    • JS, HTML, CSS
  • デザイナ
    • UI 設計
    • HTML, CSS, (JS)
  • iOS エンジニア
    • JS

1人で開発しているプロジェクトであれば

  • 設計
  • UI コーディング
  • UI インタラクション実装
  • API コールと Store (Vuex, Redux, MobX など) 実装
  • データバインディング

といった工程を順番に進めるだけでした。このままだと

  • 工程によってコミットできるメンバーが限られてしまう (リソース勿体無い)
  • タスクの競合
  • git 上でのコンフリクト多発

などが懸念として上がりました。
そこで、ワークフローを次のように見直し、時間の掛かる UI コーディングと設計を同時に進行できるようにしました。また、チームメンバーとワークフローに合わせて設計なども見直しています。

全体設計

役割の違うメンバーが同時にコミットしていくため、コード側の責務も切り分ける必要があります。
Vue + Vuex を使ったプロジェクトなので、設計はよくある MVVM の考え方で下記の構成を取りました。

階層 パターン 実装
UI View Vue Component のテンプレート
UI ViewModel Vuex|Vue Component の JS
Domain Model Vuex

Vuex が Flux パターンのアイデアをベースにしているため、React + Redux や MobX でも近いパターンは作れそうです。

途中から自分以外のエンジニア (今回は iOS エンジニア) が参加することも分かっていたので、コードには TypeScript を使用しています。
型情報によって、途中参加や引き継いだ場合などでもコードの把握が容易になります。補完も強力になるため、他人が作ったコードを利用した実装なども半自動的に扱うことができます。
また、iOS エンジニアなど非 JS エンジニが JS を書いた時に型がなくてつらいということもよく聞いていたので、今回のようなケースにはとても良かったです。
プロジェクトのディレクトリは次のように構成しました。

CSS はコンポーネント単位の Scoped CSS で設計します。これにより SMACSS や ECSS など巨大な設計手法を知らない人でも CSS が書けるようになるため、チーム編成が柔軟にできます。
ネームスペースを管理するコストやバグの可能性も大きく削減できます。私達の場合

  • mixin
  • フォント
  • Vue の transition

などグローバルで必要ものを切り分け Scoped せずに読み込ませています。アプリケーションだけでなく Storybook にも全く同じものを読み込ませます。
また、画像のように PostCSS の変数を core として切り出し、各コンポーネントから import して使うことで変数を管理します。

コンポーネントの分け方と実装

コンポーネントの切り分け方はプロジェクトにより様々です。例えば

  • React + Redux の Container
  • ViewModel としての Component
  • ループアイテムの Component

などが挙げられます。
今回デザイナーが UI のコーディングを行うということもあり

  • アプリケーションの設計、ロジックに影響を受けづらコンポーネント
  • 実装においてループ処理や this を適切なスコープで扱える

という点が課題でした。そのため、既に取り入れているプロジェクトも多い筈ですが、デザイナの間でよく用いられる AtomicDesign を導入することにしました。
AtomicDesign に関して詳しい情報はたくさんあるので省略します。ざっくりになりますが

  • Lv1: Atoms
  • Lv2: Molecules
  • Lv3: Organisms
  • Lv4: Templates
  • Lv5: Pages

の5つのレベルに分割して UI パーツを設計していくデザインパターンです。Sketch などのデザインツール上でシンボル化することによって実現されており、コンポーネント指向や WebComponents の考え方によくマッチします。
今回のプロジェクトでは5つのレベルに対して次のような実装ルールを設けてコンポーネントを切り分けました。本来の AtomicDesign のルールそのままのもの、関係ないけどプロジェクトにおいて取り決めたものと含まれています。

Lv1 Atoms

Atom (原子) はその名前の通り単体で意味を成すことのない最も原始的なコンポーネントです。実装上では

  • 他のコンポーネントに依存しない
  • ロジック及び State を持たない (Functional Component, Pure Component)
  • Iconfont に依って表現される場合もある

と定義しました。特に State を持たない Functional Component とすること、自身に UI 操作などをハンドリングするロジックを持たせないことで、与えられた Prop に対する出力が常に 1:1 となります。
これにより再利用性やテストのしやすさ、Lv1 でバグが生まれる可能性の低さを担保しています。

Lv2 Molecules

Molecule (分子) は2つ以上の Atoms から成り立つものです。当然 Atoms を配置する上で必要になる要素もここで表現します。ここでの実装は

  • Atoms への依存
  • Molecules 同士の依存はなるべく避ける

というルールを設けました。但し Atoms に依存しないけれども単体で意味が成立するケース、どうしても state が必要なケースでは Lv2 として実装します。
Molecule 同士の依存は AtomicDesign で禁止されてはいませんが、これがあった時そのコンポーネントが Lv3 足り得る可能性が高くなるため、要検討です。

Lv3 Organisms

  • Molecules, Atoms への依存が可能
  • Organisms 同士での依存は要検討

Lv1 と Lv2 を組み合わせながら作る要素です。インタラクションのハンドリングや後述している Model へのアクセスの関係から、Lv3 以上は全て TypeScript での記述としました。

Lv4 Templates

Template は実装上 Lv5 Pages と意味合いが同じになります。命名上コンポーネント以外の部分 (Webpack に挟む html のテンプレート等) と紛らわしくなるため使用していません。

Lv5 Pages

Sketch 等デザイン段階では Lv4 と表現されてる状態かもしれません。レンダリング後の状態が Lv5 と等しくなりますが、前述した理由から Pages を採用しています。
Router に必ず紐づくコンポーネントになります。また、プロジェクトのルールとして唯一 Model へのアクセスが可能 (state, getter 参照) なコンポーネントとしました。
通常 Prop のバケツリレーは嫌われる事が多いので “なんで?” と思われる方が多いかもしれません。
どこからでもこれをできるようにしてしまった時に

  • 初見時にデータの流れを追うのが大変になる
  • ドメイン層の変更があった時に UI 層の修正が多くなる

というケースが起こりえます。また、Lv5 から渡したデータ (Prop) が表示に使われるのが Lv3 でも2でも1でも、あくまで 配下のコンポーネントで使われる という宣言をすべきだと捉えています。
また、AtomicDesign + 前述のプロジェクトのルールにより、Prop のリレーはは最大でも4つまでと保証されています。結果メリットがデメリットを上回ると判断しました。
ViewModel の中でも一番 ViewModel や Controller ぽい振る舞いをする ViewModel です。

Storybook

前述のコンポーネント実装ルール策定と同時に Storybook を導入しました。
これにより私達の場合

  • デザイナがコミットできること
  • アプリケーションの設計及び Model と並行して進めること

の2つ大きなメリットがあります。実装されたコンポーネントの UI カタログは

  • 複数人で実装を進める
  • エンジニアが Lv5 を組み込む
  • プロジェクトへの途中参加
  • コードの引き継ぎ

など様々なケースにて恩恵が期待できますね。Storybook+AtomicDesignでデザイナーが開発に参加した話 の記事でも書かれていますが、AtomicDesign + Storybook はかなり上手く機能しています。
実際 HTML と CSS のコーディング経験しかないデザイナーが、Lv5 含め全ての Vue コンポーネントを書けるようになるまであっという間にマスター & コーディングを終えてしまいました。
実装段階でレベルデザインを間違えていて Sketch の変更を行うこともありますが、実装レイヤーでのコンポーネントを理解することでデザイン時のレベルデザイン精度が上がる効果もあるようです。

Vuex (Model) の設計

Vuex (Redux, MobX) をモデルとして扱う場合、State はとてもシンプルになります。
コンポーネント間のイベント情報伝達など UI 層のデータと、API 経由で取得したビジネスロジックとして扱うようなデータを切り分けて扱うことができます。結果 State が巨大なグローバル変数に成り下がってしまう、というケースをよく目にします (私もやらかしました)。
逆に規模が小さければここをそれほど明確に切り分けるメリットは感じられないかもしれません。

Vuex には module という機能があり、module 間のアクションや state 等はネームスペースで区切られ基本的には互いに干渉し合いません。今回は

  • auth
    • ログイン情報
    • チケット情報
  • pages
    • ログインを必要としない全ページの API リクエストと結果
  • viewer

という粒度で分割しています。管理画面など1つ1つのページが複雑な要件を持っている場合にはページ毎に module を分割するのもありだなと思いました。

このレイヤーは勿論全て TypeScript で記述しています。State には API のリクエスト結果などが保持されており、それなりの深さを持つこともあります。画面数が増えればパターンもいちいち覚えていません。
型情報によって State の形や予測させる値。キャストすべきところや、考慮しなくて良い場所。null や undefined を考慮するところ、しないところなど様々な意図を汲み取ることができるようになります。
チーム開発において、これらのルールを取り決めることは必須です。それだけでなく型情報としてコードに記述できることは、他人が書いた Store を View レイヤから拾う際など本当にこのメリットは大きいです。

API のリクエストはここに含まれていることもありますが、私達の場合 Service として切り出しました。
API のリクエストには axios というライブラリを使用しています。これを Vuex から分離することで、Vuex を使わなくなっても Service はそのまま使える。axios を入れ替える場合も Vuex は影響を受けない、という状態を作れます。
記述は多少冗長にはなりますが、次のような構成です。

UI 層におけるコンポーネント間通信

UI 層とドメイン層を分離しました。ここで、今まで State においていたコンポーネント間でやりとりしたいデータ (グローバルメニューやローディングのプログレスバーなど) はどうするか、という問題に当たります。
Vuex のドキュメントではアプリケーション規模が小さい内は bus を、大きくなったら Vuex を推奨しています。

if your app is simple, you will most likely be fine without Vuex. A simple global event bus may be all you need.
https://vuex.vuejs.org/en/intro.html#when-should-i-use-it

私たちはこれを両方取り入れることにし、UI 層のデータのやり取りを bus で行うことにしました。
数が多くなってくるとカオス間違いないため、emmiter key を定数として列挙し、アプリケーション全体でどのような bus イベントがあるのかひと目で分かるように、またタイプミスなどがビルド時に分かるようにしています。
gloval event bus に関してはアンチパターンとしても触れられているので、扱いは要注意です。
Vue では bus と呼ばれていますが、React にも facebook/emitter というのがあったので、同じことができるかもしれません。

CI とビルド環境

CI はチーム開発をするのであれば絶対に欲しいですよね。
GitHub のプルリクをトリガーにして Travis から

  1. ESLint, TSLint
    • Airbnb ルール
  2. Stylelint
    • Sort order plugin
  3. Unit Test
  4. ビルド

の順番でスクリプトを走らせ、書き方の違うコードや間違っているコードがマージされることのないようにしています。
Unit Test はどこまでやるか難しいですが、Model と一部重要なロジックを持つユーティリティ、コンポーネントから優先して書いています。

ビルドに関してはフロントエンドの世界で大きな闇になっている部分です。Web の正しさを考えるのであればそもそもビルドすべきではない、等々色々な意見があると思います。私達のプロジェクトでは、今あるフレームワークや新しい仕様、TypeScript などを生かしながら効率よく進めるため、Webpack でのビルドを行っています。
Webpack と心中する、という言葉が生まれるくらい、Webpack を使った時のロックイン度合いは強くなりますが、なるべく大体手段があるかを確認しながらプラグインやローダ等を組み合わせていきます。

アプリケーションとして成立させるために、環境変数は標準のルールを拡張して設定をしています。私達のアプリケーションでは次の3種類を用意しました。

Key Value Note
NODE_ENV development, production フレームワーク等デバッグモードの切り替え
APP_ENV development, staging, production エンドポイントやソケットパス等アプリケーション設定の切り替え
VUE_ENV client, server SSR と CSR の区別
PORT dev サーバのポート上書き

そして、APP_ENV の値と設定ファイルを Webpack の alias で紐付けています。各環境で特別な設定をしなくても動くように、また設定をスタティックなファイルとして定義することが目的です。
この設定はビルドされたソースコードと、Express を起動する CommonJS から読み込む必要があったため、Json ファイルを採用しました。

これにより誰でも開発環境を作れるようにし、CI を組み合わせることでデプロイを自動化しつつ属人化しないような構成を狙っています (勿論ドキュメント等でチームに周知できている前提になりますが)。

このあたりの設定と運用はプロジェクトによって色々やり方がありそうですね。

まとめ

当たり前のことからプロジェクトのローカルルールまでつらつらと書いてきましたが、

  • チームとコミッターから最適なワークフローを描きたい
  • プロジェクトの規模に合わせて最適な設計をしたい
  • ワークフローと設計が適切であれば、いろいろな人がコミットしてもカオスに落ちづらくなる

ということだと思います。Web Components の思想を始め、Scoped Style などの新しい技術はチームとワークフローを柔軟にしてくれます。流れの速いフロントエンドですが、必要なものを必要なケースで使い分けられる、必要なものがまだ無ければ生み出せるようになっていきたいですね。
何より コードの秩序が保たれればスピードと品質を両立しつつ、仕事が楽しくなる なと思いました。最後まで読んで頂きありがとうございました!