この記事はドワンゴ Advent Calendar 2018 25日目の記事です。
ドワンゴでニコニコ生放送のPC Webフロントエンジニアをやっています、 @kondei です。
いつまでも新人気分だったのに、いつの間にか入社5年目になり、この前ナチュラルに新卒に歴史的経緯を語る立場になっていて勝手にショックを受けました。いにしえの時代(最後期のとはいえ)から現在まで実際に開発して知っているフロントエンド開発者が数人になってしまって貴重なので、いい機会なのでまとめようと思います。
内容的には ニコニコ生放送の watch ページを MobX で作り直している話と重なる部分もあります。
はじめに
まず、大人数のフロントエンド開発でアーキテクチャをどうするべきかの一つの解を得たので、それを述べます。
そして、どうしてそうなったのか、つまりニコニコ生放送が過去にどういう問題を抱えてきて、どういう解決策を講じてきたのか、自分の知る範囲で フロントエンドのアーキテクチャ面 からざっくり述べます。
前提
今、生放送チームのバックエンドとフロントエンドの開発者(デザイナー含む)はそれぞれ20人くらいいます。
また、8~9年とかなり長く続いているサービスを開発しています。
また、生放送webにおいてはPCのwatchページとプレーヤーが一番機能が多く常に開発が盛んであり開発者が多く、それ以外のPCページやSmart Phone(SP)のページはそれに比較すると長期スパンでは落ち着き気味で開発者が少なめであるという特徴があります(比較的です)。
少人数で開発する場合や、長期的に保守するか不明なプロダクトの場合、などはまた違うスタイルが適切だと思います。実際、後述するようにSPは若干違う技術スタックや単位でリポジトリを構成しています。でも大きくは変わりません。
TL;DR
- 責務で分業して人数でスケールしよう
- 強い型付け言語で、ドメインモデルとチーム間のインターフェースを定義しよう
- モノリシックやめよう
- まず横(技術)に切らず、縦(提供したい機能)に切ろう
- 例:「ページ」で切ったリポジトリや、マイクロサービス
- その後横に切って、横もなるべく揃えて、いいとこ取りしよう
- 技術を横に揃えると効率が上がりスケールアップするけど、トップレベルでズバッと横に切ってしまうと人数でスケールアウトしにくくなる
現在最新の方針
PC watchページのように開発が盛んなページを、以下の3層に分けた1つのリポジトリを作る。
- Front-End Server (FES)
- バックエンドからページのための情報をかき集めるサーバー
- CCが規定したPropsを渡して、Server Side Renderingする
- Container Component (CC)
- 状態管理、APIなどからデータ取得
- VCにPropsを渡して描画
- View Component (VC)
- 見た目の管理
また、開発が落ち着いたページは、上の単位でひとまとまりとして更にまとめたモノレポに置いて管理を楽にする
最新の方針がこうなった経緯と、上がそれぞれ何かを後述します。
昔の歴史
生1 (2007?~現在)
偉大なる初代。
技術スタック
- PHP
- Flash
- ECMAScript 3
- prototype.js
- jQuery
アーキテクチャ
- モノリシック
- サーバーサイド
- PHPで自社フレームワーク
- クライアントサイド
- 割と皆モジュール単位で雰囲気でjsを設計して書いてた気がする
- 部分的にknockout.jsがあったり、jQueryが使われてた気がする
良かったこと
- サービスを生んでくれた
- ビルドがないので(修正すべき箇所が分かっている状態なら)修正とデプロイは速い
抱えていた問題
- たくさん
生2 (2013~現在)
問題点がいっぱいあって、 「エンジニア歴17年の俺が、事業系の開発タスクをバンバン投げてくる非エンジニアに、保守の必要性を死ぬほど分かりやすく説明する。」 みたいなことになっていたので、手始めにPC webのwatchページの部分から作り直されました。
Developers Summit 2014 「Play2/Scalaでドメイン駆動設計を利用した大規模Webアプリケーションのスクラム開発の勘所」 に詳しいです。 ちなみにurlが live2.~
になってるやつです。
技術スタック
- Scala
- Play2 framework
- Groovy Template
- Flash
- TypeScript
- jQuery
- gulp
アーキテクチャ
- モノリシック
- サーバーサイド
- DDD
- クライアントサイド
- 割と皆モジュール単位で雰囲気でjsを設計して書いてた気がする
- 部分的にRxJsが使われたりしたが、ずっと模索していて結局解が見つからなかったのが実情
良かったこと
- DDDという概念が開発に根付いた
- TypeScriptによりフロントエンド開発に型の概念が持ち込まれ、大規模開発しやすくなった
抱えていた問題
- フロントエンドがくっついていて、バックエンドとリリースフローが一緒だった
- デプロイ時間がScalaビルドで50分以上…
- バックエンドの切り戻しにより、フロントエンドの機能リリースも延期
- フロントエンドとしては洗練されていなかった。
- 途中までnpm package による開発じゃなかった
- フロントエンドのフレームワークも導入されていなかった
- モノリシックなせいで、やり方が揃っていない部分が気になる
- 何度も「どういうアーキテクチャで書くべきか」「ここ古いやり方だけどどうする?」みたいな議論をした
- サーバーサイドのHTMLテンプレートも、Groovy Template がつらいし、Scalaによるコードとかなり直結しており、スキルセット的につらい
マイクロサービス化(2016~現在)
生2になってもつらいので、サーバーサイドがマイクロサービス路線になっていきます。また、徐々に バックエンドサーバーとフロントエンドサーバーが完全に分離 されはじめます。これはいくつかの革命的な効果がありました。
また、これも重要なことなのですが、フロントエンドのコードはバックエンドとは別のリポジトリで管理するようになっていきます。
この後は自分はフロントエンドをメインに開発したのでそこを述べていきます。
最近のフロントエンド部分で抱えていた問題点と解決策
第一世代HTML5プレーヤー(外部プレーヤー)
iframeとかで外部のサイトに張れるプレーヤーです。
PCにもSPにも貼れます。
技術スタック
- TypeScript
- react
- scss
アーキクチャ
reactコンポーネントが樹状になるようにして、なるべくルートに状態を集めた。原始的ですね。
抱えていた問題
- React Componentが状態を持ちまくって巨大な責務になっていく
- scssがグローバルスコープなのでクラス名を決めるのがつらい
- CSS in JS するか、 CSS Modules の導入するか悩んだ
第二世代HTML5プレーヤー(PC watchページのプレーヤー)
今動いているやつです。
この辺から ViewComponent と ContainerComponent のような分離が始まりました。
ViewComponentについては @misuken さんの React + TypeScript + CSS Modules によるコンポーネント指向フロントエンド開発の流れと知見 や、 @ln-north さんの コンポーネント指向フロントエンド開発におけるデザイナーの参画について という素晴らしい記事を見てください
技術スタック
- TypeScript
- react
- scss
- css modules
アーキテクチャ
- facebook/flux
- 全StoreをEventEmitterにして、ルートで全部監視(!)して、どこかに変更があるたびに全Props再生成
解決した問題
- 見た目の全てを担当するVCとの分離により、それを使う側がfluxでプレーヤーの状態だけを管理すれば良くなった
- デザイナーさんがscssのみに集中でき、開発しやすくなった
- → 今後VC方式は生放送webに全面的に採用されていくことになる
- css modules により、CSSがグローバルモジュールな問題が解決した
- → これもその後は全面的に採用されている
- 初めて、モダンなフレームワークで同じやり方でフロントエンドをまとまった人数で開発できて、人数スケール性が高かった
抱えていた問題
- fluxつらい!
- ボイラープレートが多い
- 処理がイベントで切れてて追いづらい
- modulesの切る単位が割と雰囲気で決まっており明確な基準がない
- パフォーマンスが出ないのでfluxを諦めて逸脱してる部分がある
など。 https://www.slideshare.net/_kondei/watch-mobx がわかりやすいので見てください。
watch ページを生2からCCへ作り直し
プレーヤーで採用したfluxとか、一部のライブラリで採用したクリーンアーキテクチャ(持続可能な開発を目指す ~ ドメイン・ユースケース駆動(クリーンアーキテクチャ) + 単方向に制限した処理 + FRP で過去に考察した)の何がつらかったかを見返して、新たな設計をした結果「結局僕らのほしいのはReactiveなMVCだったんだ」ということが分かり、MobXを導入しました。
抱えていた問題と解決されたことなどについては 自分の記事 がわかりやすいので見てください。
MobXを利用してどう具体的にどういうアーキテクチャで構築したかについて、このとき説明した構成と今もほとんど変わっていないですし、「正解パターン」だったと思っています。
実際、fluxのプレーヤーをこのやり方で作り直す予定です。
全ての入ったモノレポ化
モノリシックの反動であまりにもバラバラにリポジトリが20個以上乱立したせいで、管理がつらくなりました。
「フロントエンドの、ライブラリを除くPCのアプリーケーション部分全てを入れたモノレポを作って解決しよう!」という方針に一旦決まりましたが、試しに一部を入れてみてつらかった(後述)ので、やめることにしました。
そして最新の方針へ
冒頭でも述べたように、最新の方針は以下のようになりました。
PC watchページのように開発が盛んなページを、以下の3層に分けた1個のリポジトリを作る。
- Front-End Server (FES)
- Container Component (CC)
- View Component (VC)
また、開発が落ち着いたページは、上の単位でひとまとまりとして更にまとめたモノレポ(各ページをgit submoduleとかsubtreeでまとめるだけのリポジトリでもいいかも)に置いて管理を楽にする。
図にするとこんな感じです。この他に各ページ共通のライブラリのリポジトリがたくさんありますが、割愛します。
Front-End Server (FES)
- バックエンドからページのための情報をかき集めるサーバー
- CCが規定したPropsを渡して、Server Side Renderingする
- 絶対にDOMを持たないただのサーバーに徹する
- docker imageを作ってdockerのクラスタ上に乗って動作
Container Component (CC)
- 状態管理、APIなどからデータ取得
- VCにPropsを渡して描画
- ページというアプリーケーションとしてVCを統合して動く
- ページ全体がSSR時もCSR時も Isomorphic に動くように管理する
- 絶対にサーバーサイドテンプレートのDOMをFESに管理させないでここで持つ
- サーバーサイドテンプレートは最低限に留める
- ページの中身のDOMはVCに委譲
View Component (VC)
- いわゆるアプリーケーションの状態を持たない Presentational Component
- ページに描画される全ての部品のDOMをReactで持つ
- デザイン(css)を持つ
- 絶対にページの中身のDOMと見た目の管理をCCやFESにさせず、ここで持つ
- 必要なものをすべてPropsでCCから要求する
- SSRできるように作る
これら3つに分けることによって、フロントエンドにおける「関心の分離の原則」と「単一責務の原則」が厳密に守られます。
また、SSRによるメリットをとるとサーバーとクライアントでコードの重複が発生する問題がありますが、 Isomorphic JavaScriptとして単一のコードで管理することで開発が楽になります。
また、この単位でのモノレポ化によって解決する問題は以下になります
バラバラすぎるときに発生していた問題
- FES, CC, VCの互いの変更の取り込みが非効率になりがち
- いちいちCIが終わってから、バージョン上げるだけのPR作るのつらい…
- exportし忘れたせいでビルドできない、とか
- 共通で使うライブラリの依存を上げる作業が大変
- 同じページの変更なのに、リリースノートがFESとCCとVCで途切れてて追いづらい
- FES, CC, VCそれぞれに同じfeatureブランチを作るのが面倒
- ページの中でバージョン違いのライブラリが混ざる
- リポジトリの切り方に一貫性がない
- FES, CCはそれぞれページ単位、VCは全ページ入りだった
- 同じページを作っているのに、FES, CC, VC で割と互いに無関心になりやすい
- 1つのページで全技術スタック通して考える必要がある作業がやりづらい
- パフォーマンス最適化など
あまりにも全部入りにするとき発生する問題
- 生1、生2のモノリシックのつらみの再来
- CIが重すぎ (watchページのCCを入れただけでproduction buildが25分…)
- git-flow, github-flow, の両方が運用されていたが、リポジトリによって適切なものが違うのにどっちかに寄せなきゃいけない
- ページAの開発のせいでページBをコードフリーズする事態が生じる
- エンジニアチーム1, エンジニアチーム2, デザイナーチームで分担して開発を人数スケールさせづらい
- Separate Ways(別々の道)パターン に反する
- 特定のページだけ別のやり方で作り直したくなったときに他のページと違ってつらくなる
- 例えば、watchページだけ性能要件が厳しいので、今後watchだけ色々変わってくるみたいな例はありえる
余談:SPのプレーヤーとページ
自分は開発者じゃないので詳細に触れないですが、開発しやすそうなので、これも一つの正解だと思います。
Smart Phone用の全ページがある単一リポジトリで管理されて、3,4人ほどで全ページが開発されています。
生成されるdocker imageも全ページ込み。
SSRもしています。
PCのページと違って、URLをまたいだSPAを実現しています。
技術スタック
- react
- redux-saga
- css modules
- ViewCompoent
まとめ
どうしてもニコニコ生放送というサービスは巨大であり、開発者も多いので、モノリシックにしてもバラバラにしすぎてもつらい、という歴史が今まで辿られてきました。現在改めて見ると長い迷走に見えますが、その時その時でトレンドやサービスの事情が違ったりするので、関わった人はみんな当時にベストを尽くしたと思います。
最近思うことは、縦(機能)で切るとそこで人数でスケールアウトできるが、同じものを別々に実装したりして効率が悪くなる。逆に横(関心事や技術)で切ると、同じやり方で揃うので一人あたりの効率がスケールアップするが、どんどん増える開発者に仕事を任せてスケールアウトしにくい、というところです。どこまで分業すると生産性が上がるかを見極めて、良いとこ取りをしたいですね。
導入して効果のあった、重要なパラダイム
生放送で長いことかけて培ってきたフロントエンドのアーキテクチャに関わる知見として、以下のパラダイムが有用であったと思います。
- DDD
- フロントエンドのアプリケーションでもドメイン層とUI層を分けよう
- マイクロサービス
- 開発者が多くなったらフロントエンドを含むモノリシックにするのはやめようね
- マイクロフロントエンド
- https://micro-frontends-japanese.org/
- 完全に徹底するのは非効率だし無理だけど、縦に切るというエッセンスは重要
- Backends For Frontends (BFF)
- バックエンドの影響から守られる
- TypeScriptによる型
- ドメインモデルを型で表現する
- Propsに型をつけることで、VC-CC間、CC-FES間のつなぎこみがスムーズになる
- Isomorphic JavaScript
- DOMをクライアントサイドコードと共に一括管理でき、SSRもする1つの機能に対し修正箇所が2箇所にならずに済む
- サーバーがサーバーに徹することができる
- reactive programming
- 宣言的に書けて保守性が上がる
- React.js
- MobX
- 宣言的に書けて保守性が上がる
- モノレポ化
- ライフサイクル・リリースサイクルが同じコード(1つのページとか)は同じ場所で管理すると効率が上がる
- 依存関係を一方向にする
- 容易にリファクタ可能になる