Next.jsとTypescriptが奏でるUniversal JSの世界 ~commmune を支えるアーキテクチャ~

https://cdn-ak.f.st-hatena.com/images/fotolife/c/commmune/20190704/20190704140625.png

APIサーバのアーキテクチャ
APIサーバのアーキテクチャ

commmuneとは?

企業独自の「ユーザーコミュニティ」のカンタンな構築と、効果的な運用を実現する日本初の企業向けクラウド型顧客ポータルです。

ユーザー画面

管理画面



アーキテクチャの全体像

概観


ほぼGCPです。「鶏を割くに焉んぞ牛刀を用いん」という考え方を私たちは大切にしており、極力シンプルなWeb3層アーキテクチャを採用、維持しています。トレンドを追うことは大切ですがそれ自体を目的にするのではなくcommmuneに真に適するものを導入していこう、というスタンスです。

* フロントエンド:Next.js
* APIサーバ群:Express
* アプリ:React Native

がベースライブラリになります。

組織アーキテクチャ

一般的には「サーバサイド」「フロントエンド」という区分で所属やタスク割り振りをするチームが多いと聞きます。一方で私たちは原則「サーバサイドとフロントエンドを別け隔てなく」書いています。

技術領域ベース タスクベース

もちろんどちらかが絶対的に優れているわけではないと考えています。しかし、私たちはスタートアップです(リソースが非常に限られています!)。また、リモートワークが盛んです(対面よりも意思疎通の難易度が上がります!)。そういう事情を鑑ると私たちには後者のほうが適していると判断しました。良い意味で責任の所在も明らかになり、トラブルもないため当面はこの形を維持する予定です。

ただし馴染みが薄いアーキテクチャのため、実践する場合各メンバーの理解と協力が必須になりそうです。

おもな要素技術のご紹介

バックエンド

Express + Passport + Sequelize


(画像は closed-api-server/services)伝統的なM(V)CSアーキテクチャに沿って実装しています。クラス(モジュール)の分け方は難しい部分もありますが、画像の通り概ね機能ドメインに沿っています。

Cloud SQL(MySQL)

O/Rマッパを使用していることもあり、極力JOINクエリは避ける戦略をとっています(吐き出されるクエリを理解した上でのJOINであればもちろん可)

尚、キャッシュとしてRedisなどのNoSQLを併用する例が多いですが、commmuneでは現在のところ導入していません。キャッシュレイヤーを導入する場合、導入によるアップサイド(大半のケースではパフォーマンスでしょうか)と、ダウンサイド(アーキテクチャ、実装が多少複雑になってしまう、など)を比較することになります。そして私たちのケースではまだまだ前者よりも後者が大きい、と考えているためです。

なお、弊社ではビジネスサイドも全員MySQLを駆使し、仮説構築から分析まで自律駆動できる体制になっています。

nginx (Let's Encrypt)

SSL対応のために linuxserver/letsencrypt というdockerイメージを使用しています。PHPとの連携を推しているようですがNode.jsでも全く問題なく動作します。

SendGrid

メール通知用です。Compute Engineの公式ドキュメントで紹介されているサービスなので素直に採用を決めることができました。

Elasticsearch

全文検索エンジンとして使用しています。開発初期はMroonga(MySQL)が担っていました。しかし将来のスケーラビリティ(DBをCloud SQLに移行するなど)を考慮して移行を決断、実行しました。

尚オンプレミスではなくElastic CloudというSaaSを利用しています。GUIでポチポチするだけでスケールアウト、スケールアップ、クラスタリングなどが可能なのでおすすめです(現状最も近いサーバの所在がoregonであるという悲しみを乗り越え)

BigQuery

ユーザーの行動ログの格納、解析に用います。

imgix

画像最適化で用います。事実、Webサービスに於いて50%以上は画像リソースです。ここを最適化できる最強のSaaSです。Next10でOptimizerが組み込まれましたが、あれはWebサーバ上で画像操作を行うのでややスケーラビリティに難があります。キャッシュされるとは言え、imgixのような完全CDN型の最適化のほうが「安定稼働」に繋がります

フロントエンド

Next.js


フルスタック寄りのReact.jsフレームワークです。Server RenderingやZero Setupが特徴です。国内ではNuxt.js(というよりVue.jsの勢い)に押されていますが、世界的には一定の支持を得ているようです。 もはやNext.jsの勝利は決定的!!

commmuneではcreate-react-appとNext.jsで迷いましたが、ここは攻めた判断(攻めると言っても当時すでに十分すぎるスター数、情報量がありました)を採り後者にしました。

* 規模の割に学習コストが小さい(フレームワーク特有の記法が少ない)
* 公式のexamplesが大量にある(本当に多いです…)
* Redux化やTypescript化が容易(拡張性の高さ)

など、思った以上に素直で扱いやすいフレームワークです。もっと日本でも布教していきたいので私たちが先頭を切り礎を築く所存であります。

Redux

(Smalltalk由来の)MVC実装を持ち込む際の定番ライブラリです。reducerをどの粒度で分割するかは永遠の課題(と考えています...)ですが、commmuneでは現在4つに分割しています。

// ログイン中ユーザのアカウント情報
user
  id
  nickname
  email
  .....

// (readonly)当該テナントの静的なサイトデザイン
site
  logo
  favicon
  colors
  .....

// ユーザ画面上での動的なグローバルステート
app
  news
  point
  badge
  .....

// 管理画面上での動的なグローバルステート
app_admin
  posts
  pointSetting
  adminLibrary
  .....

site は管理画面上でのみ更新が許されるreducer(即ち管理者ユーザしか更新しない)で、ユーザ画面上では参照専用のstateという位置づけです。一方 app は動的な状態変化を受け持つので、両者は対照的な存在になります。一般的な分割法と比べると粒度は大きめですが、はじめから小さく割るよりは必要に応じて都度リファクタリングしていこう、という姿勢です。これもやはり「鶏を割くに」の思想に基づいた判断です。

redux-saga

非同期通信のベストプラクティス的なライブラリです。ただNext.jsで useSWR がより広く使われるようになってくると、そちらに乗り換えてもいいなと思っています。

プロジェクト基盤

Typescript


開発当初は100% JavaScriptでした。しかしながら予想以上のスピードで規模が拡大しており「ズルズルとJSで進むよりはTSに全置換したほうがグロスの生産性は上がりそうだ。」と判断し「人類TS化計画」を開始しました。これは計3ステップから成る大計画で現在2ステップ目(anyをなくそう!)です。CTOの私を含めTS未経験者が半数近くでしたが、「やると決めたらやり抜く」を胸に皆で一緒に前進しています。

尚1ステップ目(JSからTSに拡張子を変えよう!)は3ヶ月間で全ソースコードの85.6%を移行できました。これはチームメンバーのとてつもない士気と「ボーイスカウト・ルール」の実践があったからこそ実現できたものです。本当に感謝しております...。

Circle CI


「デプロイは早期から自動化して損なし」と感じています。属人化を防げますし、正味作業ではないから(本質的な価値を生まない作業ということ)です。高速(計5分以内)、完全無停止(pm2docker-swarmによる完璧なグレースフルリロード)を目標にして日々研鑽しています。デプロイの質は(間接的に)開発工程の質に繋がります。

Cloud Functions


Slack上でBotくんに話しかけると、Cloud Functionsがイベントを検知し好きな環境にビルド、デプロイが可能です。私たちのユースケースではコストゼロで運用できるので大変懐にやさしいです。

Cloud DNS


commmuneはいわゆるマルチテナント型のアーキテクチャです。その中でも「最も効率的な真のマルチテナンシー」と呼ばれる形式を採用しています。実際に運用してみると、他の方式に比べ間違いなく実装の難易度は上がりますが、その分サーバアーキテクチャはとてもシンプルになります。そしてなにより技術的なチャレンジは常に推奨されるべきです。

(参考)Universal / Isomorphic JavaScript

commmuneは全レイヤーがTypescript(JavaScript)実装になります。この方式で開発を進めると2つの利点が見えてきました。

1. 型定義ファイル、定数ファイルを楽に共有できる

バックエンド(APIサーバ群)とフロントエンドで型や定数を共有したい場面がしばしば訪れます。その際 .ts ファイルをそのまま使い回せるのはやはり便利です。通常YAMLなど汎用フォーマットを中間に挟むようですがその手間がなくなるイメージです。

2. 未知の領域への不安を和らげることができる

私たちのチームの開発体制(後述)と関係します。簡単に言うと例えば、「Expressは経験がないけど同じJavaScriptだしやってみよう!」と感じやすい(フロントエンドメンバー談)。あるいは逆に「Reactやるぞ!」と勇気を出しやすい(バックエンドメンバー談)。そんなイメージです。些細ですが皆のやる気や勇気はきっと全てに勝るのです。