22新卒だけで新規サービスの企画から開発までやってみた話――2022新卒研修 技術編

はじめに

みなさん、こんにちは!今年の4月からエンジニアリング統括部サービス開発部に配属された、上野 司、鈴木 健太、渡辺 爽太です!
前回の記事では新卒エンジニアの紹介と、グループ研修についてざっくりご紹介しました。今回は開発フェーズにフォーカスして技術的な内容となっております!
主な技術構成や、各自のこだわった部分や学びになったものなどをご紹介したいと思います。
エンジニアを目指している就活生の方々はもちろん、現役のエンジニアの方々にも何か収穫がある内容になっているかもしれません。
ぜひ最後まで読んでいただけたら幸いです!

技術構成

インフラ
サーバ:App Engine
データベース:Firestore

フロントエンド
言語: TypeScript
フレームワーク: Vue/Nuxt 2系
UIフレームワーク:Vuetify

バックエンド
言語: TypeScript
フレームワーク: Express(NuxtのseverMiddlewareを利用)

学び(工夫)ポイント・詰まったポイント

ツカサ

私が今回の研修で学んだこと、詰まったことを以下に記載します。
私はこの3人の中では一番経験が浅いと自負しております(先輩には自信を持てと怒られる)。
それでも過去の自分と比べるとこの研修を通して確実に成長したと断言できます。
そんな私がこの研修で何を学び、どこでつまずいたのか、ご一読いただけたら幸いです。(完全な文章ではなく箇条書きとなっております。ご了承ください🙇‍♂️)

学び(工夫)ポイント

  • Vuexストアを利用したトップページのメッセージ切り替え機能(初めてストア触りました)
    • 工数的に余裕が出てきたタイミングで自分が申し出た(切り替え機能をつけたいという話は前々から出ていた)
  • css には隣接兄弟結合子というものがある img(直前の要素) + p(対象の要素) {}
    • プロフィール画像を選択するという場面で、選ばれたアイコンに枠線をつけるという仕様だった。以下のように実装。
    input:checked + .each-animal-icon {
      outline: 4px solid var(--v-secondary-base);
      outline-offset: 3px;
    }
    
  • cssはとりあえず見た目を綺麗にするためにゴテゴテとスタイルをつけるのではなく、まずhtml要素、cssやvuetifyのコンポーネントの挙動をしっかりと理解した上でなるべく最小のスタイルをつける
    • ゴテゴテとスタイルをつけると非常にコードが読みづらくなってしまい、メンテナンス性も落ちる。シンプルな設計を心がける
  • コンポーネントの再利用性はなるべく高く設計する
    • あるプルリクでコンポーネント単位ではなく、ページ単位でバリデーションを行おうとしたため。致し方ない場合を除いてなるべく繰り返し使う機能はコンポーネントにまとめる

詰まったポイントが多いので項目ごとにまとめました

  • Vue/Nuxt

    • vuexではリロードするとstateが初期化されてしまう(vuex-persistedstateを使えば解決できるが今回は見送り)
    • Vuetifyコンポーネントのクラスの書き換えが難しい
      • ::v-deepなどを利用し、vuetifyコンポーネントによって生成されたDOMに対するスタイルの上書きを行う
      • いざとなったら!importantも辞さない(Vuetifyコンポーネントのスタイルの中にはデフォルトで!importantがついているものもある)
  • HTML/CSS

    • 謎の(謎じゃない)余白があり、パニクった
      • HTMLは基本左詰めで、要素の幅を指定した場合要素以外の何もないところはマージンとなる
    • クラスの当て方をチームで決めたはずなのに(無自覚に)違う方法で当てていた
      • 今回のルールはVuetifyコンポーネントを使わない場合、スタイルは<style scoped></style>内にゴリゴリ書き、Vuetifyコンポーネントを使う場合はCSSヘルパー(例えばclass="d-flex justify-content pa-4"など)を書くというもの
      • しかし私は通常のdivタグでCSSヘルパーを大量に使ってしまい、チームメンバーを困らせてしまった
      • 【結論】開発ルールは必ず守りましょう
  • Git

    • 不用意にgit pull --rebase origin mainをかけたためにcommit履歴がおかしくなり、レビュワーに迷惑をかける(ほんとはもう少し複雑だが、大体こんな感じ)
      • ベースブランチの更新を取り込みたい場合はgit merge mainを使う方が安全
    • 間違ったブランチを親にしてしまったので、正しい親ブランチを指定した新しいブランチを作成し、cherry-pickで欲しいコミットを適用した。

すずけん

今回開発したサービスは全てGoogle Cloud上で構築しました。
この構築を通して、工夫した点 / つまずいた点を紹介しようと思います。

まず、工夫した点について2点ご紹介します。
今回は開発期間が1ヶ月半と短いこともあり、なるべく開発工数を減らせる構成にすることを意識していました。

工夫Point1. ログイン機能はGoogleに任せよう
開発工数を減らす点で意識した点の1つ目は、ログイン画面などを実装しないで良くする点です。
今回は、GCPが提供しているIdentity-Aware Proxy(以下IAP)を利用し、 こちらにより、ユーザ認証をGoogleに託すことができ、自分達でログイン周りの実装をする必要を無くしました。
具体的には、部内で使用しているGoogle Workspaceアカウントでログインすることによって、指定したApp Engineへのアクセスを許可する設定ができます。

工夫Point2. サービスアカウントのキーは使用しない
他に工夫した点としては、CI/CD環境でGCPのキーを使用しないように気をつけました。
最初は、CI/CD環境にGitHub Actionsを使用しようと考えていましたが、 調べていると、GitHub ActionsでApp Engineにデプロイする際にデプロイ用のサービスアカウントのキーを作成して利用する必要がありそうでした。

しかし、デプロイのためだけにキーを作成するのがどうだろうか?と考え、GitHub ActionsではなくGCPのサービスの1つであるCloud Buildを使用することにしました。
それにより、Cloud Buildの管理画面から各トリガーごとに使用するサービスアカウントを指定することができるため、CI/CDのためだけにキーの作成を行わなくてすんだことも良い気づきでした。

デプロイ以外のテストの実行などではGCPのサービスアカウントが無くても実行できますが、CI/CDは全てCloud Buildにまとめると視認性が良いと判断してGitHub Actionsは利用せずにCloud Buildにまとめました。

次に、つまずいた点について紹介します。
つまずいた点も2点あります。
つまずいたPoint1. Firestoreへの接続
1点目は、今回使用したデータベースのFirestoreへの接続についてです。
今回は、以下の理由からFirebaseを利用せずにGCP内のFirestoreを利用しました。

  • IAPを利用する点
  • FirestoreのセキュリティルールをGCP側で自動でやってほしい点

調べたところ、Firebaseを利用したFirestoreの情報は多かったのですが、GCP内のFirestoreを利用するケースの記事があまり見つからず、どうやればいいんだ?...と行き詰まってしまいました。 調べた結果、@google-cloud/firestoreというライブラリがあり、こちらを使用することで解決することが分かりました。

また、今回のプロジェクトではjestを利用してテストコードを書いていたのですが、jestを利用したテストの中でFirestoreへの接続が上手くできない症状出くわしました。
最終的に原因は分かりませんでしたが、firestore-jest-mockというライブラリを使用することでFirestore部分をモック化しテストを実行出来るようにしています。このように、今回のプロジェクトではFirebaseを利用しなかったことで調べても情報を見つけることができず、行き詰まることが多くなってしまいました。
今後は事前に情報を収集して、問題無さそうかの確認をすることが大事だと感じました。

つまずいたPoint2. App Engineでwebsocketの利用
つまずいたポイント2点目は、今回開発したサービスのメイン機能であるチャット機能で利用しているwebsocketが正常に動かない状況に陥りました。
今回websocket含めたAPIが動いているNuxtのアプリケーションはApp Engineで動かしています。

App Engineでwebsocketを利用する方法を調べていたところ、Google Cloudのドキュメントに以下のように書かれていました。

通常、App Engine では使用可能なインスタンス間でリクエストが均等に分散されます。ただし、http のロング ポーリングを使用する場合は、特定のユーザーからの複数の連続したリクエストを同じインスタンスに到達させる必要があります。

App Engine が同じユーザーからのリクエストを同じインスタンスに送信できるようにするには、セッション アフィニティを有効にします。有効にすると、App Engine は Cookie を調べて同じユーザーから送信されたリクエストを特定し、これらのリクエストを同じインスタンスにルーティングします。
https://cloud.google.com/appengine/docs/flexible/nodejs/using-websockets-and-session-affinity?hl=ja#session_affinity

最初は、上記のドキュメントをしっかり読んでおらず、websocketがダメであればロングポーリングに変更したら動いたりしないかな?と根拠なく考えてロングポーリングを使用するように変更を入れました。
その結果、アクセス数が少ないときは動きましたが、アクセス数が増えると送受信が正常に出来ないことが増えてしまいました。

そこで改めて上記のドキュメントを読み直し、ロングポーリングだとしてもApp Engineをフレキシブル環境にする必要があると知り、切り替え作業を行いました。
フレキシブル環境へ切り替えるためのApp Engineの設定は https://cloud.google.com/appengine/docs/flexible/custom-runtimes/configuring-your-app-with-app-yaml こちらを参考にしました。

簡単に切り替え作業できると思ったのですが、いざデプロイを終えてアクセスしたところ503エラーが表示されてしまいました。
この解決のために、package.jsonのstartスクリプトを

"start": "nuxt start",

から

"start": "HOST=0.0.0.0 PORT=8080 nuxt start",

にHOSTとPORTを指定するように書き換えました。 このようにする事で無事にアクセスする事ができるようになりました。

今回の研修を通して、調べても情報を見つける事に時間が掛かってしまう事が多くありましたが、
その時々で先輩社員の方に声をかけて相談すると、一緒に調べていただけたりして解決する事ができました。
1ヶ月半サポートしてくださった先輩方ありがとうございました!

そーた

僕はこのプロジェクトではフロントエンドをバリバリやっていました。
その際に気をつけようと思ったところを書いていこうと思います。

工夫.コンポーネントに状態を持たせない
本プロジェクトで印象に一番残ったものはこれですね。
状態はコンポーネントに持たせず、ページに持たせる
なんのこっちゃ?と思うので具体例を出します。

コンポーネント構成の具体例
コンポーネント構成の具体例

例えばこの図のようなページがあったとします。

  • このページには2つのつぶやきコンポーネントが存在し、 このコンポーネントにはいいねコンポーネントがあります。
  • つぶやきコンポーネントの数は変更するかもしれません
  • いいねは押下されるとAPIを叩き、更新成功時に色が変わる

この場合、いいねを押している状態はどこで管理すればいいでしょう?

僕は初めの頃、コンポーネントごとで全て完結したいと思い以下のように実装していました。

改良前の実装>

  • いいねコンポーネントの中にisLike変数を定義
  • 初期状態はpropsで受け取り
  • 押下されるといいねコンポーネント内でAPIを叩く
  • 更新後にisLike変数を更新
  • isLike変数によって見た目を変更

この実装方法は直感的だと思います。
いいねに関するものはいいねコンポーネントですべて完結させる方針ですね。

しかし、この実装だと別コンポーネントやページはいいねの状態を確認できていません。
そのため、仮に状態を取得したい場合はストア等を利用する必要があります。
また複数のコンポーネントでAPIを叩くことも把握が難しくなってしまいます。

そこでこのプロジェクトではほとんどの処理をページで行い、コンポーネントは描画のみに集中するという構成にしました。

改良後の実装>

  • propsで状態を受け取り
  • 押下されると親へイベントを発生させる
  • 状態により見た目変更

押下すると 「いいね → つぶやき → ページ 」の流れでイベントを発生させます。
ページはイベントを受け取るとAPIを叩き、propsを更新させます。
このようにすることで コンポーネントは受け取るpropsのみ気にすれば良いことになります。

また、ページはいいね状態を持っているのでページの別コンポーネントはpropsからいいね状態のデータを受け取ることができます。

つまずき. Vuetify君との喧嘩と仲直り
本プロジェクトではVuetify というUIフレームワークを利用しました。
これは既にある程度の機能が備わったコンポーネントを提供するものであり、ゼロから機能を作らずにすむという代物です。

とても便利なのですが細かい調整ができないことがあるという欠点があります。
そのためコンポーネントをそのまま利用すると、デザイナーさんが考えてくれたデザインと異なってしまいます。
Vuetifyは使わないほうがいいかもしれない...と思いました。(喧嘩)
しかし、やはりタブ切り替え等の機能が備わっていることは破格的に魅力的なため、
使えるときは使うといういい距離感で利用するということに落ち着きました。(仲直り)

さいごに

いかがでしたでしょうか?3人が持っている知識やレベル感も違う中、お互い知っていることを共有し合い、個々の力以上のものが発揮されていたと思います。

この記事が誰かのお役に立つ、もしくは心に残るものとなれば幸いです。

今後はそれぞれ違うプロジェクトでの業務となりますが、3人とも確実に入社当初と比べてパワーアップしていますし、優秀な先輩方に囲まれてなんとか業務をこなしていくことでしょう。

今回の研修を計画してくださった皆様、日々サポートしてくださった皆様に改めて感謝申し上げます。
本当にありがとうございました。

エンジニアリング統括部 サービス開発部 第1グループ 上野 司

上野 司 Tsukasa Ueno

エンジニアリング統括部 サービス開発部 第1グループ

エンジニアリング統括部 サービス開発部 第1グループ 鈴木 健太

鈴木 健太 Kenta Suzuki

エンジニアリング統括部 サービス開発部 第1グループ

エンジニアリング統括部 サービス開発部 第2グループ 渡辺 爽太

渡辺 爽太 Sota Watanabe

エンジニアリング統括部 サービス開発部 第2グループ

※2022年9月現在の情報です。