for Startups Tech blog

このブログのデザインを刷新しました。(2023/12/26)

開発に至る前の要件定義で四苦八苦した話

こんにちは、エンジニアの藤田です。 普段は社内向けのプロダクト「タレントエージェンシー支援システム(SFA/CRM)」の開発をしています。

ヒューマンキャピタリストはTA(タレントエージェンシー)本部という部署に所属しており、そのTA本部が使うシステムを内製で開発しています。

エンジニアとしてジョインして約半年、ここでの開発手法はアジャイルでフラットな開発チームであり、優先順位はあるもののタスクは開発者の裁量で取って進めていくスタイルで割と自由に開発しています。

書籍『アジャイルサムライ』第2章冒頭で書かれている、

典型的なアジャイルチームには、あらかじめ決まった役割分担は存在しないッ!!

というやつですね。

普段のタスク内容は、RubyやVue.jsを使ったシステム開発、バージョンアップ対応、インフラに強いメンバーはインフラ整備など行っています。

さて、ここ最近私が取ったタスクが「POと共にユーザヒアリングに同席しながら要件定義していく」というタスクでした。

正確に言えば、当初は「とある機能追加の為のUI、設計を考える」タスクであり、平行で動いている別の改修タスクと切り離し可能な作業と考えていたのですが、進めていくうちに他の改修機能との兼ね合いを考慮する必要もでてきたり、別のタスクが自分のタスクにも影響でてきたりして、こう思ったわけです。

「もっと上流から考え直す必要があるじゃん。。」

ということで色々苦労したのですが、ユーザの声を聞けたいい経験でもあったのでその時の記録を書いていこうと思います。

TA本部のとある課題と我々開発チームのミッション

おおよその業務は社内向けシステムで運用を管理できているのですが、当然ですが組織や事業は日々変化していきます。
そのため我々が気づかないうちに新たな運用が発生し、別のツール(スプレッドシートやその他管理ツール)を現場でカスタマイズし管理していくケースが往々にして発生します。
今回要件定義したものも、詳細は書けませんがこういった業務のひとつです。

別のツールだとしても管理できているならいいじゃんとなるかもしれませんが、次のようなデメリットもいくつかあるので

  • 情報が分散されてデータが管理しにくい
  • 属人化してしまう

今回は組織として挙がった課題を解決できるよう、システムに組み込むといったものになります。

開発者の課題

要件定義をしようとしたところ、以下の課題に直面しました。

  • 何となくやりたいこと、現場の課題があることはわかったが、具体的な「システムで管理できていない運用」のところがよくわかっていない。
  • 通常の運用も具体的なところまではわかっていない部分はある。

というわけで、いきなり躓くわけです。

困った時のヒミツ道具ということで手元にある『エンジニアリング組織論への招待』を開いてみると以下のことが書かれていました。

ソフトウェアにおける実現

それは誰かの曖昧な要求からスタートし、それが具体的で明確な何かに変わっていく過程が実現で、その過程のすべてがエンジニアリングという行為です。
つまり、「曖昧さ」を減らし、「具体性・明確さ」を増やす行為が「エンジニアリングとは何か」という答えでもあるのです。

1-2. 不確実性とエンジニアリング

具体性・明確さを増やすために、まずはユーザの運用から整理しようと思い、業務フロー図を作成しました。

業務フロー図作成

ユーザヒアリングまで少し時間があったので、業務の理解と整理の為に業務フロー図の作成準備に入りました。
先にこれをやるメリットは以下が考えられます。

  • 事前に図として整理することでヒアリングの質が向上する。
  • 質問の準備ができる。
  • 図を共有しながら話もできる。

どのような運用をしているかはslackのやり取りにもヒントがあったり、開発チームでも知見のあるメンバーはいるのでそれらの情報を集め整理しながら簡単な業務フロー図を作成しました。
あくまでチームで理解の共有ができればOKなので、結果以下のような図になりました。

登場人物の関係性と下半分は時系列の処理フローのような図があったり、パターンを考えた図も含まれています。
教科書通りの業務フロー図とは異なりますが、いいのでしょうか?

いいということにしましょう!

最初の段階では仮説思考的に作成したものとなり、その後にチーム内で相談、議論したりユーザから正確な運用をヒアリングしながら図を更に肉付けしていきました。

ユーザヒアリングや業務フロー図作成の過程で以下のようなことがわかってきました。

  • ユーザの業務運用フローはどういったものか
    • どのように管理しているのか
  • 課題の具体的な特定

また、今回システム改修をする上で考慮しなければならない点も整理できるようになりました。

組織として

  • 現場の運用をシステムで管理したい。

現場として

  • スプレッドシート管理の拡張性は手放せない。
  • システム化することで業務が回りづらい状態になっては困る。

開発チームとしては上記、両方考慮しながら開発しなければなりません。

ちなみにユーザヒアリングについては1.5ヶ月の間にトータル10回程行いましたが、POは別途回数こなしてヒアリングしていました。 様々なチームや役割があるので多角的に情報収集し、その中でベストな方向性を模索しました。

システムの新機能について要件定義

システムで管理出来ていない運用フローについて明らかになってきました。次のステップはどのように既存システムに新機能を組み込むかになります。

まずはPOが大まかな仕様を考えたり、壁打ちしたり、開発チームで仕様を検討するMTGを行いました。
仕様について検討する機会が多くなったため、仕様検討MTGを増やして議論を重ねました。

チームミーティングは1回のMTGで1時間〜2時間を週に2〜4回くらい、その時の課題によって増やしたり減らしたりしました。トータルで10~20時間は仕様検討に使いました。
この段階でシステム開発における「達成すべきもの」が徐々に明確になってきました。

ワイヤーフレーム、画面遷移図、ER図を作成

また、この工程でワイヤーフレーム、画面遷移図、ER図も作成します。
ワイヤーフレームはチーム内で理解のズレが無いよう、できる限り正確な情報でデザインしていき、それを基に更に開発チーム内で議論、ユーザとも感触を伺ったりしていき、改良を重ねていきました。

この段階での正確なワイヤフレーム作成手法はその昔、ECサイト開発をしていた時に一緒に働いたWeb制作会社のやり方を真似ました。
完成品をイメージしやすいのはもちろんですが、なかなか綺麗なワイヤーフレームを見て感心したものです。
バックエンドエンジニアの自分がやっても下手で時間的コストが掛かるので簡単な対応の時はやりませんが、他人と理解のズレが発生しそうな仕様を考える時などはこのやり方を意識してます。

話を戻しますが、ここで重要になるのが、先に挙げた以下2点です。

  • この新たな機能追加が組織の課題解決になっているか、かつ
  • 現場の業務改悪になっていないか。

ユーザは既に別の管理ツールで運用しており、我々が下手な追加機能を開発しても使ってもらえません。社内のユーザとはいえ、使いたいと思う機能を提供しないと使われないのは一緒です。
開発者として使われないまま負の遺産になることは何としても阻止しなければなりません。

そこで開発チームの提案とユーザの考えの違いに差がないか、慎重に仕様を詰めていきます。
ズレていたら再度、仕様見直しと共にドキュメント修正、開発チームで議論、ユーザへ提案またはヒアリングを繰り返していきます。
週一2時間でプラニングポーカーの見積もりミーティングがあるのですが、この対応の話をしていていたら長引いて見積りができない回が何回かありました。
もちろん見積りミーティングはリスケです。

またここまで、できる限り正確なワイヤーフレームを作成してきましたが、開発中に「こっちの方がいい」はどうしても出てくるのであくまでチームの共通理解が主な目的になります。

ER図等のDB設計についても「DB設計検討作業」をチケット化し、担当者が数パターンを開発チームに提案、チームで合意を得ながら進めていきました。

ユーザーストーリー作成、タスク化

この段階まで来たらかなり「曖昧さ」が減り、「具体性・明確さ」が増えてきました。
あとはユーザーストーリー作成や作業のタスク化をし、開発メンバーでプランニングポーカーの見積もりをしていきます。

また、この段階でフロント→バックエンドの仕様を描いたシーケンス図も作成しましたが、これも開発段階で変わってくることはあるので、あくまで参考程度の情報にしています。

最後に

組織の課題解決をどのようにシステムに落とし込むかを皆で頭抱えながら進めていき、話し合いの最中や開発の段階でも新たな課題が発生しては、仕様詰め、設計、見積もりを繰り返してきましたが、無事に開発も着手でき、部分的にリリースもできてきました。

今回の開発作業では、「組織の課題を解決できるだけでなく、現場のユーザの使い勝手」の2つを考えなければならないのが大変でした。
開発チームの提案がユーザ側には通らない場面も多々あり、運用ヒアリングしていく中で「なるほど」と納得する場面が多く勉強になりました。

手段として、色々ドキュメント書いたりミーティング増やしたりしましたが、「何の為に作るか」の目的は常に意識する必要があるかなと思いました。
もちろん手段もいいやり方を吸収していきたいです。

-社員と別け隔てがない環境- Startups Firstの為に常に挑戦し続けるフォースタのエンジニアインターンで学んだこと

初めまして、杉谷です。

2021/10月から入社までの約5ヶ月間、インターンとして働き4月から正社員として入社しました。 今回は、そのインターンについてや5ヶ月間で学んだことについて書いていきたいと思います。

フォースタのインターンってどんなことするの?

最初に、インターン内容についてですが、 インターンは、大きく「課題」・「実務」の2ステップ構成になっています。

まず初めに、課題に取り組みます。

この課題は、実務で扱うプロダクト内容や扱う技術への理解を深める為に設けられています。 例を挙げると、弊社ではElasticsearchという検索エンジンを使っているので、課題では「実際にフィールドを追加してAPIのレスポンスを返す」といった実務でも行われるAPIの改修に取り組んでもらったりしています。 フォースタ以外でも通用する技術を効率よくキャッチアップ出来る機会なので、1つの魅力的な点かなと個人的には思っています。 課題を修了することで、一定レベルのプロダクト・技術的な知識を共有した状態になり、スムーズに実務に着手することが出来ます。

またこれはユニークな点ですが、課題はインターン生自身の手によって常に更新されています。 更新というのは具体的に、課題を修了し実務に携わっているインターン生が、前もって学んでおいた方が良いことをまとめ、それを課題に反映させています。彼らが主体性を持って自身の学びを課題にアウトプットすることで、常に課題は改善され、同時に彼ら自身の成長にも繋がるというサイクルです。 実際、私が課題に着手していた際は課題の数は6つでしたが、現在では11個に増えています。ただ多ければ良いという話でもないので、初期に追加された課題を複数まとめてアップデートし、より密度の高い課題にすべくインターン生の方々が自身の経験をもとに試行錯誤しています。(課題10くらいまでに抑えるのが理想です)

そんな優秀なインターン生が寄稿している記事もありますので是非ご一読下さい。

tech.forstartups.com

課題を修了すると、いよいよ実務に取り組みます。 主にインターン生は、『STARTUP DB』やその裏側にあたる管理システム(STARTUPS DATA PLATFORM)に携わります。 社内にいるユーザーからの意見を基に改修を行なったり、新規機能開発を行うなど幅広いタスクに着手しています。 またタイトルにある通り、社員とインターンの間に垣根がなく、実務に当たる際は優先的なタスクから順にインターン生・正社員問わず着手しています。

さらに、自身の興味のあるプロダクトや磨きたい技術が実務にあれば、手を挙げて挑戦できます。 私自身、インターンとして働いている時にフロントエンドの開発に興味があったので、手を挙げた結果翌月からフロントの開発にもアサインされました。(他にもインフラをやりたくて手を挙げて、現在terraformをバリバリかいているインターン生もいます) 社員との垣根が無く自由度が高い分、それに伴う責任やプレッシャーもありますが、やりたいことに挑戦できる環境はとても貴重な為、モチベーション高く業務にコミット出来る環境だなとインターンを通して感じています。

スキルアップ会や勉強会で幅広い知識をインプット

エンジニアインターン生は、業務時間内で課題や実務以外に2つの会に参加しています。

スキルアップ会(輪読会)では、主に1冊の本をみんなで輪読しディスカッションを行いスキルアップを図っています。その一冊は、社員・インターン生関係なくビブリオバトルを通して選ばれます。 詳しい内容は下記の記事にて紹介していますのでご参照下さい。

tech.forstartups.com

勉強会では、発表者を募りそれぞれが興味のあることや共有したいことをテーマにして30分プレゼンしています。 テーマは様々で、実務に即したものから自身の興味のあることについてなど話しています。 もちろん、インターン生も発表することが可能で、自身の学んだことをアウトプットする場所として推奨されています。 私自身もこの記事を書く前に、勉強会でこれまで学んできたことについて発表し、改めてこの5ヶ月で学んできたことを自分の中により深く落とし込むことが出来ました。

この他にスプリント定例や各種mtgもあるのですが、それはまた別の記事で書ければなと思っています。上記の内容でもっと詳しく知りたいと思った方は、ぜひ一度カジュアル面談に来て頂けると嬉しいです。

インターンを通して学んだこと

ここからは自身がインターンを通して学んだことについて書いていきたいと思います。 細かいところまで列挙してしまうと、長くなってしまうので特に自身への影響が大きかったものを挙げたいと思います。

理解しやすくパフォーマンスを考慮したコードを書けているか

これは、自身のコードが他人にとって可読性が高いか、プロダクトにどれくらいの負荷がかかるのかといった点を考慮しながら書けているか常に意識すべきであるという学びを表しています。

フォースタのインターンでは、エラー調査や改修作業などは主にインターン生が担当しています。 エラー調査はプロダクトを知るのにとても良い方法で、色々な箇所のソースコードを読み漁ります。そうして改修箇所を特定しリファクタリングを行うのですが、当然調査をすればするほどプロダクトのソースコードに詳しくなっていきます。そうなると、「あの箇所、ページの描画速度が遅いから改善したほうがいいな」とか「ここら辺は共通化できそうだ」という点が出てきます。 そうして改修を重ねるうちに、自身が書くソースコードでもパフォーマンスを考慮して書けているかを自然と意識するようになりました。 これは、実務に入らないと分からないことだなとインターンを通して深く理解しました。

自走できないと活躍できない

今まで「自走力」というのは、ある一定の技術力を有することであると考えていました。 しかしそれ以外にも「目標を持って取り組み、それに見合った行動・成果を出す」といったことも「自走力」であるとインターンを通して学びました。

これを実感したのは、実務に取り掛かった初期の頃です。 課題を通してプロダクトへの理解は一定程度得たものの、いざタスクに取り掛かろうとすると何から着手すべきかの順序立てが上手く出来ませんでした。結果、当初予定していたよりもだいぶかかってしまうという結果に終わりました。

そこから、「まずはタスクをしっかり期日までに完了させる」という目標を持つようになりました。 そのような目標を持つことで、段階的にやるべきことを順序立てていけるようになりました。 また、質問の仕方も目標を持ったことで徐々に変わっていきました。質問も同じように順序立て、具体的にどうしたかったのか、それが出来ない理由と試したことは何かなどを落とし込んで質問を行えるようになりました。 そうすることで煮詰まることが少なくなり、タスクをこなすスピードが改善され目標に対して自身の行動や成果が追いつくようになりました。

この学びは、自由度が高いからこそ高い自走力を求められる、フォースタのエンジニアインターンの環境があったからだと実感しています。

ユーザーの領域に関する知識も深く蓄えないとvisionは遠退く

これは、どんな領域・分野のユーザーが『STARTUP DB』をどのように活用し、どのような情報を求めているのか、自分もユーザーのことを深く理解する必要性があるということです。 言い方を変えれば、エンジニアだからと言ってユーザーと接する最前線に行かずにいるのではなく、むしろ前のめりに出て話をしに行き、そこから常にユーザーの本質的な課題を探す姿勢を持つことも重要であるということです。

結局のところ、フォースタのエンジニアが技術力を高めるのはビジョンを達成する為であり、それがモチベーションとなり日々挑戦し続けられます。

www.wantedly.com

私はこの5ヶ月、なるべく自身のプロダクトを使う領域の人や他部署の方と話をするように心がけ、「STARTUP DBが今どんなユーザーに使われているのか」・「STARTUP DBにはどんな情報を求めているのか」といった開発しているだけでは中々見えてこない情報を蓄えるようにしました。そうすることで、ユーザーの視点でプロダクトを見たり、それを基に課題を自分なりに見つけ壁打ちするといったことが出来る様になってきました。そして日々ユーザーと話しその中から見つけ出した課題の解決策をプロダクトに反映させることが、ビジョン実現の一番の近道ではないかとインターンを経て改めて強く実感しています。 またこの学びは、共に目指すべき目標が同じであり挑戦する仲間が集まったフォースタのインターンだったからこそ学べたものと深く実感しています。

ここまで読んで頂きありがとうございます。 この5ヶ月の間、インターンとして挑戦し上記に述べた学びは勿論、それ以外にもたくさんのことを学ぶ貴重な機会でした。 これからは、正社員として日本からグローバルに戦えるスタートアップを創出するべくモチベーション高く挑戦していきたいと思います!

エンジニアが週末に作ったアプリケーションをオフラインイベントで披露した話

こんにちは.エンジニアの藤井(@yutafujii)です. フォースタではおよそ2年ぶりに”感謝祭”というイベントを開催いたしました.(イベントレポートはこちらでご確認いただけます)

イベント当日は,来場者が受付されるたびに会場内のスクリーンにお名前と写真がポップアップ表示されていました.これは個人開発で作成された簡単なアプリケーションだったのですが,今回はこの開発経緯や技術的検討点についてお話ししようと思います.

スクリーンにお名前と写真がポップアップ

感謝祭とは

フォースタ感謝祭とは,日頃お世話になっている起業家や投資家,スタートアップエコシステムに関わるみなさまをオフィスにお招きし,立食パーティ形式で交流していただくイベントです.なお,抗原検査を行った上での参加を必須とするなど,必要な感染対策は講じての実施です.

開催にあたり,今回私は運営委員に入り込み,イベントをよりよいものに仕上げるためにエンジニアリングの視点で関わってきました.

モチベーション

さて,その感謝祭での我々の悩みが,「どうやったらゲスト同士の交流を促進できるか」というものです. 立食パーティという性質上,場だけを提供すると,どうしても知り合い同士で話をしてしまう・知っている人が少なく孤立してしまう,という状況になりがちです.

こうした交流促進の課題感に対して,運営メンバーのキックオフミーティングで一意見として

「会場にスクリーンを設置して,ゲストが来場するごとにそこに顔写真をバーンって映せたら,”あ,いまこの人きたんだ・この人も来ているんだ”ってのがわかって,ゲスト同士がコミュニケーションを取るきっかけが作れるよね」

という提案をしてみたところ.即座に「藤井さん,これできる?」

アサインされてしまいました.流石,みんな仕事できる.

「うーん,とりあえず検討してみます」

プロトタイプ

個人開発としてプロトタイプ作成に取り掛かります.

当初の要件からすると,スクリーン(=クライアントPC)は受動的に来場者を表示する必要があります.

WebSocketか,定期的なAPIリクエストでのデータ取得のどちらかで実現可能だとは感じ,せっかくなので実装経験がなかったWebSocketを使おうと思いました.その方がリアルタイム性でも勝りますし.

となると,ステートフルなサーバーが必要になるので,S3/CloudFrontでの静的ホスティングではなくEC2かECSに載っけて動かさないといけなさそうです.

ここまでの要件だけなら言語やフレームワークは何でも良さそうだったので,せっかくならと思い実装経験がなかったNext.jsをチョイス.ちなみに弊社では基本的にNuxt.jsを使っています.

起点となるユーザーの操作ですが,ゲストの受付管理はスプレッドシートで行うのが好ましかったため,以下のようなフローを検討しました:

  1. 受付で名刺をお預かりし,スプレッドシートにチェックをつける
  2. GAS(Google App Script)でセルに記載されたゲスト情報をAPIエンドポイントに送信
  3. リクエストを受け取ったNext.jsサーバーは,WebSocketを通してクライアントに対してデータを送信
  4. クライアントPCはゲスト情報を画面に表示する

想定するデータフロー

画面への映し方については,ランダムな座標にフェードイン・フェードアウトのアニメーションで表示させました.

できあがったローカルでのプロトタイプはこんな感じです.スプレッドシートにチェックを入れるたびに,画面にゲスト情報が表示されます

フィードバック

さて,運営チームのミーティングで見せてみました.

「おお〜...」

こういうのってレスポンスからなんとなくわかりますよね.”悪くはなさそうだが,もうちょっと何か”というところですね.

ミーティング中に追加で要件をいくつか整理して時間内に実現可能か試してみることに.

プロトタイプに対する追加の要件

ブラッシュアップ

というわけでこれらに対応していきます

要件1:ゲストのスクリーン出現場所はランダムではなく中央に固定

画面の真ん中にゲスト写真を出現させること自体はもちろん難しくなさそうです.

ただ,一気にゲストが来場して受付担当者がスプレッドシートチェックボックスを次々に押しても見栄えが悪くならないようにしたい.一瞬で別のゲストの写真に切り替わっちゃったら寂しいですから.

そこで,キューを用いることにしました

キューを用いた表示方法

  1. WebSocketで受理したデータはまずはキューにpush
  2. 中央の表示部が空いていたらそのままpop
  3. 表示部では一定時間経過したらデータを落としてキューから次のゲストデータをpopする

こんな感じで実装をしてみます.

// pages/spread.js
// *表示パターンごとにページを作り,パターンをコンポーネント名称にした

const reducer = (state, action) => {
    switch (action.type) {
        case 'ENQUEUE':
          // 3箇所の表示場所が空いていたら,すぐに表示
          // そうでなければキューにpush
          // 詳細は省略
        case 'POP_LEFT':
          // キューの先頭をpopしてleftに代入
        case 'POP_CENTER':
          // 省略
        case 'POP_RIGHT':
          // 省略
        default:
            return state
    }
}


const Spread = () => {
    // キューとゲストを表示する中央3箇所をステート管理
    const [newGuestState, dispatch] = useReducer(reducer, {
        queue: [],
        left: undefined,
        center: undefined,
        right: undefined,
    })

    // websocket経由でデータを受理したらキューに入れる
    const socketInitializer = async () => {
        await fetch('/api/socket')
        socket = io()

        socket.on('update-input', msg => {
            dispatch({ type: 'ENQUEUE', data: JSON.parse(msg) })
        })
    }
    useEffect(() => socketInitializer(), [])

    // 中央3箇所の表示場所はそれぞれデータの変更をウォッチ
    // 一定時間経過後にキュー先頭をpopして表示する
    useEffect(() => {
        if (newGuestState.left) {
            const timer = setTimeout(() => {
                dispatch({ type: 'POP_LEFT' })
                readyToRender(n)
            }, TIMEOUT)
            return () => clearTimeout(timer)
        }
    }, [newGuestState.left])
    useEffect(() => {
      // ...
    }, [newGuestState.center])
    useEffect(() => {
      // ...
    }, [newGuestState.right])

    // ...

要件2:ゲスト写真はフェードアウトせず残し続ける

さて,フェードアウトさせないことはもちろん容易にできそうですが,データの永続性が気になります.

実際のイベント開催時には,何らかの原因で画面が動かなくなってリロードしてみるということが確実に起きると考えていました. その際に画面をリロードしてもこれまで来場されたゲストがきちんと残ってスクリーンに映っているようにするためには,WebSocketでフロー情報を受け取るだけでは実現が難しそうです.ここにきてデータベースが必要そうでした.

そこで,PostgresQLを導入することにしました.ORMにはPrismaを選定しました.はい,公式ドキュメントに載ってたのをそのまま使おうとしただけです.

データベースをシステムに追加

これに伴い,GASからPOSTリクエストを受けるAPIエンドポイントにはデータベースへのINSERT処理を追加し,画面コンポーネントファイルには getServerSideProps を追加しました.

// pages/api/arrive.js

const Arrive = catchErrorsFrom(async (req, res) => {
    if (req.method == 'POST') {
        try {

            // APIエンドポイントの処理にデータベース保存を追加
            const guest = await prisma.guest.create({
                data: {
                    name: req.body.name,
                    company: req.body.company,
                    position: req.body.position,
                    imageUrl: req.body.imageUrl,
                    checked_in_at: new Date().toISOString()
                }
            })

            res.status(200).json(guest)
            if (res.socket.server.io) {
                res.socket.server.io.emit('update-input', JSON.stringify(guest))
            }
        } catch (e) {
// pages/spread.js

// ...

// 既に来場されたゲスト一覧を取得し最初から表示する
const getServerSideProps = async () => {
    const guests = await prisma.guest.findMany()

    return {
        props: {
            initialGuests: JSON.parse(JSON.stringify(guests))
        }
    }
}

const Spread = ({ initialGuests }) => {
    // 来場されたゲストをステート管理
    const [guests, setGuests] = useState(initialGuests)
    const [newGuestState, dispatch] = useReducer(reducer, {
        queue: [],
        left: undefined,
        center: undefined,
        right: undefined,
    })


    useEffect(() => {
        if (newGuestState.left) {

            // 新しく来場されたゲストをguestステートに追加し,
            const guest = { ...newGuestState.left }
            const n = guests.length
            setGuests(prevGuests => [...prevGuests, guest])

            const timer = setTimeout(() => {
                dispatch({ type: 'POP_LEFT' })

                // 一定時間経過後に画面周辺に表示させる(メソッド内容は省略)
                readyToRender(n)
            }, TIMEOUT)
            return () => clearTimeout(timer)
        }
    }, [newGuestState.left])

// ...

クライアント側の処理はおよそ以下の図のようになります

画面の中にゲスト写真をどう配置するのかは次に検討します

要件3:ゲスト写真は来場された順に周辺に寄せて表示していく

要素divにあたるCSSposition: absolute にしておき,topleft属性をJS側で与えてあげれば各要素を好きな場所に表示させることはできます.

実際,ランダムな場所に表示させていた最初のプロトタイプはこうして実装していました.

来た人から順に画面の外側に並ぶようにするにはどうしたらいいのか...

まず,ゲスト写真の配置場所としてあり得る座標を計算しておき,中心からの距離が遠い順にソートして,来場された順に先頭の座標から割り当てていく,という方針をとることにしてみました.

要素を配置する座標を全て計算

画面中心からの距離でgridをソート

実装としては,座標(grid)をステート管理し,画面(window)サイズが変化するたびに再計算する処理を加えたのと,gridが変化するたびにゲスト写真のポジションを再度割り当て直す処理を追加します.

// pages/spread.js

import useWindowDimensions from '../hooks/useWindowDimensions'
import shuffle from '../utils/distance_ord'

const Spread = ({ initialGuests }) => {
    // ...

    // windowサイズをウォッチしてgridを再計算
    useEffect(() => {
        if (windowDimensions.width) {
            let newGrid = []

            // for each item has width, height = 80px
            const xMax = windowDimensions.width / UNIT_OF_GRID
            const yMax = windowDimensions.height / UNIT_OF_GRID
            for (let y = 0; y < yMax; y++) {
                if (y % 2 == 0) {
                    for (let x = 0; x < xMax; x++) {
                        newGrid.push([y/yMax * 100, x/xMax * 100])
                    }
                } else {
                    for (let x = 0; x < xMax-1; x++) {
                        newGrid.push([y/yMax * 100, (x+0.5)/xMax * 100 ])
                    }
                }
            }
            // 画面中心の一定のエリアは除外した
            newGrid = newGrid.filter(grid => !(25 < grid[1] && grid[1] < 78 && 33 < grid[0] && grid[0] < 65))

            // shuffleはgridを中心からの距離(の2乗)でソートする関数
            setGrid(shuffle(newGrid))
        }
    }, [windowDimensions])

    // gridをウォッチしてゲストの配置座標を振り直す
    useEffect(() => {
        const postGuests = guests.map((guest, index) => {
            const n = grid.length
            const top = n === 0 ? 0 : grid[index % n][0]
            const left = n === 0 ? 0 : grid[index % n][1]
            return {
                ...guest,
                styleProps: {
                    top: `${top}%`,
                    left: `${left}%`,
                },
                render: true,
            }
        })
        setGuests(postGuests)
    }, [grid])

windowサイズが変化するたびに座標を再計算しソートするには O(n) + O(nlogn) の計算がクライアント側で必要ですが,イベントの参加者は1000人もこないですし,n < 1000 を前提に作れたので,現実的な処理時間になりそうです.

要件3番外編:集合体恐怖症の方も気分を害さないように

さて,ここまで実装してみて,途中経過をSlackで運営メンバーに共有してみると...

スタンプが1個もつかない...! むしろ「気持ち悪い」という趣旨のコメントが...

たしかに,ギョッとする見た目なんですよね.テストデータは写真素材が少なかったので,デフォルトにした蛍光カラーのアバターが目立つとはいえ.

「気持ち悪い」という趣旨のコメント,冗談めかして言われてますがクリティカルな問題だと認識しました.当日大きなスクリーンに映るこの画面を見て同じ気分になるゲストがいるかもしれない,と.

そこで,画面の縮尺を変化させて,集合体っぽく映らないように対応できるようにしておきました.つまり,

  • 画面全体にわたって整列しない程度にまで縮小する
  • 画面内に20個くらいしか映らない程度まで拡大する

といった操作を当日行えるようにしておきました.

ただし,どのような縮尺だったとしても画面中央に出てくる新たなゲストは大きく映したかったので,これらの要素は全てCSS属性を vhvw で指定し,他は px指定するように変更しました.

CSSを使い分ける

画面サイズを変更して気分を害さない見た目に調整できる

要件4:背景は感謝祭の画像を検討

実装者としてはもっとも簡単なこの部分の実装が,一番ユーザーの印象にインパクトを与えるんですよね.

背景を変えて,ついでにちょっとしたアニメーションも追加します.

画面中央に常に表示するアニメーションを追加する

さて,開催数日前にここまでの状態に仕上がり,運営メンバーがいるチャネルに連絡してみると...

上出来!この反応を見た時に,「これなら当日出してもゲストが見てくれそう」と直感しました.

そのほか

一時のプロジェクトでAWSリソースを変に汚したくなかったので,終わったら全てもれなく削除が可能なようにIaC(Terraform)管理しておきました.デプロイについてはGithub Actionsでdocker-composeファイルを元にECS serviceを起動するようにしています.

こうして完成したアプリケーションは,会場受付においてあるスプレッドシートと連動して次のように動きます.

完成

感謝祭当日

幸いなことに何の問題もなく,きちんと動きました.

また,ありがたいことにSNSへの投稿素材にしてくださった方もいらっしゃいました.

エンジニアとして大変うれしいですね.

おわりに

社内から複数のフィードバック聞かせていただき,目的だった「ゲスト同士の交流を促進」に多少なり貢献できていたようです.よかった.

一方で,「もう帰ったのかわからない」「フォースタ社員がより交流を促進できるためにはxxといった使い方ができたらよかった」など次の課題も見えました. 次回やるなら改善してみたいところです.

We are Hiring!

フォースタートアップスでは共に働く仲間を募集中です。本記事を読んで興味を持っていただけましたら採用情報をご覧ください。

Nuxt Bridgeを使ったNuxt3の導入調査してみた

こんにちは。エンジニアの大野です。主にフロントエンド周りを担当しています。

去年の話題にはなりますが、2021年10月にNuxt3のベータ版がリリースされました。

f:id:yyohno:20220329150316p:plain
Nuxt3beta

公式のリリーススケジュールでは、このブログを書いている2022年3月にrc版、 そして2022年6月には安定版をリリースする予定となっており、着々とバージョンアップへの準備が進められている雰囲気が伺えます。

弊社にもNuxt2の環境で運用しているプロジェクトがあるのですが、vue3のリリース直前に作成した環境であるため、Composition Api やTypeScriptは別途runtimeやbuildのモジュールを個別に入れたりしており、package.json やnuxt.config.ts の設定周りがやや煩雑になっていました。

Nuxt3ではこのあたりのモジュールが標準搭載されるということもあり、チームとしても移行を前向きに検討したいという話が挙がり、実環境にNuxt3を入れてもろもろ調査してみよう、ということになりました。

今回は、実際にNuxt3をプロジェクトに導入しようとして行ったことを書いていきます。

はい。あくまで導入「しようとした」だけです。

f:id:yyohno:20220329152103p:plain
https://v3.nuxtjs.org/getting-started/introduction/

Nuxt3自体もまだ本番環境対応はしてませんので、ご注意ください。

Nuxt3へのアップグレードを検討されている方々の参考にでもなれば幸いです。

プロジェクトのフロントエンド事情

Nuxt3導入予定プロジェクトのバージョン周りは以下のようになっています。

node -v
v14.15.4

npm --version
6.14.10

yarn --version
1.22.10

package.jsonのフロントエンド周り

"vue": "^2.6.11",
"@vue/composition-api": "^0.6.6",
"nuxt": "^2.13.0",
"@nuxt/typescript-build": "^1.0.3",
"@nuxt/typescript-runtime": "^0.4.10",

@vue/composition-api がいまだ0.x.x 版なあたりにミーハー感が漂ってますね。

ちなみに、@nuxtjs/composition-api という便利なnuxt用モジュールがあることも後から知ったのですが、 リファクタリング作業が面倒だったので現状未対応でした。

当初の導入想定フロー

スプリント(1スプリント = 2week)開始前に、以下のようにざっくりと計画を立てました。

  1. Nuxt2からの移行用ツールとして公式に用意されている Nuxt Bridge を導入
  2. 開発環境にて動作確認(yarn dev)
  3. ステージング環境にて動作確認(yarn build、CIによるデプロイ)
  4. 既存コードをリファクタし、Nuxt3 に対応させる

結論から言ってしまうと、1スプリントで4の作業まで到達することは出来ませんでした。

本記事にもリファクタ内容などは特に記載していませんので、ご承知おきください。

以下、思い出すだけでも頭が痛くなってくるのですが、実際に導入作業を行った際にハマったポイントなどについて記載していきます。

開発環境が動くまで

影響範囲の大きかったエラーと対処法について、3点ほど書かせていただきます。

500エラーがお出迎え

まずはnodeのバージョンを上げておかないと、yarn install 時にエラーが出て Nuxt Bridge がインストールできません。 そのため、v14.15.4 から、本ブログ執筆時点のLTE版である v16.14.0 にアップグレードしています。

この時点で少々嫌な予感はしていたのですが、その後は公式手順通りに package.jsonnuxt.confit.tstsconfig.json などを書き換えていきました。

前述の通り @nuxtjs/composition-api は使用していなかったので、既存コードの書き換えはこの時点では特に発生せず、良かったな、程度に思っていました。

が、書き換えを終えた時点で yarn dev (nuxi dev)を実行してみると…ローカルサーバーは起動こそするけれどもページ自体は500エラーで開かず、という状態。 この後しばらく新しくなったNuxtのエラーページと戦うことになります。

とりあえずは500エラーのログに node-sass の記載があったため、node-sass もモジュールアップデートする必要があるのかな?と思い、npmを見に行ったところ

f:id:yyohno:20220329152347p:plain
https://www.npmjs.com/package/node-sass

非推奨モジュールになっていました…

仕方ないのでページに記載されている通り、Dart Sass への移行を合わせて行うことにします。

が、置き換えた後に再度 yarn dev を実行してみるも500エラーは解消されず。 詳細を調べてみると、今度はDart Sassのコンパイル時に使用している fiber というモジュールが node v16 に対応していませんでした。

この時点で、nodeのバージョンを下げるか、コンパイル速度の低下を承知でfiberを切るかの2択を迫られたのですが、コンパイル速度の低下がまだ目に見えるほどではなかったため、後者を採用してnuxt.config.ts 内の設定値を書き換えました。

f:id:yyohno:20220329153446p:plain
fiberは使いません

その後ようやくビルドが通りこそすれど、 Dart Sass に準拠していない記法が警告を大量に出してきたため、個別に未対応モジュールのバージョンUPを行ったり、既存コードのディープセレクタ周りの記法を書き換えたりする作業が発生しました。

configで別ポートを指定しているにも関わらず、開発サーバーが3000番で起動する

nuxt.config.ts の設定で portを指定して localhostを動作させていたのですが、なぜか yarn dev を叩いても docker-compose up を叩いても localhost:3000 しか開けない、という状態でした。

NuxtConfigの型定義を見に行くと

f:id:yyohno:20220329153307p:plain
あら?

ignoreされるようになったみたいですね。

docker-compose.yml 内で指定するか package.json のコマンドに環境変数として付与するかはちょっと迷ったのですが、最終的には起動コマンドと一緒に渡すように修正しました。

package.json

"dev": "HOST=\"X.X.X.X\" PORT=XXXX nuxi dev",

ホストやポートを指定するため、docker-compose.yml 周りもちょっと確認する必要が出てきたりで、こんなはずではなかった感がほのかに漂ってきています。

src/pages/ に配置したvueファイルを読んでくれない

ルートに置いた app.vue は読み込んでくれるのに src/pages/xxx.vue は読み込んでくれない…という現象でした。

あまり深く追っていないのですが、Nuxt3 では pages ディレクトリがオプション構成に変更されているなどの変更があったので、このあたりのディレクトリ解釈ロジックにも変更が入ったのでしょうか。

f:id:yyohno:20220329153500p:plain
特に注記はなさそう

以前は srcDir の設定値に src/ を指定していたのですが、 ディレクトリ名がまずいのかな? ということで、上記デフォルト値通りに client/ を指定できるようにディレクトリ構成を変更したところ、想定通りの読み込みをしてくれました。

が、こちらは当初から想定外の大きな変更であり、最終的にはdocker周りのbuildspecやMakefile まで修正しにいく羽目になりました。

ステージング環境が動くまで

上記の様々なエラーを乗り越え、ようやく開発環境が今までと近い形で動くようになったので チームメンバーに動作確認を依頼するため、ステージングデプロイを行ったのです、が。 ここからも割と長めの対応作業に追われることになってしまったので、こちらも3点ほど挙げさせていただきます。

@nuxtjs/fontawesome が使えない

fontawesome-svg-core の export がES6形式に未対応のようでエラーを吐いてました。nuxt.config.ts の transpile にfontawesome関連のモジュールを全て放り込んでも解決せず。

@nuxtjs/fontawesome のモジュール更新頻度が低そうなこともあったため、 fontawesome 使用箇所を全てSVGファイルを読み込む形式に修正して対応しました。モジュール側で何かしらの対応がされると良いのですが…

ちなみにこのエラーが開発環境構築時に出てこなかった理由は、単にfontawesomeの設定をコメントアウトにして開発環境を動かしたからです。 問題の後回しは良くないですね。

__dirnameが使えなくなるので書き換えたimport.meta.url も使えない

上記使用したファイル読み込み処理があったため、import.meta.url から情報取得してファイル読み込むように書き換えたのですが、import.metaconst import_meta のようにコンパイルされてしまう始末…

以下の設定を nuxt.config.ts に追記し、nitroのビルド設定を変えることで解決しました。

f:id:yyohno:20220329153832p:plain
nitroではesbuild使ってるんですね

server配下に静的ファイルを配置しても.output(dist)に持っていってくれない

ステージング環境のe2eジョブがこけていたので原因調査したところ、表題にぶち当たりました。

以前は server/api に置いた静的ファイル(JSON)を dist に配置してくれたのですが、NuxtBridgeではimport形式で書かれていない静的ファイルをbuild対象外と判断するようになったみたいです。

jsonファイルを読み込んでいる処理をまるごとimportに置き換えることで解決はしたのですが、既存のコードを追って改修する必要が出たため、こちらも地味に面倒な作業でした。

あとがき

最終的に調査結果としてチームメンバー宛に出したPull Requestは

f:id:yyohno:20220329154051p:plain
152!?

だいぶ気まずい仕上がりとなっていました。

この後にも script setup 記法への変更や inject -> useStateの 置き換え、definePropsの使用などなど…のリファクタリング作業が山程控えており、

まだまだ道のりは長そうなのですが、いつかNuxt3最高、と言える日が来ることを願っております。