JavaScript(JS)のフレームワークは、何を使うのがベストなのでしょうか。ここ3年ほどで数々のJSフレームワークが誕生していますが、React、Angularという二大巨塔を超えるものはなかなか現れていません。
そんな状況の中、GitHubではReactを上回るスター数を誇っている*1のが、2014年のリリース後、着実に進化を重ね、日本でも採用事例が増えてきているVue.jsです。「JavaScript ベスト・オブ・ザ・イヤー」に2016年、2017年と2年連続で選ばれているほか、Ruby on Rails(Rails)やLaravel といったサーバサイドのフレームワークが公式サポートを開始。11月には「Vue Fes Japan」という日本初のVue.jsカンファレンスが開催されます。
そこで今回は、10年以上前に作られたレガシーサービスの大規模リプレイスにVue.jsを採用した「エムスリー株式会社」にお話を伺いました。導入決定当時、チーム内にVue.jsの開発経験者がほとんどいなかったにもかかわらず、リプレイスの要としてVue.jsを選択したのはなぜなのか。ライブラリの活用方法や開発したからこそ分かったノウハウなどと合わせて紹介します。
- 前原秀徳(まえはら・ひでのり/@maeharin)(写真右)
- 2014年にエムスリーに入社。チームリーダーやグループ会社の取締役を歴任後、Vue.jsとサーバーサイドにKotlinを用いたシステムリニューアルプロジェクトを立ち上げ、リーダーとしてチームを牽引する。
- 鈴木健太(すずき・けんた/@suusan2go)(写真左)
- 2017年入社。入社以前はReactの開発経験があり、入社直後にレガシーシステムのリニューアルを経験。現在は社内の新規事業立ち上げにエンジニアとして関わっており、機械学習を活用したサービスのアーキテクチャ設計など、技術選定から携わる中心メンバーとして開発を行っている。
Vue.jsを選んだ3つの理由
――エムスリーさまではレガシーなシステムをVue.jsを用いて1年ほどかけてリプレイスされており、そろそろ完了が見えてきているころだと伺いました*2。どのようなサービスをリプレイスしたのでしょうか?
前原:私たちは主に医療関係のサービスを提供しているのですが、その中でも今回は、医師のキャリア支援サービスと薬剤師のキャリア支援サービスの2つをリプレイスしました。フロントはVue.js、バックエンドはRailsとKotlinを使っています。事業規模は年間売上が約100億円。各システムのデータベースのテーブル数はそれぞれ150〜200個くらいの規模感です。
―― 大規模なサービスですね。具体的に、どのようなリプレイスを行ったのでしょうか。
前原:私たちが手掛けているサービスには、一般ユーザーが求人情報を検索するための“表”側と、医療機関や企業が求人情報を管理する“裏”側があるのですが、今回は医師向けと薬剤師向けの2つのサービスで、“裏”側の管理画面改修を行いました。
ビジネスは10年で約10倍の売上規模に成長と大成功したのですが、パッチ的に機能追加を繰り返した結果、システムのレガシー化が進み、特に”裏”側の管理画面がまともに開発ができない状態になっていました。これを何とかしたいと思い、今回のリプレイスを行いました。2017年の5月から医師向けのサービス改修を始め、2017年の11月から、鈴木さんに薬剤師向けのサービス改修を始めてもらいました。
―― リプレイスで特に苦労した点があれば教えてください。
前原:10年ほど前の古い技術スタックが使用された、かなりのレガシーシステムで、バックエンドからフロントエンドまでリプレイスが必要でした。バックエンドはJavaの独自フレームワークを使っていて、ドキュメントもない。「ググっても分からない」というのはかなりつらかったですね(笑)。また、単体テストがないので、動作確認がつらい。バグを生んでも気付きづらいため、追加改修がほとんど行えていない状態でした。
フロントはテンプレートにXSL Transformations(XSLT)を使用し、動的な部分はjQueryで制御していました。管理画面は機能が多くUIが動的に変化する箇所も多かったのですが、jQueryでゴリゴリに組んでいる部分はさまざまな箇所にデータがバインディングされていて、誰も触れないような状態になっていました。
鈴木:バックエンドでやるべき処理をフロント側で無理やり行っている部分もあって、XSLTにいろんなロジックが漏れていたので、ソースコードを読むだけでもつらかったですね(笑)
前原:なので今回のリニューアルではバックエンドからフロントエンドまでまるっとリプレイスしました。APIサーバはKotlinを使用し、それをRailsがプロキシのような形で叩いて、フロントはVue.jsに任せるという構成です。鈴木が担当した薬剤師向けのサービスはNuxt.jsを使用しているので、よりシンプルな構成になっています。私が医師向けサービスを改修し始めたときは、Nuxt.jsがまだベータ版だったので使用していませんが、今からやるのであれば絶対に使っていますね。
―― Vue.jsの開発経験者はチーム内にいたのでしょうか?
前原:チーム内はもちろん、全社的にもほぼいませんでした。そのため、リニューアルプロジェクトで導入する前に、他の小さめのプロジェクトで本番導入してチームメンバーに触れてもらったり、リニューアルのためのプロトタイプをVue.jsで実装してみるという方法からスタートしました。
―― 開発経験者がほぼいない状況の中で、なぜVue.jsを選んだのでしょうか?
前原:理由は大きく分けて3つあります。1つ目は触ってみたときに「分かりやすい」と感じたからです。日本語のドキュメントが多く、単一ファイルコンポーネントでHTML、CSS、JSが一つのファイルとしてまとめて管理できるのが良いと思いました。
2つ目は公式のライブラリが充実しているところです。Fluxアーキテクチャを使うときにはVuexが使用できますし、ルーティングもVue Routerが用意されています。ライブラリの選定に迷わないというのは、フレームワークにおいて大事な要素の一つだと思います。
3つ目はVue.js自体の採用例が多くなってきている点です。PHPのフレームワークであるLaravelもVue.jsをデフォルトで採用していますし、GitLabもVue.jsを使っていると発表しています。国内のコミュニティも活発化していますね。
Vue.js用の状態管理ライブラリ。アプリケーションの状態(state)を集中的に保持できるコンテナの役割を果たす。グローバル変数と違い、リアクティブに更新を行うことができる。
鈴木:また、フロントの経験がそこまでなくても始めやすいところがVue.jsの魅力の一つです。今回の開発は、エンジニアがサーバサイドもフロントエンドも行う形だったので、ちょうどマッチしたのだと思います。逆にフロントエンジニアのエキスパートがそろい、TypeScriptで型を守ってガチガチに進めるような開発であれば、ReactやAngularを採用した方がいい場合もあると思いますね。
また、Vue.jsは始めるときの学習コストはそれほど高くはないですが、さまざまな書き方ができる分、関わる人数が増えるとコードの足並みをそろえるのが大変になってくる面もあるので、状況によってフレームワークの選定は変わると思います。
―― Vue.jsはライブラリが充実しているとのことですが、何かおすすめのライブラリや、開発において必須なライブラリなどあれば教えてください。
前原:迷ったらまずは、Vuex、Vue Routerなどの公式でサポートされているライブラリを使用してみるのがいいと思います。あと、PC版の管理画面を作るのであればelement-uiはパーツが大量にそろっているためオススメです。私たちも今回のリプレイスで全面的にelement-uiを使っています。
鈴木:SPAを作るのか、一部分に適用するのかで使用するライブラリは変わりますが、SPAを作るのであればNuxt.jsは検討すべきだと思います。あと静的検証ツールのESLintやprettierは、足並みをそろえてくれるのでおすすめです。
あとライブラリではないですが、Google Chromeの拡張機能である「Vue.js devtools」はとっても便利です。想定しない動きをした場合に、コンポーネントにデータが渡っていないのか、データが渡っているが表示がうまくできていないのかデバッグするのは大変ですが、Vue.js devtoolsを使うとコンポーネントが何を持っているのか、Vuexのストアにいまどんな値が入っているかが簡単に分かるんです。なので役に立つというよりは、むしろ「ないと開発がキツイ」くらいに活用していました。
レガシーシステムのリプレイスは「考古学」のよう
―― 鈴木さんは前職でReactを触っていたとのことですが、Vue.jsへの移行は苦労しませんでしたか?
鈴木:それほど苦労はありませんでしたね。むしろ、Reactで大変だったところが、Vue.jsだと楽に書けるということがあるくらいでした。例えばReduxでは、設計の際に「非同期処理をどこに書くべきか」が迷うポイントになりますが、Vue.jsはVuexのActionに書けるのでそこまで悩まなくて済みました。
ただユースケースが似ているとはいえ、Reactの設計をそのままVue.jsに持ってくるとつらいと思います。Reactではハイオーダーコンポーネントという、コンポーネントを別のコンポーネントでラップして、ロジックは上位のコンポーネントに持たせるという設計がよく知られています。しかしVue.jsで同じことをやろうとすると、propsの受け渡し方もReactとは異なりますし、Reactのようには型を付けられないので難しい。
―― やはり、それぞれに特徴や機能の違いはありますよね。ではVue.jsを採用した今回のリプレイスで、特に困った点は何でしょう。
前原:正直、Vue.jsというよりも仕様のリバースエンジニアリングが一番大変でした。動作確認するにもテストがないので、期待されるものが分からないという状況が多々ありました。 ER図を自動生成したり、DBの変更内容を検知するスクリプトを仕込んだりしながら、仕様を理解しつつ、昔のソースコードを読んで、ビジネスチームのメンバーに「これいるんですか?」とヒアリングして機能を整理して、コツコツ改修して……。どちらかというと開発よりコミュニケーションの方で時間がかかりました。レガシーシステムのつらいところですね。
鈴木:「こんな機能ありました!」というふうに新機能が発掘されることも多くて、まるで考古学みたいでしたね(笑)
―― まさに遺跡発掘ですね……(笑)
前原:Vue.jsに関してという観点だと、古いブラウザの対応をどうするかという問題はありました。移行前はIE8でも動作していたのですが、Vue.jsはIE9以上でしか動かないのでサポートを切る方針にしました。ただこれも、技術的な話というよりは、コミュニケーションの話かなと。
前原:あと、TypeScript関連は少し妥協が必要でした。Vue.jsはバージョン2.5でTypeScript対応が強化され、オブジェクト構文でもthisの型チェックが効くようになったのですが、今でもテンプレート部分の型チェックは効かないため、そこは諦めています。
鈴木:ReactだとJSXでJavaScriptをHTMLっぽく書けるので、そのあたり上手くやれるんですよね。Vue.jsでJSXを使用することもできますが、デファクトからは外れてしまうかなと思います。型のサポートはまだReactやAngularの方が上だと思います。
―― 改修を進める中で体得したVue.jsのバッドノウハウはありますか。
前原:バッドノウハウではないのですが、動的に変化するフォームを開発する際、APIの値をそのままv-modelでinput要素に双方向バインディングさせて、ポストするまで一つのオブジェクトでやろうとするとキツイ、ですかね。そういう場合は、APIから返ってくる値を他のオブジェクトにクローンし、ローカルな値で双方向バインディングして、ポストするときにオブジェクトへ詰め替えた方がいいと思います。
例えば「A」と選択した場合に次のページで10項目の選択肢があり、「B」と選択すると次のページで5項目になるようなフォームだと、フォームとデータのバインドが状況によって変わってきます。選択状態によって持つべき値が変わるとAPIサーバとオブジェクトの形が変わるので、今持っているデータがどんなものか分からなくなり、コントロールが効きづらくなるんですね。
フォームに対して最適化したローカルなオブジェクトを作り、APIに投げる方が楽です。もちろんケースによると思いますが。
鈴木:「watch」という変数の値の変更を検知して動作する非常に便利な機能が、意図せぬタイミングで発火するというケースもありました。
「watchした値だけど、状況によっては発火させたくない」というようにif文が複雑になる場合は、watchを使用するのはやめた方がいいと思います。例えば、ある値が変化したらフォームの一部をクリアするというwatchの処理がページ読み込み時に必ず発火してしまい、フォームの値が必ずクリアされて表示されるということが起こりました。
SPAを作るならVue.js+Nuxt.js
―― 鈴木さんが担当された薬剤師向けのサービスではNuxt.jsを使用されているとのことで、先ほども「SPAを作るのであればNuxt.jsは検討すべき」と断言されていましたがどのあたりにメリットを感じるのでしょうか。
鈴木:Vue.jsでSPAを作るときのベストプラクティスを用意してくれるという点です。サーバサイドレンダリング(SSR)が簡単に実現できる点も魅力的ですが、プロジェクトを作っていく際のディレクトリ構造が設定されていたり、VuexやVue Routerが標準で組み込まれているので、個人的にはプロジェクトの構成で考えなくてはいけないことがかなり少ない点に恩恵を感じます。
どういうディレクトリ構成で、どういったライブラリを使ってプロジェクトを進めていくのかはそのチームによって変わってくると思いますが、Nuxt.jsを使用していれば「Nuxt.jsの公式ドキュメントを見ておいて」とメンバーに伝えるだけで、だいたいのアプリケーション構成が把握できるのは楽だと思いますね。
前原:先ほども少し話したように、医師向けサービスの管理画面はNuxt.jsを使わずにVue.jsのみで開発しました。そのためディレクトリ構造やルーティングのルールなどを全て個別に検討する必要がありました。それらを全てNuxt.jsの“レール”に乗っかってできるのは大きいと思いますね。今から同じ管理画面を作るとしたら、私は迷いなくVue.js + Nuxt.jsを選択します。
―― ちなみに、前原さんがリプレイスを担当されたサービスはどういった構成なのでしょう。
前原:Kotlin製のAPIサーバーがバックエンドのコアロジックを提供しており、RailsがSwagger Codegenで自動生成したgemを使ってそのAPIを叩く構成です。ルーティングはRailsに任せ、画面部分はVue.jsとelement-uiで制御しています。RailsのフロントをVue.jsに任せている感じで、いわゆるMPA(Multi Page Application)と呼ばれているものがイメージに近いと思います。
開発当初は全てのページにVue.jsを入れるわけではないだろうと考えMPAにしたのですが、結局全てのページにVue.jsを入れることになり、今思えば、SPAでやった方がよかったなって思います(笑)
―― Rails + Vue.jsの開発は、設計的に難しいのでしょうか?
前原:もちろんダメというわけではありません。セッションなどはRailsのやり方に任せられるため、サーバサイドの知見が生かされる方が良い開発パターンもあると思います。MPAの方が適しているシステムもあると思いますし、結局はそのチームに合っているかどうかだと思います。
鈴木:セッションの管理の方法などはNuxt.jsで専用の仕組みが用意されているわけではないので、MPAにしてそういった部分をRailsに任せるのはありだと思います。
―― 鈴木さんが担当されているサービスのリプレイスでは、SSRでの開発はされていないのでしょうか?
鈴木:途中まで使用していましたが、SSRは必須じゃなかったのでSPAに切り替えました。リプレイスしていたのは企業ユーザーにのみ公開する管理画面なのでSEO対策が必要なく、SSRにするメリットがそれほどありませんでした。
実装面では、SSRしたときだけ発生する見つけづらい不具合への対応がそれなりに発生しており、リリース後のSSRサーバの運用まで考えると、かけるコストがメリットを上回らないと思い途中でSSRなしに切り替えました。
―― なるほど。では、どういったサイトを作成するときにNuxt.jsを使用するべきか、指針のようなものがあれば教えてください。
前原:SPAを作るのであれば、私はVue.js + Nuxt.jsを最有力候補に挙げると思います。SPAのレールを敷いてくれる、あんなに完成度の高いものはないと思います。もちろんチームによってそれぞれ解決したい問題やスキルセットが違うので、それによって技術を選ぶべきだというのは大前提ですが。
鈴木:私も同意見です。仕事でNuxt.jsを使ったあと、別の案件でNuxt.jsなしでSPAを作ろうと思ったのですが、結構大変に感じました(笑)ただ、裏側をまったく知らないで開発を続けるのは良くないので、Nuxt.jsの仕様を理解して、どこをカバーしてどう動いているのかを理解するのは必要だと思います。
―― お二人が太鼓判を押すNuxt.jsですが、ハマるポイントはありましたか?
鈴木:TypeScriptの初期設定はハマりやすいかもしれません。Nuxt.jsは正式にはTypeScriptがサポートされていないので、自分で型定義を用意してあげる必要があります。
型定義以外にも例えばチームでコードの記述をそろえたいとき、ESLintのような静的解析ツールの導入を検討することがあると思います。Vue.jsにはeslint-plugin-vueというESLintのプラグインがあります。これはNuxt.jsに限った話ではありませんが、TypeScriptは標準ではTSLintを使用するので、eslint-plugin-vue を使うためにはESLintをTypeScriptに対しても使えるようセットアップする必要がありました。
そういった最初のセットアップは大変ですが、乗り越えたときのメリットは大きいです。開発のロードマップにはTypeScriptについて掲載されているので、期待しています。
複雑な親子関係でデータを受け渡すならVuexを検討すべき
―― Vue.js公式だとVuexもよく使用されるライブラリだと思いますが、今回のリプレイスでは使用されていますか?
鈴木:当初は使用しておらず、APIからのレスポンスを素直にコンポーネントに入れてレンダリングするという方法で開発していました。Vuexを入れた理由は、コンポーネントの分割や共通化を行おうとすると、propsで値を渡すのがしんどいからです。
前原:コンポーネントの親子関係まではpropsとemitで大丈夫ですが、孫同士でデータのやり取りをしだすと途端につらくなるんです。Vuexを使用しない場合に孫同士で値のやり取りをするときは、孫Aから親の親にemitし、そこから孫Bへとpropsで渡していく形になるため、見通しが悪くなってしまいます。
ページごとに選択肢を選んで進むフォームってありますよね。そういった場合、入力した値を常に次の画面に持っていき、遷移しても何を選択しているか分かるようにする必要があると思います。ああいうフォームはVuexが有効かなと。あとは複雑な検索ボックス。現在選択している検索条件をVuexにもたせてあげると、検索ボックスの各コンポーネントは現在の検索条件をVuexに集約できるので見通しがよくなります。
―― なるほど。開発規模が大きくなることが事前に予想できるのであれば、すぐに導入を検討した方が良さそうですね。
前原:Vue.js devtoolsでVuexの値を見ることができるので、デバッグが簡単に行なえるのもよいですね。
ちなみに、Vuexを使わず独自のイベントバスを作ることもできますが、あまりおすすめしません。一つのシステムの中でさまざまなイベントシステムが乱立しだすとつらいので、Vuexを入れた方が統制がとれてよいと思います。
―― Vuexはグローバル変数のように値をなんでも共有できてしまうので、どこで使用するか難しく感じるのですが、そのあたりを切り分ける基準はありますか?
鈴木:指針としては「確定したデータを入れる」が一例ですね。例えば、Vuexを使用せず問い合わせフォームを作るというパターンだと、「フォームの入力を中止して全て元に戻す」というような処理があった場合、編集中のフォームの内容と編集前のフォームの内容を維持しないといけないので、ロジックが複雑になりがちです。
なので開発中は、APIから取得したデータはVuexに入れてフォームの初期値として渡し、編集中の状態はローカルのコンポーネントで持ち、更新が完了したら更新したデータをVuexに入れるというような動作を意識してやっていました。
前原:ページを遷移しない場合の動作は他のコンポーネントに関係がないので、値はローカルで持つなど開発者同士で決めておくと楽になります。Vuexを導入したからといって急に複雑になるわけではないと思います。
鈴木:そうですね。まずはいろいろな画面で使われるようなデータをVuexに入れていく、という使い方から始めてみるのがよいと思います。
Vue.jsを使用した大規模リプレイス。これからの課題は
―― Vue.jsを使用した大規模な改修もそろそろ大詰めだと思うのですが、現状で抱えている課題や、今後取り組みたいことなどあれば聞かせてください。
前原:TypeScriptをもう少し全体的に導入したいです。鈴木が担当している薬剤師向けサービスは導入ができているのですが、私が担当しているサービスは先に開発を始めたこともあり導入できていません。型定義をしっかりしておけば、APIサーバから値を取得したときにフロントがミスしてもエラーが出て気付くことができるので、TypeScript対応はやっておくべきだと思っています。
あとはVue.js用のテストがまだそれほど書けていないので、ここはもう少しやっていきたいですね。E2Eテストをどこまでやるかというのも難しい問題の一つです。
鈴木:私も似たような課題ですが、何に対してどこまでテストを書くのかという指針を決めたいと思っています。私のプロジェクトではコンポーネントに対するテストはある程度書けていますが、あまり細かく書き過ぎても今度はUIの変更コストが高くなってしまうと考えており、そのバランスを考えていきたいです。あとは、今回のプロジェクトでVue.jsの知見がたまったので、他のエンジニアに伝えて、新しいプロジェクトに生かしたいですね。
前原:1年以上かけてやってきたのは既存のシステムを最新の技術を用いてリプレイスすることで、正直、新しい価値を提供するところまでいけていません。ただリプレイスしたことにより、足場は確実に整いました。UXをより改善したり、便利な機能を提供したり、今後はそういったことに時間を使っていきたいです。
取材:megaya megayaのブログ