SmartHR Tech Blog

SmartHR 開発者ブログ

RubyKaigi 2021 Takeout でスケジュールアプリを提供したのでソース公開します

こんにちは、エンジニアのkinoppydです。こんにちは。

RubyKaigi 2021 Takeout、参加しましたか? 僕は楽しすぎたので、RubyKaigi 2021 Takeout 感想戦@仮想三重 というイベントまで開催しちゃいました。RubyKaigi最高!

はい、そんな最高なRubyKaigiですが、今年のSmartHRはカスタムスポンサーとしてスケジュールアプリ「Schedule.select」を提供しました。これは、RubyKaigiのタイムテーブルの中から自分だけの聴講スケジュールを作成できて、しかもそれをSNSで公開することも可能! というスグレモノです。

rubykaigi.smarthr.co.jp

Schedule.select

開発の背景として、RubyKaigiがオンラインセッションに移行したことによって減ってしまったお祭り感をちょっとでも取り戻す手伝いができればなという考えがありました。社内のRubyKaigi大好きメンが集まって協議したところ、会場が失われたことでわからなくなってしまった「お、○○さんもこのセッション聴くんだ」という感覚を取り戻そうという気持ちが盛り上がりました。そこから、スケジュールアプリで【私の今年のRubyKaigiセットリスト】を作成・共有することで「お、○○さんこのセッション注目してるんだ」という感覚をSNS経由で感じてもらおうというのが、「Schedule.select」を作成するに至った経緯です。

このエントリでは、Schedule.selectを開発する上でどんなことをしていたのかを共有します。ソースコードはOSSとしてGitHub上のゴタゴタと一緒に公開するので、ぜひ御覧ください。

github.com

Schedule.selectの開発

フレームワーク

SmartHRでは、ほぼ例外なくRuby on Railsを使って全プロダクトを構築しています。またRubyKaigiはRubyのカンファレンスなので、バックエンド実装はRails以外にとくに検討はありませんでした。そしてフロントですが、SmartHRのプロダクトはこれまた例外なくSmartHR UIというReact + TypeScript + StyledComponents で作成された共通のUIコンポーネントを使ってフロントを実装しています。しかもSmartHR UI も OSS。そのため、自社プロダクトのアピールのためにも、Rails + TypeScript + SmartHR UI の構成で作成しようと決定しました。また、SmartHRでは多くのアプリをSPAとして実装していますが、SPAは考えるべきことが多く今回の開発期間では十分に考慮した実装にできないと判断したため、MPAで作成し、Railsの機能を活かすようにしました。

名前

アプリケーションの正式名称は「Schedule.select」です。Rubyist(Railistかな?)ならイメージがつくと思いますが、RubyKaigiのスケジュールの中から自分の欲しい物を集める感じです。この名前が決定したのは開発のだいぶ最後の方で、開発コードはmieでした。SmartHRのプロダクトの開発コードには日本語を用いるという伝統のようなものがあるため、本来開催予定だった三重に思いを馳せて、mieという名前でリポジトリを作成しています。

プロジェクト管理

SmartHRでは、開発に関するタスクは全てJIRA上でチケット管理がされています。ですが、今回のプロジェクトは開発期間が一ヶ月もない超短命プロジェクトだったので、わざわざJIRA上で管理する必要もないと思い、GitHub Projectsのカンバンで管理を行いました。小規模だったので特に困ることはありませんでしたし、結果をOSS公開するときにどんなバタバタがあったのかを世界に晒せるという意味ではJIRAではなくソースコードに紐付いたプロジェクト管理方法で良かったと思います。ただ、そもそもGitHub Projectsをちゃんと使ったことがなかったので、チケットとIssueとPRの関係とかでどうやるのがよくある方法なのかわからず、非常にテキトーな運用になっていたので今後はプラクティスを集めたいと思います。

プロジェクトのカンバン
プロジェクトのカンバン

インフラ

インフラはHerokuを選択しています。既に社内のアプリケーションで本番環境も含め多く採用実績があったことと、特にトリッキーな設定が必要なアプリケーションでもなかったのでシンプルにデプロイまで完了することを最優先の目的としました。唯一普段と違うところと言えば、Webpackを使ってReactコンポーネントをビルドする必要があるため、buildpacksには heroku/ruby の前に heroku/nodejs を追加する必要があります。

ドメインの設定や証明書の準備などは、同僚のyoshinarlさんが手伝ってくれました。インフラに関して何も心配することなく開発ができたので、とても心強かったです。

RubyKaigiは国際カンファレンス

RubyKaigiは国際カンファレンスです。そのため、公式として提供するモノは英語が第一言語である必要があります。もちろんこれはSchedule.selectも例外ではありません。したがって、Schedule.selectはi18n対応が必須となりました。公式のWebサイトにならい、Schedule.selectでも英語を第一言語、選択可能な言語として日本語を用意します。

ここで問題になるのは、i18nの機能をRails側で実現するのか、React側で実現するのかです。最初はプレゼンテーションの問題なので、React側で実現するのが良いかなと思いました。Rails側で実現すると、Reactで表示するテキストの情報を全てRailsから渡さなくてはならなくなるため、たまらなく無駄なPropsを渡し続けることになることが予想されました。しかし、一点だけ問題があり、最終的にRails側で実現することにしました。

その問題とは、TimeZoneの問題です。オフラインイベントだと全員が現地にいるのであまり気にしないのですが、オンラインカンファレンスの場合は参加者が各地のタイムゾーンに散らばったまま一つのライブ放送を視聴します。もちろん、DB側で全てのDateTime情報をUTCで保持し、プレゼンテーション時にReactで表示を切り替えればよいのですが、タイムゾーンによっては日を跨ぐセッションなどがあり、ページネーションなどの問題をサーバー側で解決するためにはサーバー側で時間の計算を行う必要がありました。今思えば、容赦なく全部の値を返した上でフロントで計算していい感じにしても良かったとは思いますが、開発初期の考えとしてはMPAでできるだけReactの持ち物は小さくしたいと思っていました。そこで、i18nを全てサーバー側で持ち、Reactに渡す表示用のテキストなども含め全てRailsのi18n機能に寄せることにしました。

トップページの日本語表示
日本語の表示
トップページの英語表示
英語の表示

Rails、ReactのProps、テスト

i18n機能をRailsに寄せることになったので、表示テキストも含めたPropsをReactにView内で渡すことになりました。そのため、Rails内部で処理した情報を、ReactにわたすためのHelperを定義しました。ロジックを持ったViewが使用する情報は全てこのHelperを経由してレンダリングされるため、ここが事実上のAPI部分だと考え、テストに関してもここを重点的に(というか、ここしか書いてる時間がなかった)用意していくことにしました。複雑な動きをする場合は必要に応じてmodelなどのテストも用意しましたが、基本的には機能を新しく追加するときにはRailsとReactのやり取りをするHelperに対してテストを追加していきました。

この方針は一定の成功を収めました。実質的に書いているのはSPAとAPIサーバーの間のE2Eのようなものなので、そこが壊れない限りはある程度のRails側のロジックの正当性と、フロントに渡されるJSONの型の安全が保証できました。このプロジェクトはTypeScriptで書かれていましたが、View経由でReactにPropsを渡す箇所は実行時なので、型による保護がなく知らない間にReact側が壊れているという可能性もありました。それを回避できたという意味でも、Helperのテストを優先して書くことはこの規模のアプリケーションでは良い戦略でした。

ただ、開発の後半になってくると、もはやHelperのテストを書いている時間すらなくなり、開発のクライマックスにはテストが壊れたまま開発を突き進めていたことは反省点です。

テストが通ってないのにマージされるプルリク
テストが通ってないのにマージされるプルリクたち(CIすらない)

あれ、これってMPA?

Reactを使いつつ、Railsの機能をできるだけ活用したいと思い、できるだけReactの責務は小さくしようと思いました。しかし、思いもよらぬ落とし穴があることがわかりました。今まで会社ではSPAばかり書いていたので気づいていなかったのですが、SmartHR UI はかなりSPAに向いた、あるいはページの構成要素を全てReactで作る用途に向いたコンポーネント集でした。アクセシビリティの確保などの様々な観点からもそのようなコンポーネント集になることは仕方なかったのですが、親要素の中にループで回した子要素を作る場合などに、Rails側のViewでループ処理を入れたりすることが難しく、またstyled-componentsとガッチリ結合している点からCSSの管理もCSS in JSで管理する他なく、最終的にほとんどページの構成要素をReactのコンポーネントで作ることになりました。結果として、ViewにはReactを呼び出すコードが一行だけ書かれるページがほぼ全てになってしまい、あまりRailsの機能を活かすことができなかったなという反省がありました。

一つ大きく誤算だったのが、Formの処理などでRailsのform_helperを使えないことによるコードが増えたことです。MPAであるにも関わらず、Ajaxによる処理をしている箇所が多く混ざってしまいました。具体的には、スケジュールに追加したり削除したりする処理はRailsのForm機能で作られているのでブラウザのリクエストが飛ぶのですが、メモの更新やスケジュールタイトルの更新などはモーダルを使った関係でJSでリクエストが処理されます。この動作の違いによって、ボタンを押したときにブラウザの挙動としてリダイレクトが発生したり、しなかったり……動作の一貫性の無さはユーザーの体験として良いとは言えず、なんなら全部Ajaxに寄せていればよかったなと思いました。

Railsのフォームでどうにかできなかったモーダル
Railsのフォームでどうにかできなかったモーダル

TimeZone

i18nをRails側に寄せたために生まれたもう一つのトレードオフとして、タイムゾーン問題が後々発覚しました。言語のi18nはそれほど難しくはなかったのですが、タイムゾーンのローカライズはそう簡単にRails側で解決できる問題ではありませんでした。おそらくGeoIPなどによる推測などが利用されるのですが、動作検証などを含め実装する時間が取れなかったため、ユーザーに選択してもらう形式にしました。また、ロケーション情報をセッションに持っており、このやり方はRailsガイドでも推奨されていない方法なので、そもそもタイムゾーンの情報はフロントからDay.jsのguessingなどで渡してもらうのが良いのではないかと思います。とはいえ、フロント側からタイムゾーン情報をもらうにしても、一度ブラウザ側でJSを実行してもらってからRails側にリクエストが必要になるので、結局ファーストビューでタイムゾーンを正確に取れないという問題があり、i18nとの兼ね合いも含めこのあたりの「こうするべきだった」はいくつか難しく最適解が思いつかないまま終わりました。

OGPイメージ

Schedule.selectの体験の一つとして、ソーシャルネットワークで「俺のRubyKaigiセットリスト」を共有してもらうというものがあります。そのため、Twitterなどに予定のURLを共有したとき、いい感じのOGPイメージが表示されていてほしいと思いました。そこで、予定に設定した概要を動的に画像に変換して、OGPイメージとして表示しようと目論みました。

この実装は同僚のRonさんがやってくれたため、私は安心してSchedule.selectのロジックの実装に取り掛かることができました。OGPイメージの動的生成に関しては、後日Ronさんのテックブログが公開されます! Railsから動的なOGPイメージを配信する方法をお楽しみに!

イケてるOGPイメージ
イケてるOGPイメージ

デザイン

Schedule.selectの全体のデザインは、同僚のnaoさんが担当してくれました。スケジュールが逼迫している中で、「誰か助けて!」って叫んだら「はいっ!」とnaoさんが心強く声を上げてくれたため、Schedule.selectは完成しました。組織の困りごとを、皆が他人事として無視することなく、自分にできることを提案してくれるSmartHRならではの非常に心強い組織文化です。

デザインに関しても、後日naoさんのテックブログが公開されます! お楽しみに!

Schedule.select の figma
Schedule.select の figma

リリース後

RubyKaigi当日の動き

RubyKaigi当日は、配信のトラブルのためスケジュールの変更が発生しました。これに対応してくれたのが、同僚のmkmnさんです。開発時から、RubyKaigiの公式サイトから情報を引っ張ってきてSchedule.select用にデータを入れてくれるスクリプトを整備してくれていました。ただ、当日のスケジュール変更は一部想定してない動作だったため、翌日以降のために備えて改めてスクリプトを強化してもらいました。外部とのやり取りをおまかせできていたので、安心して開発に専念できました。

また、当日は実際に使用して頂いたユーザーからのフィードバックもありました。今年のRubyKaigiではトラックが2つ(オフラインイベントの頃は4つくらいありました)並行で走っていので、聞きたいセッションの選択によっては一日のうちにTrakcAとTrackBを行ったり来たりすることになります。そのため、常に「このセッション終わったら次のセッションの会場どこだっけ……」と公式のタイムテーブルを見返すことになり、無精者の自分は事前に聴講メモすら用意してないので自分が何を聞こうとしたのかすら思い出せず20秒くらい頭をひねることがしばしばでした。そこで今年はSchedule.selectで予定が管理できたのですが、自分の予定を見る画面にTrackがAなのかBなのかを表記することを忘れていました。その点に関して「あるともっと便利!」と要望をもらったので、当日に急いで対応しリリースを行っています。その様子も、公開したリポジトリで御覧ください。

要望のツイート
すっかり忘れていた機能を当日に指摘される様子

OSSとしての公開

最後に、RubyKaigi感想戦も終わったので、Schedule.selectをOSS公開します。リポジトリ作ったときから公開用に何の手も加えていないので、エンジニアのライブ感が伝わるのではないかと思います。

github.com

多分このまま使うのは、SmartHR UIの関係で大変だと思うので、Rails部分をAPIサーバーとして分離するか、違うCSSフレームワークでフロントを作り直すのが良いのかなと思います。何かのイベントで使いたい的なお話があれば、協力できるかも知れないのでkinoppydまでご連絡ください。

そういえば

公開しましたが、ちょっとまだテスト落ちてます。ちゃんと後日なおすのでゆるして。

いつもの

いつものです、OSSポリシーとか策定していきたいですね。

hello-world.smarthr.co.jp