【ハッカソン】優勝した僕たちが如何に短時間で効率よくプロダクトを仕上げたか@Firebase Hackathon+もくもく会


Firebase とっても便利ですよね~!
弊社でもLPのホスティングや、認証部分等でふんだんにFirebaseを利用しています。Firebase様様です。そんなFirebaseのユーザ会イベントがあるということで、先日(2019/09/14) 参加させていただきました。

 イベントで行われた Firebase ハッカソン にて、ありがたいことに優勝することができました!優勝賞品は Oculus Quest 128GB でした!これからたくさん遊ぼうと思います(笑)

 そのふりかえりとして、僕たちのチームがどのような段取りでプロダクトを仕上げたのかを紹介したいと思います。
(ハッカソンガチ勢の方々には非効率に見えるところが多々あると思うので、コメント欄にて色々アドバイスいただけると嬉しいです!!)

参加したイベントについて

 Firebase Japan User Group (FJUG) さん主催の Firebase Hackathon+もくもく会@pieceofcake HALL というイベントに参加しました。Firebaseに興味を持つ全ての人を歓迎するという太っ腹なイベントで、50名以上の方が参加し、会場やお菓子、お弁当などすべて無償で提供していただきました。イベントではfirebaseを用いたハッカソンと、firebaseの使い方等を学ぶもくもく会がありました。こういう形式は初めてだったので、とても面白かったです!
 またイベントには、Windows 95の開発をされた中島聡さんをお迎えして、基調講演・審査・懇親会などを通して、様々なお話をしていただきました!特に印象に残っているのは、「アイディアなんて誰でも持っている。エンジニアはそれを形にできることが強い」というお話でした。
(実は中島さんの著書 「なぜ、あなたの仕事は終わらないのか」を予め持ってきていたので、サインもいただけました^^)
 

イベントのタイムライン

イベントページ より抜粋

時間 内容 スピーカー
9:45 〜 10:00 開場/受付
10:00 〜 10:15 コミュニティ説明、会場説明
10:15 〜 10:35 基調講演 中島 聡
10:35 〜 10:45 Q&A タイム 中島 聡
10:45 〜 ハッカソン開始
12:00 〜 13:00 Lunch Break
〜 19:00 ハッカソン終了
19:00 〜 19:30 Break
19:30 〜 20:30 プレゼンテーション
20:30 〜 20:45 Break / 審査
20:45 〜 21:00 受賞者発表・講評・締め
21:00 〜 22:00 After Party・タッチ&トライ

※ 多少のずれはありましたが、概ねこのタイムライン通りに進行しました。
※ 実際は参加チームが想定より多かったみたいで、19:00からすぐプレゼンが始まりました。

開発及び発表準備は昼食中も含め8時間ほどで、気を抜いたらすぐ終わってしまう絶妙な時間だったと思います。

ハッカソンのルール

ルールはざっくり以下のような感じでした。

  1. firebaseを使っていること
  2. 19:00の終了時点で、できたところまでを発表すること
  3. 発表時間は5分厳守で、過ぎたら打ち切り
  4. 以下の4つのテーマのいずれかに関連するプロダクトを作ること
- MaaS (Mobility as a Service)
- EdTech (教育)
- HealthTech (健康・医療)
- PoliTech (政治・コミュニティ)

※ これらのテーマはいずれも今後伸びうる領域とのことなので、ハッカソンに限らずビジネスを考えてもよいかもしれません。非常に参考になりました。

作ったもの

 僕らのチームでは、EdTech(+ PoliTech (コミュニティという文脈で))をテーマにして、Slackの slash command で勉強会を企画し、reaction(スタンプ)が多くついたものをランキングで表示する、というプロダクトを作りました。シンプルですが、やはりSlackはエンジニア受けはよかったみたいですw

投稿

ランキング

Github上で公開しているので、よかったら見てみてください。
https://github.com/inner-meetup/inner-meetup-api
(ハッカソンなのでスピードを重視した関係で、結構ガバガバなところがあります。温かい目でご覧ください。)

発表に至るまでの流れ

 チーム自体は3人で申し込んだのですが、一人が寝坊->そのままお休み というアクシデント?に見舞われたので、2人で開発しました。気心が知れる間柄だったのでスムーズに開発できました。(そもそも、ハッカソンでは知らない人同士でプロダクトを作り上げるのが醍醐味だ!、というご意見もあるかもしれませんが。。)

11:00~13:00 アイディア出し

 開会式にてアイディアを出すコツや、アイディアは数で勝負!とのアドバイスをいただいたので、念のため各4テーマそれぞれについてアイディアを出し合いました。出し方としては、お互いにテーマに合致したアイディアを出し合い、否定することなくお互いのアイディアを膨らまし合うという感じで自由気ままにやりました。
 正直なところ12:45くらいまで中々パッとするアイディアが思いつかず、周りはちらほら実装を始めていたので焦っていました。最終的にテーマに合致して、自分たちが欲しいと思えるアイディアが出たのでそれにすることにしました。
※もしアイディア出しのコツがあったら、教えてください!!

13:00~13:30 設計及び下準備

開発体制と開発要素の取捨選択

 開発はGithubを利用して行いました。普段の開発ではGithubのIssueやProjectでタスク管理をしていたのでこちらを利用しようとしましたが、整備する時間がなかったので SlackのDM上で諸々やり取りしました。
 また、主に開発要素は

  • フロントエンド (見えるところ)
  • バックエンド (見えないところ)
  • データベース (データを保存するところ)

の3種類が必要になるかと思いますが、Slackを使うということで、フロント要素を全捨てしてslackによせるという作戦に出ました。またデータベースは Realtime Database を用いてさくっと用意し、Cloud Function でバックエンドの機能充実に専念する作戦としました。個人的には時間の削減ができてよかったと思います。

機能分類

作成する機能としては大まかに

  1. slackの入力情報をdbに保存する機能
  2. slackのreactionが押されると、db上のreaction数を更新する機能
  3. db登録情報のランキングを表示する機能

を考えました。
3. についてはdbから値を取ってきて表示するだけなので独立でよいですが、1. 2. についてはslack, db 操作の2要素が絡むので、slack系をいじる人とdb系をいじる人で分担するためにそれぞれ分割することにしました。
以上より、作成することにしたfunctionは以下の5つになりました。

  1. slackの入力情報を整形して2.に投げ、成功したらslackに結果を通知する
  2. 1.で受け取った情報をもとにdbに登録する
  3. reactionが押されたときに、メッセージ情報を整形して4.に投げる
  4. 3.の情報をもとにdbを更新する
  5. dbの登録情報のランキングを表示する機能

開発体制の準備

開発環境の準備として、

$ firebase init

 
Cloud Function, Realtime Database を選択して、言語は Typescript にしました。やっぱり型があると、バグが発見しやすく結果としてスピードが上がると判断しました。
※ 注意点としては、Cloud Function は無料プランだと外部API(今回はslackのAPI)に接続できないので、有料プランにしないといけません。今回は従量課金制のBlazeプランにしました。Cloud Function は月間12.5万呼び出しするまで課金対象にならないので、まず大丈夫だとおもいます。
(参考: https://cloud.google.com/functions/pricing?hl=ja)

初期設定後、以下のようなディレクトリ構造になります。

function/
 ├ node_modules/
 ├ src/
 │ └ index.ts
 ├ package.json
 ├ package-lock.json
 ├ .gitignore
 ├ tsconfig.json
 └ tslint.json

同一ファイルをいじると頻繁にconflictが起きうるので、作業ファイルを分割しました。また、 Cloud Function の仕様上? index.ts が参照されるので、各ファイルのfunctionを index.ts 内でimport し、それをexport するようにしました。
分割の単位としては、

  • slack関連の処理 (slack_cf.ts)
  • データベース関連の処理 (cf_db.ts)

の2つに分けました。(命名は適当です。。)

変更後のディレクトリ構造は以下です。

function/
  ├ node_modules/
  ├ src/
  │ ├ index.ts 
+ │ └ apis/
+ │   ├ slack_cf.ts
+ │   └ cf_db.ts
  ├ package.json
  ├ package-lock.json
  ├ .gitignore
  ├ tsconfig.json
  └ tslint.json

index.ts はこんな感じです。(もっとスマートに書けた気もします)

index.ts
import { functionA } from "./apis/slack_cf";
import { functionB, functionC } from "./apis/cf_db";

export { functionA, functionB , functionC };

この辺の設定を整えた上で作業に当たりました。
(この作業中、相方にはドキュメント等の情報収集にあたってもらいました)

13:30 ~ 分業

 一通り設計が固まったので分業をはじめました。それぞれの機能開発についてつらつら書いていきます。
方向性が固まったので、あとは調べつつ作っていくのみになります。(相方の項目は雑になるかもしれませんw)

1. slackの入力情報を整形して2.に投げ、成功したらslackに結果を通知する

一旦叩けばslackに投稿するfunctionの作成

 いきなり要件を満たすものを作るのも困難なので、slackへの投稿を行う、いわゆる Hello world 的なものを作ることを考えました。
 slackに文字を表示させるには以下の形式で返せばひとまずOKです


res.send({
  response_type: "in_channel",
  text: "Hello world"
});

※ response_type は in_channel (シンプルに投稿) と ephemeral (本人にのみ表示) があります。例えばエラー表示などは ephemeral にしたほうが良い気がします。 (~13:45)

slash command の用意

 slackの入力を取得するために、slash command を利用することを考えました。slash commandを作るためには 自作のslack App を作らないといけません。以下のリンクから一旦イベント用のslack workspace に紐づける形でアプリを作りました。
https://api.slack.com/apps
 それで手順通り進めて、上記のfunctionをアプリをslash command で呼び出せるようになったので、channelに紐づけようとしたタイミングで問題にぶち当たりました。考えてみれば当然ですが、アプリをchannelに紐づけるにはadmin権限が必要だったのです。それで仕方なくデモ用のworkspaceを新規に作って1からやり直しです。ちょっぴりロスしました。 (~14:15)

slash command の入力を整形する

 入力としては、勉強会をやりたい/やってほしい (give/take) と 勉強会のタイトル の二つを想定しました。それぞれ、space で区切られるという想定です。
 function側で入力を req.body.text.split(" ") で分割して取得しました。リクエストのmetadataでchannel_id, user_id, user_name なども取れたので、それらも取得しました。
 そして、 別の function を呼ぶために request-promise を利用しました。
(今更ですがfunctionは外部のAPIでもないので、最終的には別functionはfile参照で呼び出せばよかったかもしれません。)
ここまで終わった時点では 2. の機能ができていなかったので、この部分はコメントアウトして一旦ペンディングにしました。 (~14:30)
 2. 終了後はheaderのcontent-typeなどのすり合わせをして何とか結合できました。(~15:30)

2. 1.で受け取った情報をもとにdbに登録する

Realtime Database の用意

 firebaseの管理画面からボタンポチポチでdbを作成しました。ロックモードとテストモードがあり、今回は動くのが先決なのでテストモードで作成しました。(実運用するときはロックモードでちゃんと設定しましょう) (~13:45)

データベースの書き込み

 まずはドキュメントのチュートリアルを参考に、適当な文字列をdbに登録するものを作成しようとしました。ただその操作にはfirebaseのconfigを定義して初期化する必要があったので、その設定に少し手間取りました。(~14:30)
 適当な文字列が登録出来たら、実際にリクエストの値を登録するように書き換えました。(~14:45)

3. reactionが押されたときに、メッセージ情報を整形して4.に投げる

Event Subscriptions の登録

reaction が付けられた、外されたeventをtriggerにするために、Event Subscriptions を作成しました。今回初めて触ったのですが、有効化するためにはevent発火時のリクエストに含まれる challange という値を返すものを一旦作らないといけないらしく、それにたどり着くまでに時間がかかりました。
参考: https://api.slack.com/events/url_verification

また、reactions:read のscopeを登録しないといけなかったので、再度アプリを認証する必要がありました。(~15:30)

reaction event からメッセージを特定する

実はreaction event の情報では、reactionを付けたメッセージ情報そのものは抜き出すことができず、channel, ts(timestamp) から別途 channel.history() を用いてメッセージ情報を取得する必要がありました。
https://api.slack.com/methods/channels.history
そのためにはfunction 内でslackの認証情報となるtokenを扱う必要が出たので、slack app のkey を利用しました。 (~16:15)
※ このkeyは非常に大切なものなので、リポジトリにはpushしないようにしましょう。
※ 恥ずかしながら僕はpushしてしまったのですが、slack運営が優秀なのか、1hほどでそのkeyが自動で無効になりました。。すごい。。

メッセージの特定後、4. へのリクエスト作成

 上記の channel.history() メッセージには付与されているreactionが配列になっているので、そこでreactionの数を数えました。そして4. へのリクエストを一通り書いて、4. の作業完了までペンディングとしました。(~16:30)
 4. 終了後、結合してめちゃくちゃエラーが出ましたが何とか形になりました。(~18:45)

4. 3.の情報をもとにdbを更新する

登録されているデータを、適当な文字列で置き換える

 update処理もドキュメントを参考にしつつ、登録されているデータの更新を行いました。念のため、更新しようとしているデータがなければ404を返すようにしました。こちらは試行錯誤しながらで結構な時間を使いました。(~17:30)

データベースの更新

 リクエストされた値をもとにデータをupdateする処理を加えました。(~17:30)

5. dbの登録情報のランキングを表示する機能

incoming webhook の作成

 ランキングを通知するために incoming webhookを作成しました。正直ランキングを表示するだけならそのままレスポンスを書けばよかっただけかもしれませんが、通知するときのアイコンやユーザ名を設定するという遊び心が入ってきました (~17:45)

ランキング集計

 db の値を全取得し、reactionsの数で降順に並べました。ここでも、発案ユーザや、勉強会をやりたい/やってほしい などの表示にも力を入れました。 (~18:20)

slash command の登録

 上記をslash commandで呼び出せるように登録しました。もう慣れてきたので、そこまで詰まりませんでした (~18:30)

上記のような段取りで何とか計画通りの機能を作ることができました。
ただ18:00くらいからのつなぎこみ作業は、毎回デプロイしないと確認できなかったので、デプロイを開始してその隙に資料を作成し、デプロイが終わったら動作確認して修正、再デプロイ、、という感じで回していました。

まとめ

 今回の段取りでよかったのは、以下の3点だと考えています。

  • フロントを全捨てして、バックエンドに集中したこと
  • 事前準備でファイル分割し、極力conflictによる時間のロスを減らしたこと
  • slack系を触る人、db系を触る人というふうに分けて、それぞれマイクロサービス化?できたこと

ただ、ハッカソン内で新規に学習することも多かったので、また別の機会があればもう少し勉強してから臨みたいと思います。また、ほかのチームの発表を聞いていて、自分たちが思いもよらなかったアイディアをたくさん得ることができたので、次に生かしたいと思います。

最後に、このような素晴らしいイベントを開催していただいた Firebase Japan User Group (FJUG) さんをはじめとして、イベントに関わったすべての皆様に感謝を申し上げます。非常に有意義な経験になりました!

コメント色々お待ちしています!