リクルートテクノロジーズのフロントエンド開発

古川陽介氏(以下、古川): みなさん、こんばんは。第5回目となる今回は、「リクルートにおけるフロントエンド開発」というタイトルでお送りいたします。

リクルートは大規模サービスを複数抱えています。そのフロントエンド開発がこれまでどのように行われ、扱われてきたのかという話を、今後どうやっていくのかという未来の話も交えつつお伝えできればと思っております。

ということで、現場における苦労や課題をどう乗り越えたのかなど、泥臭いリアルな話を中心にお届けしようと思っております。その上で、本日お越しいただいたエンジニアのみなさまとの交流を深めていけるといいなと思っております。

というわけで、いきなりですが「React/Reduxを活用したリクルートテクノロジーズのフロントエンド開発」の話を、私、古川から発表しようと思います。よろしくお願いいたします。

私は、「yosuke_furukawa」というハンドルネームでtwitterやGitHubもやっております。リクルートテクノロジーズの所属で、シニアソフトウェアエンジニアという肩書きがついております。ただ、この肩書きよりも今プライベートでやっているNode.jsの日本ユーザーグループの代表という肩書きのほうが、みなさんには馴染み深いのではと思います。僕は今、Node.jsの日本ユーザーグループの代表 兼 リクルートテクノロジーズのシニアソフトウェアエンジニアです。

ここでちょっと宣伝です(笑)。2月号の『WEB+DB PRESS』にある「Reactで作るシングルページアプリケーション」という特集の第4章、「新サーバサイドレンダリング」という章の大部分を担当しました。宣伝ではありますが、1~5章の5章構成になっていて、どの章も今のシングルページアプリケーションの作り方の基礎から応用まで含めた内容で、読みやすく仕上がっているのでオススメです。

さて、本題に戻りまして今日一番伝えたいこと、「フレームワークは作るものに合わせて作るのが一番いい」という話をしていきたいと思います。従来型のフレームワークというのもいろいろありますが、リクルートのWebはいくぶん特殊なところもあるので、それに合わせたフレーム作りがいいねということです。

まずは、そんなリクルートのWebについて解説します。リクルートのWebって大体こういうかたちをしているんですね。

リクルートのWebというのは、サイト系とアプリ系とで大別できます。

サイト系は、トップページに検索できるものがあって、そこでキーワードを入れて検索すると結果が出て、その検索した結果をタップするとリストの詳細ページが出る。だいたい典型的なWebサイトのかたちをしているかなと思います。

アプリ系は、例えばチャットができたり、掲示板でメッセージ書いてリアクションを付けられたり、アンケートなどでユーザーと相互にコミュニケーションがとれたり。いわゆるインタラクティブなWebアプリといった感じですね。このサイトとアプリ、両方について触れていきたいと思います。

サイトとアプリ、両方の要望を満たすために

まず、Webサイト側の要望としては、とにかくサイトのパフォーマンスが重要なんです。一覧から検索するユーザーもいますが、直接リストの詳細にいく人もいますね。

リクルートのWebサイトというと予約サイトが多いんですが、だいたい詳細のほうがアクセス数は多いですね。なぜかというと、例えば居酒屋を予約したらみなさん詳細URLをなにかしらの手段でシェアしませんか? 不特定多数に詳細をシェアするようなタイミングのある予約サイト系は、詳細ページに直接いく人のほうが基本的には多いんですね。

一覧から回遊して検索して詳細に辿りつくパターンと詳細に直にいくパターン、両方のパフォーマンスが重要というところですね。

その一方でWebアプリ側の要望としては、ネイティブアプリっぽいルック&フィール、見た目と操作感があります。特にインタラクティブな動きを求められることが多くて、例えばチャットとか、いいねを通知してほしいとか、そういうことですね。要は、ネイティブアプリと同じような動きをしてほしいと言われることが多いんですね。

つまり、リクルートのWebの要望というのは、サイト系はパフォーマンスが重要視されることが多くて、アプリ系はルック&フィールが重要視されることが多い。

この両者の要望を満たすには、これまでのWebアプリケーションのフレームワーク(従来通りのHTMLの階層やWebアプリケーションフレームワーク)では、対応しきれないんですよね。

それで今、リクルートテクノロジーズがどういうことをやっているかというと、ReactとReduxとNode.jsで「新たなWebアプリケーションフレームワーク」みたいなものを作っております。それを使って作ったWebサイト系の事例、Webアプリ系の事例がもう1つあるので、それを少しお見せします。

Webサイト系事例としては、すでに『ブッキングテーブル』というサイトがあります。これは1回Node学園祭での発表があったので、それで見ている人もいるかもしれないですね。

(デモが流れる)

これは同じでReactのページからいって、ページ遷移して予約ができるみたいな。いわゆるWebサイト、『ホットペッパー』みたいなものを想像してもらえるとわかりやすいかなと思うんですけども。

これがサイト系のほうで、Webアプリ系のほうでいくとこういうサービスも作っていて、これはどういうサービスかというと、まぁSNSみたいなものですね。

(デモが流れる)

トークボードと言われているさっきの掲示板みたいなものを理想としていて、掲示板にメッセージがつけられて、こんな感じで画像が表示できて、いいねとかもできて、編集もできる。こういうサービスが今のリクルートのなかで使っているSNSみたいなものですね。

これ自身は『Raico』と呼ばれています。何を目的にしたサイトかというと、例えば『ゼクシィ』のユーザーだったり、『カーセンサー』のユーザーだったり、『タウンワーク』のユーザーだったり、いろいろなユーザー間でコミュニケーションをとることで、リアルユーザーモニタリングするためのサイトですね。なので、掲示板で「どこの結婚式がいい」とかサイトで交流してもらったり、チャットで、リアルタイムで話をしたりという機能もなかには含まれています。

なので、チャットで全員にフォローリクエストするみたいな機能も入っています。先ほどお伝えした通り、リクルートテクノロジーズのフレームワークで、『ブッキングテーブル』も今回の『Raico』みたいなものも両方作っています。

次に、今回のフレームワークやサイトを作る上で特に気をつけていることもいくつかありまして、それを紹介しようと思います。

サーバーサイドレンダリングとは

本当にたくさんあるので、今回はかいつまんで3つぐらいしか紹介できないんですが、本当に興味がある方は懇親会で聞いてください。

1つ目が「サーバーでもクライアントでもレンダリングする」。これはすごく気を付けてやってます。サーバーでもクライアントでもレンダリングするというのは、サーバーサイドレンダリングと呼ばれる技術なんですが、クライアントのサーバーで同じロジックを使ってレンダリングすることですね。クライアントサイドはReactを使っていますが、サーバーでも同じようにReactを使ってレンダリングする。

この効果としては、初期表示の改善というのにまず効果があって、さらにTwitterやFacebookでシェアすると、今、シェアした瞬間に画像が出てきたり画像のスニペットが出てきたりしますよね? そういうものをOGPタイプとかOGPイメージというんですが、そのOGPのメタ情報を入れると効果があったり、SEO等の対応がしやすいという副次的な効果もあります。

なぜそういうことをやっているのかという話をすると、クライアントサイドでのみレンダリングした場合、いわゆる普通のシングルページアプリケーションの場合ですね。この場合はリクエストしたら、最初に白いカラーのHTMLが返ってくるんですね。それで、そのカラーのHTMLが返ってきた後でJavaScriptがとられて、JavaScriptでまたローディングみたいな画面を表示して、さらにJavaScriptからコンテンツをAjax通信でとってきて、最後に画像を表示すると。

それで、これがサーバーサイドレンダリングの場合なんですけど、これはもう少し早くなっていって、最初に画面をとってきたときには、サーバーサイドでもうすでにHTMLはレンダリング済みなので、コンテンツをとってくるとかJavaScriptでなんかうにょうにょやるっていうタイミングは別にやらなくていいですね。最初にコンテンツは返ってくる。

その後でサイト内のリンクをクリックしたり、サイト内でなにか別のアクションを起こしたときは、React等で部分レンダリングを行って、シングルページアプリケーションとして動かすというようなことができるのがサーバーサイドレンダリングです。

想像していただいてわかるかもしれないんですけど、こちらのほうは初期表示が上がるんですね。最初にリクエストしたタイミングでもうすでにHTMLとしては成立しているので、そのタイミングでアカウントをReactを読み込み変換するとか、そういう時間がなくなって初期表示速度が上がるわけです。

(デモが流れる)

サーバーサイドレンダリングしているページと、サーバーサイドレンダリングしてないページを紹介します。

これはサーバーサイドレンダリングしてない時の動きですね。ちょっと今わかりにくかったかもしれないですけど。これはヘアサロンなわけですね。ヘアサロンを検索するというAPIが裏に書かれているんですけど、ヘアサロン検索用のリクエストをしています。

それで、なんの因果か「Scala」という文字列で検索しているんですけど、Scalaという文字列で検索したとき、一瞬クルっとインジケーターが上がってきたのわかると思うんですけど、要は最初に画面が表示された後で、Ajax通信指定からとってくるまでの時間というのがちょっと関わってるわけですね。

これが、サーバーサイドレンダリングしてるとき、これは要はHTMLとして完成している状態で返ってくるので、このほうが初期表示が早いわけですね。こっちはリロードすると1回空になってからという感じになりますよね。ちょっとこれはなかにキャッシュのコードが入ってるから早くなっちゃうんだけど、こっちのほうはHTMLがすでにもう最初から完成しているのでそのほうが早いと。

それで、先ほどお伝えした通りの初期表示の速度の改善と、レンダリングした後の部分レンダリングをするというのが、サーバーサイドレンダリングですね。

それで、これがなにに効いてくるかというと、回遊中のユーザーのUXともその初期表示の速度も改善できるというところで、先ほどの例でいうと居酒屋で検索した人というのは、回遊中のユーザーなわけですね。その人たちは普通に上から検索して、検索しながらサイトを回遊しながらやっていく。

でもそうじゃなくて、居酒屋にシェアされたURLに直接いく人たちもいるという話なんですけども、シェアされたURLに直接いく時は、サーバーサイドレンダリングされていることによって、HTMLが即座に早く返ってくるという効果がある。なので、その検索して一覧を表示させる操作も、表示だけする操作も両方ともパフォーマンスを向上させやすいという効果があります。なのでこういうものを使っていますね。

ただし、この注意点もあって、とくにこれは少しアドバンストの話なんですけど、今のそのReactのサーバーサイドレンダリングというのは、あまり早い操作ではないんですね。すごく時間のかかるような操作をしていて、レンダリングしているだけで100ミリ秒とか50ミリ秒とかかかっちゃう場合もあります。

しかもこの処理というのは、その処理をやっている間すごくCPU量を使っていて、次のリクエストが来るまで、その処理が終わるまでちゃんと待たされるというのがあります。それで、これを回避するためになんですけど、いろいろなレンダリングする仕組みはやりますけど、レンダリング結果を一時的にキャッシュしたりすることが多いですね。プロセス内でキャッシュしておいたりとか、あとはnginxのcache dirに保存したりということも検討しています。

あとは、そもそもキャッシュさせる範囲を減らす。全部をレンダリングさせないということですね。レンダリングする範囲を減らす。それで、Above the foldと呼ばれている、最初にユーザーが見る部分だけをレンダリングさせるというところの効果があります。そうやって、サーバーサイドレンダリングを限定的に行って速度を上げるということも、未検証ですけど検討しています。

ただ『ブッキングテーブル』とかは、部分レンダリングみたいなこれと似たようなことをすでにやっていて、すごく限定的なところだけサーバーサイドレンダリングさせるということをやっています。

「Historyを壊さない」こと

次。気を付けている「Historyを壊さない」という話です。Historyを壊さないというのは、「戻る」「進む」とかを押した時に、以前に見えていた状態がちゃんと復元されているということですね。

これ普通ですよね? みなさんからすれば「当然じゃん」って思うんですけど、実はできているサイトは少ないんですよ。とあるSNSの例を話すと、それには無限スクロールが実装されていて、下にリクエストを伸ばしていける。でも別のページに行って戻ってくるとスクロールされた位置が復元されていないんですね。戻るためには、もう1回その無限スクロールされていた位置までいかなきゃいけない。

これはつまりスクロール位置が崩れる事案なんです。それで、スクロール位置が崩れる主な理由としては、適当な位置にスクロールしてから別のページに遷移するわけですよね。そうすると、遷移したページから適当な位置に遷移して、そこから戻るという後処理をします。

そうすると、戻ったときには元のHTMLというのは消えちゃっているんですね。なぜかというとJavaScriptで構築したHTMLというのは、ブラウザで保存してくれないんですよ。JavaScriptでHTMLが構築される前に、ナビゲートされる。つまり戻っちゃうと、そういうふうに位置が復元されてない状態、HTMLが復元してない状態で戻ってしまうと。

このときに、本当はブラウザというのは、その元のスクロール位置をちゃんと覚えてるんですね。ただしHTMLというのが完全にレンダリングされているわけではないから、それで位置が復元されないんです。というのが今起きているスクロール位置の制限みたいなやつですね。

位置が再現できないので、スクロール位置がHTMLで表示できているところまでしかわからない。つまり最初にHTMLにとられたところまでしか表示されないんですね。なので、さっきみたいに、表示スクロールされる位置がずれるみたいなことが起きてしまう。

回避策としてはすごく単純で、「戻る」「進む」を押した時に、データ読み込み中はレンダリングをスキップさせればいいわけです。普通にそんなに大した話じゃない。それで、要は戻るときにAjaxの再リクエストが走っているので、再リクエストが走っている間はそのHTMLのレンダリングを止めて、レスポンスがちゃんと返ってきてからレンダリングを再開させると、その間でそのHTMLがちゃんと復元されてから、ブラウザはスクロール位置を復元してくれるので、ちゃんとスクロール位置が復元される。

それで、これも僕らのフレームワークでは実用、実装はされていて、例えばデモでやると……これはあんまりデモ映えしないので、サクッといきますけど。

(デモが流れる)

これはさっきの「Scala」という言葉で検索した結果ですけど、例えばどこかのページに行って、戻ったときに同じ位置にきてますよね? これは普通のことなんですけど(笑)。デモ映えしないというのはそういうことで、普通のことしか起きてないのでデモ映えしないんです。

よくシングルページアプリケーション作っているサイトを見ると、けっこうそれがうまく復元できなくて、トップのページに戻っちゃったりするところと、トップの位置にスクロール位置が戻っちゃったりしているところがけっこう多いですね。そういうのはちゃんとWebサイトでも回遊するところが多いというところは、さっき言った「戻る」「進む」を繰り返すところが多いので、それをちゃんと復元させるということを重視しています。

Consumer Driven Contractという考え方

さらに、リクルート全体で開発するときに、我々はConsumer Driven Contractという概念を使って開発しています。これはどういうことかというと、従来APIを決めるときにだいたいサーバーを書いている人が決めていたと思うんですね。バックエンドが書いている。

それで、そのバックエンドのことをProviderと呼んでいて、そのAPIの仕様をフロントエンド、それをConsumer、消費者と呼んでいて、そのConsumerが主導して要求、Contractを書くということで、API仕様を決めていくというスタイルの仕様策定方法です。

これは今までは圧倒的にサーバーサイドの人たちが強かったんですね。サーバーサイドの人たちがAPIを作って、フロントエンドの人たちに「これを使ってください」と言うことが多かったんですけど、それって往々にしてフロントエンドからすると、「こういうAPIを書かなきゃいけない」「このAPIのレスポンスはこうなってるといいのに」ということが稀に起きる。稀に起きるというか、よく起きているんですね。

それで、それをConsumerもProviderも立場を逆転させるというのが、このConsumer Driven Contractという考え方ですね。これはたぶん用語として出てくるはずです。

これも一応実際にやっていって、ある程度リクルートテクノロジーズとかいうと規模も大きくなってくるので、バックエンドとフロントエンドで分かれて話すこともやっぱり多いんですね。そういうときにこのConsumer Driven Contractの考え方を取り入れていって、最初にContractを定義するわけですね。

これはAPIの仕様なので、それをリクエストとレスポンスの対をちゃんとJSONかなにかで、プログラムが理解しやすいようにして書いていくこと。その上で記述されたファイルを使ってmockとして活用していく。記述されたファイルはmockになり得るので、そのままレスポンスして使えます。

さらに言うと、そのmockがクライアントに今度変わって、Providerができてきたタイミングだとサーバーでできてきたら、そのサーバーのレスポンスをチェックするチェック側ともなるわけです。テストツールとしても動くという感じですね。こうするとフロントエンドが使いやすいAPIになる。

さらに言うと、フロントエンドが使いやすいという希望をそのままバックエンドが実装してくれるので、それでいいんですけどおそらくバックエンドがハッピーになるわけじゃない。つまり、バックエンドはバックエンドできっと言い分があるんですね。スケールがそれだと効率的じゃないとか、いろいろあると思うので、それがハッピーになるわけじゃないけど、こっちのほうが使いやすいAPIにはなると。

ただ、これは実践してきたんですけど、これを使ってもコミュニケーションコストはとらなくていいってわけじゃなくて、これを使ってよりコミュニケーションを密にとっていかなければいけないということがわかりました。言いたいのは、さっき言った通りバックエンドがハッピーになるわけじゃないので、フロントエンドの人たちバックエンドの人たち両方と共通して話を進めていくっていう必要があるかなというところですね。

Consumer Driven Contractについては、従来のやり方ではフロントエンド開発の都合が悪くなるので、なるべくならフロントエンドに近いところでAPI仕様を決めていくほうが、物事というのは早くなるんですけど、それを実践していますというところですね。

Agreedというライブラリがあるんですが、我々は今それを使ってmockを書くことでConsumer Driven Contractを実現しています。

最後に駆け足なんですけど、いま言ったような我々が作った大きな仕組みというのをどうやって広めていくかという話をしてまとめにしたいと思います。

まず1つは事例を作っていって広めるというパターンですね。さっき言った『ブッキングテーブル』みたいな予約サイトで採用するのと、あと『Raico』みたいなSNSサイトで採用事例を増やしていきますというところですね。来期もまたこれを採用して、新しいWebアプリケーションを作っていく予定です。

あとは教育して広めるというパターンもあって、このときには三本柱で広めていきたいなと思っています。教科書とプロトタイプとアプリケーションの構築ガイドですね。教科書としては、「リッチなWebアプリケーションになるための7つの原則」というタイトルの記事があります。これ自身は僕が翻訳しているので、それを見ていただければある程度掴めるかなと思うんですけど、その原則に従って開発しています。

あと、プロトタイプというのは、さっき言ったReact/Redux/Node.jsのその実装例集みたいなものを作っています。それで、react-routerや先ほど出てきた無限スクロールを使った参考実装もたくさん含まれています。

それで、これは完全に僕の言葉なんですけど、Living Recruit Standardとして常に最新のライブラリに追従していって作っていきます。こういうものってReactとReduxはすぐバージョンが上がってしまうので、そのバージョンをずっと管理していくのは大変だと思うので、その時その時で必ず最新に追従していくというプランでやっています。

ちなみに、アプリケーションの構築ガイドも作っています。これは鋭意作成中です。完全に手が足りていないので、今一緒に作っていく人を募集したいと思っております。さっきのフロントエンド開発だけじゃなくて、いろいろと手狭になってきているので、誰か助けてください。

というわけで、リクルートテクノロジーズでは社員を募集していますというところで、終わりとさせていただきます。ありがとうございました。

(会場拍手)