SMARTCAMP Engineer Blog スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。 2024-03-26T13:00:00+09:00 smartcamp Hatena::Blog hatenablog://blog/10257846132690085746 マネーフォワードからこんにちは!ジョインして感じたスマートキャンプの魅力と、入社直後の取り組み7個 hatenablog://entry/6801883189093459744 2024-03-26T13:00:00+09:00 2024-03-27T13:57:00+09:00 ご挨拶 これまでの経歴と出向に至るまで 新卒でSIerへ マネーフォワードへ転職 そして、スマートキャンプへ出向! スマートキャンプにジョインしてみて ユーモア溢れる開発チーム オンボーディングでの有り難いサポート 2ヶ月半でYATTEKITAこと チームジョイン後に意識して取り組んだ7つのこと 1. レスポンスのスピード感 2. Slackでのリアクション・スタンプ、ピアボーナスの活用 3. チームメンバーとの1on1 4. ドキュメントづくり 5. 気づきの丁寧な共有と、仕組み化の提案・実行 6. マネーフォワードとの積極的な連携、グループ知見の有効活用 7. なんでもござれの姿勢 チーム… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240325/20240325121815.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#ご挨拶">ご挨拶</a></li> <li><a href="#これまでの経歴と出向に至るまで">これまでの経歴と出向に至るまで</a><ul> <li><a href="#新卒でSIerへ">新卒でSIerへ</a></li> <li><a href="#マネーフォワードへ転職">マネーフォワードへ転職</a></li> <li><a href="#そしてスマートキャンプへ出向">そして、スマートキャンプへ出向!</a></li> </ul> </li> <li><a href="#スマートキャンプにジョインしてみて">スマートキャンプにジョインしてみて</a><ul> <li><a href="#ユーモア溢れる開発チーム">ユーモア溢れる開発チーム</a></li> <li><a href="#オンボーディングでの有り難いサポート">オンボーディングでの有り難いサポート</a></li> </ul> </li> <li><a href="#2ヶ月半でYATTEKITAこと">2ヶ月半でYATTEKITAこと</a><ul> <li><a href="#チームジョイン後に意識して取り組んだ7つのこと">チームジョイン後に意識して取り組んだ7つのこと</a><ul> <li><a href="#1-レスポンスのスピード感">1. レスポンスのスピード感</a></li> <li><a href="#2-Slackでのリアクションスタンプピアボーナスの活用">2. Slackでのリアクション・スタンプ、ピアボーナスの活用</a></li> <li><a href="#3-チームメンバーとの1on1">3. チームメンバーとの1on1</a></li> <li><a href="#4-ドキュメントづくり">4. ドキュメントづくり</a></li> <li><a href="#5-気づきの丁寧な共有と仕組み化の提案実行">5. 気づきの丁寧な共有と、仕組み化の提案・実行</a></li> <li><a href="#6-マネーフォワードとの積極的な連携グループ知見の有効活用">6. マネーフォワードとの積極的な連携、グループ知見の有効活用</a></li> <li><a href="#7-なんでもござれの姿勢">7. なんでもござれの姿勢</a></li> </ul> </li> <li><a href="#チームの皆さんからの反応">チームの皆さんからの反応</a></li> </ul> </li> <li><a href="#これからYATTEIKUこと">これからYATTEIKUこと</a><ul> <li><a href="#とある新規PJの成功と組織づくり">とある新規PJの成功と組織づくり</a></li> <li><a href="#IPOに向けて">IPOに向けて</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="ご挨拶">ご挨拶</h2> <p>はじめまして!2024年1月にジョインしました<a href="https://twitter.com/7coAim">黒沢</a>です! スマートキャンプでは、<a href="https://boxil.jp/">BOXIL SaaS</a>の開発に携わっております。</p> <p>今回は入社(出向!?)エントリということで、 簡単な経歴や、スマートキャンプにジョインしてみてどうだったか、 入社して2ヶ月半でやってみたこと・これからのことなどをご紹介できればと思います。 ジョインしてすぐに取り組んだことは、この春に異動や転職をする方にも参考になれば幸いです。</p> <h2 id="これまでの経歴と出向に至るまで">これまでの経歴と出向に至るまで</h2> <p>まずは、簡単に現在までの経歴を紹介したいと思います。</p> <h3 id="新卒でSIerへ">新卒でSIerへ</h3> <p>新卒では、大手SIerに入社しまして、システムエンジニア(SE)として政府系金融機関や中央省庁向けの実証実験やシステム開発に携わりました。 小さな技術検証案件から重厚長大な年単位のプロジェクトまで、要件定義・設計・実装・試験・保守運用を幅広く経験しました。</p> <p>そんな中、社外の勉強会に参加したり、ベンチャー企業で楽しく働く友人の声を聞いたりする中で、徐々に外の世界にも目を向けるようになり、勇気を持って新しい環境へ飛び出す決意をし、転職活動を始めました。</p> <h3 id="マネーフォワードへ転職">マネーフォワードへ転職</h3> <p>事業会社でエンジニアをやってみたいという思いや、家計簿アプリ「<a href="https://moneyforward.com/me">マネーフォワード ME</a>」のリリース当初からのユーザーであり、そのミッションやビジョンへの共感から、マネーフォワードにジョインしました。</p> <p>入社当初は、バックオフィス向けSaaS領域において、「<a href="https://biz.moneyforward.com/attendance/">マネーフォワード クラウド勤怠</a>」のサービス立ち上げに、エンジニアとして携わりました。</p> <p>その後は、「<a href="https://biz.moneyforward.com/payroll/">マネーフォワード クラウド給与</a>」の機能開発や、「<a href="https://biz.moneyforward.com/employee/">マネーフォワード クラウド人事管理</a>」のリリースにも関わりつつ、QA組織作りやSREの導入を進めながら、エンジニア組織の急拡大を主導し支えるべく、プロダクト開発チームのリーダーや、部長としてエンジニアリングマネージャーの役割に邁進しました。</p> <p>このあたりの詳しい内容は、マネーフォワードのエンジニアブログやnoteに投稿しておりますので、是非ご覧ください。</p> <ul> <li><a href="https://moneyforward-dev.jp/entry/2021/12/13/cloud-hr-qa-sre/">HR領域急拡大で爆誕!立ち上がったQA・SRE組織や新サービスをふりかえる。Money Forward Developers Blog</a></li> <li><a href="https://note.com/kurosawat/n/n418cdaec5cb7">なぜ いまHR領域でQA組織を立ち上げるのか?立ち上げに込めた想い - note</a></li> </ul> <h3 id="そしてスマートキャンプへ出向">そして、スマートキャンプへ出向!</h3> <p>マネーフォワードでの経験は、そのフェーズでしか体験し得ない貴重なもので、どれも素晴らしい財産になりました!そして、もはや戦友とも言える皆さんと出会えたことに何よりも感謝しています。<br/> しかしながら、育休取得を経て、マネジメント層の採用も進み、一定の目処がついたこのタイミングで、次の方々へバトンを渡し、新たな機会をいただくことにしました。</p> <p>スマートキャンプを含むマネーフォワードグループは、全体で2100名を超える組織規模であり、グループ内でのチャレンジ機会も多数あります。今回は、私も社内での異動だけでなく、グループ会社への参画という選択肢も含めて検討し、2024年1月にスマートキャンプへ出向という形でジョインするに至りました。</p> <p>組織の規模や文化、技術スタック、事業領域など、マネーフォワードでの経験とはまた異なる挑戦ができると感じており、新たなステージにワクワクしています。</p> <h2 id="スマートキャンプにジョインしてみて">スマートキャンプにジョインしてみて</h2> <p>現在は、<a href="https://boxil.jp/">BOXIL SaaS</a>の開発チームにジョインして、半分プレイヤーに戻ったような状況です。まずは、そこで感じたこと・有り難いと思ったことを簡単に共有したいと思います。</p> <p><figure class="figure-image figure-image-fotolife" title="入社のお祝いケーキをボスが用意してくださったものの、家族全員で体調を崩してしまい参加できなかった懇親会の一幕"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240325/20240325121436.png" width="796" height="592" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>入社のお祝いケーキをボスが用意してくださったものの<br />家族全員で体調を崩してしまい参加できなかった懇親会の一幕</figcaption></figure></p> <h3 id="ユーモア溢れる開発チーム">ユーモア溢れる開発チーム</h3> <p>ジョインしてみて、まず感じたのは、ウェットな組織雰囲気の中で、非常にユーモアがあふれるチームだということです。 20代〜30代前半を中心とした若いメンバーで構成されていますが、リモートワークでもオフィスでも、いつも冗談が飛び交い、笑いの絶えないチームで、とても楽しく思います。これは、チームメンバーの個々の人柄や、チームの文化によるものだと思いますが、それが相談しやすい雰囲気や、スムーズなコミュニケーションにつながっており、少なからずエンジニアリングにも良い影響を与えている部分もあると感じています。</p> <p>加えて、オフィスに出社する機会をより有意義に活用しようと、出社日には技術選定の議論やワークショップの会などをメンバー同士で企画して取り組んでいます。これは、仕事を楽しむ姿勢という点でも刺激になっています。</p> <h3 id="オンボーディングでの有り難いサポート">オンボーディングでの有り難いサポート</h3> <p>私自身、久しぶり(約3年ぶり!)にプレイヤーの役割にも入るということで、感覚を思い出すことに苦労しました。 そんな中、<a href="https://tech.smartcamp.co.jp/entry/2022-new-grads-01">braavaさん</a>をはじめとするチームメンバーの皆さんが、丁寧にサポートしてくれたことが、非常に有り難かったです。</p> <p>また、チームには、単なるドキュメントだけなくFigma上に図を書いて互いに共有する文化(些細な課題でも図示していく姿勢でいくから、認識ズレを起こしにくい!)や、気軽にペアプロをしていく文化もあり、キャッチアップに非常に役立ちました。<br/> 最初の頃のペアプロでは、まるでリハビリをしているような状態で申し訳なかったですが、話しながら概要をおさえ、実際のコードを追って理解を確かめながら、加速度的にキャッチアップが進みましたし、皆さんのサポートのおかげで、少しずつですがプレイヤーとしての感覚を取り戻していると思います。</p> <p>今後は、SaaS Marketing領域のドメイン知識や、新しい技術スタック(特にフロントエンド)についても、議論に追いつけるよう精進して参りたいと思います!</p> <h2 id="2ヶ月半でYATTEKITAこと">2ヶ月半でYATTEKITAこと</h2> <h3 id="チームジョイン後に意識して取り組んだ7つのこと">チームジョイン後に意識して取り組んだ7つのこと</h3> <p>新しい組織やチームに移ることは久しぶりだったため、自分の中で意識して取り組んでいたことがあります。 これらは、自分自身のキャッチアップや初期の信頼獲得としても、大事なことのひとつかと思いますので、 異動や転職をする方にとっても、参考になればと思い、ここでご紹介します!</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240325/20240325121751.png" width="1189" height="729" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="1-レスポンスのスピード感">1. レスポンスのスピード感</h4> <p>Slackでメンションをもらったもの・もらってないけど自分に関係ありそうに感じたものは、できるだけ早く返すようにしていました。<br> チームにジョインすると、アカウント発行後のログイン確認や、資料の確認などオンボーディング上の細かな対応があると思いますが、それらに対してもできるだけタイムリーに返すようにしていました。</p> <p>また、「ざっと確認して内容について不明点があるときには、返答の中で質問する(自分がボールを持たない)」「すぐ確認できないときは、確認できる時間の目処を伝える」など、社会人1年目に戻ったつもりで意識しました。</p> <h4 id="2-Slackでのリアクションスタンプピアボーナスの活用">2. Slackでのリアクション・スタンプ、ピアボーナスの活用</h4> <p>レスポンスの早さと近い部分ですが、チームメンバーの投稿に対して、いちはやくスタンプを押すことで、盛り上げ役となり、自分の存在をアピールするようにしていました。必要に応じて、Slackにスタンプも追加してみました。<br> これは、リモートワークが主体となる働き方であるチームにおいては、新参者の自分が相手に対して気持ちや好意(一緒に頑張っていきたい気持ち・貢献欲)を示す機会のひとつとして有効だと思っています。そして、ジョインしたタイミングに限らずですが、直接相対していないリモートワークでは、返信などのリアクションをするときは、3割増くらいで気持ちを込めるのがちょうど良いと考えています。</p> <p>また、ピアボーナスと呼ばれる「従業員同士がお互いに仕事の成果や貢献に対して賞賛したり認めたりするだけでなく、それとともに少額の報酬を送り合う仕組み」が導入されているため、オンボーディングなどのサポートをいただいたときは、隙かさず感謝を伝えるようにしていました。こうしたツールも組み合わせて活用することで気持ちを伝えるハードルも下がる気がしています。</p> <h4 id="3-チームメンバーとの1on1">3. チームメンバーとの1on1</h4> <p>メンバーの皆さんとの相互理解を深めるため・プロジェクトや組織の状況をより正確に把握するために、1on1をお願いしました。<br/> 1on1では単純な自己紹介だけでなく、<a href="https://note.com/kagewasabi/n/n67d9a63e0709">スキキライマップ</a>といったコンテンツを用意して、より有意義に深く理解していくための流れを作りました。</p> <p>また、1on1を自分だけの場にするのではなく、相手からの相談や質問がないかを必ず毎度確認しました。些細なことでも良いので、一緒に何かを考えていくキッカケにしたいと思っていました。</p> <h4 id="4-ドキュメントづくり">4. ドキュメントづくり</h4> <p>オンボーディングの流れや、取り組んでみたタスクの中で不足していたドキュメントや、整備されていないフォーマットなどがあれば、たたき台を積極的に例示して共有しました。<br/> 意外とメンバーの皆さんには新しい視点だったり、資料化を忘れている内容だったりすることがあります。</p> <h4 id="5-気づきの丁寧な共有と仕組み化の提案実行">5. 気づきの丁寧な共有と、仕組み化の提案・実行</h4> <p>ジョインしたチームでは、毎週振り返りの時間があり、KPTを使った手法で実施していました。<br> これまでの経緯や背景知識が少ない状態なので、フラットな目線でチームに新たな気づきを与えやすいと思いますので、素直に提案していきました。</p> <p>一方で、既存のメンバーから見れば、いまの仕組みやルールを批判されていると感じやすい場合もあるので、内容によっては丁寧に経緯を確認・質問するような投げかけをする方が良いこともあります。せっかく築いている小さな信頼関係をより大きく育てるためにも大切な視点だと思います。</p> <p>また、気づきに対する具体的な解決策(仕組み化など)の提案も、これまでの経験を元に行いました。さらに、解決策としてのTRYをチームとして合意したら、アクション実行のボールも自分が持つようにして、口だけで終わってしまわないようしました。</p> <h4 id="6-マネーフォワードとの積極的な連携グループ知見の有効活用">6. マネーフォワードとの積極的な連携、グループ知見の有効活用</h4> <p>せっかくグループ会社間で異動しているため、それを活かすためにマネーフォワードでの経験や事例を、タイミングを見て共有するようにしました。<br/> 加えて、新規PJが始まるタイミングでもあったため技術選定の場面などにおいては、マネーフォワードとのSlack共有チャンネル(技術領域ごとに相談や知見共有をする場)で私から質問を投げかけてみたり、特定の技術スタックに詳しいメンバーに繋いだり、といった「橋渡し役」を意識しました。</p> <p>グループ会社と言えど、組織が異なると最初の相談や質問のハードルはやはり高く感じるものです。複数の組織に跨っている自分が、その最初のステップを軽くするような行動をとることで組織間の交流も進むものだと思います。</p> <h4 id="7-なんでもござれの姿勢">7. なんでもござれの姿勢</h4> <p>最後は、取り組みではなく姿勢のお話ですが、「これをやりたい」「これはやりたくない」という選り好みせずに、手を挙げる・首を突っ込んでみる・依頼を受けたら断らない(無理のない範囲でw)という姿勢で業務に入りました。 自分自身がもともと責任領域や仕事内容に対して、強いこだわりを持つ方ではないところもあり、プレイヤーとしての機能開発だけなく、社内の問い合わせ対応や、採用活動、その他組織運営に関わることなど、比較的幅広く取り組めました。</p> <p>そうすることで、技術的な理解やドメイン知識が深まるだけでなく、この事業に関する業務の全体フローはどうなっているのか、いつも他部署とどのように連携しているのか、といったことを(点と点が線になり、面になり)より早くキャッチアップできたと思いますし、課題発見・改善提案もしやすくなると感じています。</p> <h3 id="チームの皆さんからの反応">チームの皆さんからの反応</h3> <p>メンバーの皆さんとの1on1などでいただいた反応やフィードバックとしては、以下のようなものがありました。</p> <ul> <li>これまで曖昧だった課題がハッキリして、仕組み化が進んだ。</li> <li>グループ間の知見共有が有り難い。繋がりができた。</li> <li>課題を提起するときの、コミュニケーションの仕方が勉強になる。</li> <li>マネジメント経験者がプレイヤーに入ったことで、中間的存在として1on1などで相談しやすい。</li> <li>いままで素通りしていたところ、手に負えないところとなっていた課題の発見・拾い上げが進んだ。</li> <li>チーム全体でSlackでのリアクションが増えた気がする。</li> </ul> <p>というようなポジティブなコメントをいただきまして、プロジェクトや組織に少しでも良い影響を与え、ちょっとずつ信頼獲得もできていそうで、良かったです。<br/> 新しい組織に入るのは緊張もしますし、プレッシャーもあったので、新参者としてはホッとひと安心です!</p> <h2 id="これからYATTEIKUこと">これからYATTEIKUこと</h2> <p>今後は、以下のことに特に注力していきたいと思います!</p> <h3 id="とある新規PJの成功と組織づくり">とある新規PJの成功と組織づくり</h3> <p>スケジュールがとてもタイトですが、ユーザーに向けて良い価値が提供できるようになり、技術的にも新しい取り組みや負債の解消にも繋がるプロジェクトです。これをチームの皆さんとともに成功させるために、あらゆる取り組みを行っていきます。</p> <p>また、直近で少しずつチームの人数を増えてきていて、これからもジョイン予定のメンバーが決まっているという状況で、組織拡大が進んでいます。そんな中でより自己組織化され、成果を出せるチームを一緒に作りたいと思っています。</p> <h3 id="IPOに向けて">IPOに向けて</h3> <p>CEOの林から発表しているとおり、<a href="https://note.com/smartcamp_tent/n/nc9ad8887a5af">スマートキャンプはIPOを目指しています。</a>こちらに向けたシステム関連の課題整理や仕組み化などに取り組んでいきます。泥臭いところを含めて整えるのは好きなので、どんどんやっていきたいと思います。</p> <p>また、IPOは目指す中では、それのみが目的ではなく、世の中への良い影響(事業成果)を加速させる手段であると認識して、全体を俯瞰して取り組むことを忘れないようにしていきたいです。サービス開発・運用上の成果だけでなく、ビジネス全体の目線をも持って、事業成果にも貢献していきたいと思っています。(そのためにも、まずはドメイン知識などのキャッチアップをがんばらねば!)</p> <h2 id="まとめ">まとめ</h2> <p>今回は、マネーフォワードから出向した黒沢が、この2ヶ月半を振り返り、スマートキャンプにジョインしてみての感想や、自分自身が入社当初に意識して行なった取り組みなどを入社エントリとしてご紹介しました。 これからも精進してまいりますので、皆さん!よろしくお願いします!!</p> smartcamp 新卒で入社したエンジニアの半年の振り返り hatenablog://entry/6801883189086728436 2024-02-28T13:00:00+09:00 2024-02-28T13:26:24+09:00 はじめに 私の仕事内容 新卒入社から半年間の振り返り キャッチアップが追いつかず、タスクが遅れる 心がけたこと 年齢差による意見の遠慮 心がけたこと ドキュメントによるコミュニケーションの難しさ 心がけたこと 成果 まとめ はじめに こんにちは!開発エンジニアの小宮です! 私は入社エントリで、述べたとおり、 23年新卒でスマートキャンプに入社し、早いもので半年が経過しました。今回は、半年間の振り返りを書く機会をいただいたので新卒ならではの 挑戦や困難などについて書いていきたいと思います。あまりテックな話は少ないかもしれませんが、 最後までお読みいただけると幸いです! 私の仕事内容 私は、入社し… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240228/20240228100605.png" width="1200" height="615" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#私の仕事内容">私の仕事内容</a></li> <li><a href="#新卒入社から半年間の振り返り">新卒入社から半年間の振り返り</a><ul> <li><a href="#キャッチアップが追いつかずタスクが遅れる">キャッチアップが追いつかず、タスクが遅れる</a><ul> <li><a href="#心がけたこと">心がけたこと</a></li> </ul> </li> <li><a href="#年齢差による意見の遠慮">年齢差による意見の遠慮</a><ul> <li><a href="#心がけたこと-1">心がけたこと</a></li> </ul> </li> <li><a href="#ドキュメントによるコミュニケーションの難しさ">ドキュメントによるコミュニケーションの難しさ</a><ul> <li><a href="#心がけたこと-2">心がけたこと</a></li> </ul> </li> </ul> </li> <li><a href="#成果">成果</a></li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>こんにちは!開発エンジニアの小宮です!</p> <p>私は<a href="https://tech.smartcamp.co.jp/entry/experience-scrum-and-training">入社エントリ</a>で、述べたとおり、 23年新卒でスマートキャンプに入社し、早いもので半年が経過しました。今回は、半年間の振り返りを書く機会をいただいたので新卒ならではの 挑戦や困難などについて書いていきたいと思います。あまりテックな話は少ないかもしれませんが、 最後までお読みいただけると幸いです!</p> <h2 id="私の仕事内容">私の仕事内容</h2> <p>私は、入社してから<a href="https://balescloud.jp/">BALES CLOUD</a>(以下「BC」)という事業部でエンジニアとして働いています。業務内容は主に、新規機能の開発、テスト、バグ調査などの保守運用開発です。<br> BCのエンジニアチームは、社員が自分を含めて4人、業務委託の方が1人で構成されています。 みなさん経験豊富な方が多く、私がみなさんに比べ一回りほど年下という、組織です。</p> <h2 id="新卒入社から半年間の振り返り">新卒入社から半年間の振り返り</h2> <p>新卒入社して半年間を振り返ると、大きな悩みなどはなかったと思いますが、新卒ならではの細かい課題にはたくさん直面しました。<br> それらの課題について振り返ってみたいと思います。</p> <p>新卒ならではの直面した課題は以下の3つです。</p> <ul> <li>キャッチアップが追いつかず、タスクが遅れる</li> <li>年齢差による意見の遠慮</li> <li>ドキュメントによるコミュニケーションの難しさ</li> </ul> <h3 id="キャッチアップが追いつかずタスクが遅れる">キャッチアップが追いつかず、タスクが遅れる</h3> <p>新卒研修が終わり、BCに配属後にはじめて立てた目標は、毎週のタスクを約70%完了させることでした。これだけ聞くと、「70%?100%が当たり前じゃないの?」と思われるかもしれませんが、<br> BCでは1スプリントを一週間で行ないその間に要件の詳細決め、開発、テスト、レビュー、リリースを行ない、 複数人が1つのタスクに携わるため、当時のチームの完了率見ても80%程度でした。<br> そのなかで70%という目標を立てたのですが、当初の私としては「どうせやるなら100%目指そう」と思っていました。<br> ただBCの技術スタックは今まで触れたことがないものも多く、キャッチアップしながらタスクを進めていくという流れでした。<br> 入社して右も左もわからない私は、まずはとにかくコミット量を増やしていこうと思いました(脳筋)その結果、配属直後はタスクの量や難易度が調整されていたので、コミット量だけで順調に進んでいました。<br> しかし、タスクの量が増えたり、難易度が上がると、完了率がだんだんと下がり、キャッチアップの時間もほとんど確保できず、タスクが遅れることが増えました。<br> その時は、自分のかけれる時間をとにかくかけてもタスクが終わらないという状況に陥り、かなり焦っていたのを覚えています。</p> <h4 id="心がけたこと">心がけたこと</h4> <p>上記の課題に対しては、抜本的な解決策はなく、地道にタスクをこなして少しずつドメイン・技術の理解を深めるしかないと思いました。ただ、そのなかでも意識的にやっていたことが3点あります。</p> <p>1点目は、最初から全部をやろうとせず、段階を踏んでからタスク量を増やそうと決めたことです。私の場合は、コードレビューをすることに配属当時に多くの時間を使ってしまっていたので、<br> 期限を決め、ある程度業務に慣れるまで、担当から外れるようにを上長に相談し、一定期間コードレビューから外れました。コードレビューを通じて、他の人のコードを見ることで、成長できるというメリットもあると思いますが、<br> 私の場合、新卒で技術もドメインも知識が乏しい場合は、ある程度タスクベースでキャッチアップすることの方が効率的だと感じました。</p> <p>2点目は、技術のキャッチアップをする際に公式ドキュメントを読むことです。<br> 私はもともと技術書などを読むことに苦手意識があり、公式ドキュメントを読むこともなんとなく避けていてネットにある記事やQiitaなどを読むことやchatGPTに聞くことが多かったです。<br> しかし、トレーナーからのアドバイスもあり少しずつ公式ドキュメントを読む癖をつけていきました。読む習慣がついてからは開発で困ったときに、自分の力で必要な情報にたどり着けるようになったと思います。<br></p> <p>3点目は、質問をする際にwhyを意識するようにしたことです。スマートキャンプでは、新卒のエンジニアにトレーナーという業務上のサポートしていただける方がついてくれます。<br> そのトレーナーに実装に関することを質問する際に、入社当初は、自分で考えてわからないことを、「どうやって実装するのか?」というHowばかりきいてました。<br> もちろんその形式の質問でも問題は解決されるので、満足感がありそれ以上調べたりせずにその場しのぎで終わることが何度もありました。その結果同じような質問を期間を開けてからまたしてしまうことがありました。<br> その経験から質問する際には、「なぜそのような実装をするのか?」どのような思考のプロセスでその実装にするかなど、答えだけではなく、答えに至るまでの過程まで聞くようにしました。<br> whyを意識して質問するようになると、自分との考えの違いや、プロセスを知ることなり今まで以上に理解が深まるようになりました。</p> <h3 id="年齢差による意見の遠慮">年齢差による意見の遠慮</h3> <p>私が所属しているBCのエンジニアチームは、冒頭でも少し述べたとおりエンジニア経験が豊富で、社会人経験も長い方が多く、私との年齢差が少しあります。<br> そのため私が配属したときは、リファイメントやプランニングなどのスクラムイベントで、自分の意見を遠慮していました。コードレビューにおいても、コメントを書くのを遠慮していてLGTMおじさんになっていました。<br> 理由としては、年齢差により過剰に技術・ドメインの知識の差があると考えていて、自分が発言してMTGを止めない方がいいのではないか?という思いや、コードレビューでベテランだからこのような実装にしているかもしれない?と考えていました。</p> <h4 id="心がけたこと-1">心がけたこと</h4> <p>こちらの課題に関しても、抜本的な解決策はないのですが、どんな切り口でも良いので自分から関わっていく意識を持つようにしました。<br> まず、MTGにおいては、質問ベースでもいいので発言すること・ファシリなどを積極的に行なうことをしてMTGに少しずつ参加していく意識を持つようにしました。<br> その際に、どんな質問や意見なども丁寧に対応していただいたチームメンバーの方々には感謝です。<br> また、コードレビューにおいても、なぜこのようなコードになったのかなどの質問書くことから始めました。<br> そこからはじめ、今ではバグなどを未然に防ぐようなレビューもできるようになったと思います。</p> <h3 id="ドキュメントによるコミュニケーションの難しさ">ドキュメントによるコミュニケーションの難しさ</h3> <p><figure class="figure-image figure-image-fotolife" title="社内で利用しているツールの分類"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240228/20240228100647.png" width="937" height="532" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>社内で利用しているツールの分類</figcaption></figure></p> <p>BCでは、連続した開発時間を確保するために一部のMTGの非同期化しています。<br> 上の画像は緊急度や発散性に応じて、コミュニケーションを取るツールを使い分ける際に参考にしている図です。<br> 図から分かるように緊急度が高くないやり取りはドキュメントベース(Notion)でのコミュニケーションが多くなります。新卒として入社した当初、この取り組みにすぐには慣れませんでした。<br> その時の私の思いとしては、「ドキュメントベースでやり取りするより、直接話した方が早く解決できるのではないか?」という疑問がありました。<br> さらに、単純にドキュメントを書く習慣がなかったため、書くことに対する苦手意識がありました。</p> <h4 id="心がけたこと-2">心がけたこと</h4> <p>まず、ドキュメントを書く習慣を少しずつ身につけていきました。ただいきなり書けと言われても、どのようにを書けばいいのかわからないので、とにかく他の人のドキュメントを真似るところから始めました。<br> BCのチームの皆さんはドキュメントを書くのが上手で、1人1人のドキュメントのスタイルが若干違うので、それを見て良いと思った箇所を真似ていくことで、自分も少しずつ書けるようになってきました。自分はテーブルなどの表を用いてドキュメントをまとめるのがかなりお気に入りですw。<br> また、自分がドキュメントを書けるようになってくるとMTGの非同期化への理解が深まってきました。開発時間の確保もそうですが、ドキュメントを書くことで自分の考えを整理してから伝えることができ、<br> 記録にも残るので後から見返すことができるというメリットなどを感じることができました。</p> <h2 id="成果">成果</h2> <p><figure class="figure-image figure-image-fotolife" title="新人賞の景品"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240228/20240228100736.png" width="1200" height="556" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>新人賞の景品</figcaption></figure></p> <p>タスクの完了率は結果として、目標の70%を超えて、111%に到達しました。さらに、FourKeysのリードタイムも86.9時間とgoogleが定める<a href="https://arc.net/l/quote/sdrgujjm">ハイパフォーマーに分類される数値</a>に到達しました。<br> その成果が認められ、年に2回開催されるスマートキャンプ全体のキックオフで新人賞をいただくことができました。中途の方々も対象になる中で賞をいただいて、自分が向き合ってきたことをしっかりと認められたことがとにかく嬉しかったです。画像は新人賞の賞品でいただいた本です。 ただ自分が選ばれると思っていなかったので、表彰の場にジャージで行なってしまったことが少し後悔ですw。</p> <h2 id="まとめ">まとめ</h2> <p>半年間での振り返りをしてみると、新卒ならではの課題にぶち当たりながらも、それに対して解決策を見つけ、少しずつ成長できたと思います。<br> 前期は自分自身のパフォーマンスを上げることに必死でしたが、今期は主語を大きくして、チームのパフォーマンスを上げるために行動していきたいと思います!<br> また、エンジニアとして技術的にインパクトがあることにも挑戦していきたいと思います!これからもよろしくお願いいたします!!</p> smartcamp バージョンアップしんどい!!って思ったから仕組み化した話 hatenablog://entry/6801883189083091759 2024-02-15T12:00:00+09:00 2024-02-15T12:00:01+09:00 どうも、職人です! バージョンアップ?なにそれおいしいの? バージョンアップの何が辛い? メインタスクとの兼ね合い そのライブラリがどこで使用されているか バージョンアップをして問題ないだろうか バージョンアップするときの面倒な作業 どこを効率化できるだろうか Dependabot Dependabotを導入した結果 あれ、このライブラリ既視感あるな... GitHub Actionsで過去のPRを漁る その結果 まとめ どうも、職人です! スマートキャンプでBOXIL SaaSのエンジニアをやってます職人こと袴田です! 最近は趣味のサウナが好きすぎて、熱波とアウフグースに目覚めました。 気が… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240214/20240214131437.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#どうも職人です">どうも、職人です!</a></li> <li><a href="#バージョンアップなにそれおいしいの">バージョンアップ?なにそれおいしいの?</a></li> <li><a href="#バージョンアップの何が辛い">バージョンアップの何が辛い?</a><ul> <li><a href="#メインタスクとの兼ね合い">メインタスクとの兼ね合い</a></li> <li><a href="#そのライブラリがどこで使用されているか">そのライブラリがどこで使用されているか</a></li> <li><a href="#バージョンアップをして問題ないだろうか">バージョンアップをして問題ないだろうか</a></li> </ul> </li> <li><a href="#バージョンアップするときの面倒な作業">バージョンアップするときの面倒な作業</a></li> <li><a href="#どこを効率化できるだろうか">どこを効率化できるだろうか</a><ul> <li><a href="#Dependabot">Dependabot</a></li> <li><a href="#Dependabotを導入した結果">Dependabotを導入した結果</a></li> <li><a href="#あれこのライブラリ既視感あるな">あれ、このライブラリ既視感あるな...</a></li> <li><a href="#GitHub-Actionsで過去のPRを漁る">GitHub Actionsで過去のPRを漁る</a></li> </ul> </li> <li><a href="#その結果">その結果</a></li> <li><a href="#まとめ">まとめ</a></li> </ul> <h1 id="どうも職人です">どうも、職人です!</h1> <p>スマートキャンプでBOXIL SaaSのエンジニアをやってます職人こと袴田です!<br/> 最近は趣味のサウナが好きすぎて、熱波とアウフグースに目覚めました。<br/> 気がついたらとある温浴施設で熱波師としてデビューしたのをお知らせいたします。</p> <p>そんなことはさておき、弊社サービスのBOXIL SaaSはRuby on Railsを使用して開発しています。<br/> サービス保守運用に欠かせない作業といえばバージョンアップですよね。<br/> 皆さん大好きバージョンアップ作業を少し効率化した話を書かせていただきます!</p> <h1 id="バージョンアップなにそれおいしいの">バージョンアップ?なにそれおいしいの?</h1> <p>「バージョンアップとは、ソフトウェアのバージョンを上げることです。」ってCopilotが教えてくれました。</p> <p>なんでバージョンアップなんかしないといけないんや!<br/> って思いますが、バージョンアップをしないとセキュリティリスクがあったり、新しい機能を使えなかったり、開発体験も悪くなる一方です。<br/> デメリットしかないですね。<br/> 大変な作業ですが、サービスを保守運用していくためには必要な作業です。</p> <h1 id="バージョンアップの何が辛い">バージョンアップの何が辛い?</h1> <p>バージョンアップは一般的に後回しにされるイメージがありますが、何がそんなに辛いのか考えてみました。</p> <h2 id="メインタスクとの兼ね合い">メインタスクとの兼ね合い</h2> <p>もちろんバージョンアップタスクのために、メインタスクをおざなりにすることはできません。 <br/> 効率化をしなければどうしてもバージョンアップタスクは後回しになってしまいがちです。</p> <h2 id="そのライブラリがどこで使用されているか">そのライブラリがどこで使用されているか</h2> <p>バージョンアップ対象のライブラリがサービスのどこで使用されているかを確認する必要があります。 使用箇所が確認できなければ、動作確認対象もわかりません。</p> <h2 id="バージョンアップをして問題ないだろうか">バージョンアップをして問題ないだろうか</h2> <p>バージョンアップをしたらok!リリースしまーす!<br/> なんてことはもちろんできません。<br/> バージョンアップをしたことによって、サービスに影響がないか、バグがないかを確認する必要があります。</p> <h1 id="バージョンアップするときの面倒な作業">バージョンアップするときの面倒な作業</h1> <p>まず何がバージョンアップのネックになっているか、面倒な作業を考えてみました。</p> <ul> <li>どのライブラリがバージョンアップできるのか調査する</li> </ul> <p>単純に調査自体が面倒です。</p> <ul> <li>使用箇所を特定する</li> </ul> <p>バージョンアップしたいライブラリの使用箇所を確認する必要があります。</p> <ul> <li>動作確認をする</li> </ul> <p>ライブラリをバージョンアップし、ライブラリ同士の依存関係の解決します。<br/> それができたらライブラリを使用している箇所の機能が正常に動作するか確認する必要があります。</p> <ul> <li>PRを作成する</li> </ul> <p>弊社はGitHubを使用しているので、バージョンアップのPRを作成する必要があります。</p> <ul> <li>レビューする</li> </ul> <p>バージョンアップのPRをレビューする(してもらう)必要があります。</p> <ul> <li>リリースする</li> </ul> <p>バージョンアップしたライブラリをリリースします。</p> <h1 id="どこを効率化できるだろうか">どこを効率化できるだろうか</h1> <p>まず</p> <ul> <li>どのライブラリがバージョンアップできるのか調査する</li> </ul> <p>作業をどうにかしたいので、GitHubが提供しているDependabotを使用してみることにしました。</p> <h2 id="Dependabot">Dependabot</h2> <p><a href="https://docs.github.com/ja/code-security/dependabot/working-with-dependabot">Dependabot</a>はライブラリをバージョンアップするPRを自動で作成してくれるbotです。</p> <p>BOXIL SaaSはRuby on Railsを使用しているため、DependabotがGemfile.lockを監視し、バージョンアップが必要なgemのPRを自動で作成してくれます。</p> <p><a href="https://docs.github.com/ja/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file">こちら</a>を参考に、<code>.github/dependabot.yml</code>を作成します。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">version</span><span class="synSpecial">:</span> <span class="synConstant">2</span> <span class="synIdentifier">updates</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">package-ecosystem</span><span class="synSpecial">:</span> <span class="synConstant">&quot;bundler&quot;</span> <span class="synIdentifier">directory</span><span class="synSpecial">:</span> <span class="synConstant">&quot;/&quot;</span> <span class="synIdentifier">open-pull-requests-limit</span><span class="synSpecial">:</span> <span class="synConstant">5</span><span class="synComment"> # 作成するPRの最大数を5に設定</span> <span class="synIdentifier">schedule</span><span class="synSpecial">:</span> <span class="synIdentifier">interval</span><span class="synSpecial">:</span> <span class="synConstant">&quot;daily&quot;</span> <span class="synIdentifier">time</span><span class="synSpecial">:</span> <span class="synConstant">&quot;10:00&quot;</span> <span class="synIdentifier">timezone</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Asia/Tokyo&quot;</span> <span class="synIdentifier">labels</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">&quot;レビュー求む&quot;</span> <span class="synIdentifier">ignore</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">dependency-name</span><span class="synSpecial">:</span> <span class="synConstant">&quot;*&quot;</span> <span class="synIdentifier">update-types</span><span class="synSpecial">:</span> <span class="synSpecial">[</span> <span class="synConstant">&quot;version-update:semver-major&quot;</span> <span class="synSpecial">]</span><span class="synComment"> # メジャーアップデートは無視する</span> </pre> <p>これで、毎日10時に5個のバージョンアップのPRが作成されるようになりました。<br/> いきなりメジャーアップデートのPRが連続して作成されると、大変なのでいったん無視するように設定しました。</p> <h2 id="Dependabotを導入した結果">Dependabotを導入した結果</h2> <ul> <li>どのライブラリがバージョンアップできるのか調査する</li> <li>PRを作成する</li> </ul> <p>という作業はDependabotに任せることができました。<br/> しかし、PRは上がってきたものの、そのPRのライブラリはどこで使用されているのかがわかりません。<br/> こればかりはどうしようもないので、重い腰を上げて片っ端から確認することにしました。<br/> 確認をしていくうえで、PRのコメントに使用箇所や動作確認に必要な情報を書き込んでいくことにしました。</p> <h2 id="あれこのライブラリ既視感あるな">あれ、このライブラリ既視感あるな...</h2> <p>同じライブラリのバージョンアップPRが何度か上がってくることがあり、その度に「あれ?これ前にもあったような?」と思いながら確認していることに気づきました。<br/> 過去に対応した内容はPRのコメントに残しているので、それを確認すればいいのですが<br/> 「これ毎回過去のPRを漁りにいくのしんどいな・・・」と思ったので、この作業自体をGitHub Actionsで自動化できないか考えてみました。</p> <h2 id="GitHub-Actionsで過去のPRを漁る">GitHub Actionsで過去のPRを漁る</h2> <p>実現したいことは</p> <ul> <li>DependabotがPRをオープンしたタイミングでGitHub Actionsを実行する</li> <li>Dependabotが作成したPRのライブラリのバージョンアップを過去に行っているかを過去のPRを漁って確認する</li> <li>過去に行っている場合は、そのPRのリンクをコメントする</li> </ul> <p>になります。</p> <p>GitHub Actionsで過去のPRを漁るためには、GitHubのAPIを使用する必要があります。<br/> <a href="https://docs.github.com/en/free-pro-team@latest/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests">こちら</a>のページに使えそうなAPIがあったので、活用してみます。</p> <p>そして作ったのがこちらのGitHub Actionsです。</p> <p>.github/workflows/fishing-for-past-upgraded-prs.yml</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Fishing for past upgraded PRs <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">pull_request</span><span class="synSpecial">:</span> <span class="synIdentifier">types</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">&quot;opened&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;reopened&quot;</span> <span class="synIdentifier">permissions</span><span class="synSpecial">:</span> <span class="synIdentifier">pull-requests</span><span class="synSpecial">:</span> write <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">fishing_for_past_upgraded_prs</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Add Links Comment <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/github-script@v6 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">script</span><span class="synSpecial">:</span> | const <span class="synSpecial">{</span> owner, repo, number <span class="synSpecial">}</span> = context.issue; const pr_title = context.payload.pull_request.title; const regex = /Bump (.*) from (.*) to (.*)/; const match = pr_title.match(regex); if (match) <span class="synSpecial">{</span> const gem_name = match<span class="synSpecial">[</span><span class="synConstant">1</span><span class="synSpecial">]</span>; let previous_prs = <span class="synSpecial">[]</span>; for (let page_number = 1; page_number &lt; 11; page_number++) <span class="synSpecial">{</span> const <span class="synSpecial">{</span> <span class="synIdentifier">data</span><span class="synSpecial">:</span> prs <span class="synSpecial">}</span> = await github.rest.pulls.list(<span class="synSpecial">{</span> owner, repo, <span class="synIdentifier">state</span><span class="synSpecial">:</span> <span class="synConstant">&quot;closed&quot;</span>, <span class="synIdentifier">sort</span><span class="synSpecial">:</span> <span class="synConstant">&quot;updated&quot;</span>, <span class="synIdentifier">direction</span><span class="synSpecial">:</span> <span class="synConstant">&quot;desc&quot;</span>, <span class="synIdentifier">per_page</span><span class="synSpecial">:</span> <span class="synConstant">100</span>, <span class="synIdentifier">page</span><span class="synSpecial">:</span> page_number <span class="synSpecial">}</span>); const target_prs = prs.filter( (pr) =&gt; pr.title.includes(`Bump $<span class="synSpecial">{</span>gem_name<span class="synSpecial">}</span>`) ) previous_prs = previous_prs.concat(target_prs) <span class="synSpecial">}</span> if (previous_prs.length &gt; 0) <span class="synSpecial">{</span> let comment = <span class="synConstant">&quot;過去の関連PR:</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>; previous_prs.forEach((pr) =&gt; <span class="synSpecial">{</span> comment += `- <span class="synSpecial">[</span>#$<span class="synSpecial">{</span>pr.number<span class="synSpecial">}]</span>($<span class="synSpecial">{</span>pr.html_url<span class="synSpecial">}</span>)\n`; <span class="synSpecial">}</span>); await github.rest.issues.createComment(<span class="synSpecial">{</span> owner, repo, <span class="synIdentifier">issue_number</span><span class="synSpecial">:</span> number, <span class="synIdentifier">body</span><span class="synSpecial">:</span> comment, <span class="synSpecial">}</span>); <span class="synSpecial">}</span> } </pre> <p>過去PRを釣り上げるということで、<code>fishing-for-past-upgraded-prs</code>という名前にしました。</p> <p>やっていることを簡単に説明すると</p> <ul> <li>Dependabotが作成したPRのタイトルからライブラリ名を取得する</li> <li>過去に作成したPRを更新日時の降順で取得する</li> <li>過去に作成したPRのタイトルにライブラリ名が含まれているものを抽出する</li> <li>抽出したPRのリンクをコメントする</li> </ul> <p>ということをしています。</p> <p>このコードのポイントは2つあります。</p> <p>1つ目はGitHub APIが1回のリクエストで100件のPRしか返さないことです。</p> <p>BOXIL SaaSを管理しているGitHubリポジトリではPRが約10000件あります。<br/> それらのPRをすべて取得してしまうと、1回のアクションで約100回リクエストを送信することになり、時間がかなりかかってしまいます。<br/> これでは現実的ではないので、リクエストの回数は10回に制限し、1000件のPRを取得するようにしています。</p> <p>1000件というのは「1000件くらい漁れば、過去のPR見つかるっしょ!」くらいの感覚です。<br/> 仮に1000件を超えたところにPRがあったとしても、PRがコメントを通じて数珠つなぎになって辿れるので、問題はなさそうです。<br/> (健全にバージョンアップ作業を続けられているのであれば、1000件より少なく、500件でも300件でも良さそうな気はしています)</p> <p>2つ目はpermissionsの設定です。</p> <pre class="code" data-lang="" data-unlink>permissions: pull-requests: write</pre> <p>この部分です。 <a href="https://docs.github.com/ja/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#changing-github_token-permissions">こちら</a>を参考にpermissionsの設定をしないと、</p> <pre class="code" data-lang="" data-unlink>RequestError [HttpError]: Resource not accessible by integration Error: Unhandled error: HttpError: Resource not accessible by integration</pre> <p>このようにGitHubのAPIを実行したときにエラーが発生してしまいます。<br/> レスポンスのHTTPステータスコードは403なので、閲覧権限がないようです。<br/> permissionsの設定をwriteとすることで、PRへの閲覧権限を付与するようにしています。 (readだとPRにコメントを書くときに、権限がないと怒られます)</p> <p>こちらを導入した結果、DependabotがPRを上げると、過去に作成したPRのリンクがコメントされるようになりました。<br/> あくまで過去の参考情報なので鵜呑みにするのはよくありませんが、</p> <ul> <li>使用箇所を特定する</li> </ul> <p>作業に関して、少し負荷の軽減ができた実感があります。</p> <h1 id="その結果">その結果</h1> <p>前期ではバージョンアップのためのノウハウを蓄積するという意味でひたすらにDependabotが上げたPRを対応していました。 その結果6ヶ月間で40PRほど対応できたのですが、今期が始まって2ヶ月ほどの間にすでに40PRほど対応できています。</p> <p><figure class="figure-image figure-image-fotolife" title="直近のバージョンアップの状況"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240214/20240214131526.png" width="1028" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>直近のバージョンアップの状況(バージョンに関わる部分は黒塗りにさせていただきました。)</figcaption></figure></p> <p>とはいえ、</p> <ul> <li>Dependabotが上げたPRのみGitHub Actionsが実行されるようにする</li> <li>ラベルでGitHub Actionsの実行対象のPRかを判別する</li> </ul> <p>などまたまだ改善の余地はありそうです。</p> <p>ちなみに</p> <ul> <li>Dependabotが上げたPRのみGitHub Actionsが実行されるようにする</li> </ul> <p>に関しては、Dependabotではない誰かがPRを上げた場合にも実行するようにあえて、DependabotによるPRの制限は行っていません。</p> <h1 id="まとめ">まとめ</h1> <p>明らかに状況が変わってきている実感があり、バージョンアップ作業の定常化に向けて大きな前進になったと思います。<br/> 引き続きバージョンアップ作業を進めて、常に最新のバージョンを維持できるようにもっと効率化していきたいと思っています。</p> smartcamp エンジニアからデータアナリストへ転職したぼくの1年半のふりかえり hatenablog://entry/6801883189075699790 2024-01-17T14:58:00+09:00 2024-01-17T15:03:44+09:00 まえがきのまえがき まえがき 入社半年編 期待と不安の滑り出し BIへの不信感を払拭 既存を大事にしすぎた問題 各部署からのお使いクエスト 転職を機に新しくはじめたこと 半年のふりかえり SMARTCAMP AWARD 入社1年編 淡々とお仕事をこなす生活 エースの喪失 1年編をふりかえる SMARTCAMP AWARD 入社1年半まで さらに淡々とお仕事をこなす生活 業務幅の広がり 入社1年半ふりかえり SMARTCAMP AWARD 最後に まえがきのまえがき 駅そばなどでよく見かけるコロッケがのった酔狂なメニュー、コロッケそば。 ジャガイモのホクホク感や肉の旨み、玉ねぎの甘み。そういった… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240117/20240117092610.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#まえがきのまえがき">まえがきのまえがき</a></li> <li><a href="#まえがき">まえがき</a></li> <li><a href="#入社半年編">入社半年編</a><ul> <li><a href="#期待と不安の滑り出し">期待と不安の滑り出し</a></li> <li><a href="#BIへの不信感を払拭">BIへの不信感を払拭</a></li> <li><a href="#既存を大事にしすぎた問題">既存を大事にしすぎた問題</a></li> <li><a href="#各部署からのお使いクエスト">各部署からのお使いクエスト</a></li> <li><a href="#転職を機に新しくはじめたこと">転職を機に新しくはじめたこと</a></li> <li><a href="#半年のふりかえり">半年のふりかえり</a></li> <li><a href="#SMARTCAMP-AWARD">SMARTCAMP AWARD</a></li> </ul> </li> <li><a href="#入社1年編">入社1年編</a><ul> <li><a href="#淡々とお仕事をこなす生活">淡々とお仕事をこなす生活</a></li> <li><a href="#エースの喪失">エースの喪失</a></li> <li><a href="#1年編をふりかえる">1年編をふりかえる</a></li> <li><a href="#SMARTCAMP-AWARD-1">SMARTCAMP AWARD</a></li> </ul> </li> <li><a href="#入社1年半まで">入社1年半まで</a><ul> <li><a href="#さらに淡々とお仕事をこなす生活">さらに淡々とお仕事をこなす生活</a></li> <li><a href="#業務幅の広がり">業務幅の広がり</a></li> <li><a href="#入社1年半ふりかえり">入社1年半ふりかえり</a></li> <li><a href="#SMARTCAMP-AWARD-2">SMARTCAMP AWARD</a></li> </ul> </li> <li><a href="#最後に">最後に</a></li> </ul> <h1 id="まえがきのまえがき">まえがきのまえがき</h1> <p>駅そばなどでよく見かけるコロッケがのった酔狂なメニュー、コロッケそば。</p> <p>ジャガイモのホクホク感や肉の旨み、玉ねぎの甘み。そういったものが一切ない、よくわからないパサパサした食感の冷凍コロッケ、自称コロッケがのったコロッケそばこそが至高の存在だと思う。</p> <p>そして正直、どう食べたらいいのかわからない食べ物である。汁が染みないようにちょっと避けて食べるか、浸して崩して食べるか、半分だけ崩したり、がっつり底において育てたり。</p> <h1 id="まえがき">まえがき</h1> <p>こんにちは、くまのみです。</p> <p>以前はマッチングアプリのエンジニアとして働いていました。</p> <p>スマートキャンプにはデータアナリストとしてジョインし1年半が経ちました。</p> <p>ひとくちにデータアナリストといっても業務内容や必要なスキルはコロッケそばの食べ方のように振れ幅があり、それぞれの会社の環境や事業フェーズに合わせて、働き方を変える必要があります。 <figure class="figure-image figure-image-fotolife" title="データアナリストに求められるスキル"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kmnmm/20240116/20240116110922.png" width="1200" height="730" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>データアナリストに求められるスキル</figcaption></figure></p> <p><a href="https://www.datascientist.or.jp/dscertification/what/">出典:データサイエンティスト協会</a></p> <p>ぼくの入社とともにデータチームも動き出し、たまたまですが社内で評価していただく機会もありました。ここまでの<strong>うまくいったこと</strong>、<strong>だめだったこと</strong>を含めてふりかえろうと思います。</p> <p>個人的には成功体験より失敗体験を聞く方が好きです。</p> <p>この記事はテックなブログではなく、完全にポエム記事になります。</p> <h1 id="入社半年編">入社半年編</h1> <h2 id="期待と不安の滑り出し">期待と不安の滑り出し</h2> <p>採用面談で出会ったエンジニアの方と、同じチームで働けることを期待して入社しました。</p> <p>しかし、入社当日にはその方は京都支社へ移動してしまっていた。</p> <p>「早々にひとりぼっちなのかな」と不安になったが、気軽でいいわぁなんて浮かれていました。</p> <p>実際にはひとりぼっちではなく、上長がいて社外のデータPMもいました。</p> <p><figure class="figure-image figure-image-fotolife" title="データチーム紹介"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kmnmm/20240116/20240116111023.png" width="1200" height="380" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>データチーム紹介</figcaption></figure></p> <p>データチームは4人で構成されており、データPMがロードマップの策定や方針を決めていました。</p> <p>その中でぼくはデータ民主化に注力していくことになりました。</p> <p>ありがたいことにデータ基盤周りはすでにエンジニアの手によってほぼ完成していたので、これを使えばサクッといけそうだと楽観視していました。しかし、実際にはそう簡単にはうまくいきません。苦戦する半年を過ごすことになります。</p> <h2 id="BIへの不信感を払拭">BIへの不信感を払拭</h2> <p>親会社でLookerを使っていることから<a href="https://cloud.google.com/looker/docs/intro?hl=ja">Looker</a>を使うことのできる環境がすでにありました。</p> <p>LookerとはDBのカラム定義、集計定義、データの関係をLookMLというもので記述できSQLの知識が浅くてもある程度のデータの可視化ができるようになるBIツールです。そのLookMLもかなり定義されていました。</p> <p>主要なKPIと呼べそうな数字を把握するためのダッシュボードもいくつかつくられており、あとは利用するだけの状態に見えました。</p> <p>Looker上でダッシュボードがいくつか作られているものの、なぜか全く利用されていませんでした。各部署の方たちはRedashを利用して数字の確認をしているようでした。</p> <p>Lookerを利用していない理由を聞くと<strong>「なんか数字がズレてるんですよね」</strong>と返ってきました。この数字の「ズレ」に苦しめられることになります。</p> <p>僕は正しい数字をちゃんと出すことができればLookerを利用してもらえるのではないかと考えていました。ズレる原因はいくつか存在し、集計軸や条件の考慮漏れ、考慮過多、集計範囲の絞り込み等ありました。</p> <p>正しい数字を出そうとすればするほど、過去に書かれたRedashのクエリ結果とは乖離が大きくなったのです。</p> <p><strong>「この数字はここの部分の考慮が漏れているので数字が合わないんです」</strong>のように伝えていたのですがデータ利用者の方には<strong>全く響きませんでした</strong>。</p> <p>「そうなんですね、わかりました」という返事をいただくものの、Lookerを一向に見ようとはしてもらえません。</p> <p>主要KPIや各種数字に関して、Redashの数字を正として扱ってきたこれまでの流れがありました。</p> <p><strong>ズレとはRedashとLookerでの数字のズレであり、正しいか正しくないかはさほど重要ではなかったのです。</strong></p> <p>ぼくはアプローチを変えることにしました。</p> <p>たとえ若干間違っていたとしてもRedashとLookerの両方で同じ数字を出せるようにしようと。</p> <p>正しい数字が出ていようと使われないダッシュボードは無用の長物だからです。</p> <ol> <li>Redashを正として数字を見ているためLookerの数字を同じ調整する</li> <li>Redashと同じ数字がLookerで確認できていることを担当者と一緒に確認する</li> <li>同じ数字にしたのち、Redashのクエリを本来あるべき条件を考慮した正しいデータに近づける</li> <li>Redashのクエリはこの点が考慮もれている等と全体に周知したうえでRedashのクエリを修正して良いかの可否を問う</li> <li>Redashの修正をしたのち、Lookerにも条件を反映していく</li> </ol> <p>この1~5を何度か繰り返しLookerでも同じ数字がでていることを意識づけ、ズレはなさそうだという認識合わせをデータ閲覧者に向けて実施しました。</p> <p>これによって抱いてしまったLooker(BI)への不信感をすこしづつ払拭していきました。</p> <h2 id="既存を大事にしすぎた問題">既存を大事にしすぎた問題</h2> <p><code>データ基盤周りはすでにエンジニアの手によってほぼ完成</code> と先に書きました。ほぼなのです。</p> <p>ちゃんと運用されれば運用フロー込みで仕上がっていくものであった広告パラメータ管理用の一部のデータが、Lookerが利用されず運用されていなかったため頓挫していたものがありました。</p> <p>これまでにほぼ作られていたものがあったので流れに乗っていこうとしましたが、うまくいきませんでした。ちゃんと整備しなおし運用フローを移譲したとしても、担当者が運用してくれるかどうかは別なのでした。</p> <p>使われないものはちゃんと捨てるという選択をするのに時間がかかりました。</p> <h2 id="各部署からのお使いクエスト">各部署からのお使いクエスト</h2> <p>今までプロダクト開発サイドに来ていたデータに関する依頼を巻き取るようになります。</p> <p>ゲームのお使いクエストのような感覚で進めていくのですが、依頼者の所属部署ごとに必要なドメイン知識が異なり、キャッチアップするだけでヒィヒィいう生活を送ります。</p> <p>各部署の方からかなり助けてもらうことも多々出てきます。依頼者の方に「私に聞かれても」というような質問を投げつけることも多々していきます。ブチギレられてもしょうが無いと思うのですが、優しい方ばかりだったので経験値をちびちび貯めていくことができました。</p> <h2 id="転職を機に新しくはじめたこと">転職を機に新しくはじめたこと</h2> <p>上長は他の部署の方も見ている関係でぼくに割ける時間は限られていると認識していました。</p> <p>コミュニケーションの場は1回15分、週2回行われる1on1がメインでした。</p> <p>ある程度ぼくが何をしているのか分からないと自由に行動させてもらえないのではと考えました。</p> <p>そのため1on1の前に現在実施中のタスク、今後やりそうなタスク、共有事項、困ったこと等を書いておき共有するようにしました。</p> <p>正直業務をサボっていても誰かにバレるわけでもないです。ただ真面目に働いていたとしても活動は見えにくいので自分の行動ログの可視化は必須でした。</p> <p>この記事の執筆時では139回の1on1のログが残っています。週に2度の能動的とも受動的ともとれるふりかえりによってDCAPサイクル(PDCAサイクルの逆順で、実行(Do)→評価(Check)→改善(Action)→計画(Plan)の4つのステップを繰り返し、業務改善や課題解決を図るフレームワーク)が割と早く回るようになりました。</p> <p>あと記憶力が良くないので、土日を挟んでしまうと先週やったことでさえほぼ覚えてなかったりします。自分がやってきたことをちゃんとふりかえるうえで大事な作業になりました。</p> <h2 id="半年のふりかえり">半年のふりかえり</h2> <p>入社早々のどこの誰かもわからない人間が今まで正しいとしていた数字に対していちゃもんをつけても「そうだよね!正しくしようね!」みたいな流れになるのは稀かなぁと思います。</p> <p><strong>結局データを扱うのは人なので人に寄り添い、人のこころを動かさないと何の意味もなさないこと気づきました。</strong></p> <p>データはただしく扱わないと!!のような情熱がちょっとだけぼくの活動の邪魔をしました。</p> <h2 id="SMARTCAMP-AWARD"><a href="https://note.com/smartcamp_tent/n/n89f0f803c4c6">SMARTCAMP AWARD</a></h2> <p>スマートキャンプでは半年間で自身がVMV(VisionMissionValue)を体現した取り組みを発表をする場があります。</p> <p>役職者を除いた全社員がそれぞれの事業部ごとにトーナメント方式で発表し、1次予選、2次予選を勝ち抜くと半期に一度の会社のキックオフで発表をしMVPを決める社内発表会です。100人くらいから1人のMVPが決まります。</p> <p>半年間のぼくの成果は、これといって目立ったものがなく1次予選で敗退でした。</p> <p>正直に言って自分の成果をドヤるというかアピールするのは得意ではありません。恥かしがり屋なので発表するのもちょっと億劫なタイプです。ひっそりとぼくの半年は幕を閉じました。</p> <h1 id="入社1年編">入社1年編</h1> <h2 id="淡々とお仕事をこなす生活">淡々とお仕事をこなす生活</h2> <p><figure class="figure-image figure-image-fotolife" title="データチーム紹介"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kmnmm/20240116/20240116111048.png" width="1200" height="506" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>データチーム紹介</figcaption></figure> 気づくとデータチームが一人減ってました。さみしい。</p> <p>今期もデータ民主化をがんばるぞ!と意気込んでいました。</p> <p>半年かけて事業のドメイン知識もちょっと溜まり、Lookerへの不信感も払拭されつつある中で問題がおこります。</p> <p>Lookerの費用が高いため、他のBIへの移行も検討しなくてはならなくなりました。</p> <p>これにより1ヶ月の間、ぼくは右往左往することになります。</p> <p>各部署にデータに関するユースケースをヒアリング実施、ドキュメント化し最終的にはLookerを使い続けて良いことになりました。</p> <p>1ヶ月ほどロスしたおかげでフラストレーションがかなり溜まりました。これが良い方向に発散され、ここからひたすらアウトプットを出し続けることになります。</p> <p>ユースケースのヒアリング時にぽろっと出てきた課題の解決や、各部署へヒアリングしたことで距離感がすこし近づき新たにデータの依頼をいただくことも増えていきます。</p> <p>施策の事前調査、効果検証、データ抽出、業務効率化に寄与していきました。</p> <p>それと並行してデータPMの方で推進していた「データを用いて事業への直接的な利益向上実績を創出」するための施策の実施しました。CVRの改善だったのですが良い結果が生まれました。</p> <h2 id="エースの喪失">エースの<strong>喪失</strong></h2> <p><code>データ基盤周り</code>を率先して整備してくださっていた通称エースさんが退職されました。</p> <p>ちょっと困ったなぁというときに助けてもらえていたのですが「だって他に頼りがいねぇ」状態になります。</p> <p>もっと迷惑かけるくらい頼っておけばよかったような気もしてます。</p> <h2 id="1年編をふりかえる">1年編をふりかえる</h2> <p>キャッチアップする力が非常に高い人が周りにチラホラいるのですが、ぼくはそういうタイプではないです。あとからふりかえると入社半年のさえない期間も非常に大事でした。</p> <p>ちびちび消化していったお使いクエストでも経験値が少しづつ溜まり、レベルがいくつか上がったと思います。自分の知らないデータを布教し民主化していくことは難しいでレベル上げは必須でした。複数部署からの依頼をこなしていった結果、ある程度データに関する理解が進んでいきました。</p> <p>もしかしたら会社が想定していた速度でのデータ活用はできなかったかもしれません。</p> <p>コツコツと積み重ねていくことしかぼくにはできませんでした。</p> <p>スマートキャンプ内にデータアナリストの枠はぼくひとりです。成果がちゃんとだせていなければ追加で人が増えることもないでしょうし、データアナリストの枠自体も不要だと思われても仕方がありません。</p> <p><strong>データPMの方がデータ民主化やデータを用いた施策等で、進むべき道を示して走り方を教えてくれるような環境がスマートキャンプにはありました。この点が非常にありがたいと感じました。</strong></p> <h2 id="SMARTCAMP-AWARD-1">SMARTCAMP AWARD</h2> <p>SMARTCAMP AWARDの1ヶ月ほど前にプロダクト開発の部長?と1on1することがありました。</p> <p>その時にエンジニアの方には他部署向けに成果をちゃんとアピールするためにも発表を頑張って欲しいみたいな話を聞いたのがきっかけで、発表するための資料をちゃんと書こうと思うようになります。</p> <p><figure class="figure-image figure-image-fotolife" title="データチーム紹介"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kmnmm/20240116/20240116111048.png" width="1200" height="506" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>データチーム紹介</figcaption></figure></p> <p>データチームで実施したことを社内に展開しようと思うと、上長は役員でPMは業務委託なこともあり、ぼくが成果を発表しなくては成果が埋もれてしまう状態でした。</p> <p><aside> 💡 役員を除いた全社員がそれぞれの事業部ごとにトーナメント方式で発表を行なう</p> <p></aside></p> <p>社内でのデータチームの認知度の向上をし、データ民主化をより推進していくために自分が変わらないといけないときがきたんだなと実感しました。</p> <p><figure class="figure-image figure-image-fotolife" title="トロフィー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kmnmm/20240116/20240116111149.png" width="1200" height="627" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>トロフィー</figcaption></figure></p> <p>結果としては普段そこまで目立たない人間が一生懸命話したところが功を奏したのか・・・。</p> <ul> <li><strong>半期MVP</strong> 🏆</li> <li><strong>社員賞</strong></li> <li><strong>SCBB賞(VisionのSmall Company, Big Business)</strong></li> </ul> <p>という3つの賞をいただくことができました。</p> <p><aside> 💡 プレゼンも非常に秀逸でしたが、チャットのコメントを見ていたとおり、テック、データといえばくまのみさんという他者評価が高くて素晴らしいなと感じていました。ぜひ他カンパニー向けにアウトプットいただけたら嬉しいです!</p> <p></aside></p> <p><aside> 💡 データはこれからのスマートキャンプが仕組みで勝っていくための鍵だと思っており、半期でしっかり整備し、かつ売上インパクトまで出していただきありがとうございました!これからもデータドリブンの会社づくりをよろしくお願いします!</p> <p></aside></p> <p><aside> 💡 プレゼンテーションの面白さだけではなく、淡々と語っている内容の中にビジョン・ミッションの体現があり素敵でした!</p> <p>データ絡みで困ることしかない毎日だと思っているので、いずれコラボレーションする機会が来るのを楽しみにしております!!</p> <p></aside></p> <p>のようなコメントもいただけてとても嬉しかったです。</p> <p>さらに嬉しかったことは<strong>データチームの告知</strong>を全社的にでき、来期以降のデータ民主化活動を円滑に行なうための地盤の強化ができたことでした。</p> <h1 id="入社1年半まで">入社1年半まで</h1> <h2 id="さらに淡々とお仕事をこなす生活">さらに淡々とお仕事をこなす生活</h2> <p>前期のAWARDで全社的に活動を告知したことで、今まで依頼が来なかった人たちからも依頼が来るようになりました。これは、データ民主化の最終形である社員自らがデータを取得できるような体制を整えるためには、まずはデータが欲しい、データを使いたいと思ってもらうことが大事なため、大きな一歩となりました。</p> <p>これまでの1年間に及んだお使いクエストによるレベル上げと、前期の活動結果が評価されたことで、円滑な業務処理ができるようになりました。持ち前のうっかりさで、定期的にうっかりしたり、早とちりしたり、誤字脱字はしょっちゅうあるものの非常に円滑でした。</p> <p>より多くの人にデータを目にする機会を増やし、ほんの少しでもデータを元にプロダクトを考えてもらえれば、組織全体のデータリテラシーが少しずつ向上する良いサイクルが生み出せるのではないかと考えています。</p> <p>鉄は熱いうちに打てに近い考えで、データが欲しいと感じた方に向けて、可能な限り早くデータを手にしてもらうように活動してきました。時間が少しあいてしまうと見ている方向が変わったり、関心が薄れたりしてしまうからです。</p> <p>また、能動的に他部署の方に1on1を実施しながら困りごとがないかヒアリングを進め、データ活用を推進してきました。</p> <p>半年間の活動を振り返ると、目立った失敗も目立った成功もなく、ちゃんとやれているのかの実感が湧かないまま過ぎていきました。</p> <h2 id="業務幅の広がり">業務幅の広がり</h2> <p>施策する際にデータを元に意思決定する習慣ができている状態を定着させていこうと考えました。</p> <p>仮説検証用のデータ抽出はどのくらいきているのか、どのくらいで対応完了しているかなど、ぼくの手元にくるタスクを分類化し来期以降どういうふうに進めていくかを考えるための土壌整備も進めました。</p> <p>施策後の効果検証を円滑に行なうためのトラッキングログの設計等も対応していき、業務の範囲というか組織貢献の範囲が広がっていった気がします。</p> <h2 id="入社1年半ふりかえり">入社1年半ふりかえり</h2> <p>困っていると言われて可視化してみたところ、使われなかったり、作った後に音沙汰がなかったりすることもありました。しかし、意外にも他で活用できたりと無駄になることがありませんでした。</p> <p>正直この半年間は山あり谷ありといったことはなく、平坦な道をまっすぐ走るような、ひたすらまっすぐ走っていた感覚でした。</p> <p>目標設定でしっかりとゴールが決まっていたことも大きかったと思います。</p> <h2 id="SMARTCAMP-AWARD-2">SMARTCAMP AWARD</h2> <p><figure class="figure-image figure-image-fotolife" title="アートボード"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kmnmm/20240116/20240116111245.jpg" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>アートボード</figcaption></figure> なんやかんやあったようななかったような。</p> <p>運が良かったので半期MVPをいただくができました(2度目)。</p> <p>MVPを取るとアートボードを親会社のマネーフォワード総会の時にいただけるみたいでした。</p> <ul> <li><strong>半期MVP</strong> 🏆</li> </ul> <p><aside> 💡 くまのみさんは、スピード感を持ってアウトプットされているだけでなく、哲学を持ってプロジェクトを進められている点で、大変刺激を受けましたし、素晴らしいと感じました。素敵なプレゼンテーションをありがとうございました。</p> <p></aside></p> <p><aside> 💡 昨年以降Collaborationのレベルが上がっており、それを定量アウトプットに繋げるOwnershipとSpeedが頭抜けていると感じました。さらに、それがメンバーとの信頼関係・感謝につながっていることを特に高く評価させて頂きました!</p> <p></aside></p> <p><aside> 💡 くまのみさん日々のアウトプットの量・質そしてスピード感がBOXILのビジネス成長につながっています!全メンバーのリテラシーを高め、データドリブンな意思決定が促進されていけるよう引き続き頑張っていきましょう!</p> <p></aside></p> <p>MVP受賞のコメントをもらえたことも嬉しかったのですが、一番嬉しかったのはMVP受賞のちょっと後に起きた出来事でした。</p> <p>1次予選の発表をした際に、別のプロダクトの新人エンジニアの方が発表されました。その方の発表がとにかく素晴らしかったので、今回のMVPはその方だと思い込んでいました。</p> <p>いい発表だったなぁとぼくのモチベーションも上がり、日報で「来期ももっと頑張ろうと思った」みたいなことを書きました。</p> <p>打ち上げの際に、その新人エンジニアの方から「日報で褒めてもらったことがとても嬉しかったです」と言われました。その言葉を聞いたとき、<strong>どこの誰かもわからない人間</strong>からちゃんと脱却できた気がして、とにかく嬉しかったです。</p> <h1 id="最後に">最後に</h1> <p>エンジニアからデータアナリストに転職して1年半が経ちました。悔いも後悔もありません。 正直、パッとしないエンジニアだったので、今の方がのびのびと働けています。</p> <p>データアナリストとしての仕事は、データの収集・分析・可視化・活用の4つに大きく分類されます。ぼくは分析よりもデータの可視化と活用に重きを置いて活動してきました。</p> <p>ただあらためて考えると、ぼくがこうして活動できたのは<strong>各部署各メンバーの人たちが助けてくださったこと</strong>が一番大きくありがたかったです。かけだしデータアナリストがちゃんと活動できるよう成長するまで面倒みてくれることは稀有だからです。</p> <p>これからはプロダクトに関わるすべてのメンバーのデータリテラシーを向上、データドリブンな意思決定を促進させ、プロダクトの成長に貢献することで恩返ししていきます。</p> <p>これまでデータアナリストというロールがなかった会社に、データアナリストではなかったぼくを雇い入れることは、そばの上にコロッケを乗せちゃおうと考えることくらいかなり挑戦的なことだったんじゃないかと思います。そして、どう扱って良いのかも迷ったんじゃないかとも思います。</p> <p>入社直後の社員リレー記事で、「<a href="https://note.com/smartcamp_tent/n/n079f9f0b246e">スマートキャンプは意外とちゃんとしていた場所だった</a>」みたいなちょっと失礼なことを書いているのですが、そんなぼくでもちゃんとチャレンジさせてくれて、評価までしていただけたことにとても感謝しています。</p> smartcamp FourKeysを横へ広げる hatenablog://entry/6801883189073907152 2024-01-11T13:00:00+09:00 2024-01-11T13:00:00+09:00 はじめに 前提 FourKeysとは FourKeysを横に広げるとは 横に広げるために必要な要素 橋を作ってくれる協力者 FourKeysの目的を明確にする FourKeysが与える身近な効果を伝える FourKeysへの取り組みをしやすくし習慣化する 今後目指したいところ はじめに こんにちは!スマートキャンプ開発エンジニアの井上です。 スマートキャンプでは少しずつFourKeysを活用し始めており、その中でも今回はプロダクト間の横の連携でFourKeysを広げた話をしていきます。 eyecatchの画像は生成AIにキャンプ✖️FourKeys✖️横へ広げる✖️テックブログで生成したらブロ… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240110/20240110122730.png" width="1200" height="686" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#前提">前提</a></li> <li><a href="#FourKeysとは">FourKeysとは</a></li> <li><a href="#FourKeysを横に広げるとは">FourKeysを横に広げるとは</a></li> <li><a href="#横に広げるために必要な要素">横に広げるために必要な要素</a><ul> <li><a href="#橋を作ってくれる協力者">橋を作ってくれる協力者</a></li> <li><a href="#FourKeysの目的を明確にする">FourKeysの目的を明確にする</a></li> <li><a href="#FourKeysが与える身近な効果を伝える">FourKeysが与える身近な効果を伝える</a></li> <li><a href="#FourKeysへの取り組みをしやすくし習慣化する">FourKeysへの取り組みをしやすくし習慣化する</a></li> <li><a href="#今後目指したいところ">今後目指したいところ</a></li> </ul> </li> </ul> <h2 id="はじめに">はじめに</h2> <p>こんにちは!スマートキャンプ開発エンジニアの井上です。<br/> スマートキャンプでは少しずつFourKeysを活用し始めており、その中でも今回はプロダクト間の横の連携でFourKeysを広げた話をしていきます。<br/> eyecatchの画像は生成AIにキャンプ✖️FourKeys✖️横へ広げる✖️テックブログで生成したらブログの内容には合わない壮大なものができてしまいました。。</p> <h2 id="前提">前提</h2> <p>スマートキャンプはBOXIL SaaS、BALES CLOUDと複数のプロダクトが存在します。<br/> この開発組織はそれぞれ独自の成長を速い速度で行えるようにそれぞれの開発組織が存在します。<br/> このため共通の文化もあれば独自の文化が存在する開発組織です。</p> <h2 id="FourKeysとは">FourKeysとは</h2> <p>DORAが実施した6年間の研究からソフトウェア開発チームのパフォーマンスを示す4つの指標があることがわかりました。<br/> これらの指標をFourKeysと呼びます。<br/> また、このFourKeysの指標の中でも下記のように速度と安定性を示すものと理解しています。</p> <ul> <li><strong>ソフトウェアデリバリの速度</strong> <ul> <li>デプロイの頻度: 組織による正常な本番環境へのリリースの頻度</li> <li>変更のリードタイム: commitから本番環境稼働までの所要時間</li> </ul> </li> <li><strong>ソフトウェアデリバリの安定性</strong> <ul> <li>平均復旧時間: 組織が本番環境での障害から回復するのにかかる時間</li> <li>変更失敗率: デプロイが原因で本番環境で障害が発生する割合(%)</li> </ul> </li> </ul> <h2 id="FourKeysを横に広げるとは">FourKeysを横に広げるとは</h2> <p>もともとFourKeysはBALES CLOUDでTry的に計測や改善を行い運用していました。<br/> ただこの取り組みはプロダクト組織が改善していることをデータで語れるようになるための大事なことになるためプロダクト組織全体でやるべきだと考えました。<br/> 考えてからは開発組織のリーダー層で相談しBOXIL SaaSとBALES CLOUDでFourKeysを導入を提案し実行していきましたが、上手く広げていくためにはどのようなことが必要なのかは私も迷いながらも進めていった経験を共有したいと思います。</p> <h2 id="横に広げるために必要な要素">横に広げるために必要な要素</h2> <p>振り返るとですが横に広げる際に下記の要素が大切だなと思っています。<br/> ただ初めから気づいて定義できていたわけではなく探索と検証を繰り返した結果この要素だなと感じている部分です。<br/> ここは実際は色々試行錯誤をしました。</p> <ul> <li>橋を作ってくれる協力者をつくる</li> <li>FourKeysの目的を明確にする</li> <li>FourKeysが与える身近な効果を伝える</li> <li>FourKeysへの取り組みをしやすくし習慣化する</li> </ul> <h3 id="橋を作ってくれる協力者">橋を作ってくれる協力者</h3> <p>これは横という表現をした意図でもありますが、FourKeysはトップダウンでやることが決まったものではなく1プロダクトが初めてFourKeysを活用し横に広げました。<br/> 私はBALES CLOUDのエンジニアなのでBOXIL SaaSチームの解像度は低くどのように伝えればいいかを考えるための情報が足りない状態でした。<br/> このためプロダクト組織として横に広げるには隣のプロダクトの知識や現場の声を拾い上げることが可能で、チーム状況の解像度が高い人に一緒に作っていってもらう必要があります。<br/> この一緒にやってもらう協力者を作るためにはまずは1人に共感してもらい仲間になってもらうことがとても大事です。<br/> このため下記の部分を資料を元に丁寧に伝えました。</p> <ul> <li>FourKeys導入の意図</li> <li>期待する効果</li> <li>今後どのようにしていきたいか?</li> </ul> <p>実際かなり助けてもらうことが多くBOXIL SaaSチームにはとても感謝しています!</p> <h3 id="FourKeysの目的を明確にする">FourKeysの目的を明確にする</h3> <p>FourKeysは生産性の指標としてよく使われますが、実現したい状態は生産性を上げた先にあります。<br/> あくまで私も含めてFourKeysは目指す場所に到達するために見るべき指標の1つとして捉えています。<br/> また一緒に目指していく人たちにも生産性を上げた先でやりがいのあるものがちゃんとある状態にしたかったというのもあります。<br/> 実際にこの定義は「生産性をあげ試行回数を上げることで価値への到達を早くすること」を目的としました。<br/> プロダクト作りは一度作って終わりではなく、FBサイクルを回して研ぎ澄ませて価値あるものにしていくのでそのためにはFBを受ける回数を増やすことが大事という考えを元にしてこの定義にしました!<br/> この部分認識が異なってはやる意義が薄れてしまうので伝える際には資料を作成したうえで説明しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240110/20240110123152.png" width="1200" height="647" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="FourKeysが与える身近な効果を伝える">FourKeysが与える身近な効果を伝える</h3> <p>生産性は人によってはすこし遠いように思えるかもしれません。<br/> そうなるとFourKeysを元に改善することでの報酬がわからなくなり、良い取り組みでも取り組まれなかったり<br/> 人によって取り組みへの温度差が発生しチームとしての取り組みができない状態になってしまいます。<br/> この問題はFourKeysというものが開発という活動の身近な部分にどんな影響を及ぼすかのが明確になっていないことが問題になり起きていました。<br/> そのためこの対策としてFourKeysを元に改善することで起こる身近な改善や問題があることを伝えました。<br/> さまざまな要素はある中で開発において身近であろう点を下記のスライドで伝えました。<br/> うまいこと伝えられたかはわかりませんが、反応が良かったので伝わったのかなと思っています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240110/20240110123214.png" width="1200" height="672" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240110/20240110123230.png" width="1200" height="664" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="FourKeysへの取り組みをしやすくし習慣化する">FourKeysへの取り組みをしやすくし習慣化する</h3> <p>どんないい取り組みも一歩目が難しく重くなります。<br/> このためいかに簡易的に確認できるかと最初にやることのハードを下げるかが重要です。<br/> またハードルを下げたうえでやることが当たり前になるように習慣化していくことが大事になります。</p> <p><strong>ハードルを下げる</strong></p> <p>こちらは協力してくれたBOXIL SaaS側のリーダーに相談しチーム状況やチームのFourKeysへの認知度を確認しアクションを一緒に考えました。<br/> 別チームのことは完全には理解できないため一人で考えずに協力を依頼したことで適切なハードル設定にできたかなと感じています。</p> <p><strong>課題に近く簡易的に確認可能なものを用意する</strong></p> <p>取り組む人たちも自分たちへのメリットがないものや課題感が無いものには取り組めないかと思います。<br/> 取り組むためには自分たちの視界に入っている身近な課題を解決できる手段の一つであることを説明したうえで関係性のある数値を出すことが重要でした。<br/> 直近の課題としてはレビューの時間が長くなっている傾向があるという話を聞いていたので、FourKeysのリードタイムの中でもレビューからApproveまでに時間がかかっていることをデータで示し<br/> 追加でレビュー時間とレビューが分散されているかをダッシュボードで可視化しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240110/20240110123250.png" width="896" height="283" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><strong>習慣化する BALES CLOUD</strong></p> <p>BALES CLOUDではFourKeysのリードタイムに関連する指標として完了率というものを定義しスプリントごとに開発アイテムの完了率を見るようにしました。<br/> まずはスプリントごとに完了することを意識したうえで、FourKeysのダッシュボードは誰でも見れる状態にしました。<br/> これによりチームが達成意識をもつとともにチーム全体でリードタイムを改善する意識がついたと思います。<br/> 言葉として「今週はレビューが遅くなってしまった」や「全体の完了率が悪かったなど」が振り返りででてきたのが印象的でした。</p> <p><strong>習慣化する BOXIL SaaS</strong></p> <p>また、BOXIL SaaSはレトロスペクティブで毎回FourKeysを確認するというアジェンダを入れたところFourKeysを意識する習慣ができたそうです。<br/> 実際にデータで見るだけでもリードタイムは順調に改善しており、コードレビューも分散されている状態になっていました。<br/> これはとても嬉しかったです。</p> <h3 id="今後目指したいところ">今後目指したいところ</h3> <p>データで生産性や価値を語れる組織を目指していくとともに自分たちが生産性をあげることで企業の競合優位性になっていきたいなと考えています。<br/> どんなプロダクトでも価値につながるものを作り続けられるわけではなく、いくつかの試行を繰り返し価値へ到達すると思います。<br/> なのでこの試行回数をいかに早く実行し価値にたどり着くのを早くするかはプロダクトや事業の競合優位性になりうると考えています。</p> smartcamp React Hook Form と Zod で非同期バリデーションがしたいの!! hatenablog://entry/6801883189073903868 2024-01-10T13:00:00+09:00 2024-01-10T13:00:01+09:00 遭遇してしまった問題 解決策 おわりに こんにちは!! BOXIL SaaSのエンジニア兼テックブログチームの平社員をしているブラーバです。最近は働きが認められ、テックブログチームで確固たる地位を築きつつあるとかないとか...。 今回は以前公開したReact Hook Form、Zod、Recoilを組み合わせたフォームを作る!にならい、React Hook FormとZodを使ったフロントエンド開発の第二弾です!! 本記事では、APIリクエストが必要なバリデーションをReact Hook FormとZodを使って実装しようとした際に、遭遇した問題とその解決策について話します。 同じような問題… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240110/20240110121623.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#遭遇してしまった問題">遭遇してしまった問題</a></li> <li><a href="#解決策">解決策</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <p>こんにちは!!</p> <p>BOXIL SaaSのエンジニア兼テックブログチームの平社員をしているブラーバです。最近は働きが認められ、テックブログチームで確固たる地位を築きつつあるとかないとか...。</p> <p>今回は以前公開した<a href="https://tech.smartcamp.co.jp/entry/react-hook-form-zod-recoil-vol1">React Hook Form、Zod、Recoilを組み合わせたフォームを作る!</a>にならい、React Hook FormとZodを使ったフロントエンド開発の第二弾です!!<br/> 本記事では、APIリクエストが必要なバリデーションをReact Hook FormとZodを使って実装しようとした際に、遭遇した問題とその解決策について話します。<br/> 同じような問題に直面している人、あるいはReact Hook FormやZodに自体に興味がある人の参考になると嬉しいです!!</p> <h2 id="遭遇してしまった問題">遭遇してしまった問題</h2> <p>BOXIL SaaSでは一部フォームをReact Hook Formで作り、バリデーションにはZodを使用しています。<br/> そのバリデーションの中には、ユーザーが入力した内容ががユニークかどうかを確認するために、バックエンドに問い合わせる項目がありました。<br/> Zodの仕様では、いずれかの項目が入力されるたびに都度バリデーションが行われるため、問い合わせが必要な項目以外を入力していても、APIリクエストをしていました。React Hook Formのリポジトリでも同様の議論がされています。<br/> <a href="https://github.com/orgs/react-hook-form/discussions/9005">https://github.com/orgs/react-hook-form/discussions/9005</a></p> <p>以下のコードは<code>isUniqueName</code>という関数で入力された<code>name</code>がユニークかどうかを確認するために、入力ごとにAPIリクエストを送信しているコードです...。</p> <pre class="code tsx" data-lang="tsx" data-unlink>import { zodResolver } from &#34;@hookform/resolvers/zod&#34;; import { useForm } from &#34;react-hook-form&#34;; import { z } from &#34;zod&#34;; const isUniqueName = async (name: string) =&gt; { console.log(&#34;isUniqueName: &#34;, name); // ここでAPIリクエストを飛ばし、DBに保存されている同一のnameがあるかを確認したい return true; }; const useUserForm = z.object({ id: z.string(), name: z.string().refine(isUniqueName), }); type UseUserForm = z.infer&lt;typeof useUserForm&gt;; const defaultValues: UseUserForm = { id: &#34;&#34;, name: &#34;&#34;, }; export function Hoge() { const { register, handleSubmit } = useForm&lt;UseUserForm&gt;({ resolver: zodResolver(useUserForm), mode: &#34;onChange&#34;, defaultValues, }); return ( &lt;&gt; &lt;form onSubmit={handleSubmit((data) =&gt; console.log(data))}&gt; &lt;input {...register(&#34;id&#34;)} /&gt; &lt;input {...register(&#34;name&#34;)} /&gt; &lt;button type=&#34;submit&#34;&gt;submit&lt;/button&gt; &lt;/form&gt; &lt;/&gt; ); }</pre> <p>実際に上記のコードを動かしてみると、<code>id</code>など他項目を入力していても<code>isUniqueName</code>が呼ばれてしまい、実際に<code>console.log</code>の部分をAPIリクエストに置き換えたとすると、計10回もAPIリクエストが飛んだことになります。</p> <p><figure class="figure-image figure-image-fotolife" title="改修前のconsole.log"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240110/20240110121741.png" width="1200" height="787" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>改修前のconsole.log</figcaption></figure></p> <p>上記画像だと、本来ならAPIリクエストは最大でも4回に抑えたいです。</p> <h2 id="解決策">解決策</h2> <p>そこでブラウザのメモリ上にすでにAPIコールしたユーザー名を保持するという方法でAPIリクエストの回数を減らしました。<br/> 具体的には、APIに問い合わせをしたユーザー名をMapオブジェクトで管理し、再度同じ名前でバリデーションが走ったときにはAPIリクエストをせずに<code>false</code>を返すようにしました。<br/> これでAPIを叩く回数を大幅に減らすことができました。</p> <pre class="code tsx" data-lang="tsx" data-unlink>import { zodResolver } from &#34;@hookform/resolvers/zod&#34;; import { useForm } from &#34;react-hook-form&#34;; import { z } from &#34;zod&#34;; const existsName = new Map&lt;string, boolean&gt;(); const isUniqueName = async (name: string) =&gt; { if (existsName.has(name)) { return false; } console.log(&#34;isUniqueName: &#34;, name); existsName.set(name, true); return true; }; const useUserForm = z.object({ id: z.string(), name: z.string().refine(isUniqueName), }); type UseUserForm = z.infer&lt;typeof useUserForm&gt;; const defaultValues: UseUserForm = { id: &#34;&#34;, name: &#34;&#34;, }; export function Hoge() { const { register, handleSubmit } = useForm&lt;UseUserForm&gt;({ resolver: zodResolver(useUserForm), mode: &#34;onChange&#34;, defaultValues, }); return ( &lt;&gt; &lt;form onSubmit={handleSubmit((data) =&gt; console.log(data))}&gt; &lt;input {...register(&#34;id&#34;)} /&gt; &lt;input {...register(&#34;name&#34;)} /&gt; &lt;button type=&#34;submit&#34;&gt;submit&lt;/button&gt; &lt;/form&gt; &lt;/&gt; ); }</pre> <p>実際に上記のコードを動かしてみると重複したログは出力されず、無駄なAPIリクエストが減っていました。</p> <p><figure class="figure-image figure-image-fotolife" title="改修後のconsole.log"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20240110/20240110121817.png" width="386" height="220" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>改修後のconsole.log</figcaption></figure></p> <h2 id="おわりに">おわりに</h2> <p>以上、React Hook FormとZodを使って非同期バリデーションを最適化した話でした。<br/> また、実際にBOXIL SaaSの開発時にこの問題に遭遇したときには<a href="https://azukiazusa.dev/blog/react-hook-form-zod-5-patterns">React Hook FormでZodを使うときの5つパターン</a>を参考に本記事のような実装をしました。<br/> 今回は、React Hook FormとZodを使ったフロントエンド開発の第二弾でしたが、第三弾も近いうちに公開する予定ですので、お楽しみに!!</p> smartcamp Mojo🔥でllama2を実行してPythonと速度比較するモジョよ hatenablog://entry/6801883189069850327 2023-12-26T12:00:00+09:00 2023-12-26T12:00:01+09:00 挨拶 初めに 対象読者 実行環境 Mojoとは 現状のMojoの導入方法 MojoとPythonの実行時間の比較 Pythonのコード Mojoのコード 結果 Local LLMの実行 llama2.py llama2.c llama.mojoの実行時間の比較 llama2.py(Python) 実行コマンド 生成された文章 llama2.c(C) 実行コマンド 生成された文章 llama2.mojo(Mojo) 実行コマンド 生成された文章 結果 結論 挨拶 京都開発拠点でインターンをしてるぱんちです(a.k.a 田中 大貴) 拙い文章ですが初めて記事を書かせてもらいました! 業務でAIの調… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231226/20231226112005.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#挨拶">挨拶</a></li> <li><a href="#初めに">初めに</a></li> <li><a href="#対象読者">対象読者</a></li> <li><a href="#実行環境">実行環境</a></li> <li><a href="#Mojoとは">Mojoとは</a></li> <li><a href="#現状のMojoの導入方法">現状のMojoの導入方法</a></li> <li><a href="#MojoとPythonの実行時間の比較">MojoとPythonの実行時間の比較</a><ul> <li><a href="#Pythonのコード">Pythonのコード</a></li> <li><a href="#Mojoのコード">Mojoのコード</a></li> <li><a href="#結果">結果</a></li> </ul> </li> <li><a href="#Local-LLMの実行">Local LLMの実行</a></li> <li><a href="#llama2py-llama2c-llamamojoの実行時間の比較">llama2.py llama2.c llama.mojoの実行時間の比較</a><ul> <li><a href="#llama2pyPython">llama2.py(Python)</a><ul> <li><a href="#実行コマンド">実行コマンド</a></li> <li><a href="#生成された文章">生成された文章</a></li> </ul> </li> <li><a href="#llama2cC">llama2.c(C)</a><ul> <li><a href="#実行コマンド-1">実行コマンド</a></li> <li><a href="#生成された文章-1">生成された文章</a></li> </ul> </li> <li><a href="#llama2mojoMojo">llama2.mojo(Mojo)</a><ul> <li><a href="#実行コマンド-2">実行コマンド</a></li> <li><a href="#生成された文章-2">生成された文章</a></li> <li><a href="#結果-1">結果</a></li> </ul> </li> </ul> </li> <li><a href="#結論">結論</a></li> </ul> <h1 id="挨拶">挨拶</h1> <p>京都開発拠点でインターンをしてるぱんちです(a.k.a 田中 大貴)<br/> 拙い文章ですが初めて記事を書かせてもらいました! 業務でAIの調査などをしておりその過程でMojoでLLMを動作させたので最後までお付き合いください!</p> <h1 id="初めに">初めに</h1> <p>京都開発拠点のインターンではAIの調査、AIを用いた開発を行っています。<br/> デバッグ時はローカルでAIのモデルを動作させることも多い上にPythonなので、高速に動作させる方法を模索しています。<br/> その過程で速度の問題を根本的に解決できそうなプログラミング言語Mojoを見つけたので、LLMを動作させてPythonと速度を比較したのでまとめてみたいと思います。</p> <h1 id="対象読者">対象読者</h1> <ul> <li>Mojoの速度が気になる人</li> <li>Local LLMに触れたいけど重いから実行できない人</li> </ul> <h1 id="実行環境">実行環境</h1> <p>Intel MacBook Pro 2.3GHzクアッドコアIntelCorei7 メモリ32GB<br/> Docker Ubuntu(イメージ:mcr.microsoft.com/devcontainers/base:jammy)</p> <h1 id="Mojoとは">Mojoとは</h1> <p>MojoはPythonの代替のAIを拡張するプログラミング言語。Pythonと比較して68000倍の速度が出るらしい……。<br/> Pythonと互換がありPythonのライブラリをMojoで使うことができます。ただし、Pythonのライブラリはインタプリで動作するので高速化はしないようです。<br/> 型は強く構文などはPythonに似ています、ありがたい。</p> <h1 id="現状のMojoの導入方法">現状のMojoの導入方法</h1> <p>現状MojoをインストールするにはModulerにアカウント登録、ログインが必要です。<br/> <a href="https://www.modular.com/mojo">https://www.modular.com/mojo</a><br/> OSによって導入方法が違いますがIntel Macでは動作しないようなので、今回はDockerで導入します。<br/> DockerでUbuntuのコンテナを立てて必要なものをインストールします。</p> <pre class="code shell" data-lang="shell" data-unlink>apt-get install -y apt-transport-https &amp;&amp; keyring_location=/usr/share/keyrings/modular-installer-archive-keyring.gpg &amp;&amp; curl -1sLf &#39;https://dl.modular.com/{hash}/installer/gpg.0E4925737A3895AD.key&#39; | gpg --dearmor &gt;&gt; ${keyring_location} &amp;&amp; curl -1sLf &#39;https://dl.modular.com/{hash}installer/config.deb.txt?distro=debian&amp;codename=wheezy&#39; &gt; /etc/apt/sources.list.d/modular-installer.list &amp;&amp; apt-get update &amp;&amp; apt-get install python3.10-venv &amp;&amp; apt-get install -y modular # Moduler CIのインストール</pre> <p>Moduler CIを認証してMojoをインストールします。</p> <pre class="code shell" data-lang="shell" data-unlink>modular auth {token} &amp;&amp; modular install mojo</pre> <p>最後にパスを通します。</p> <pre class="code shell" data-lang="shell" data-unlink>echo &#39;export MODULAR_HOME=&#34;/home/ubuntu/.modular&#34;&#39; &gt;&gt; ~/.bashrc echo &#39;export PATH=&#34;/home/ubuntu/.modular/pkg/packages.modular.com_mojo/bin:$PATH&#34;&#39; &gt;&gt; ~/.bashrc source ~/.bashrc</pre> <h1 id="MojoとPythonの実行時間の比較">MojoとPythonの実行時間の比較</h1> <p>MojoとPythonの実行時間を試し割り法を使い、素数を一億個まで求めてるコードの実行時間を比較してみます。<br/> 参考:<a href="https://transparent-to-radiation.blogspot.com/2023/05/20235.html">https://transparent-to-radiation.blogspot.com/2023/05/20235.html</a></p> <h3 id="Pythonのコード">Pythonのコード</h3> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">def</span> <span class="synIdentifier">calc</span>(upper: <span class="synIdentifier">int</span>): prime_numbers = [] <span class="synStatement">def</span> <span class="synIdentifier">is_prime_number</span>(n): <span class="synStatement">for</span> pn <span class="synStatement">in</span> prime_numbers: <span class="synStatement">if</span> pn * pn &gt; n: <span class="synStatement">break</span> <span class="synStatement">if</span> n % pn == <span class="synConstant">0</span>: <span class="synStatement">return</span> <span class="synIdentifier">False</span> <span class="synStatement">return</span> <span class="synIdentifier">True</span> <span class="synStatement">for</span> n <span class="synStatement">in</span> <span class="synIdentifier">range</span>(<span class="synConstant">2</span>, upper + <span class="synConstant">1</span>): <span class="synStatement">if</span> is_prime_number(n): prime_numbers.append(n) calc(100_000_000) </pre> <h3 id="Mojoのコード">Mojoのコード</h3> <pre class="code mojo" data-lang="mojo" data-unlink>from utils.vector import InlinedFixedVector fn calc(upper: Int): var prime_numbers = InlinedFixedVector[Int](100_000_000) for n in range(2, upper + 1): if is_prime_number(n, prime_numbers): prime_numbers.append(n) fn is_prime_number(n: Int, prime_numbers: InlinedFixedVector[Int]) -&gt; Bool: for i in range(0, len(prime_numbers)): let pn = prime_numbers[i] if pn * pn &gt; n: return True if n % pn == 0: return False return True fn main(): calc(100000000)</pre> <h3 id="結果">結果</h3> <p>実行時間はtimeコマンドのrealの数値です。 | | Python | Mojo | | ---- | ---- | --- | | 実行時間(一回目) | 7m28.549s | 0m12.865s | 実行時間(二回目) | 7m42.270s | 0m12.370s</p> <p>7分ほどかかっていたものが12秒に短縮されました。 思っていた以上に速い〜!</p> <h1 id="Local-LLMの実行">Local LLMの実行</h1> <p>Hugging Face(モデル,ライブラリ)などは現状インタプリタで動作するためllama2.mojoを実行してみます。 llama2.mojoはMetaのllama2をmojoで実装したものになります。 まずllma2.mojoを<code>git clone</code>してきます。</p> <pre class="code shell" data-lang="shell" data-unlink>git clone https://github.com/tairov/llama2.mojo.git cd llama2.mojo</pre> <p>次にモデルをダウロードします。</p> <pre class="code shell" data-lang="shell" data-unlink>wget https://huggingface.co/karpathy/tinyllamas/resolve/main/stories15M.bin</pre> <p>モデルのダウンロードが終われば以下のコマンドで実行できるようになります。</p> <pre class="code shell" data-lang="shell" data-unlink>mojo llama2.mojo stories15M.bin -s 100 -n 256 -t 0.5 -i &#34;Mojo is a language&#34;</pre> <h1 id="llama2py-llama2c-llamamojoの実行時間の比較">llama2.py llama2.c llama.mojoの実行時間の比較</h1> <p>llama2は言語ごとに実装されているのでPythonとC,Mojoでの実行時間を比較します。</p> <h2 id="llama2pyPython">llama2.py(Python)</h2> <h3 id="実行コマンド">実行コマンド</h3> <pre class="code shell" data-lang="shell" data-unlink>time python3 llama2.py stories15M.bin 0.8 256 &#34;Dream comes true this day&#34;</pre> <h3 id="生成された文章">生成された文章</h3> <pre class="code" data-lang="" data-unlink>&lt;s&gt; Dream comes true this day. Behinda&#39;s eyes, there is a boo-boo from a bird. It is happy. It has a big black stuff on its head. It can move in the night and that is when it is full. It can use necks and sticks and touch its wings. It can make sounds and pull its hair. Be careful, and be gentle. Beak to it. Beak to be gentle. Beak&#39;s friend is a firefighter. He helps put out fires with a hose. Beak and his dog are safe. Beak is very brave. &lt;s&gt; Once upon a time, there was a little girl named Lily. She loved to play games with her friends. One day, they decided to play a game of hide and seek. Lily was very good at hiding and she won the game. After the game, Lily&#39;s friends noticed that she didn&#39;t have many toys to play with. They asked if she could share her toys with them. Lily was happy to share and said yes. But then, Lily&#39;s mom came and told her that it was important to share and be kind to others. Lily achieved tok/s: 0.6090875867949811</pre> <h2 id="llama2cC">llama2.c(C)</h2> <h3 id="実行コマンド-1">実行コマンド</h3> <pre class="code shell" data-lang="shell" data-unlink>time ./run stories15M.bin -t 0.8 -n 256 -i &#34;Dream comes true this day&#34;</pre> <h3 id="生成された文章-1">生成された文章</h3> <pre class="code" data-lang="" data-unlink>Dream comes true this day. A young girl named Amy and her best friend John are playing together in the park. Amy has her doll and John has a different doll. He has a short doll. Amy is the one who comes to the park and wears her doll. &#34;Wow, Lucy, look at my doll. She is very pretty and Anna is very nice. She can sing and dance,&#34; Amy says. &#34;Hi, Anna. You are very pretty and smart. I like your doll. She is very kind. She can sing and dance too,&#34; Lucy says. They smile and hug each other. They are happy to meet each other and their doll. They decide to play a game with their doll. They take turns holding their doll and making her sing and dance. They have fun. achieved tok/s: 30.661249</pre> <h2 id="llama2mojoMojo">llama2.mojo(Mojo)</h2> <h3 id="実行コマンド-2">実行コマンド</h3> <pre class="code shell" data-lang="shell" data-unlink>time mojo llama2.mojo stories15M.bin -n 256 -t 0.8 -i &#34;Dream comes true this day&#34;</pre> <h3 id="生成された文章-2">生成された文章</h3> <pre class="code" data-lang="" data-unlink>num parallel workers: 8 SIMD width: 64 checkpoint size: 60816029 [ 57 MB ] | n layers: 6 | vocab size: 32000 Dream comes true this day! Someone had managed to use the magic language. A prince appeared with a smile on his face and a beautiful cape. The prince smiled and said, &#34;You are a very special prince.&#34; Dream and the prince were in a big fight. But no one wanted to fight and they all said, &#34;You are too lazy!&#34; They kept arguing until the prince was tired. He said, &#34;If you don&#39;t fight, I will come and get you!&#34; The prince said, &#34;Ok, I will fight you and you cannot do it!&#34; So they all fought and laughed. But the prince was too lazy to fight and said: &#34;No, I won&#39;t fight you!&#34; The prince was so mad and he left and never talked to the prince again. The prince was sad but he was glad he was safe. He still had a tiny piece of the magic language that he wanted to remember, and he was never lazy again. achieved tok/s: 34.066479245907061</pre> <h3 id="結果-1">結果</h3> <table> <thead> <tr> <th> </th> <th> Python </th> <th> C </th> <th> Mojo </th> </tr> </thead> <tbody> <tr> <td> 実行時間(一回目) </td> <td> 7m0.722s </td> <td> 0m6.186s </td> <td> 0m6.902s</td> </tr> <tr> <td> 実行時間(二回目) </td> <td> 7m13.986s </td> <td> 0m3.542s </td> <td> 0m7.099s</td> </tr> </tbody> </table> <h1 id="結論">結論</h1> <p>生成された文字数にもよるがPythonと比較するとかなり速いです。約35倍速。 流石にCよりは遅いです。 今回は言語モデルがパラメーターが少なく軽い物なので全体的に短時間ですが、他のモデルがMojoで動作すればローカルLLMが軽快に動作しそうというロマンを感じます!!! Ptyhonの速度に満足できない人はぜひ使ってみてください。</p> smartcamp RSpecの実行時間を短縮した話 hatenablog://entry/6801883189060855212 2023-11-27T13:00:00+09:00 2023-11-27T13:00:00+09:00 RSpecの実行時間が長くなってきており、開発に少し支障をきたすようになってきました。 そこで開発の生産性を上げるべく、RSpecの実行時間短縮を試みたので、今回は、こちらの件についてお話ししたいと思います! <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231122/20231122183154.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#まずは結果">まずは結果</a></li> <li><a href="#遅かった要因と改善方法">遅かった要因と改善方法</a><ul> <li><a href="#対応1生成するテストデータを減らした">対応1:生成するテストデータを減らした。</a><ul> <li><a href="#背景">背景</a></li> <li><a href="#対応">対応</a></li> <li><a href="#結果">結果</a></li> </ul> </li> <li><a href="#対応2観点外の処理をMock化しテスト観点において不要な処理が実行されないようにした">対応2:観点外の処理をMock化し、テスト観点において不要な処理が実行されないようにした。</a><ul> <li><a href="#背景-1">背景</a></li> <li><a href="#対応-1">対応</a></li> <li><a href="#結果-1">結果</a></li> </ul> </li> <li><a href="#対応3テストの実行を並列化した">対応3:テストの実行を並列化した。</a><ul> <li><a href="#背景-2">背景</a></li> <li><a href="#対応-2">対応</a><ul> <li><a href="#なぜparalell_testsにしたか">なぜparalell_testsにしたか?</a></li> <li><a href="#導入--手順-">導入 ~ 手順 ~</a></li> <li><a href="#導入--困ったこと-">導入 ~ 困ったこと ~</a></li> <li><a href="#導入--並列数の決定-">導入 ~ 並列数の決定 ~</a></li> </ul> </li> <li><a href="#結果-2">結果</a></li> </ul> </li> </ul> </li> <li><a href="#今回の改善を通してわかった今後RSpec記述時に気をつけたいこと">今回の改善を通してわかった、今後RSpec記述時に気をつけたいこと</a><ul> <li><a href="#テストデータは最小限にしよう">テストデータは最小限にしよう。</a></li> <li><a href="#大量件数でのcreate_listはやめよう大量件数ではcreate_build--importを使おう">大量件数での"create_list"はやめよう(大量件数では"create_build + import"を使おう)。</a></li> <li><a href="#テスト観点を分け観点に関係ない部分で時間がかかる処理がないか考えよう">テスト観点を分け、観点に関係ない部分で時間がかかる処理がないか考えよう。</a></li> </ul> </li> <li><a href="#今後の展望">今後の展望</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>こんにちは!スマートキャンプ開発エンジニアの末吉(だいきち)です。</p> <p>私がチームメンバーとして日々開発している「BALES CLOUD」では、RSpecを割としっかりと書いております。 <br /> ただ近頃、RSpecの実行時間が長くなってきており、開発に少し支障をきたすようになってきました。<br /> そこで開発の生産性を上げるべく、RSpecの実行時間短縮を試みたので、<br /> 今回は、こちらの件についてお話ししたいと思います!</p> <h2 id="まずは結果">まずは結果</h2> <ul> <li>Before: 40〜50[min]</li> <li>After: 11〜18[min]</li> </ul> <p>上記通り、30[min]ほど短縮できました! <br /> 参考までに、circleciでのテスト実行時間推移を貼っておきます。(対応時期:9月14日〜10月3日)</p> <p><figure class="figure-image figure-image-fotolife" title="circleciでのテスト実行時間推移"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231122/20231122183521.png" width="1200" height="455" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>circleciでのテスト実行時間推移</figcaption></figure></p> <p>またこれは狙ったわけではないのですが、テスト実行時間を短縮できたことにより、circleciのcredit使用量も減らせました! <br /> 参考までに、circleciのcredit使用量推移を貼っておきます。(対応時期:9月14日〜10月3日)</p> <p><figure class="figure-image figure-image-fotolife" title="circleciのcredit使用量推移"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231122/20231122183559.png" width="1200" height="511" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>circleciのcredit使用量推移</figcaption></figure></p> <h2 id="遅かった要因と改善方法">遅かった要因と改善方法</h2> <p>今回行った大きな対応が3つあるため、その対応と背景について書いていきたいと思います。</p> <h3 id="対応1生成するテストデータを減らした">対応1:生成するテストデータを減らした。</h3> <p>まずは1つ目。</p> <h4 id="背景">背景</h4> <p>ある特定クラスのテストにおいて、一通りの標準的なデータを、すべてのケースで作成する構成となっておりました。 <br /> 具体的には、最上位階層部分で<code>let!</code>が使用されていたり、FactoryBotのコールバックでデータを生成していたりという感じです。 <br /> このような構成にすることで、DRYに書けることがあったりなどのメリットはあるかと思います。 <br /> しかし今回は、データ準備に1ケースあたり30[sec]近くかかっていたため、過剰な状態となっておりました。</p> <h4 id="対応">対応</h4> <p>以下を行い、各テストケースで作成するデータを削減しました。</p> <ul> <li>最上位階層の<code>let!</code>を<code>let</code>に変更し、データが必要なケースでのみデータを生成するようにした。</li> <li>FactoryBotコールバックで生成するデータを減らした。<br/> (今回は行いませんでしたが、Active Recordのコールバックでデータ生成している場合は、テストにおいてはそちらを停止することも検討してみて良いか思っております。)</li> </ul> <h4 id="結果">結果</h4> <p>これで8[min]ほど削減できました!</p> <h3 id="対応2観点外の処理をMock化しテスト観点において不要な処理が実行されないようにした">対応2:観点外の処理をMock化し、テスト観点において不要な処理が実行されないようにした。</h3> <p>次に2つ目。</p> <h4 id="背景-1">背景</h4> <p>バリデーション観点のテストで2000件のデータを作成しており、<br /> これによってテスト対象の処理が2000件分行われていました。</p> <h4 id="対応-1">対応</h4> <p>バリデーション観点のテストでは、メイン処理部分をMockにしました。<br /><br/> これによって、大量データで確認する部分を最小限にできました。 <br /> また大量データを生成する際は、<code>create_list</code>をやめて、<code>build_list + import</code>を使うようにしました。 <br /> <code>create_list</code>では指定した件数分のクエリが発行されます。 <br /> <code>build_list + import</code>にすることで、発行されるクエリの件数を減らすことができ、データ準備の時間を削減できました。</p> <h4 id="結果-1">結果</h4> <p>これで7[min]ほど削減できました!</p> <h3 id="対応3テストの実行を並列化した">対応3:テストの実行を並列化した。</h3> <p>一番効果のあった対応がこちらであり、導入も簡単であったためおすすめです!</p> <h4 id="背景-2">背景</h4> <p>「<a href="#%E5%AF%BE%E5%BF%9C%EF%BC%91%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B%E3%83%86%E3%82%B9%E3%83%88%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%B8%9B%E3%82%89%E3%81%97%E3%81%9F">対応1</a>」「<a href="#%E5%AF%BE%E5%BF%9C%EF%BC%92%E8%A6%B3%E7%82%B9%E5%A4%96%E3%81%AE%E5%87%A6%E7%90%86%E3%82%92Mock%E5%8C%96%E3%81%97%E3%83%86%E3%82%B9%E3%83%88%E8%A6%B3%E7%82%B9%E3%81%AB%E3%81%8A%E3%81%84%E3%81%A6%E4%B8%8D%E8%A6%81%E3%81%AA%E5%87%A6%E7%90%86%E3%81%8C%E5%AE%9F%E8%A1%8C%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E3%82%88%E3%81%86%E3%81%AB%E3%81%97%E3%81%9F">対応2</a>」によって、特定のテストに極端な時間がかかることは無くなりました。 <br /> ただし実行時間がゼロになることないので、テストの総量が多い場合は、<br /> 「書き方を工夫しての実行時間の削減」には限界があります。 <br /> BALES CLOUDでは冒頭では書いたとおり、割としっかりRSpecを書いております。 <br /> そのため、「書き方の工夫」だけでこれ以上大幅な改善をするのは、難しい状況にありました。</p> <h4 id="対応-2">対応</h4> <p><code>paralell_tests</code>を使用してテストの実行を並列化しました。</p> <h5 id="なぜparalell_testsにしたか">なぜ<code>paralell_tests</code>にしたか?</h5> <p>並列化は<code>circleci</code>で行なう方法もあったのですが、今回は<code>paralell_tests</code>による並列化を選択しました。 <br /> 理由としては、以下です。</p> <ul> <li>導入が簡単そうであったこと。</li> <li><code>circleci</code>で並列化する場合は、使用クレジットの増加が考えられるが、<code>paralell_tests</code>で並列化する場合は、使用クレジットの削減の可能性もあること。</li> <li><code>circleci</code>から別のサービスへ乗り換えた場合に、<code>paralell_tests</code>で並列化しておくと、無駄にならないこと。</li> </ul> <h5 id="導入--手順-">導入 ~ 手順 ~</h5> <p>手順については、すでにさまざまな方が紹介しているためここでは省略しようかと思います。</p> <h5 id="導入--困ったこと-">導入 ~ 困ったこと ~</h5> <p>並列化の導入準備が終わり、いざ実行してみると、テストが落ちてしまいました。 <br /> 「直列で全テストを流した場合」および、「単体でテストを実行した場合」では成功するテストが、<br /> 「並列で全テストを流した場合」には、失敗してしまう状況です。</p> <ul> <li>原因<br/> BALES CLOUDではテストに<code>webmock</code>を導入しているのですが、<br /> <code>after</code>で<code>WebMock.disable!</code>している箇所があり、後続のテストでスタブが使われておらず、落ちておりました。</li> <li>対応<br/> スタブが必要な箇所に<code>WebMock.enable!</code>を追加して対応しました。 <br /> <code>after</code>での<code>WebMock.disable!</code>を取り除く方法もあり、最終的には方針を決めてコードも揃えたいと思っておりますが、<br /> 今回の目的とは異なるため、いったん<code>WebMock.enable!</code>を追加する方法で対応しました。</li> </ul> <h5 id="導入--並列数の決定-">導入 ~ 並列数の決定 ~</h5> <p>並列数は結果としては「4」にしました。<br /> 決定方法としましては、「2 -> 4 -> 6」と試していき、「4」が頭打ちとなりそうであったため決定しました。</p> <h4 id="結果-2">結果</h4> <p>これで20[min]ほど削減できました!</p> <p>実は「<a href="#%E5%AF%BE%E5%BF%9C%EF%BC%91%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B%E3%83%86%E3%82%B9%E3%83%88%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%B8%9B%E3%82%89%E3%81%97%E3%81%9F">対応1</a>」「<a href="#%E5%AF%BE%E5%BF%9C%EF%BC%92%E8%A6%B3%E7%82%B9%E5%A4%96%E3%81%AE%E5%87%A6%E7%90%86%E3%82%92Mock%E5%8C%96%E3%81%97%E3%83%86%E3%82%B9%E3%83%88%E8%A6%B3%E7%82%B9%E3%81%AB%E3%81%8A%E3%81%84%E3%81%A6%E4%B8%8D%E8%A6%81%E3%81%AA%E5%87%A6%E7%90%86%E3%81%8C%E5%AE%9F%E8%A1%8C%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E3%82%88%E3%81%86%E3%81%AB%E3%81%97%E3%81%9F">対応2</a>」の前にいったん試したときは、7[min]ほどしか改善しませんでした。 <br /> しかし、「<a href="#%E5%AF%BE%E5%BF%9C%EF%BC%91%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B%E3%83%86%E3%82%B9%E3%83%88%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%B8%9B%E3%82%89%E3%81%97%E3%81%9F">対応1</a>」「<a href="#%E5%AF%BE%E5%BF%9C%EF%BC%92%E8%A6%B3%E7%82%B9%E5%A4%96%E3%81%AE%E5%87%A6%E7%90%86%E3%82%92Mock%E5%8C%96%E3%81%97%E3%83%86%E3%82%B9%E3%83%88%E8%A6%B3%E7%82%B9%E3%81%AB%E3%81%8A%E3%81%84%E3%81%A6%E4%B8%8D%E8%A6%81%E3%81%AA%E5%87%A6%E7%90%86%E3%81%8C%E5%AE%9F%E8%A1%8C%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E3%82%88%E3%81%86%E3%81%AB%E3%81%97%E3%81%9F">対応2</a>」によって極端に重いテストを取り除くことで、大幅な改善ができました。 <br /> おそらく処理詰まりが解消されたためかと思われます。</p> <p>もし並列化をしても思ったより改善しない場合は、極端に重い処理の見直しをすると良いかもしれません!</p> <p>または、<code>paralell_tests</code>には「runtime log」というものがあり、<br /> これを残すことで、次回のテストの振り分けを実行時間がなるべく均等になるようにしてくれる機能もあるようです。 <br /> そのため、そちらを試してみても良いかもしれません!</p> <h2 id="今回の改善を通してわかった今後RSpec記述時に気をつけたいこと">今回の改善を通してわかった、今後RSpec記述時に気をつけたいこと</h2> <p>最後に、今回の対応を通して今後気をつけたいと感じたことを書いていきたいと思います。</p> <h3 id="テストデータは最小限にしよう">テストデータは最小限にしよう。</h3> <p>DRYに書くためなど、さまざまな理由でロジックに直接関係のないテストデータを用意したくなることもあるかと思います。 <br /> しかし、そのようなコードが大量に生成されてから後々削ろうと思うと、<br /> 1つ1つの効果が薄くて速度改善に時間がかかったり、FactoryBotコールバックの場合は修正範囲が大きくなってしまったりする可能性も考えられます。 <br /> そのため、直接ロジックに関係ないテストデータを作成することは安易にせず、よく考えてから行った方が良いかと感じました。</p> <p>特に、最上位階層で<code>let!</code>を使用していたり、FactoryBotのコールバックでデータ生成している場合は要注意かなと感じました。</p> <h3 id="大量件数でのcreate_listはやめよう大量件数ではcreate_build--importを使おう">大量件数での&quot;create_list&quot;はやめよう(大量件数では&quot;create_build + import&quot;を使おう)。</h3> <p><code>create_list</code>で大量のテストデータを作成すると、件数分のクエリが発行されてしまってデータ生成時間が少し多くなるため、<br /> 大量データ生成では、<code>create_build + import</code>を使った方が良いと感じました。</p> <h3 id="テスト観点を分け観点に関係ない部分で時間がかかる処理がないか考えよう">テスト観点を分け、観点に関係ない部分で時間がかかる処理がないか考えよう。</h3> <p>重たい処理におけるバリデーションのテストなど、観点に関係ない部分で時間がかかる可能性を考えて、もし存在する場合は、観点外の部分をモックにすることを積極的に検討してみても良いかと感じました。</p> <h2 id="今後の展望">今後の展望</h2> <p>今回大幅に実行時間を短縮できたRSpecですが、<br /> まだ短縮の余地はあるので更なる短縮に努めたいと思っております!</p> <p>また今後機能拡張と共にRSpecを書いていく中で、書き方によっては実行時間が再度増大することも予想されます。 <br /> レビューやチームへの知見展開でもカバーできますが、<br /> より安定した維持を実現するためには、静的解析などの「人依存」でない方法を取れると良いかと思っております。 <br /> そのため次は、短縮した実行時間をより簡単に維持できる方法を模索していきたいと思っております!</p> <h2 id="おわりに">おわりに</h2> <p>ここまで読んでいただきありがとうございました! <br /> この記事が皆さまの参考になれば幸いです! <br /> また更なる進展がありましたら、ご紹介できればと思っております!<br/> それでは!</p> smartcamp 最適なRailsアプリケーションのディレクトリ構造を目指して hatenablog://entry/6801883189056855352 2023-11-08T12:00:00+09:00 2023-11-08T12:00:01+09:00 はじめに 対象読者 理想のディレクトリ構成 取り組んだこと リファクタリングに至った背景 チームで決めたこと、行ったこと 現状把握 理想の構成 トライ 結局シンプルがいい デザインパターンを積極的に取り入れた結果 取り除いたもの Interactor Facade Query View Component Service リファクタリングしてどうなった? 小 ~ 中規模であれば おわりに はじめに こんにちは。 イベントプラットフォーム「BOXIL EVENT CLOUD(以下、BECといいます)」開発エンジニアの石井です。 今回はRuby on Rails(以下、Railsといいます)アプリ… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231107/20231107185201.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#対象読者">対象読者</a></li> <li><a href="#理想のディレクトリ構成">理想のディレクトリ構成</a></li> <li><a href="#取り組んだこと">取り組んだこと</a><ul> <li><a href="#リファクタリングに至った背景">リファクタリングに至った背景</a></li> <li><a href="#チームで決めたこと行ったこと">チームで決めたこと、行ったこと</a><ul> <li><a href="#現状把握">現状把握</a></li> <li><a href="#理想の構成">理想の構成</a></li> <li><a href="#トライ">トライ</a></li> </ul> </li> </ul> </li> <li><a href="#結局シンプルがいい">結局シンプルがいい</a><ul> <li><a href="#デザインパターンを積極的に取り入れた結果">デザインパターンを積極的に取り入れた結果</a></li> <li><a href="#取り除いたもの">取り除いたもの</a><ul> <li><a href="#Interactor">Interactor</a></li> <li><a href="#Facade">Facade</a></li> <li><a href="#Query">Query</a></li> <li><a href="#View-Component">View Component</a></li> <li><a href="#Service">Service</a></li> </ul> </li> <li><a href="#リファクタリングしてどうなった">リファクタリングしてどうなった?</a></li> <li><a href="#小--中規模であれば">小 ~ 中規模であれば</a></li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> </ul> <h1 id="はじめに">はじめに</h1> <p>こんにちは。</p> <p>イベントプラットフォーム「<a href="https://boxil-event-cloud.jp/">BOXIL EVENT CLOUD</a>(以下、BECといいます)」開発エンジニアの石井です。</p> <p>今回はRuby on Rails(以下、Railsといいます)アプリケーションにおけるコードリファクタリングについてお話します。</p> <p>BECはオフショア開発でずっと進めていたのですが、1年半ほど前からオンショア(社内)開発に切り替えました。</p> <p>そこからシステム統合、Railsのバージョンアップ対応を経て、少し落ち着いてきた頃から開発メンバーから「可読性が悪く開発し辛い」と声があがり、リファクタリングする流れになりました。</p> <p>本稿では、チームでのリファクタリングの取り組み方を中心に話したいと思います。</p> <h1 id="対象読者">対象読者</h1> <ul> <li>チーム開発におけるリファクタリングへの取り組み方に悩んでいる方</li> <li>中規模のRailsアプリケーションディレクトリ構成のオススメを知りたい方</li> <li>デザインパターンを積極的に取り入れた結果について知りたい方</li> </ul> <h1 id="理想のディレクトリ構成">理想のディレクトリ構成</h1> <p>はじめに、チームで決めたリファクタリング方針(ディレクトリ構成)をお見せします。</p> <p>appディレクトリ構成</p> <pre class="code text" data-lang="text" data-unlink>app/ アプリケーション用のディレクトリ app/assets/ アプリケーション用のリソースを置くディレクトリ app/cache_storages/ キャッシュデータ操作用のディレクトリ app/channels/ Action Cableファイル用のディレクトリ app/controllers/ コントローラ用のディレクトリ app/decorators/ ModelとViewの中間に位置しており、データの装飾用のディレクトリ app/helpers/ ヘルパー用のディレクトリ app/javascript/ JavaScript関連のスクリプト用のディレクトリ app/jobs/ Active Job用のディレクトリ app/mailers/ Action Mailerファイル用のディレクトリ app/models/ モデル用のディレクトリ app/models/modules/ データ加工処理用のディレクトリ app/reflexes/ stimulusReflex用のディレクトリ app/views/ ビュー用のディレクトリ app/workers/ sidekiq用のディレクトリ</pre> <p>↑を目指すうえで削除すると決めたもの</p> <pre class="code text" data-lang="text" data-unlink># システム統合時の負債 app/models/concerns/event_management_concerns app/models/event_management_models lib/event_management # デザインパターン app/queries/ app/components/ app/interactors/ app/facades/ app/services/</pre> <p>ディレクトリ構成は<a href="https://railsdoc.com/page/folder_structure">Railsの標準フォルダ構造</a>を参考にしました。</p> <p>削除対象や理由については、後ほど説明します。</p> <h1 id="取り組んだこと">取り組んだこと</h1> <h2 id="リファクタリングに至った背景">リファクタリングに至った背景</h2> <p>「はじめに」でも触れましたが、システム開発をもともと100%オフショアで行っており、その体制を2022年7月から本格的にオンショア開発に切り替えました。</p> <p>当初は、引き継いだインフラ構成やコードを元に軽微な不具合改修および追加機能実装を進めていく予定でした。しかし「クリティカルな不具合が頻発する」「応答速度が遅くユーザー離脱の可能性が高い」など、追加機能実装より優先して取り組まないといけない課題がたくさん出てきました。</p> <p>それらを解決するために、不具合修正を進めつつ、不必要に分かれていたシステムのシステム統合、RubyおよびRailsのバージョンアップ対応など大きな改修を中心に取り組んできました。</p> <p>大きな改修を経てコードと向き合う時間が増えてきた頃、「コードの可読性が悪い」「影響範囲が追いづらい」など、チーム内でコードに対する不満の声が大きくなっていきました。</p> <p>引き継いだコードは、デザインパターンを積極的に取り入れた構造になっており汎用的に使える一方、抽象度が高いのでコードリーディングのコストが高い課題がありました。</p> <p>そういった背景から、開発メンバーで集まって「開発しやすい状態・構成」について議論するミーティングを行ったのがきっかけで、リファクタリングが本格的にスタートしました。</p> <h2 id="チームで決めたこと行ったこと">チームで決めたこと、行ったこと</h2> <h3 id="現状把握">現状把握</h3> <p>課題に対して開発チームで認識のすり合わせを行い、2つの理由で開発体験が損なわれていることが分かりました。</p> <p>■システム統合時の負債が残っている</p> <p>BECは過去の開発事情から2つのシステムに分かれていましたが、これが負債の一つになっており、2つのシステムを片方に寄せる形でシステム統合しました。</p> <p>システム統合で不具合があった際に、システム統合起因による原因か切り分けるため、可能な限りコードをそのまま移行しました。</p> <p>その結果、動作的には問題ないものの「データ連携処理」「連携データ加工クラス」「連携データを扱うモデルおよびライブラリ群」など、本来1つのシステムを動かすには不要な構成が残っている状況となりました。</p> <p><figure class="figure-image figure-image-fotolife" title="システム統合前のイメージ図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231107/20231107185257.png" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>システム統合前のイメージ図</figcaption></figure></p> <p>システム統合前のイメージ図<br/> イベント視聴とアーカイブ視聴でシステムが分かれており、APIを利用してデータ連携をしていました。</p> <p>■デザインパターンを使うことで複雑になっている</p> <p>「Query」「View Component」「Interactor」「Facade」4つのデザインパターンが使用されており、データ取得処理・加工処理が複数ファイルに分散している状況でした。</p> <p>また、データの受け渡しは基本的にインスタンス変数を用いているので、インスタンス作成しているコードとインスタンスメソッドを実行しているコードが離れていることが多く、ファイルを行き来するのに時間がかかっていました。</p> <h3 id="理想の構成">理想の構成</h3> <p>「システム統合の負債によるコードの冗長化」「デザインパターンによる複雑化」「インスタンス変数によるコードの読みにくさ」を解決するため、できる限りシンプルな構成を目指してチームで議論しました。</p> <p>その結果、現状のBEC規模のWebアプリケーションであれば<a href="https://railsdoc.com/page/folder_structure">Railsの標準フォルダ構造</a>に「デコレーター(app/decorators)」「モジュール(app/models/modules)」を追加した形で問題なさそうだという結論になりました。</p> <p>再掲:理想のディレクトリ構成</p> <pre class="code text" data-lang="text" data-unlink>app/ アプリケーション用のディレクトリ app/assets/ アプリケーション用のリソースを置くディレクトリ app/cache_storages/ キャッシュデータ操作用のディレクトリ app/channels/ Action Cableファイル用のディレクトリ app/controllers/ コントローラ用のディレクトリ app/decorators/ ModelとViewの中間に位置しており、データの装飾用のディレクトリ app/helpers/ ヘルパー用のディレクトリ app/javascript/ JavaScript関連のスクリプト用のディレクトリ app/jobs/ Active Job用のディレクトリ app/mailers/ Action Mailerファイル用のディレクトリ app/models/ モデル用のディレクトリ app/models/modules/ データ加工処理用のディレクトリ app/reflexes/ stimulusReflex用のディレクトリ app/views/ ビュー用のディレクトリ app/workers/ sidekiq用のディレクトリ</pre> <p>■デコレーター(app/decorators)</p> <p>引き継いだコードから組み込まれていたもの。</p> <p>ModelとViewの中間に位置しており、Modelの値をViewで使用したい形に装飾処理が入っています。</p> <p>必須で必要ではないものの、削除する必要もなかったので残しました。</p> <p>デコレーターを使用しない場合は、Modelクラスに装飾処理を追加するといいと思います。</p> <p>■モジュール(app/models/modules)</p> <p>ファットモデルを回避するためのコード置き場で、新しく追加したディレクトリです。</p> <p>簡単なデータ操作はモデル(app/models) 、 複数テーブルを跨いでデータ操作を行なうなどの複雑な処理はモジュールに切り出しました。</p> <p>モジュールの導入ですが、Rails経験が10年以上あるベテランエンジニア(以下、Mさんといいます)から提案頂きました。</p> <p>ネット検索しても特にヒットしないので一般的な考えではないかもしれませんが、BECのコードにおいて存在感は大きく、リファクタリングで一番恩恵を感じている概念になります。</p> <h3 id="トライ">トライ</h3> <p>2023年6月に方針を固めて、7月からリファクタリング着手予定でした。</p> <p>方針を固めたタイミングで、リファクタリングへのモチベーションが高まっており「不具合タスクの中で取り組みはじめよう!」ということで前倒しでスタートしました。</p> <p>不具合タスクのコードレビューコストは大きくなりましたが、コードレビューを通じて、具体的な対応方法についてのメンバー間の認識ズレが次第に無くなっていくのを感じました。</p> <p>「鉄は熱いうちに打て」というように、方針決めてから取り組みまでの期間は短いほうが良いと思いました。リファクタリングをスケジュールする際は「方針決め ~ トライ」のセットが短いスパンで実行できるようにスケジュール調整することをオススメします。</p> <h1 id="結局シンプルがいい">結局シンプルがいい</h1> <h2 id="デザインパターンを積極的に取り入れた結果">デザインパターンを積極的に取り入れた結果</h2> <p>うまく活用できず、メリットよりデメリット(可読性の悪さ)が目立ちました。</p> <p>活用できなかった原因として、引き継いだコードだったことも関係してますが、アプリケーション規模が大きくない内はデザインパターンは使わなくていいと感じました。</p> <p>「Query」「Facade」の処理はモジュール(app/models/modules)に置き換え、「View Component」は部分テンプレート(render partial)に置き換えることで開発体験が向上しました。</p> <h2 id="取り除いたもの">取り除いたもの</h2> <h3 id="Interactor">Interactor</h3> <ul> <li>ユーザー登録動線のワークフローを中心に使用。</li> <li>ワークフローの各ステップのデータ連携にcontextを使用していた。</li> <li>context起因でエラーが発生した場合、contextの状態がパッと見で判断できない、ユーザー登録なのでエラー発生時はロールバックしてほしいなどの理由で廃止。</li> </ul> <h3 id="Facade">Facade</h3> <ul> <li>データ取得・データ整形など複数モデルのデータ操作をしており複雑で読み辛かった。</li> <li>インスタンスをビューに渡し、ビューからインスタンスメソッドを実行するのでクエリ制御があまく、無駄なクエリ実行によりパフォーマンスを悪くしていた。</li> <li>モジュールに切り出すことで上記の問題を解決。</li> </ul> <h3 id="Query">Query</h3> <ul> <li>本来、Active Record::Relationに対して操作し、Active Record::Relationを返す必要がある。</li> <li>しかし、実装はそうなっておらずActive Record::Relationやデータ整形後のhashを返却する状態になっていた。</li> <li>hash利用がパフォーマンス悪化・可読性悪化の原因になっていたので修正しつつ、一度に置き換えることができないのでデータ操作はモジュールに切り出すことにした。</li> </ul> <h3 id="View-Component">View Component</h3> <ul> <li>再利用性が高いView Componentが、実装上そこまで再利用されていなかった。</li> <li>機能部分と描写部分を分けられることがメリットですが、機能部分をほとんど活用しておらず部分テンプレートを使用するのと変わりがなかった。</li> <li>コードを読む際に、機能部分を冗長に経由するので可読性が悪く部分テンプレートに置き換えた。</li> </ul> <h3 id="Service">Service</h3> <ul> <li>インスタンスメソッドになると処理が追いづらく、インスタンスのライフサイクルも考えないといけない。</li> <li>ベテランエンジニアのMさんの経験上、Serviceクラスを上手く活用できているプロジェクトを見たことがなく、Serviceクラスが存在することで困ったケースが多かった。</li> <li>以上の理由から、処理をモジュールに切り出した。</li> </ul> <h2 id="リファクタリングしてどうなった">リファクタリングしてどうなった?</h2> <p>直感的にコードが追えるようになり、コードレビューや調査タスクにかかる時間が短縮されました。</p> <p>データ取得処理が分散していることで複雑になり「N+1問題」を抱えているコードが多い印象でしたが、モジュールにまとめることで見通しがよくなり発生頻度を減らすことができたと感じています。</p> <h2 id="小--中規模であれば">小 ~ 中規模であれば</h2> <p>小 ~ 中規模のWebアプリケーション開発であれば、標準的なRailsの構成(MVC)で問題ないと思いました。</p> <p>ファットモデルになりそうであればモジュール(app/models/modules/)を追加して、簡単なデータ操作はモデル、複雑なデータ操作はモジュールで行なうことで解決できます。</p> <p>デザインパターンの導入は、規模が大きくなりモジュールの運用で課題が出てきた際に検討し始め、モジュールをデザインパターンにどう落とし込めばいいのかチームで話し合って決めることをオススメします。</p> <h1 id="おわりに">おわりに</h1> <p>引き継いだコードのリファクタリングだったので、少し特殊なケースのご紹介となりましたが、「Railsの標準構成 + モジュール」にすることで可読性がグッと上がり生産性が向上しました。</p> <p>今回のリファクタリングは、ベテランエンジニアのMさんがチーム内にいることで、方針決めや実装がスムーズに対応できました。</p> <p>経験豊富なエンジニアはデザインパターン・アンチパターンに精通していると思うので、積極的に意見を伺うことをオススメします。</p> <p>また、Railsのアーキテクチャは調べてみると考案された様々なものが出てきます。 プロダクトの規模や特性にあったものを取り入れることも大事だと思います。特にServiceクラスの利用は意見が分かれるので、チームの意見・プロダクトとあっているか調べてみると良さそうです。</p> <p>リファクタリングは現在も進めており、削除対象をすべて置き換えれていない状況です。</p> <p>今後、モジュールの肥大化など新しい課題が出てきた際は随時アップデートしつつ、このような形でアウトプットできたらなと思います。</p> smartcamp React Hook Form、Zod、Recoil を組み合わせたフォームを作る! hatenablog://entry/6801883189055300556 2023-11-02T12:00:00+09:00 2023-11-02T12:00:03+09:00 スマートキャンプでBOXIL SaaSのエンジニアをやってます職人こと袴田です! 今回は新規会員登録の画面に関してUI/UXの向上のための施策を対応したことについて紹介します。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231101/20231101215406.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#こんにちは職人です">こんにちは、職人です!</a></li> <li><a href="#BOXIL-SaaSとは">BOXIL SaaSとは</a></li> <li><a href="#新規のフォームを作る">新規のフォームを作る</a><ul> <li><a href="#React-Hook-Formとはなんぞや">React Hook Formとはなんぞや</a><ul> <li><a href="#基本的な使い方">基本的な使い方</a></li> </ul> </li> <li><a href="#Recoilとはなんぞや">Recoilとはなんぞや</a><ul> <li><a href="#基本的な使い方-1">基本的な使い方</a></li> </ul> </li> <li><a href="#Zodとはなんぞや">Zodとはなんぞや</a><ul> <li><a href="#基本的な使い方-2">基本的な使い方</a></li> </ul> </li> <li><a href="#React-Hook-Form--Recoil--Zod-を組み合わせると">React Hook Form &amp; Recoil &amp; Zod を組み合わせると</a></li> <li><a href="#苦労したこと">苦労したこと</a><ul> <li><a href="#入力した値がRecoilのステートに保存されない">入力した値がRecoilのステートに保存されない</a></li> <li><a href="#select-boxのonChangeが機能しない">select boxのonChangeが機能しない</a></li> <li><a href="#今入力している項目以外の項目のバリデーションが実行されてしまう">今入力している項目以外の項目のバリデーションが実行されてしまう</a></li> </ul> </li> </ul> </li> <li><a href="#次回へ続く">次回へ続く</a></li> </ul> <h2 id="こんにちは職人です">こんにちは、職人です!</h2> <p>スマートキャンプでBOXIL SaaSのエンジニアをやってます職人こと袴田です!<br /> 今回は新規会員登録の画面に関してUI/UXの向上のための施策を対応したことについて紹介します。</p> <h2 id="BOXIL-SaaSとは">BOXIL SaaSとは</h2> <p>BOXIL SaaSはSaaSを導入したいユーザーとSaaSを提供しているベンダーをつなぐリボンモデルのプロダクトです。</p> <h2 id="新規のフォームを作る">新規のフォームを作る</h2> <p>BOXIL SaaSでは一部Reactを使用しています。</p> <ul> <li>React Hook Form</li> <li>Zod</li> <li>Recoil</li> </ul> <p>今回はこちらのライブラリを組み合わせてフォームを作成しました。</p> <h3 id="React-Hook-Formとはなんぞや">React Hook Formとはなんぞや</h3> <p><a href="https://react-hook-form.com/">React Hook Form</a>とは、Reactでformを作るときに便利なライブラリです。</p> <h4 id="基本的な使い方">基本的な使い方</h4> <p>useFormから<a href="https://react-hook-form.com/docs/useform/register">register</a>と<a href="https://react-hook-form.com/docs/useform/handlesubmit">handleSubmit</a>を取得し、それぞれinputタグのpropsとsubmit時の処理に渡します。<br /> registerはスプレッド構文で展開されて、onChangeやonBlurなどのイベントハンドラーに渡されます。</p> <pre class="code tsx" data-lang="tsx" data-unlink>// react-hook-formからuserFormをimport import { useForm } from &#34;react-hook-form&#34;; type FormValues = { firstName: string; lastName: string; }; function MyForm() { // useFormからregisterとhandleSubmitを取得 const { register, handleSubmit } = useForm&lt;FormValues&gt;(); // submit時の処理を定義 const onSubmit = (data: FormValues) =&gt; console.log(data); return ( // onSubmitにhandleSubmitを渡す &lt;form onSubmit={handleSubmit(onSubmit)}&gt; &lt;input {...register(&#34;firstName&#34;)} /&gt; &lt;input {...register(&#34;lastName&#34;)} /&gt; &lt;input type=&#34;submit&#34; /&gt; &lt;/form&gt; ); } </pre> <h3 id="Recoilとはなんぞや">Recoilとはなんぞや</h3> <p><a href="https://github.com/facebookexperimental/Recoil">Recoil</a>とは、Reactで状態管理をするときに便利なライブラリです。</p> <h4 id="基本的な使い方-1">基本的な使い方</h4> <p>1.RecoilRootを設定する</p> <p>RecoilRootを使用して、Recoilの状態を管理します。 下記の例ではAppコンポーネントに包括されるコンポーネントでRecoilの状態を取得できるようになります。 これが設定されていないと、Recoilの状態を取得できません。</p> <pre class="code tsx" data-lang="tsx" data-unlink>import React from &#34;react&#34;; import ReactDOM from &#34;react-dom&#34;; import { RecoilRoot } from &#34;recoil&#34;; import { BrowserRouter } from &#34;react-router-dom&#34;; import { App } from &#34;./App&#34;; ReactDOM.render( &lt;React.StrictMode&gt; &lt;RecoilRoot&gt; &lt;BrowserRouter&gt; &lt;App /&gt; &lt;/BrowserRouter&gt; &lt;/RecoilRoot&gt; &lt;/React.StrictMode&gt;, document.getElementById(&#34;exampleApp&#34;) );</pre> <p>2.<code>atom</code>を定義する</p> <p>atomを使用して、管理したい状態を個別に定義します。<br /> atomとはアプリケーションの状態を管理するための単位と思っていただければいいと思います。</p> <pre class="code tsx" data-lang="tsx" data-unlink>import { atom } from &#39;recoil&#39;; export const countState = atom({ key: &#39;countState&#39;, default: 0, });</pre> <p>3.<code>useRecoilState</code>で状態を読み書きする</p> <p>useRecoilStateを使用して、値とセッターを取得できます。<br /> セッターを使用し、値を更新できます。</p> <pre class="code tsx" data-lang="tsx" data-unlink>import { useRecoilState } from &#39;recoil&#39;; import { countState } from &#39;./atoms&#39;; function Counter() { // countが値 // setCountがセッター // というイメージ const [count, setCount] = useRecoilState(countState); const increment = () =&gt; { // セッターを使って、countを更新する setCount(count + 1); }; return ( &lt;div&gt; &lt;p&gt;Count: {count}&lt;/p&gt; &lt;button onClick={increment}&gt;Increment&lt;/button&gt; &lt;/div&gt; ); }</pre> <h3 id="Zodとはなんぞや">Zodとはなんぞや</h3> <p><a href="https://github.com/colinhacks/zod">zod</a>とは、Reactでバリデーションをするときに便利なライブラリです。</p> <h4 id="基本的な使い方-2">基本的な使い方</h4> <p>z.objectを使用し、バリデーション実行対象の項目を設定したスキーマを定義します。</p> <pre class="code ts" data-lang="ts" data-unlink>import { z } from &#34;zod&#34;; const schema = z.object({ email: z.string().email().max(10), name: z.string().max(10), }); type Data = z.infer&lt;typeof schema&gt;; const data: Data = { name: &#34;tarou&#34;, age: 20 };</pre> <h3 id="React-Hook-Form--Recoil--Zod-を組み合わせると">React Hook Form &amp; Recoil &amp; Zod を組み合わせると</h3> <p>これらを組み合わせたフォームの作成例を簡単ではありますが、以下にまとめました。<br /> フォームの状態、バリデーションなどはカスタムフック(useUserForm.ts)としてまとめており、MyForm側ではimportして使用しています。</p> <p>useUserForm.ts</p> <pre class="code ts" data-lang="ts" data-unlink>import { zodResolver } from &#34;@hookform/resolvers/zod&#34;; import { useForm } from &#34;react-hook-form&#34;; import { atom, useRecoilState } from &#34;recoil&#34;; import { z } from &#34;zod&#34;; const userSchema = z .object({ email: z.string().nonempty().email().max(10), name: z.string().nonempty().max(10), company: z.string().nonempty() }) type UserForm = z.infer&lt;typeof userSchema&gt;; const defaultValues: UserForm = { email: &#34;&#34;, name: &#34;&#34;, company: &#34;&#34; } const form = atom&lt;UserForm&gt;({ key: &#34;useUserFormAtom&#34;, default: defaultValues, }); export const useUserForm = () =&gt; { const [formValues, setFormValues] = useRecoilState(form); const { register, handleSubmit, getValues, control, trigger, formState: { errors }, } = useForm({ resolver: zodResolver(userSchema), mode: &#34;onSubmit&#34;, defaultValues: formValues, }); const handleSetFormValues = () =&gt; { console.log(JSON.stringify(getValues())) setFormValues(getValues()); }; return { handleSubmit, register, control, trigger, setFormValues, formValues, handleSetFormValues, errors }; };</pre> <p>MyForm.tsx</p> <pre class="code tsx" data-lang="tsx" data-unlink>import { useUserForm } from &#34;./useUserForm&#34;; import { Controller } from &#34;react-hook-form&#34;; import Select from &#34;react-select&#34;; export function MyForm() { const userForm = useUserForm(); const handleSubmit = async (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; { e.preventDefault(); await userForm.trigger([ &#34;email&#34;, &#34;name&#34;, &#34;company&#34; ]); userForm.handleSetFormValues(); }; const OPTIONS = [ { label: &#34;A社&#34;, value: &#34;1&#34;, }, { label: &#34;B社&#34;, value: &#34;2&#34;, } ] return ( &lt;form onSubmit={(e) =&gt; handleSubmit(e)}&gt; {userForm.errors.email?.message &amp;&amp; &lt;p&gt;{userForm.errors.email?.message}&lt;/p&gt;} &lt;input {...userForm.register(&#34;email&#34;)} /&gt; {userForm.errors.name?.message &amp;&amp; &lt;p&gt;{userForm.errors.name?.message}&lt;/p&gt;} &lt;input {...userForm.register(&#34;name&#34;)} /&gt; {userForm.errors.company?.message &amp;&amp; &lt;p&gt;{userForm.errors.company?.message}&lt;/p&gt;} &lt;Controller name=&#34;company&#34; control={userForm.control} render={({ field }) =&gt; ( &lt;Select options={OPTIONS} value={OPTIONS.find( (option) =&gt; option.value === field.value )} {...userForm.register(&#34;company&#34;)} onChange={async (e) =&gt; e != null ? field.onChange(e.value) : null} /&gt; )} /&gt; &lt;input type=&#34;submit&#34; /&gt; &lt;/form&gt; ); }</pre> <h3 id="苦労したこと">苦労したこと</h3> <p>ここからは実際にプロダクトに落とし込む際に試行錯誤したことをご紹介します。</p> <h4 id="入力した値がRecoilのステートに保存されない">入力した値がRecoilのステートに保存されない</h4> <p>例えば入力したフォームの値を次の画面やフォームに移動したときも保持したい場面があるかと思います。<br /> しかし何も工夫せず画面遷移をしてしまうと、フォームに入力した値は保持できません。<br /> Recoilの状態にも保存されていない状態になります。<br /> Recoilを使用する場合はフォームに入力した値をRecoilの状態に保存するため、セッターを必ず呼ばなければいけません。</p> <pre class="code ts" data-lang="ts" data-unlink> const handleSetFormValues = () =&gt; { console.log(JSON.stringify(getValues())) // ここで入力した値を出力 setFormValues(getValues()); };</pre> <pre class="code tsx" data-lang="tsx" data-unlink> const handleSubmit = async (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; { // 省略 userForm.handleSetFormValues(); }; </pre> <p>前述の例ではonSubmitが発火したときに、handleSetFormValuesを経由してsetFormValuesを呼び出しRecoilの状態に保存しています。</p> <h4 id="select-boxのonChangeが機能しない">select boxのonChangeが機能しない</h4> <p>選択式の入力項目には<a href="https://react-select.com/home">react-select</a>を使用していました。<br /> しかしReact Hook Formと組み合わせると、registerで展開されたonChangeは機能しなくなります。</p> <p>react-selectはcontroledなコンポーネントであり、React Hook Formのregisterはuncontroledなコンポーネントにしか対応してないためです。</p> <p>これを解消するためにReact Hook Formの<a href="https://www.react-hook-form.com/api/usecontroller/controller/">Controller</a>という機能を使用します。<br /> Controllerでselect boxを囲い、renderの中でreact-selectを使用するとonChangeが機能するようになります。</p> <pre class="code tsx" data-lang="tsx" data-unlink>&lt;Controller name=&#34;company&#34; control={userForm.control} render={({ field }) =&gt; ( &lt;Select options={OPTIONS} value={OPTIONS.find( (option) =&gt; option.value === field.value )} {...userForm.register(&#34;company&#34;)} onChange={async (e) =&gt; e != null ? field.onChange(e.value) : null} /&gt; )} /&gt;</pre> <h4 id="今入力している項目以外の項目のバリデーションが実行されてしまう">今入力している項目以外の項目のバリデーションが実行されてしまう</h4> <p>React hook formではバリデーションの実行タイミングを指定できる<a href="https://react-hook-form.com/docs/useform#mode">モード</a>があります。<br /> これはonChange, onBlur, onSubmitなどのモードがあり、デフォルトではonChangeになっています。<br /> onChangeを使用している場合は、入力している項目の値を変更するとバリデーションが実行されますが、フォームに存在する他の入力項目に対しても実行されています。<br /> これは今現在仕様のようですので、あきらめましょう。</p> <p>解決策はonSubmitにすることです。</p> <pre class="code tsx" data-lang="tsx" data-unlink>const { register, handleSubmit, getValues, control, trigger, formState: { errors }, } = useForm({ resolver: zodResolver(userSchema), mode: &#34;onSubmit&#34;, // ここをonSubmitにする defaultValues: formValues, });</pre> <p><a href="https://react-hook-form.com/docs/useform/trigger">trigger</a>を使って任意のタイミングでバリデーションを実行できます。<br /> 何かのボタンをクリックしたときにバリデーションを実行する場合は以下のような実装になります。</p> <pre class="code tsx" data-lang="tsx" data-unlink>&lt;button type=&#34;button&#34; onClick={() =&gt; { userForm.trigger([ &#34;email&#34;, &#34;name&#34;, &#34;company&#34; ]); }} &gt;</pre> <h2 id="次回へ続く">次回へ続く</h2> <p>今回はReact Hook Form、Zod、Recoilを組み合わせてフォームを作成したときに苦労したことを紹介しました。</p> <p>実は今回紹介したこと以外にもまだ苦労したことがいくつかあります。<br /> 次回引き続きご紹介したいと思います!</p> smartcamp フロントエンドテストのはじめかた hatenablog://entry/6801883189051674058 2023-10-20T12:00:00+09:00 2023-10-20T15:54:55+09:00 はじめまして、もしくはまたお会いしましたね。BALES CLOUD(以下BC)エンジニアのてぃがです。 BCでは、最近フロントエンドのテストを始めました。 また、個人としても社内でフロントエンドのテストの普及啓蒙活動をやっております。 今回はこれらについてお話ししたいと思います。 ※注意※ はじめに 補記 BCはフロントエンドのユニットテストをどう始めたのか 1. 各種決め事 2. 手段の決定・詳細化 3. やってみる とはいえ、各ステップをどう流したのか? そうしてどうなった? おわりに ※注意※ この記事で取り扱う「フロントエンドテスト」は主に「フロントエンドのユニットテスト」です。 ご了… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231019/20231019101632.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>はじめまして、もしくはまたお会いしましたね。<a href="https://bales.smartcamp.co.jp/bales-cloud">BALES CLOUD</a>(以下BC)エンジニアのてぃがです。<br /> BCでは、最近フロントエンドのテストを始めました。<br /> また、個人としても社内でフロントエンドのテストの普及啓蒙活動をやっております。<br /> 今回はこれらについてお話ししたいと思います。<br /></p> <ul class="table-of-contents"> <li><a href="#注意">※注意※</a></li> <li><a href="#はじめに">はじめに</a><ul> <li><a href="#補記">補記</a></li> </ul> </li> <li><a href="#BCはフロントエンドのユニットテストをどう始めたのか">BCはフロントエンドのユニットテストをどう始めたのか</a><ul> <li><a href="#1-各種決め事">1. 各種決め事</a></li> <li><a href="#2-手段の決定詳細化">2. 手段の決定・詳細化</a></li> <li><a href="#3-やってみる">3. やってみる</a></li> <li><a href="#とはいえ各ステップをどう流したのか">とはいえ、各ステップをどう流したのか?</a></li> </ul> </li> <li><a href="#そうしてどうなった">そうしてどうなった?</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="注意">※注意※</h2> <p>この記事で取り扱う「フロントエンドテスト」は主に「フロントエンドのユニットテスト」です。<br /> ご了承ください。</p> <h2 id="はじめに">はじめに</h2> <p>これまで、BCにはフロントエンドのユニットテストがありませんでした。<br /> とはいえ、かわりにE2Eテストがあり、また、プロダクトの意品質に大きな問題もありませんでした。<br /> しかし、BCの開発フェーズは「立ち上げ」から「機能強化」に移り変わっています。<br /> その過程で、重要機能のまわりでちらほらとバグが見つかる、Vueのメジャーバージョンアップが必要になる(※)、などのことが起きはじめていました。<br /></p> <p>ところで、私はひとの十倍くらいの不安症です。<br /> 「石橋をたたきまくる」ように日々を過ごしているのですが、そんなエンジニアが、フロントエンドのリファクタや新規機能開発を、ユニットテストなしで実施するとどうなるでしょうか?</p> <p>そうですね、不安で爆発します。</p> <p>これをおりにつけてぼやいていたところ、<br /> 「じゃあつぎの半期から正式に時間取ってやってみようか!」<br /> と上長に声をかけてもらい、「BCのフロントエンドテスト始めようプロジェクト」がはじまったのでした。</p> <p>※Vueのバージョンアップについての詳細はこちらの記事が詳しい<br /> <a href="https://tech.smartcamp.co.jp/entry/vue3-migration-improve-frontend">Vue3にアップグレードしてフロントエンドを改善した話 - SMARTCAMP Engineer Blog</a></p> <h3 id="補記">補記</h3> <p>詳細は省きますが、BC開発チームでこのプロジェクトに着手できたのには理由があります。<br /> MISSION制と呼ばれている、<br /> 「各自がやりたい×チームが必要なことを、やりたい人が目標(※)に持って主導する」<br /> 仕組みがあったことです。<br /> (※人事評価上の目標)</p> <p>やりたいことをやって価値を出しつつ評価ももらえるって、とってもいいことです。</p> <h2 id="BCはフロントエンドのユニットテストをどう始めたのか">BCはフロントエンドのユニットテストをどう始めたのか</h2> <p>では、前提からあらためてお話しします。<br /> 動き始めの時点で、BCは以下のような状態でした。</p> <ul> <li>BCはSaaSで、定期的なアップデート・機能追加が必要不可欠</li> <li>フロントエンドのユニットテストがまったくない(E2Eはある)</li> <li>Vue3へのリプレイスの途中である</li> <li>重要機能でのバグがちらほら出ているが、これをできるだけ防ぎたい</li> </ul> <p>また、筆者は過去フロントエンドのユニットテストのコーディング経験がそこそこありました。<br /> これを踏まえ、BCでは以下のような流れでフロントエンドテストを始めることにしました。</p> <ol> <li>各種決め事(課題出し、目的の設定など)</li> <li>手段の決定・詳細化(技術選定など)</li> <li>やってみる(コードを書く)</li> </ol> <p>ではここからは、各ステップで実施したことをかいつまみつつご紹介します。</p> <h3 id="1-各種決め事">1. 各種決め事</h3> <p>プロジェクトの動き出しには「なぜやるのか」が必要です。(※諸説あり)<br /> ということで、各種決め事をしていきます。</p> <p>まずは、課題と目的を設定しました。<br /> 現状の具体的な分析と、理想など抽象的な思考を反復横跳びしつつ決めていきます。<br /> BCでは以下のようになりました。</p> <ul> <li>課題 <ul> <li>開発時の開発者の不安・精神的負荷が高い</li> <li>開発速度を守りつつも、品質を担保する必要がある</li> </ul> </li> <li>目的 <ul> <li>開発者の不安・精神的負荷を軽減し、BCとしてはやくコンスタントな価値提供ができるようにする</li> </ul> </li> </ul> <p>しかし、これでは抽象度が高いです。<br /> 理解しやすいよう、もう少し噛み砕いて具体化します。</p> <ul> <li>フロントエンドの実装追加・変更を安全に・気楽に行えるようにする <ul> <li>BCはプロダクトの特性上、はやく・たくさん試す(PDCAを回す、失敗する)ことが求められる</li> <li>そのための一つの手段として、主に開発者の以下の負荷を軽減する <ul> <li>システムの挙動(ロジック)が保証されていないことによる、リリースに対する不安感</li> <li>手動テストにかかる工数の削減</li> <li>テストの再現性の保証</li> </ul> </li> </ul> </li> </ul> <p>最後に、このプロジェクトの「やるやら」を決めます。<br /> このあとのステップでの動き方を明確にするためです。<br /> なお、「やるやら」はまず「やらない」ことを明確にすると、決めやすくていい感じです。<br /> 部分抜粋ですが以下の通りです。</p> <ul> <li>やらないこと <ul> <li>「見た目」(デザイン・レイアウト)に関するテスト <ul> <li>テストがあったとしても、最終的には人間の目でチェックする必要がある</li> <li>また、実装変更でテストコードが壊れやすく保護しづらい</li> </ul> </li> <li>〜略〜</li> </ul> </li> <li>やること <ul> <li>「ロジックの動作を保証する」テストをかく <ul> <li>※初期は、特に複雑なロジックについてはテストを書きたい</li> </ul> </li> </ul> </li> </ul> <p>これらの決め事はすべてドキュメントにまとめておき、いつでも参照できるようにしました。</p> <h3 id="2-手段の決定詳細化">2. 手段の決定・詳細化</h3> <p>ここからはよりエンジニア的なお仕事です。<br /> まずは技術選定をし、その後コーディングに関わるこまごましたルールを決めていきます。</p> <p>技術選定については、特殊な要件がなかったためデファクトスタンダードに従いました。<br /> これは、導入保守のしやすさなどに利点があります。</p> <p>技術ベースはこんな感じです。</p> <ul> <li><a href="https://vitest.dev/">Vitest</a></li> <li><a href="https://testing-library.com/docs/vue-testing-library/intro/">Vue Testing Library</a></li> <li><a href="https://mswjs.io/">MSW</a></li> </ul> <p>(今回の記事はあくまでテストの「始め方」にフォーカスしますので、これらの詳しい話は致しません。あしからず。)</p> <p>その後はこまごましたルール決めですが、コーディング規約から「どういう点はテストを書いて欲しいのか?」という考え方まで多岐に渡り、どこまで決めるべきかが難しいところです。<br /> 筆者はある程度ドキュメントをだ〜〜〜っと書いたところで、この沼にハマってしまいました。<br /> そこで弊社技術顧問に相談し、もらったアドバイスが以下です。</p> <p>「ある程度決まってるから、もう動いてみて考えたら?」(※要約)</p> <p>ですよね。</p> <h3 id="3-やってみる">3. やってみる</h3> <p>さて、楽しい開発のお時間です。<br /> いきなり全員に「やってくれ!」とも言いづらいので、まずは首謀者である筆者が走ってみることにしました。<br /> 「みんながテストコードを書ける」ところまでを整えていきます。</p> <p>書けることはたくさんあるのですが、ざっくり紹介します。</p> <ul> <li>環境構築 <ul> <li>技術選定したいろいろを入れる</li> </ul> </li> <li>責任を持ってサンプルコードを書く <ul> <li>必要となりそうなテストパターンを洗い出し、該当するサンプルコードを書いていく <ul> <li>ロジックのみのテスト</li> <li>componentのrenderのテスト</li> <li>componentのmethod, computedのテスト</li> <li>API callのテスト</li> <li>などなど</li> </ul> </li> </ul> </li> <li>やってみる&やってみてもらう <ul> <li>勉強会などで基本の知識を共有</li> <li>準備したサンプルコードも提供</li> </ul> </li> </ul> <p>こんな感じで走り回りました。<br /> やってみると思ったより難しくなく、急に方針転換を強いられるようなこともありませんでした。<br /> (もちろん、こまごまとハマるところはありましたが...。)<br /> 筆者の実力というよりは、Vitestが良かったのです。<br /> 公式ドキュメントがしっかり書かれていたり、JestのコンパチなのでJestの知見が流用できたりと、救われる点は色々ありました。<br /> 公式ドキュメント大好きエンジニアとして、Vitestのドキュメントは推せます。</p> <p>また、実際にコードを書くにあたって、「どこからテストを書いていくか」について追加で少しだけ決め事をしました。</p> <ul> <li>優先度の高いテストから書く <ul> <li>BCとして重要な機能まわりから書く</li> </ul> </li> <li>書きやすいテストから書く <ul> <li>新しく作った機能から書く</li> </ul> </li> </ul> <h3 id="とはいえ各ステップをどう流したのか">とはいえ、各ステップをどう流したのか?</h3> <p>ここまで各ステップについてご説明しましたが、何事もフローなので、各ステップをどう「流した」かも大切です。<br /> 各ステップごとに以下を繰り返し、物事を進めていきました。</p> <ol> <li>チームに匂わせ(頭出し)しつつ、首謀者がガーっとうごく</li> <li>チームに共有・説明</li> <li>チームの合意形成とフィードバックの受け取り</li> </ol> <p>気をつけていたポイントは以下の通りです。</p> <p>まず、チームメンバーを不安にさせないこと。<br /> 「知らない」不安感をなるべくなくすよう、情報を早い段階で共有し合意をとるよう心がけました。<br /> 一方で、「フロントエンドのテストが必要」という点も認識を合わせておいたため、全員の意見を全部聴くのではなく、動くところははやく動いて先に進めることができました。<br /> 何かあったらその時考えればよいのです。</p> <p>次に、とにかくドキュメントを書くこと。<br /> 筆者は兎角忘れっぽいので、ドキュメントが好きです。<br /> ドキュメント化することで情報は民主化されます。<br /> 後からチームに参画した人も情報を見ることができ、適宜まとめておくことで必要な情報を見つけやすくもなります。<br /> 幸いメンテナンスが必要な類のドキュメントでもないので、この時点で書いておくのはいいことづくめです。<br /> とくに、目的や方針のドキュメントは必要でした。<br /> 進んでいく過程で悩んだときに、立ち戻って考えるポイントになりました。</p> <p>最後に、「なにはともあれやってみよう!」という心意気を共有すること。<br /> できるだけ早く価値を提供し、失敗するなら失敗してリカバリし、少しでも前に進めることを意識しました。<br /> これは、BCで大切にしているアジャイルの考え方でもあります。<br /> 先ほども書きましたが、何かあったらその時考えればよいのです。<br /> 通常の開発だとここまで気楽にもいられませんが、これはテストの話なので、失敗したとてプロダクトが壊れるわけでもありません。</p> <p><figure class="figure-image figure-image-fotolife" title="フロントエンドテストドキュメントまとめページ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231019/20231019101717.png" width="683" height="867" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>フロントエンドテストドキュメントまとめページ</figcaption></figure></p> <h2 id="そうしてどうなった">そうしてどうなった?</h2> <p>このプロジェクトをへて、BCでは「みんながテストコードを書ける」ところまでは整いました。<br /> テストコードも少しずつ増えてきており、テストを書く雰囲気はぼちぼちですが醸成されてきているように思います。<br /> コードレビュー時に筆者が「ここフロントエンドテストチャンス!」などと煽って書かせているのも否めませんが(笑)</p> <p>ただし、今もなお残っている課題もあります。</p> <p>まず、フロントエンドのテストの正解がわかりません。<br /> 手探りに書いている感じがあります。<br /> とはいえ「私が『これが正解だ!』とか言い出したらひっぱたいてくれ」という気持ちもありますので、これはこれでいいのかもしれません。</p> <p>また、既存のコードのテストがなかなか書けません。<br /> 空き時間があればまとめて書きたいところですが、現実としてそんなものはなく、テストを書くだけのタスクを積むのは難しいです。<br /> これは、大きめのリファクタをする前にはテストを書く、勉強会(後述)でもくもくするなどの手段で地道に対応しています。</p> <h2 id="おわりに">おわりに</h2> <p>長々お話ししましたが、フロントエンドテストの始め方の話はこの辺で終わりです。<br /> 自ら旗を振ってやり始めたことですが、「まあなんとか軌道に乗ってよかったよかった」というかんじです。</p> <p>Vueでは、Composition APIの導入によりComponentとロジックが分離される傾向にあります。<br /> フロントエンドのテストはComposableと大変相性が良く、個人的には「今後需要も増えてくるんだろうな〜」とふくふくしております。</p> <p>また、おまけ話ですが、布教活動の一環としてフロントエンドテストの勉強会を始めたりもしました。<br /> テーマは「大人の児童館」。<br /> フロントエンドテストに親しみ、これについて話す場の提供を目的に、社内誰でも参加OKのオンラインイベントを毎週開催しています。<br /> 毎回お楽しみコンテンツ(※フロントエンドかテストに関わっているなにか)を提供してはいます。<br /> が、コンテンツに参加してリアクションする、もくもくする、ラジオ感覚で聴き流す、なんでもよしでゆるくやっています。<br /></p> <p>しかし、こんなことをやっていたら社内で「なんかフロントエンドの人」のような立ち位置になってきたので、ちょっと役者不足が不安ではありますが...(笑)</p> <p>では、またお会いしましょう!</p> smartcamp Rails+ReactプロジェクトでWebpackからViteに乗り換えたら、開発が劇的に快適になった話 hatenablog://entry/820878482973190078 2023-10-06T12:00:00+09:00 2023-10-06T12:00:23+09:00 はじめに なぜViteに移行したか 導入方針 開発環境に導入 vite側の作業 詰まったところ vite自体に付属するmanifestオプションを使用すると、manifest.jsonの形式が大幅に変わってしまう 同じスタイルを複数のエントリーポイントで読み込むとファイル名が変わってしまう Rails側の作業 ビルドの設定 Staging、Pre環境へのデプロイ検証 リリース 結果 今後 最後に はじめに こんにちは!スマートキャンプ開発エンジニアの林(ぱずー)です。 BOXIL SaaSのフロントエンドは歴史的経緯からjQuery、CoffeeScript、Vue、Reactが混在した環境で… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231005/20231005123855.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#なぜViteに移行したか">なぜViteに移行したか</a></li> <li><a href="#導入方針">導入方針</a></li> <li><a href="#開発環境に導入">開発環境に導入</a><ul> <li><a href="#vite側の作業">vite側の作業</a></li> <li><a href="#詰まったところ">詰まったところ</a><ul> <li><a href="#vite自体に付属するmanifestオプションを使用するとmanifestjsonの形式が大幅に変わってしまう">vite自体に付属するmanifestオプションを使用すると、manifest.jsonの形式が大幅に変わってしまう</a></li> <li><a href="#同じスタイルを複数のエントリーポイントで読み込むとファイル名が変わってしまう">同じスタイルを複数のエントリーポイントで読み込むとファイル名が変わってしまう</a></li> </ul> </li> <li><a href="#Rails側の作業">Rails側の作業</a></li> </ul> </li> <li><a href="#ビルドの設定">ビルドの設定</a></li> <li><a href="#StagingPre環境へのデプロイ検証">Staging、Pre環境へのデプロイ検証</a></li> <li><a href="#リリース">リリース</a></li> <li><a href="#結果">結果</a></li> <li><a href="#今後">今後</a></li> <li><a href="#最後に">最後に</a></li> </ul> <h1 id="はじめに">はじめに</h1> <p>こんにちは!スマートキャンプ開発エンジニアの林(ぱずー)です。</p> <p>BOXIL SaaSのフロントエンドは歴史的経緯からjQuery、CoffeeScript、Vue、Reactが混在した環境で開発しています。 今回はReactで作られているページにViteを導入したので、経緯やハマったことについて書こうと思います。</p> <ul> <li><a href="https://tech.smartcamp.co.jp/entry/improvement-with-react-and-monorepo">BOXIL SaaSにReactを導入した記事はこちら</a></li> </ul> <h1 id="なぜViteに移行したか">なぜViteに移行したか</h1> <p>webpackはビルドが遅く、本番環境のビルドもファイル数が少ないのにも関わらず、3〜4分ほどかかっていたり、開発環境でもコードを変更するごとに30秒ほど待たされるためスピード感を持って開発できる状態ではありませんでした。</p> <p>また、移行先としてTurbopackも検討しましたが、Railsと合わせて使う場合においては、 ドキュメントの充実度や日本語の情報も多いことから、Viteの方が移行しやすいと感じたため、選択しました。</p> <p>ちなみに過去に導入を検討したこともあったのですが、当時はInternet Explorer対応が必須だったため見送りました。 ようやくInternet Explorerがサポート外となったので(とはいっても去年ですが)、晴れて導入できるようになりました。</p> <h1 id="導入方針">導入方針</h1> <ol> <li>開発環境に導入</li> <li>ビルドの設定</li> <li>Staging、Pre環境へのデプロイ検証</li> <li>リリース</li> </ol> <h1 id="開発環境に導入">開発環境に導入</h1> <p>BOXIL SaaSのReactはRails上のhtml上でReactを読み込んでいるため、デフォルト設定では動きません。 そのため、<a href="https://vitejs.dev/guide/backend-integration.html">Backend Integration</a>に従って導入しました。</p> <h2 id="vite側の作業">vite側の作業</h2> <p>この場合、読み込む必要があるファイルをオブジェクト形式でrollupOptions.inputに記述していくのですが、そのままどんどん追記していくとかなり設定ファイルの見通しが悪くなってしまいます。 そこで、glob.syncを使用して、手動で読み込むファイルを追加する手間を解消したいと考えました。</p> <p>webpack時代はこんなコードが何行もあり、ファイルを追加するごとに書き足していくやり方でした...。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> entries <span class="synStatement">=</span> <span class="synIdentifier">{</span> hoge: <span class="synConstant">&quot;./hoge.tsx&quot;</span><span class="synStatement">,</span> ...この後何行にもわたるエントリーポイントの記述 <span class="synIdentifier">}</span> </pre> <p>ただ、現状すべてのファイルがルートディレクトリ直下にフラットに配置されており、glob.syncで自動で取得するには無視するディレクトリをignoreオプションで設定しなければなりませんでした。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> srcFileKeys <span class="synStatement">=</span> glob.sync<span class="synStatement">(</span><span class="synConstant">&quot;**/*.+(js|ts|tsx)&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> cwd: srcDir<span class="synStatement">,</span> ignore: <span class="synIdentifier">[</span><span class="synConstant">'.eslintrc.js'</span><span class="synStatement">,</span> <span class="synConstant">'vite.config.ts'</span><span class="synIdentifier">]</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> </pre> <p>それを解消するために、entriesディレクトリにすべてのエントリーポイントを集約することでglob.syncを手軽に使用できるようにしました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> convertToEntryKey <span class="synStatement">=</span> <span class="synStatement">(</span>key: <span class="synType">string</span><span class="synStatement">,</span> ext: <span class="synConstant">&quot;.js&quot;</span> | <span class="synConstant">&quot;.css&quot;</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> key.replace<span class="synStatement">(</span><span class="synConstant">/^entries\//</span><span class="synStatement">,</span> <span class="synConstant">&quot;&quot;</span><span class="synStatement">)</span>.replace<span class="synStatement">(</span><span class="synConstant">/\.[^/</span>.<span class="synIdentifier">]</span>+$/<span class="synStatement">,</span> ext<span class="synStatement">);</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synType">const</span> entries: <span class="synIdentifier">{</span> <span class="synIdentifier">[</span>key: <span class="synType">string</span><span class="synIdentifier">]</span>: <span class="synType">string</span> <span class="synIdentifier">}</span> <span class="synStatement">=</span> <span class="synIdentifier">{}</span><span class="synStatement">;</span> <span class="synType">const</span> srcDir <span class="synStatement">=</span> <span class="synConstant">&quot;./&quot;</span><span class="synStatement">;</span> <span class="synType">const</span> srcFileKeys <span class="synStatement">=</span> glob.sync<span class="synStatement">(</span><span class="synConstant">&quot;entries/**/*.+(js|ts|tsx)&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> cwd: srcDir<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synType">const</span> srcStyleKeys <span class="synStatement">=</span> glob.sync<span class="synStatement">(</span><span class="synConstant">&quot;styles/**/*.+(scss|css)&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> cwd: srcDir<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">const</span> key <span class="synStatement">of</span> srcFileKeys<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> srcFilepath <span class="synStatement">=</span> path.join<span class="synStatement">(</span>srcDir<span class="synStatement">,</span> key<span class="synStatement">);</span> <span class="synIdentifier">this</span>.entries<span class="synIdentifier">[</span>convertToEntryKey<span class="synStatement">(</span>key<span class="synStatement">,</span> <span class="synConstant">&quot;.js&quot;</span><span class="synStatement">)</span><span class="synIdentifier">]</span> <span class="synStatement">=</span> srcFilepath<span class="synStatement">;</span> <span class="synIdentifier">}</span> <span class="synStatement">for</span> <span class="synStatement">(</span><span class="synType">const</span> key <span class="synStatement">of</span> srcStyleKeys<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> srcFilepath <span class="synStatement">=</span> path.join<span class="synStatement">(</span>srcDir<span class="synStatement">,</span> key<span class="synStatement">);</span> <span class="synIdentifier">this</span>.entries<span class="synIdentifier">[</span>convertToEntryKey<span class="synStatement">(</span>key<span class="synStatement">,</span> <span class="synConstant">&quot;.css&quot;</span><span class="synStatement">)</span><span class="synIdentifier">]</span> <span class="synStatement">=</span> srcFilepath<span class="synStatement">;</span> <span class="synIdentifier">}</span> <span class="synStatement">export</span> <span class="synStatement">default</span> defineConfig<span class="synStatement">(</span><span class="synIdentifier">{</span> build: <span class="synIdentifier">{</span> rollupOptions: <span class="synIdentifier">{</span> input: entries<span class="synStatement">,</span> ...other <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> </pre> <p>これでrailsのhtml側でファイル名を指定するとjsを読み込めるようにしています。</p> <p><strong>例:</strong></p> <pre class="code" data-lang="" data-unlink>vite_javascript_tag(&#39;hoge&#39;)</pre> <h2 id="詰まったところ">詰まったところ</h2> <h3 id="vite自体に付属するmanifestオプションを使用するとmanifestjsonの形式が大幅に変わってしまう">vite自体に付属するmanifestオプションを使用すると、manifest.jsonの形式が大幅に変わってしまう</h3> <p>解決策として、<a href="https://www.npmjs.com/package/rollup-plugin-output-manifest">rollup-plugin-output-manifest</a>を導入し、今までと変わらないやり方でmanifest.jsonを出力できるようにしました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> outputManifest <span class="synStatement">from</span> <span class="synConstant">&quot;rollup-plugin-output-manifest&quot;</span><span class="synStatement">;</span> <span class="synStatement">export</span> <span class="synStatement">default</span> defineConfig<span class="synStatement">(</span><span class="synIdentifier">{</span> appType: <span class="synConstant">&quot;custom&quot;</span><span class="synStatement">,</span> plugins: <span class="synIdentifier">[</span> outputManifest<span class="synStatement">(</span><span class="synIdentifier">{</span> nameWithExt: <span class="synConstant">false</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">),</span> ...other <span class="synIdentifier">]</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> </pre> <p><strong>before:</strong></p> <pre class="code lang-json" data-lang="json" data-unlink> &quot;<span class="synStatement">_hoge-4dc41c9a.js</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">file</span>&quot;: &quot;<span class="synConstant">javascripts/hoge-4dc41c9a.js</span>&quot;, &quot;<span class="synStatement">imports</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">_styled-components.browser.esm-0475c222.js</span>&quot;, &quot;<span class="synConstant">_colors-52758e30.js</span>&quot;, &quot;<span class="synConstant">entries/hoge.tsx</span>&quot; <span class="synSpecial">]</span> <span class="synSpecial">}</span>, </pre> <p><strong>after:</strong></p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">hoge.js</span>&quot;: &quot;<span class="synConstant">/bundles/javascripts/hoge-0403de2a.js</span>&quot;<span class="synError">,</span> <span class="synError">}</span> </pre> <h3 id="同じスタイルを複数のエントリーポイントで読み込むとファイル名が変わってしまう">同じスタイルを複数のエントリーポイントで読み込むとファイル名が変わってしまう</h3> <p><a href="https://github.com/nolimits4web/swiper">swiper</a>を複数のページで使用する必要があったのですが、 読み込んだ回数によってmanifest.jsonに吐き出されるファイル名が変わってしまうことが判明しました。</p> <p><strong>1回だけ読み込んだ場合:</strong> スタイルを読み込んだ場所のファイル名で吐き出される。</p> <pre class="code lang-json" data-lang="json" data-unlink>&quot;<span class="synStatement">hoge.css</span>&quot;: &quot;<span class="synError">http</span>:<span class="synError">//localhost:3000//bundles/stylesheets/hoge.css&quot;</span> </pre> <p><strong>2回以上読み込んだ場合:</strong> なぜかswiper.cssとして吐き出される。</p> <pre class="code lang-json" data-lang="json" data-unlink>&quot;<span class="synStatement">swiper.css</span>&quot;: &quot;<span class="synError">http</span>:<span class="synError">//localhost:3000//bundles/stylesheets/swiper.css&quot;</span> </pre> <p>この状態だと、rails側でスタイルを読み込むときに指定するファイルが読み込む回数によって変わってしまいます。</p> <p>解決策としてoutputManifestのmapオプションで出力するmanifest.jsonのキーを手動でマッピングしてあげることで、glob.syncで読み込んだファイルのみ出力するように変更しました。</p> <p>これで、manifest.jsonに記載するファイルをglob.syncで読み込んだファイルに限定できるので、読み込み回数によってファイル名が変わることはなくなりました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> outputManifest <span class="synStatement">from</span> <span class="synConstant">&quot;rollup-plugin-output-manifest&quot;</span><span class="synStatement">;</span> <span class="synType">const</span> getEntryName <span class="synStatement">=</span> <span class="synStatement">(</span>name: <span class="synType">string</span> | <span class="synType">undefined</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>name<span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synConstant">&quot;&quot;</span><span class="synStatement">;</span> <span class="synType">const</span> entryPaths <span class="synStatement">=</span> <span class="synSpecial">Object</span>.keys<span class="synStatement">(</span>entries<span class="synStatement">);</span> <span class="synType">const</span> entryPath <span class="synStatement">=</span> entryPaths.find<span class="synStatement">((</span>entryPath<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> entryPath.includes<span class="synStatement">(</span>name<span class="synStatement">));</span> <span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>entryPath<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synSpecial">throw</span> <span class="synStatement">new</span> <span class="synSpecial">Error</span><span class="synStatement">(</span> <span class="synConstant">`entryPath is not found \ntarget: </span><span class="synSpecial">${</span>name<span class="synSpecial">}</span><span class="synConstant"> \nentries: \n</span><span class="synSpecial">${</span>entryPaths.join( <span class="synConstant">&quot;\n&quot;</span> )<span class="synSpecial">}</span><span class="synConstant"> `</span> <span class="synStatement">);</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> entryPath<span class="synStatement">;</span> <span class="synIdentifier">}</span> <span class="synStatement">export</span> <span class="synStatement">default</span> defineConfig<span class="synStatement">(</span><span class="synIdentifier">{</span> appType: <span class="synConstant">&quot;custom&quot;</span><span class="synStatement">,</span> plugins: <span class="synIdentifier">[</span> outputManifest<span class="synStatement">(</span><span class="synIdentifier">{</span> nameWithExt: <span class="synConstant">false</span><span class="synStatement">,</span> map: <span class="synStatement">(</span>chunk: Bundle<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> name <span class="synStatement">=</span> getEntryName<span class="synStatement">(</span>chunk.name<span class="synStatement">);</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> ...chunk<span class="synStatement">,</span> name<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span><span class="synStatement">),</span> ...other <span class="synIdentifier">]</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> </pre> <h2 id="Rails側の作業">Rails側の作業</h2> <p>Rails側でも上記のmanifest.jsonを読み込むための作業をしました。 viteをRuby上で動かすにあたって<a href="https://vite-ruby.netlify.app/">Vite Ruby</a>を使用することも検討しましたが、 ライブラリを導入することによる依存の発生や、vite以外の選択肢が今後出てきたときに乗り換えやすいようにVite Rubyの<a href="https://github.com/ElMassimo/vite_ruby/blob/main/vite_rails/lib/vite_rails/tag_helpers.rb">実装</a>を参考に必要な部分だけ使用する形で独自実装することにしました。</p> <p>すべてのコードの記載は割愛しますが、webpack時代の処理に比べてScriptタグをmoduleで読み込む点と、開発サーバーと接続する設定が入っている点が大きく異なってます。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink> <span class="synPreProc">def</span> <span class="synIdentifier">vite_react_refresh_tag</span> react_refresh_preamble&amp;.html_safe <span class="synStatement">if</span> dev_server_running? <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">vite_client_tag</span>(<span class="synConstant">crossorigin</span>: <span class="synSpecial">'</span><span class="synConstant">anonymous</span><span class="synSpecial">'</span>, **options) javascript_include_tag(vite_dev_host(<span class="synSpecial">'</span><span class="synConstant">@vite/client</span><span class="synSpecial">'</span>), <span class="synConstant">type</span>: <span class="synSpecial">'</span><span class="synConstant">module</span><span class="synSpecial">'</span>, <span class="synConstant">extname</span>: <span class="synConstant">false</span>, <span class="synConstant">crossorigin</span>: crossorigin, **options) <span class="synStatement">if</span> dev_server_running? <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">vite_javascript_tag</span>(entry, <span class="synConstant">type</span>: <span class="synSpecial">'</span><span class="synConstant">module</span><span class="synSpecial">'</span>, <span class="synConstant">skip_preload_tags</span>: <span class="synConstant">false</span>, <span class="synConstant">crossorigin</span>: <span class="synSpecial">'</span><span class="synConstant">anonymous</span><span class="synSpecial">'</span>, **options) <span class="synComment"># MEMO: 開発中の場合は開発サーバからjsを取得する</span> <span class="synStatement">return</span> javascript_include_tag(vite_dev_host(<span class="synSpecial">&quot;</span><span class="synConstant">entries/</span><span class="synSpecial">#{</span>entry<span class="synSpecial">}&quot;</span>), <span class="synConstant">crossorigin</span>: crossorigin, <span class="synConstant">type</span>: type, <span class="synConstant">extname</span>: <span class="synConstant">false</span>, **options) <span class="synStatement">if</span> dev_server_running? files = vite_manifest.fetch(<span class="synSpecial">&quot;#{</span>entry<span class="synSpecial">}</span><span class="synConstant">.js</span><span class="synSpecial">&quot;</span>) tags = javascript_include_tag(*files, <span class="synConstant">crossorigin</span>: crossorigin, <span class="synConstant">type</span>: type, <span class="synConstant">extname</span>: <span class="synConstant">false</span>, <span class="synConstant">defer</span>: <span class="synConstant">true</span>, **options) <span class="synComment"># modulepreloadタグを生成してモジュールを先読みする</span> tags &lt;&lt; vite_preload_tag(*files, <span class="synConstant">crossorigin</span>: crossorigin, **options) <span class="synStatement">unless</span> skip_preload_tags tags <span class="synPreProc">end</span> </pre> <h1 id="ビルドの設定">ビルドの設定</h1> <p>本番環境ではCIでビルドされたファイルをS3に配置しているため、manifest.jsonで参照されるURLをS3に向けてあげる必要があります。 そこでrollup-plugin-output-manifestにpublicPathのオプションを追加し、環境に合わせてURLを変更できるようにしました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">default</span> defineConfig<span class="synStatement">(</span><span class="synIdentifier">{</span> plugins: <span class="synIdentifier">[</span> outputManifest<span class="synStatement">(</span><span class="synIdentifier">{</span> publicPath: <span class="synConstant">`</span><span class="synSpecial">${</span> <span class="synSpecial">process</span>.env.PRECOMPILED_ASSETS_HOST ? <span class="synSpecial">process</span>.env.PRECOMPILED_ASSETS_HOST : <span class="synConstant">&quot;&quot;</span> <span class="synSpecial">}</span><span class="synConstant">/bundles/react/`</span><span class="synStatement">,</span> nameWithExt: <span class="synConstant">false</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">),</span> <span class="synIdentifier">]</span><span class="synStatement">,</span> ...otherConfig <span class="synIdentifier">}</span><span class="synStatement">);</span> </pre> <p>こうすることで、以下のようにURL付きのmanifest.jsonが生成されるので、読み込む先を変更できます。</p> <pre class="code lang-json" data-lang="json" data-unlink> &quot;<span class="synStatement">hoge.js</span>&quot;: &quot;<span class="synConstant">https://domain/bundles/react/javascripts/hoge.js-350806ce.js</span>&quot;, </pre> <p>詰まったところとして、swiper(7.4.1)を使用しているページがビルドがうまくいかない問題が発生したので、aliasを書いてあげることで解消できました。</p> <p><strong>エラー文:</strong></p> <pre class="code" data-lang="" data-unlink>[commonjs--resolver] Missing &#34;./swiper-bundle.min.css&#34; specifier in &#34;swiper&#34; package error during build: Error: Missing &#34;./swiper-bundle.min.css&#34; specifier in &#34;swiper&#34; package</pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">default</span> defineConfig<span class="synStatement">(</span><span class="synIdentifier">{</span> resolve: <span class="synIdentifier">{</span> alias: <span class="synIdentifier">{</span> swiper: path.resolve<span class="synStatement">(</span><span class="synSpecial">__dirname</span><span class="synStatement">,</span> <span class="synConstant">&quot;node_modules/swiper&quot;</span><span class="synStatement">),</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> ...otherConfig <span class="synIdentifier">}</span><span class="synStatement">);</span> </pre> <p>現時点ではswiperでしか発生していませんが、他のライブラリでも発生する可能性もあるので、注意が必要です。</p> <p>その他、CicleCIでメモリの問題で頻繁に落ちてしまう問題が発生したため、ジョブを分けることで回避しました。(ちなみにビルドに20秒ってだいぶ早いですね)</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231005/20231005123939.png" width="350" height="280" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="StagingPre環境へのデプロイ検証">Staging、Pre環境へのデプロイ検証</h1> <p>試行錯誤しつつも、開発環境での動作は問題ない状態になったので、staging環境にデプロイしたところ、JavaScriptの取得リクエストで404エラーが大量発生しました。 ドキュメントを追ってみると、デフォルトでは<a href="https://ja.vitejs.dev/config/build-options.html#build-modulepreload">build.modulePreload</a>オプションが有効となっており、自動でpreloadするためのjsを読み込んでいた事が原因でした。 preloadタグの生成はRails側で行なっているため、設定を無効化することで解消できました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">default</span> defineConfig<span class="synStatement">(</span><span class="synIdentifier">{</span> ...otherConfig build: <span class="synIdentifier">{</span> modulePreload: <span class="synConstant">false</span><span class="synStatement">,</span> <span class="synComment">// デフォルトではpolyfill: trueとなっていて、preloadするためのpollyfillが自動注入されてしまう</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> </pre> <h1 id="リリース">リリース</h1> <p>現状、残念ながらフロントエンドのテストがほとんど書かれていないため、開発チームで手作業で頑張りました。</p> <p>具体的には、BOXIL SaaSの主要ページシナリオを作成し、Vite導入に影響する箇所を確認していくことで、リリース後のバグが少しでも発生しないように努めました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20231005/20231005123954.png" width="1200" height="191" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>結果、幸いにも本番リリース後も特に大きな問題は起こらなかったので、無事に完遂できました。</p> <h1 id="結果">結果</h1> <p>ローカル環境でのコード反映:</p> <p><strong>30秒 → 1秒未満</strong></p> <p>これは例えば、10行のコードを変更した場合、webpackだと反映に300秒かかっていたのが、Viteだったら10秒で反映できることになります。</p> <p>ちなみに、webpack時代はコードの反映が遅すぎて、コードの保存を極力まとめて行なっていたのですが、Vite導入によって臆することなく、コード保存するショートカットキーを多用できるようになったので個人的には数字以上の開発体験の向上している実感があります。</p> <p>ビルド時間:</p> <p><strong>49秒 → 7秒</strong></p> <p>現状コード量が少ないことや、構成上、Railsのアプリケーションのビルドが速くならないと結果的にデプロイ速度は向上しないため、そこまで重視していない項目でしたが、こちらも大幅に改善していました。 今後BOXIL SaaSにおいてReactのコードがメインになってくる(していく)ことを考えると、 ビルド速度はボトルネックになりうる部分なので、早めに改善できてよかったです。</p> <h1 id="今後">今後</h1> <p>冒頭にも説明したとおり、歴史的経緯からBOXIL SaaSのフロントエンドはカオスな状態なのですが、</p> <p>ここ一年でyarn workspaces(v3)導入によるモノレポ化と、今回の記事で取り上げたVite導入をおこなってきました。</p> <p>直近あったプロジェクトではReactを使用してスピード感を持って開発できているので、 これまでの地道な改善による効果が一定数あったのかなと感じています。</p> <p>今後の個人的な展望としては直近のVitest導入に合わせて、継続的にテストを書いていける環境・方針の策定を進めて更なる開発体験の向上を推進していくことや、</p> <p>統一されておらずページごとにバラバラになっているコンポーネントをデザイナーさんと協力して整理していきたいと考えています。</p> <h1 id="最後に">最後に</h1> <p>長い歴史を持つプロダクトの負を改善していくにあたって、工数の兼ね合いから諦めなければいけない部分も出てくる事がありますが、 少ない工数で劇的な変化をもたらすこともできることをViteを導入してみて感じました。</p> <p>今後もこのような改善を継続的に行なって開発効率を上げて、さらなるプロダクトの価値向上に貢献していきたいです!</p> smartcamp 共通ID基盤開発の裏側:OIDCとビジネス要望のギャップ hatenablog://entry/820878482967049940 2023-09-13T14:00:00+09:00 2023-09-13T14:00:00+09:00 はじめに 対象読者 主なキーワード 共通ID基盤プロジェクトについて なぜプロジェクトを開始したのか? 共通ID基盤構築の要件 共通ID基盤の技術選定 認証基盤に関連する技術群 どの技術を使うべきか? アーキテクチャの検討 隠れたサービス要件の発覚 サービスに求められる要件について OIDCで必須のOPへのリダイレクト OIDCとサービス要件の不一致 別の方法を探る 1. 主力プロダクトをOPとする案 2. サービスのドメインを統合する 3. サービスをコードレベルで統合する 終わりに はじめに スマートキャンプ株式会社京都開発拠点では、自社開発プロダクトであるSaaSマッチングプラットフォー… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230913/20230913105433.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#対象読者">対象読者</a></li> <li><a href="#主なキーワード">主なキーワード</a></li> <li><a href="#共通ID基盤プロジェクトについて">共通ID基盤プロジェクトについて</a><ul> <li><a href="#なぜプロジェクトを開始したのか">なぜプロジェクトを開始したのか?</a></li> <li><a href="#共通ID基盤構築の要件">共通ID基盤構築の要件</a></li> </ul> </li> <li><a href="#共通ID基盤の技術選定">共通ID基盤の技術選定</a><ul> <li><a href="#認証基盤に関連する技術群">認証基盤に関連する技術群</a></li> <li><a href="#どの技術を使うべきか">どの技術を使うべきか?</a></li> </ul> </li> <li><a href="#アーキテクチャの検討">アーキテクチャの検討</a></li> <li><a href="#隠れたサービス要件の発覚">隠れたサービス要件の発覚</a><ul> <li><a href="#サービスに求められる要件について">サービスに求められる要件について</a></li> <li><a href="#OIDCで必須のOPへのリダイレクト">OIDCで必須のOPへのリダイレクト</a></li> <li><a href="#OIDCとサービス要件の不一致">OIDCとサービス要件の不一致</a></li> </ul> </li> <li><a href="#別の方法を探る">別の方法を探る</a><ul> <li><a href="#1-主力プロダクトをOPとする案">1. 主力プロダクトをOPとする案</a></li> <li><a href="#2-サービスのドメインを統合する">2. サービスのドメインを統合する</a></li> <li><a href="#3-サービスをコードレベルで統合する">3. サービスをコードレベルで統合する</a></li> </ul> </li> <li><a href="#終わりに">終わりに</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>スマートキャンプ株式会社京都開発拠点では、自社開発プロダクトであるSaaSマッチングプラットフォーム「<a href="https://boxil.jp/">BOXIL SaaS</a>」とオンラインイベントサービス「<a href="https://boxil-event-cloud.jp/">BOXIL EVENT CLOUD</a>」の間でアカウントを共有する共通ID基盤の開発を進めています。</p> <p>この記事では、その開発過程でOpenID Connect(OIDC)の採用を検討した結果と得られた知見について解説します。</p> <p>具体的には、共通ID基盤の開発過程を時系列で紹介し、OIDC採用の検討過程、ビジネス要件との折り合いをつけることが難しかったこと、およびOIDCを使わないケースも含めたシングルサインオンの代替案についても触れます。</p> <h2 id="対象読者">対象読者</h2> <ul> <li>ID基盤を作る際に、OIDCを導入しようか迷っている人 <ul> <li>かつOIDCの基本的なことは理解している人</li> </ul> </li> </ul> <h2 id="主なキーワード">主なキーワード</h2> <ul> <li><a href="https://boxil.jp/">BOXIL SaaS</a>: SaaSマッチングサービス。主力プロダクト。</li> <li><a href="https://boxil-event-cloud.jp/">BOXIL EVENT CLOUD</a>: ビジネスのための情報共有の場としてのオンラインイベントプラットフォーム。</li> <li><a href="https://openid.net/developers/how-connect-works/">OpenID Connect(OIDC)</a>: 認証基盤としての最有力候補技術。</li> <li>シングルサインオン(SSO): 一つのID(アカウント情報)を入力するだけで、連携する他のサービスにもログインできる仕組み。</li> <li>OpenID Provider(OP):OIDCにおけるユーザー認証のサービスを提供するサーバーまたはプラットフォーム</li> <li>Relying Party(RP):OIDCにおけるユーザーの認証情報を利用するサービスやアプリケーション</li> </ul> <h2 id="共通ID基盤プロジェクトについて">共通ID基盤プロジェクトについて</h2> <h3 id="なぜプロジェクトを開始したのか">なぜプロジェクトを開始したのか?</h3> <p>プロジェクトの対象サービスがBOXIL SaaSというサービスと、BOXIL EVENT CLOUDというサービスであることは冒頭にお伝えしました。</p> <p>BOXIL SaaSとBOXIL EVENT CLOUDとは、別々のサービスではありますが、片方はSaaSを導入したい/提供したい企業向けのマッチング、もう片方はイベント開催を通してビジネス上の知見・つながりを提供しています。 ですのでターゲットとする顧客としてはある程度重なる部分もあります。</p> <p>現状ではそれぞれ独自の認証システムを持っているので、ユーザーは各サービスで別々に登録する必要があり、サービス間のデータ連携もありません。一方のサービスに登録しているユーザーに対して、そのデータを活用したもう一方のサービスで便利な機能を提供するなどの施策を簡単に提案できない状況でした。</p> <p>このような課題を解消するため、共通ID基盤の導入により、一度登録すれば複数のサービスでアカウントを共有できるようにするプロジェクトがスタートしました。</p> <p>ここで現状におけるサービスの簡単な構成図をお見せしておきます。 それぞれのサービスがユーザー情報を独立して持ち、認証も別々に行なっていることがわかります。</p> <p><figure class="figure-image figure-image-fotolife" title="認証の仕組み(Before)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230913/20230913105659.png" width="1200" height="750" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>認証の仕組み(Before)</figcaption></figure></p> <p>では現状を踏まえたうえで、要件をまとめていく次のステップに移ります。</p> <h3 id="共通ID基盤構築の要件">共通ID基盤構築の要件</h3> <p>チームでは、ビジネスサイドのメンバーとも話を進め、共通基盤に求められる具体的な要件をまとめていきました。</p> <p>要件をかいつまんで説明すると下記のような内容になります。</p> <ul> <li>ドメインの異なる2サービス(BOXIL SaaS、BOXIL EVENT CLOUD)でIDを統合して利用できるようにする <ul> <li>もともとユーザー属性の近いサービスであるため、IDの一元化によりサービス間での相互送客を狙いたい</li> <li>会員情報の二重入力や更新の手間を省き、サービスのUXを向上したい</li> <li>各サービスのデータを紐づけたうえで、データ分析やマーケティング活動に活用したい</li> </ul> </li> <li>属性の近い別のサービスを将来的に連携する際のID連携の基盤を作る <ul> <li>新規サービスにおいても、既存サービスのユーザーを低コストで連携できるようにしたい</li> </ul> </li> </ul> <p>本プロジェクトでは上記の要件を満たすべく、まずは関連する技術選定から始めていきました。</p> <h2 id="共通ID基盤の技術選定">共通ID基盤の技術選定</h2> <h3 id="認証基盤に関連する技術群">認証基盤に関連する技術群</h3> <p>今回のように異なるドメインのサービス間でのSSOを実現する際に、関連技術をいくつかピックアップし検討していきました。</p> <p>以下に主要な検討技術をピックアップして簡単に特徴を説明します。</p> <p><a href="https://openid.net/developers/how-connect-works/"><strong>OIDC</strong></a></p> <ul> <li>OAuth2.0を拡張し、認証にも利用できるようにしたプロトコル</li> <li>IDトークンを用いて認証をして、UserInfoエンドポイントからユーザーのプロフィール情報を取得する</li> <li>異なるドメインのサービス間でのSSOが実現できる</li> <li>データのやり取りはJSON形式で行なう</li> </ul> <p><a href="https://datatracker.ietf.org/doc/html/rfc6749"><strong>OAuth2.0</strong></a></p> <ul> <li>認可のためのプロトコル(認証ではない)</li> <li>この仕様で認証を賄うには独自に拡張を行なう必要があるが、リソースサーバーから提供されるプロフィールAPIを用いての認証の方法には脆弱性があり、拡張の際にはセキュリティなどのリスクに特に気をつける必要がある</li> <li>データのやり取りはJSON形式で行なう</li> </ul> <p><a href="http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html"><strong>SAML</strong></a></p> <ul> <li>異なるドメインのサービス間でSSOを実現できるプロトコル</li> <li>SAMLは独自に定義された認証規格であり、OIDCがOAuth 2.0に基づいているのとは対照的</li> <li>SAMLはXML形式でデータをやり取りする</li> </ul> <p><strong>独自構築</strong></p> <p>規格には属さないものの、完全に独自で認証システムを設計する選択肢も存在します。この方法は、特定の要件に対してもっとも柔軟に対応できる点が強みです。しかし、OIDCなどセキュリティも考慮された規格とは異なり、セキュリティリスクも自分たちで考慮の上仕様を決める必要があるため、その点で難易度が高いと言えます。</p> <p>さらに、この手法は既存のライブラリや参考資料が少ないため、構築と運用のコストが他の方法よりも高くなる可能性があります。</p> <h3 id="どの技術を使うべきか">どの技術を使うべきか?</h3> <p>チームでは、先にまとめた既存規格の技術、独自認証などの特徴と、求められる要件を考慮したうえで検討した結果、今回の共通ID基盤プロジェクトにおいては第一候補としてOIDCを採用することになりました。</p> <p>選定の際に考慮した点については下記の通りです。</p> <p><strong>社内のスキルセットとのマッチング</strong></p> <ul> <li>OIDCはOAuth2.0の拡張であり、基本的なフローはOAuth2.0とほぼ同じであること</li> <li>既存プロダクトではOAuth2.0の導入実績があり、社内で知見がある程度蓄積されていること</li> </ul> <p><strong>低い複雑性</strong></p> <ul> <li>SAMLよりもプロトコルがシンプルで、仕様自体の見通しが良いこと</li> <li>走り出しから継続して検討をしながら進めていくような今回のプロジェクトには、シンプルであることがプラスで働くこと</li> <li>将来的に新しいサービス連携することを考慮したときに、スピーディに連携できそうなこと</li> </ul> <p><strong>セキュリティの考慮</strong></p> <ul> <li>IDトークンの署名・暗号化や、<a href="https://datatracker.ietf.org/doc/html/rfc7636">PKCE</a>(Proof Key for Code Exchange)など、多くのセキュリティ機能とベストプラクティスが組み込まれており、必要十分であること</li> </ul> <p><strong>ライブラリの充実</strong></p> <ul> <li>標準化され多く利用されている規格であるため、既存のライブラリやツールのサポートが豊富で、設定例やドキュメントも充実していること</li> </ul> <p>基盤のベースの技術としてOIDCを採用したので、それを前提としてシステム構成を考えていきます。</p> <h2 id="アーキテクチャの検討">アーキテクチャの検討</h2> <p>次にOIDCを前提として、共通ID基盤を入れたときのサービス全体のアーキテクチャを検討しました。下記に簡略化した構成図を掲載します。</p> <p>現状のシステム構成図は「<a href="#%E3%81%AA%E3%81%9C%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E9%96%8B%E5%A7%8B%E3%81%97%E3%81%9F%E3%81%AE%E3%81%8B">なぜプロジェクトを開始したのか?</a>」の章を参照してください。大きく変わったところは下記です。</p> <ul> <li>認証を担う機能を共通ID基盤として新たに構築する</li> <li>BOXIL SaaS、BOXIL EVENT CLOUDでそれぞれ持っていた認証の実装を、共通ID基盤のOIDC経由で行なう実装に置き換える。</li> <li>各サービス上にあるユーザー情報のうち、認証に使う情報を中心に共通ID基盤に移す。(それ以外の多くのサービス固有のユーザー情報は据え置き)</li> </ul> <p><figure class="figure-image figure-image-fotolife" title="認証の仕組み(After) "><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230913/20230913105759.png" width="1200" height="1001" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>認証の仕組み(After) </figcaption></figure></p> <p>OIDCの観点だと、BOXIL SaaS、BOXIL EVENT CLOUDの各サービスはそれぞれRPとなり、新しく作る共通ID基盤はOPとして振る舞う形になります。</p> <h2 id="隠れたサービス要件の発覚">隠れたサービス要件の発覚</h2> <p>チームでは、作ったアーキテクチャの想定を元に、実際にOIDCを使ったときに具体的にどのような画面遷移になるだろう?実際のUXはどうなるだろう?といったユーザー観点での調査を重ねて、ビジネスサイドとも話を進めていきました。</p> <h3 id="サービスに求められる要件について">サービスに求められる要件について</h3> <p>ところで、BOXIL SaaSでは、さまざまな施策を通じて会員の獲得効率(CVR)を最大限に高める戦略を採っています。たとえば、資料請求フォームの入力後に会員登録処理と並行してログインができるよう実装したり、EFO(Entry Form Optimization)によるUXの最適化に重点を置いています。</p> <p>今回のプロジェクトでも、会員登録の手間を減らしてUXを向上したり、サービス間での相互送客を実現するなどの目的がありますが、実際問題としてはCVRを損なわないという前提条件下で考慮される必要があります。</p> <p>ただ、この要件は事前に明文化できていたわけではなく、チーム間で明確に認識の形成ができずにプロジェクトが進行して、徐々に懸念が強く浮き彫りになってきたという感じでした。</p> <h3 id="OIDCで必須のOPへのリダイレクト">OIDCで必須のOPへのリダイレクト</h3> <p>OIDCの仕様の中にはいくつかの認証フローが定義されていますが、特に今回採用を予定していた認可コードフローにおいては、"OPへのリダイレクト"という仕様があります。</p> <p>下記のシーケンス図をご覧ください。</p> <p><figure class="figure-image figure-image-fotolife" title="シーケンス図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230913/20230913105910.png" width="1200" height="761" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>シーケンス図</figcaption></figure></p> <p>認証の開始時にRPであるBOXIL SaaSから、OPである共通ID基盤にリダイレクトされ、認証が完了した後にOPからRPに認可コード付きでリダイレクトされて戻ってくる流れがあることがわかります。 このリダイレクトはOIDCの仕様上重要で、RPとOP間の疎結合性を保つことで、安全に認可コードをRPに渡すことを実現しています。</p> <h3 id="OIDCとサービス要件の不一致">OIDCとサービス要件の不一致</h3> <p>さて、前述したように、BOXIL SaaSとしては会員獲得におけるCVRに影響を与える変更は極力避けたい事情がありました。プロジェクトチームでは、OIDCのリダイレクト仕様がBOXIL SaaSのCVRにどのような影響を与えるかを慎重に考察しました。</p> <p>結果、リダイレクトを入れることで会員登録やリード獲得のプロセスに影響が生じ、ユーザーが外部の認可エンドポイントへ移動することで、会員獲得の動線が分断されて、CVRやEFOによるユーザー動線の最適化にも悪影響を及ぼす懸念が出てきました。</p> <p>それを避けるために、なんとかリダイレクトしつつも極力CVRに影響を与えない次善策を含め、調査と検討を重ねたのですが、決定的な策が提案できず、最終的には独立した共通ID基盤経由でOIDC認証をする構成を採用しない決断をしました。</p> <p>高い自由度のUXを確保するために、OIDCのリダイレクト要件は今回のプロジェクトには合わないと判断したのです。</p> <h2 id="別の方法を探る">別の方法を探る</h2> <p>では、前述した「リダイレクトしたくない」というビジネス要件と、「リダイレクトが必要」というOIDCの仕様を受けて、どういった意思決定をすべきでしょうか。</p> <p>私たちが現状調査・検討してきた案をいくつかご紹介します。</p> <ol> <li>BOXIL SaaSをOPとする案</li> <li>サービスのドメインを統合する</li> <li>サービスをコードレベルで統合する</li> </ol> <p>なお、この記事を執筆時点では、目下、案の検討を進めている途中のため、どれを採用したのかは言及しません。どの案もUI/UXへの影響や、開発・保守運用工数への影響などなどトレードオフがあり影響度も大きいため、慎重に検討を進めていく必要があります。</p> <p>また考えるうえでは、短期的なメリット、中長期的なメリットなど時系列も含めて議論が必要と考えています。</p> <p>以下にそれぞれを簡単に紹介します。</p> <h3 id="1-主力プロダクトをOPとする案">1. 主力プロダクトをOPとする案</h3> <p><figure class="figure-image figure-image-fotolife" title="認証の仕組み(BOXIL SaaSをOPとする案)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230913/20230913110002.png" width="1200" height="648" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>認証の仕組み(BOXIL SaaSをOPとする案)</figcaption></figure></p> <p>この案はOIDCの認証を管理するサーバー(マイクロサービス)を新規で開発・運用するのではなく、考慮すべきビジネス要件が多いBOXIL SaaSをOPとして振る舞わせ、BOXIL EVENT CLOUDはRPとしてそのエンドポイントを利用する形です。</p> <p>そうすることでBOXIL SaaSでは従来の会員登録のフローを変更する必要がなくなるため、ネックだったリダイレクトの影響に対する懸念は考えなくて良くなります。</p> <p>デメリットとしては、認証機能自体のBOXIL SaaSへの依存が強くなってしまうため、連携するサービスの負荷でBOXIL SaaSも影響を受けてしまったり、例えばBOXIL SaaSがクローズする際に連携サービスが影響を受ける可能性などがでてきてしまいます。</p> <p>また、この案は一度しか使えず、今後連携するサービスでは通常のOIDCによるリダイレクトが強制される形になります。</p> <p>以降の案はOIDCを使わない案です。</p> <h3 id="2-サービスのドメインを統合する">2. サービスのドメインを統合する</h3> <p>一方のサービスのコードベースを、もう片方のドメインに丸ごと移すやり方です。</p> <p><figure class="figure-image figure-image-fotolife" title="認証の仕組み(サービスのドメインを統合する案)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230913/20230913110058.png" width="1200" height="1132" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>認証の仕組み(サービスのドメインを統合する案)</figcaption></figure></p> <p>例えば、BOXIL SaaSは現在 <code>boxil.jp</code> 、BOXIL EVENT CLOUDは <code>boxil-event-cloud.jp</code>でホスティングしていますが、これを <code>boxil.jp</code> 、 <code>event.boxil.jp</code> に変更すれば、サードパーティクッキーの制限がなくなります。 片方でログインして、もう片方のサービスとログインセッションを共有することも技術的に可能になります。</p> <h3 id="3-サービスをコードレベルで統合する">3. サービスをコードレベルで統合する</h3> <p>これはできるケースが限られると思いますが、もし連携するサービスのビジネスドメインや会員属性が近く、数が多くない場合は、サービス自体を一つのコードベースに統合する案も考えられそうです。</p> <p><figure class="figure-image figure-image-fotolife" title="認証の仕組み(アプリケーションをコードレベルで統合する)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230913/20230913110219.png" width="1200" height="1140" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>認証の仕組み(アプリケーションをコードレベルで統合する)</figcaption></figure></p> <p>当然ながら自由度は非常に高いですが、引越しする側のサービスについてゼロベースに近い開発が必要になるため、完了までにかかるコストや、統合にあたっての双方のデータスキーマの見直しなど、多岐にわたる検討が必要になります。</p> <h2 id="終わりに">終わりに</h2> <p>本プロジェクトの開発過程では、技術選定だけでなく、ビジネス要件との整合性も重要であることが明らかになりました。OIDCは多くの場合に有効な技術ですが、それありきで進めるのではなくUXとビジネス要件をしっかりと両立させ、最適な技術選定と実装を広い視野で追求していく必要があることを今回学ぶことができました。</p> <p>この記事が皆さまの参考になれば幸いです。</p> smartcamp プロダクトバックログをNotionで管理して生産性が爆上がりしたかもしれない話 hatenablog://entry/820878482954693094 2023-08-25T12:00:00+09:00 2023-08-25T12:00:04+09:00 弊社テックブログチームのスクラム月間(勝手に言ってる)ということで、プロダクトバックログの管理をNotionで行っているお話をしようかと思います。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802093609.png" alt="pbl_with_notion_eyecatch" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#BOXIL-SaaSの開発チームについて">BOXIL SaaSの開発チームについて</a></li> <li><a href="#Notionでプロダクトバックログの管理をやってみた結果">Notionでプロダクトバックログの管理をやってみた結果</a></li> <li><a href="#尋常じゃない量のプロパティ">尋常じゃない量のプロパティ</a><ul> <li><a href="#Sprint番号">Sprint番号</a></li> <li><a href="#PointとPlanning-Point">PointとPlanning Point</a></li> <li><a href="#親タスクとサブタスク">親タスクとサブタスク</a></li> <li><a href="#ドキュメントのリレーション">ドキュメントのリレーション</a></li> <li><a href="#実装の効果">実装の効果</a></li> </ul> </li> <li><a href="#所感">所感</a><ul> <li><a href="#メリット">メリット</a><ul> <li><a href="#ドキュメントツールとタスク管理ツールが同じだって">ドキュメントツールとタスク管理ツールが同じだって?!</a></li> <li><a href="#自由性">自由性</a></li> <li><a href="#新しい機能が増えていくのが楽しい">新しい機能が増えていくのが楽しい</a></li> </ul> </li> <li><a href="#デメリット">デメリット</a><ul> <li><a href="#あくまでドキュメントツール">あくまでドキュメントツール</a></li> <li><a href="#多機能すぎる">多機能すぎる</a></li> <li><a href="#自由すぎる">自由すぎる</a></li> </ul> </li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <p>こんにちは!!</p> <p>BOXIL SaaSのエンジニア兼テックブログチーム平社員をしているブラーバです。今週は弊社テックブログチームのスクラム月間(勝手に言ってる)ということで、プロダクトバックログの管理をNotionで行っているお話をしようかと思います。</p> <p>もともと、弊社では社内のドキュメントを他のドキュメントツールで管理していましたが、以下のような理由でNotionへの移行チャレンジをしています。</p> <ul> <li>同時編集を手軽に行いたい</li> <li>柔軟にドキュメントの階層を設定したい</li> <li>そのドキュメントの作成者がオーナーのような感覚になり、同じようなドキュメントが乱立してしまう</li> </ul> <p>そして、同時にNotionはタスク管理ツールとしてもさまざまな機能や<a href="https://www.notion.so/ja-jp/templates/tasks-and-issues">テンプレート</a>が存在するため、BOXIL SaaS開発チームではプロダクトバックログの管理まで試みました。</p> <p>このNotionチャレンジから半年以上経ちましたが、さまざまな試行錯誤をしてきました。 今回はその試行錯誤の中身をこのような方々に共有できればなと思います。(ここ半年くらいの試行錯誤をアウトプットしたかっただけなんてことはない)</p> <ul> <li>スクラムをある程度知っていて、Notionを社内ドキュメントとして利用している方</li> <li>Notionをタスク管理ツールとして使えるか不安な方</li> <li>すでにNotionでプロダクトバックログの管理をしていて、困っている方</li> </ul> <h2 id="BOXIL-SaaSの開発チームについて">BOXIL SaaSの開発チームについて</h2> <p>まずはBOXIL SaaSの開発チーム(以降、開発チーム)がどのようにスクラム開発をしているかを説明しようかと思います。 (スクラムなんか余裕で知ってるZE☆という方々や早く本題に入らんかい!!という方々はぜひ本題の<a href="#notion%E3%81%A7%E3%83%97%E3%83%AD%E3%83%80%E3%82%AF%E3%83%88%E3%83%90%E3%83%83%E3%82%AF%E3%83%AD%E3%82%B0%E3%81%AE%E7%AE%A1%E7%90%86%E3%82%92%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E7%B5%90%E6%9E%9C">Notionでプロダクトバックログの管理をやってみた結果</a>に進んでください!!)</p> <p>開発チームの人数は現在10人弱です。 スプリントの期間は2週間であり、以下のスクラムイベントが存在します。</p> <ul> <li>スプリントプランニング</li> <li>デイリースクラム</li> <li>リファインメント</li> <li>スプリントレビュー</li> <li>レトロスペクティブ(振り返り)</li> </ul> <p>そして以下図の流れでアイテムが生成され、リファインメントでアイテムの理解をし、スプリントプランニングで2週間でこなせそうなレベルまでタスクを細分化しています。</p> <p><figure class="figure-image figure-image-fotolife" title="開発手順"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802093711.png" alt="" width="1200" height="529" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>開発手順</figcaption></figure></p> <p>各部門から上がってきた機能改善や施策等をPdMの方々が管理しているデータベースで管理されています。</p> <p>それらのアイテムをリファインメントを通じて見積もりをし、優先度順にNotionのテーブルビューとして存在する<code>プロダクトバックログのテーブルビュー</code>にアイテムが追加されます。</p> <p>そしてスプリントプランニングにて<code>プロダクトバックログのテーブルビュー</code>から次のスプリントで達成できそうなアイテムを<code>スプリントバックログのテーブルビュー</code>に移動し、スプリントが始まるとそれらを着手するという流れです。</p> <p>プロダクトバックログとスプリントバックログのデータベースが同じであるため、それぞれのテーブルビューに表示される条件にアイテムを変更すればスムーズにアイテムを移動させることができるようにしています。</p> <p>さて今回はプロダクトバックログとスプリントバックログのテーブルビューに表示されているアイテムのデータベースについて半年強(一年弱と言っても過言ではない..?)の試行錯誤を書いていきます!</p> <h2 id="Notionでプロダクトバックログの管理をやってみた結果">Notionでプロダクトバックログの管理をやってみた結果</h2> <p>ここまで順に読んでくださった皆さん、読み飛ばしてきた皆さん、ここまで長々と書いてきましたがここから本題です。 前述のプロダクトバックログとスプリントバックログのテーブルビュー、そしてアイテムの中身をそれぞれ説明したいと思います。(早く説明せい)</p> <p><figure class="figure-image figure-image-fotolife" title="プロダクトバックログのテーブルビュー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802093757.png" alt="" width="1200" height="606" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>プロダクトバックログのテーブルビュー</figcaption></figure></p> <p>こちらはプロダクトバックログのテーブルビューです。(流石にテストで作成したアイテム以外は載せたら怒られそうだったのでマスキングしてます、怒られるのヤダ...)</p> <p>スプリントプランニング時には優先度の高い順からスプリントで達成可能なレベルにまでタスクを細分化するため、タスクがどうしても階層構造になります。そのため、Notionの<code>サブアイテム</code>という機能を利用し階層構造を表現しています。</p> <p><a href="https://www.notion.so/ja-jp/help/guides/tasks-manageable-steps-sub-tasks-dependencies">タスクデータベースでサブタスクを使用する方法</a></p> <p>また、上のキャプチャでは表示されていませんがフィーチャーやエピックなどの大きめの粒度のタスクも同様のデータベースで管理することがあるため、その際は<code>開発カテゴリ</code>というマルチセレクトでラベル付けをしたりします。(そのラベルだけを表示するテーブルビューなどを作れたりする)</p> <p>ここからチームのベロシティを考えつつ、スプリントで達成可能なアイテムをスプリントバックログに入れていきます。</p> <p><figure class="figure-image figure-image-fotolife" title="スプリントバックログのテーブルビュー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802093838.png" alt="" width="1200" height="530" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>スプリントバックログのテーブルビュー</figcaption></figure></p> <p>そしてこちらがスプリントバックログのテーブルビューです。 <code>Sprint番号</code>というプロパティを入力するだけでプロダクトバックログからスプリントバックログに移動できるようにしたかったため、スプリントバックログのテーブルビューのフィルターに<code>Sprint番号が入力されているか</code>という条件を入れています。</p> <p>また、このテーブルビューはNotionの<code>グループ</code>という機能を利用し<code>Sprint番号</code>でグループ化しています。 完了したスプリントはグループ横の三点リーダーから非表示をしています。(アーカイブみたいな感じ)</p> <p><a href="https://www.notion.so/ja-jp/releases/2021-10-19">Notion 2.13:データベースのグループ化・サブグループ化</a></p> <p>そしてNotionはもちろんドキュメントツールなので、スクラムにおいての<code>生きているドキュメント</code>として活用しています。 以下がテストで作成したアイテムの中身です。</p> <p><figure class="figure-image figure-image-fotolife" title="アイテムの本文"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094101.png" alt="" width="716" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>アイテムの本文</figcaption></figure></p> <p>開発チームではスプリントプランニング時にアイテムの完成の定義を決めています。 そのために必要な情報である<code>背景</code>や<code>ユーザーストーリー</code>、<code>受け入れ条件</code>などをアイテムの本文に書くようにしています。(執筆時にちょうどお腹が痛かったわけでは断じてない)</p> <p>これらの項目はNotionの<code>データベーステンプレート</code>という機能を利用し、アイテム作成時に自動で挿入されるように設定をしています。 リファインメントに持ってくる前にこれらの項目を埋めるだけで、リファインメントがスムーズに行えるため、非常に便利です。</p> <p><a href="https://www.notion.so/ja-jp/help/database-templates">データベーステンプレート</a></p> <p>このようにNotionでプロダクトバックログのアイテムの管理をしています。 次の章では、このデータベースに次々と追加されていったプロパティの紹介をしていきます。(たぶんここがミソ..?)</p> <h2 id="尋常じゃない量のプロパティ">尋常じゃない量のプロパティ</h2> <p>使っていないプロパティも存在していますが現在36のプロパティがあります。(絶対ありすぎるので棚卸しを今、誓いました)</p> <p>その中でもこのプロパティの運用はうまくいっているな、というものを紹介しようと思います。</p> <h3 id="Sprint番号">Sprint番号</h3> <p>前述の通り、<code>スプリントバックログのテーブルビュー</code>に追加するときに設定しているプロパティです。</p> <p><figure class="figure-image figure-image-fotolife" title="Sprint番号"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094141.png" alt="" width="1200" height="154" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Sprint番号</figcaption></figure></p> <p>もともとこのプロパティは日付になっており、スプリントの期間が入っていましたが管理が大変ということからセレクトになりました。 しかしながらどうしてもスプリントを跨いでしまうタスクがあり、現在は<code>マルチセレクト</code>に落ち着いています。 おかげでNotionの<code>グループ</code>という機能を使いつつ、開発チームが見るテーブルビューのメンテナンスがとても楽になりました。</p> <p>また、このプロパティで簡単にどんなタスクを行っていたかのテーブルビューを作れたりするのもとてもいいところです。</p> <p><figure class="figure-image figure-image-fotolife" title="自分の消化したタスク群"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094207.png" alt="" width="761" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>自分の消化したタスク群</figcaption></figure></p> <h3 id="PointとPlanning-Point">PointとPlanning Point</h3> <p>開発チームではリファインメント時に工数をストーリーポイントで見積もっており、その際に<code>Planning Point</code>を入力します。</p> <p><figure class="figure-image figure-image-fotolife" title="pointとplanning point"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094239.png" alt="" width="772" height="234" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>pointとplanning point</figcaption></figure></p> <p>そして実際に着手しアイテムが完成したタイミングで、着手した人たちの体感に基づいて<code>Point</code>を入力するようにします。 これはリファインメントやスプリントプランニングの精度の振り返りに利用するための分析用に入力をしています。</p> <p>このプロパティ導入時はベロシティが不安定で開発スケジュールがうまく立てることができておらず、その対策として追加されました。 これにより振り返りで分析ができたり、実際に消化できるベロシティがどのくらいなのかが大体把握できる様になりました。 (今はブレても±1ptなのであまり振り返ることも少なくなった)</p> <h3 id="親タスクとサブタスク">親タスクとサブタスク</h3> <p>このプロパティも少し出てきましたが、親子関係を表すために追加しました。(親タスクなのに子タスクじゃなくてサブタスクなのは目を瞑ってください)</p> <p><figure class="figure-image figure-image-fotolife" title="親タスクとサブタスク"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094308.png" alt="" width="1200" height="363" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>親タスクとサブタスク</figcaption></figure></p> <p>小さなバグ改修から1ヶ月以上かかる施策などアイテムの大きさはバラバラです。開発チームでは2週間のスプリントで終わらせる大きさにまでアイテムを細分化しますが、その親子関係を表現するために利用しています。</p> <p>また、Notionのアップデートによりテーブルビューでも親子関係を表示できるようになったときは、感動しましたね。(ただのNotionヘビーユーザー)</p> <p><figure class="figure-image figure-image-fotolife" title="プロダクトバックログのテーブルビュー(再掲)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802093757.png" alt="" width="1200" height="606" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>プロダクトバックログのテーブルビュー(再掲)</figcaption></figure></p> <p>また、親タスクのテンプレートはタスクを細分化する前提で作られています。そのアイテムを親タスクにしているアイテムを表示するテーブルビューがテンプレートに入っており、わざわざ親タスクを設定しなくてもテーブルビューの<code>+新規</code>からサブタスクを作成できます。</p> <p><figure class="figure-image figure-image-fotolife" title="親タスクのテーブルビュー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094449.png" alt="" width="1200" height="424" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>親タスクのテーブルビュー</figcaption></figure></p> <p>親タスクには背景やユーザーストーリーは必要ですが、サブタスクには必要ないことが多々あります。 そのため、サブタスクで利用するテンプレートでは現状と、そのタスクで何をしたいかなどだけを書けるようにしています。 Notionはテンプレートを複数用意できるため、非常に便利ですね!</p> <p><figure class="figure-image figure-image-fotolife" title="サブタスクのテンプレート"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094520.png" alt="" width="1200" height="602" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>サブタスクのテンプレート</figcaption></figure></p> <h3 id="ドキュメントのリレーション">ドキュメントのリレーション</h3> <p>開発チームではできるだけスプリントプランニングでどのように着手するかの認識の共有しており、手戻りを防いでいます。 しかしながら複雑なロジックや仕様理解が必要なアイテムも存在します。 そのような場合は<a href="https://tech.smartcamp.co.jp/entry/solution-by-design-document">社内の他のチーム</a>のようにDesign Documentを作成することにしています。</p> <p><figure class="figure-image figure-image-fotolife" title="Design Docs"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094548.png" alt="" width="1200" height="368" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Design Docs</figcaption></figure></p> <p>このリレーションも親タスクのテンプレートに追加しており、<code>+新規</code>からドキュメントの作成ができるようにしてあります。(これめっちゃ便利というのを伝えたい...)</p> <h3 id="実装の効果">実装の効果</h3> <p>レトロスペクティブや期末の振り返りなどでよく出てくる話題として「自分たちがどのくらい価値提供したかわからない」というものがあると思います。</p> <p>そんな意見をPdMの方が吸い上げてくださり<code>実装の効果</code>というリレーションが生まれました。</p> <p><figure class="figure-image figure-image-fotolife" title="実装の効果"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094629.png" alt="" width="1200" height="102" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実装の効果</figcaption></figure></p> <p>これはアイテムのオーナーなどが実際にどのくらい効果があったのかを記載してくださる、別のデータベースへのリレーションです。 このデータベースにもテンプレートが用意されており、開発業務とあまり関わりのない人でも記入しやすいようになっています。(この記事を書いているときに初めてテンプレート見たなんて言えない)</p> <p><figure class="figure-image figure-image-fotolife" title="実装の効果テンプレート"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094651.png" alt="" width="1200" height="972" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実装の効果テンプレート</figcaption></figure></p> <p>(だんだん楽しくなってきた...)</p> <h2 id="所感">所感</h2> <p>ここからは、1メンバーとして他のタスク管理ツールからNotionにチャレンジしてみて感じた所感を述べたいと思います。(一応移行チャレンジからずっと試行錯誤してきたメンバーの一人のはず...)</p> <h3 id="メリット">メリット</h3> <h4 id="ドキュメントツールとタスク管理ツールが同じだって">ドキュメントツールとタスク管理ツールが同じだって?!</h4> <p>何と言っても一番のメリットはドキュメントツールとタスク管理ツールが同じことです。 今まではタスク管理ツールにドキュメントにリンクを貼ったりPRにアイテムのリンクとドキュメントのリンクの二つを載せていたりしましたが、一つに集約していることでオーバーヘッドがかなり減ったと思います。</p> <p>またアイテム自体がドキュメントにもなり得るため、スクラムにおける<code>生きているドキュメント</code>としての振る舞いがストレスなくできます。(アイテムの中に書きすぎて別途ドキュメントに起こすときにコピペしたり同期ブロックとしてペーストしたり...)</p> <p>このメリットが個人的に一番大きいなと思います。</p> <h4 id="自由性">自由性</h4> <p>開発チームでは<code>パーソナル部屋</code>というものを作っており、その中ではそれぞれの思い思いのNotionの使い方をしています。 Tipsや業務知識をドキュメントの仮置き場として、ひたすらページを作っている人やたくさんテーブルビューを作っている人もいます。</p> <p>私はNotion上でも個人のタスク管理をしており、プロダクトバックログのデータベースに一方的なリレーションを貼ったり、次のデイリースクラムで話すことをメモったりしています。</p> <p><figure class="figure-image figure-image-fotolife" title="ぶらーば部屋"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230802/20230802094720.png" alt="" width="1200" height="895" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ぶらーば部屋</figcaption></figure></p> <p>(ToDoを早く消化しろ自分...)</p> <p>このようにカスタマイズ性に優れていたり、データベースが参照しやすかったりするため私達エンジニアとも相性が良いのかなと思います。</p> <h4 id="新しい機能が増えていくのが楽しい">新しい機能が増えていくのが楽しい</h4> <p>Notionチャレンジ当初は親子関係を表す機能や依存関係を表す機能などはなかった(たぶん)ですが、どんどん新しい機能が追加されることによって、個人的にNotionの沼にハマっていきました。 (新しいもの好きとしてはたまらんです)</p> <h3 id="デメリット">デメリット</h3> <h4 id="あくまでドキュメントツール">あくまでドキュメントツール</h4> <p>さっきと言ってることが真逆な気がしますが、Notionはやはりあくまでドキュメントツールです。 そのため、<a href="https://asana.com/ja/uses/task-management">Asana</a>や<a href="https://www.atlassian.com/ja/software/jira">Jira</a>でできる◯◯◯ができない!!みたいなことはあります。</p> <p>私達のNotionの使い方が悪い説もかなりありますが、親子関係や依存関係が複雑になりすぎて管理しづらくなってしまうアイテムがあったり、半期の振り返りのときにわざわざAPIを叩いたりと手間がやはりかかってしまいます。</p> <h4 id="多機能すぎる">多機能すぎる</h4> <p>とはいえ、Notionは機能が豊富だと思います。そのおかげで痒いところに手が届いたりするんですが、私達のチームにとって必要な機能と必要でない機能の取捨選択が難しいなと感じるときもあります。</p> <p>便利ということと管理が煩雑になるということはトレードオフなのかなと思います。(頑張って管理します)</p> <h4 id="自由すぎる">自由すぎる</h4> <p>管理が難しいという文脈で同じではありますが、やはり自由なことがデメリットになるときもあります。</p> <p>ここ半年でプロダクトバックログのアイテムが突然消えていたり(フィルターにかからない条件になっていた)、ロックをかけていないテーブルビューのプロパティが勝手に変わっていたり(あとでロックをかけた)しました。 他にも、誰が追加したか分からないプロパティがあったり(データベース自体にもロックをかけたが誰が使ってるか分からないから消すに消せない)と、管理が難しいなという印象があります。</p> <p>しかしながらこれはしっかりとデータベースを管理するオーナーを決め、使い方をまとめたドキュメントなどが整備されていれば解決する問題なのかもなと思います。(ドキュメントって大事..!!)</p> <h2 id="まとめ">まとめ</h2> <p>弊社ではドキュメントツールをNotionにしようという動きがあり、タスク管理もできるんじゃないか?という淡い期待を寄せて半年強ほどNotionチャレンジをしてきました。そのメリットとしてはやはりドキュメントとタスク管理が一つのツールに集約されていることだな(当たり前)と執筆しながら感じました。おかげで価値のあるドキュメントが作れたり、オーバーヘッドがかなり減ったりと開発チームの生産性が爆上がりしています。</p> <p>しかしながら多機能が故、管理が難しく、しっかりとしたルール整備や取捨選択が必要だと思います。</p> <p>まだNotionチャレンジは1年生ですので、これからももっとアップデートしつつ、このような形でアウトプットできたらなと思います!!</p> smartcamp 最近のスプリントプランニング事情 -ちょっとした改善事例集- hatenablog://entry/820878482956245720 2023-08-08T12:00:00+09:00 2023-08-08T12:00:38+09:00 スプリントプランニングとは 最近のBOXIL SaaS開発について 先に結論 施策 1.ポモドーロ・プランニング ポモドーロ・テクニックとは やってみた感想 おまけ(ChatGPTのプロンプト) 2.ファシリテーター・書記の順番交代制 ルーレット 3.内職を我慢する 4.おやつを食べる まとめ スマートキャンプでBOXIL SaaSのエンジニアをやっております永井です。 猛暑のみぎりでございますが、皆さまいかがお過ごしでしょうか。 今回はスクラム開発におけるスプリントプランニングに関してブログを書きました。 というのも正直スプリントプランニングって結構大変じゃないですか? そこで今回は最近のス… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230807/20230807173313.png" width="720" height="378" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#スプリントプランニングとは">スプリントプランニングとは</a></li> <li><a href="#最近のBOXIL-SaaS開発について">最近のBOXIL SaaS開発について</a></li> <li><a href="#先に結論">先に結論</a></li> <li><a href="#施策">施策</a><ul> <li><a href="#1ポモドーロプランニング">1.ポモドーロ・プランニング</a><ul> <li><a href="#ポモドーロテクニックとは">ポモドーロ・テクニックとは</a></li> <li><a href="#やってみた感想">やってみた感想</a></li> <li><a href="#おまけChatGPTのプロンプト">おまけ(ChatGPTのプロンプト)</a></li> </ul> </li> <li><a href="#2ファシリテーター書記の順番交代制">2.ファシリテーター・書記の順番交代制</a><ul> <li><a href="#ルーレット">ルーレット</a></li> </ul> </li> <li><a href="#3内職を我慢する">3.内職を我慢する</a></li> <li><a href="#4おやつを食べる">4.おやつを食べる</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <hr /> <p>スマートキャンプでBOXIL SaaSのエンジニアをやっております永井です。<br/> 猛暑のみぎりでございますが、皆さまいかがお過ごしでしょうか。</p> <p>今回はスクラム開発におけるスプリントプランニングに関してブログを書きました。</p> <p>というのも正直スプリントプランニングって結構大変じゃないですか?<br/> そこで今回は最近のスプリントプランニング状況や、チームの取り組みについて共有しようと思います。</p> <h1 id="スプリントプランニングとは">スプリントプランニングとは</h1> <p>そもそもスプリントプランニングとは何かを軽くおさらいします。</p> <p>スクラムガイドには以下のように書かれています。</p> <blockquote><p>スプリントプランニングはスプリントの起点であり、ここではスプリントで実⾏する作業の計 画を⽴てる。結果としてできる計画は、スクラムチーム全体の共同作業によって作成される。 <a href="https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf">https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf</a></p></blockquote> <p>ゴールの策定など多くの要素がありますが、第一にすることは「次のスプリントで何するかしっかり皆で計画を立てようぜ!」と私は認識しています。解釈違いの方ごめんなさい。</p> <p>現在BOXIL SaaSチームでは、開発チームが中心となり、次のスプリントでは対象のタスクをどう進めるかを詳細に詰めていく作業をしています。</p> <p>開発タスクにおいては既存仕様の共通認識を取ったうえで、実現の方法を話し合います。調査タスクでは何をどこまで調査すれば良いかを話し合います。</p> <p>ここでは「どうやってタスクを進めるか、皆で話し合ってるんだな」くらいに思ってもらえればOKです。</p> <h1 id="最近のBOXIL-SaaS開発について">最近のBOXIL SaaS開発について</h1> <p>BOXIL SaaSチームでは、ここ数ヶ月の間に業務委託の方や新卒の加入が相次ぎ一気にメンバーが増えました。</p> <p>また人数も増え、大きめのタスクも増えたため、作業時間を多めに確保するためにスプリント期間を1週間から2週間に変更しました。</p> <p>スクラムガイドによると、1ヶ月のスプリントでは、プランニングにかける時間は最大8時間とされています。したがって、今回は2週間のスプリントに対して、プランニング時間を半分の最大4時間に設定し、定期的に確保して開催しています。</p> <p>また弊社ではリモートワークを中心としているため、スプリントプランニングもオンラインで行っています。</p> <p>これらをまとめたものが下記です。</p> <ul> <li>スプリントは2週間</li> <li>プランニングにかける時間は最大で4時間</li> <li>つまり4時間で2週間分のタスクのプランニングを行なう。</li> <li>最近JOINしたメンバーが多め</li> </ul> <p>もし参考にされる場合はこれらも頭の片隅に置きつつ読み進めると良いかもしれません。</p> <h1 id="先に結論">先に結論</h1> <p>こういった話はスクラムに限らず一発逆転ホームランのような手は無いです。また「スプリントプランニングが楽勝に進む最強のメソッド!」みたいな怪しささえ感じるものもありません。あったら教えてください、本当に。取り入れたい。</p> <p>以前も「ホームランは狙わず地道に改善しました」といった内容の記事を書いていますので気になる方はご参照ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.smartcamp.co.jp%2Fentry%2Ffirst-pm-actions" title="初めてのPMでつまづかないためにやったこと - SMARTCAMP Engineer Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.smartcamp.co.jp/entry/first-pm-actions">tech.smartcamp.co.jp</a></cite></p> <p>そんな中でも楽しく参加できたり、継続することで良くなっていくような施策を振り返りの中で考え、実行に移しているので、いくつかご紹介できればと思います。</p> <h1 id="施策">施策</h1> <h2 id="1ポモドーロプランニング">1.ポモドーロ・プランニング</h2> <p>いきなりですが「ポモドーロ・プランニング」は造語です、すいません。</p> <p>名前の通りポモドーロ・テクニックを用いてプランニングをしよう、というものです。</p> <h3 id="ポモドーロテクニックとは">ポモドーロ・テクニックとは</h3> <p>ポモドーロ・テクニックは、25分の作業と5分の休憩を繰り返す作業管理法です。4回に1度、長めの休憩を取ります。作業と休憩を明確に分けることで、集中力を維持できるというものです。</p> <p>ポモドーロ・テクニックは、個人の勉強や作業などで使用されるのが一般的ですが、我々はそれをプランニングに適用しています。とはいっても、25分プランニングして5分休憩を繰り返し、4回に1度長めに休むだけです。特別なことはしていません。</p> <h3 id="やってみた感想">やってみた感想</h3> <p>プランニングは長時間話し続けて疲弊することが多いため、明確な休憩の時間があると疲労が抑えられているように思います。個人的には目を閉じてボーッとすることが多いのですが、その間に頭も少し整理されるので効率があがっているように感じています。</p> <p>また、ファシリテーターも定期的に交代をしているため良い区切りになっています。</p> <p>あとは切れ目があることで「この25分で話しきりたい」という意識が働くのですが、これには一長一短があります。</p> <p>良い点としては、重要なポイントを見極める意識が働くことです。また、それにより話が脱線しにくくなると感じています。</p> <p>悪い点としては、急いでしまうことで考慮漏れができてしまう点があげられます。少しだけ脱線することで新しく考慮すべき内容が見えてくる事もあるので、脱線=悪という訳でも無いように思います。</p> <h3 id="おまけChatGPTのプロンプト">おまけ(ChatGPTのプロンプト)</h3> <p>スケジュールを考えるのが面倒なときはChatGPTに以下のプロンプトを投げるとよしなに組んでくれます。ご活用ください。</p> <pre class="code" data-lang="" data-unlink>◯時から◯時まで以下のフローに従ってスケジュールを組んでください - 25分間スプリントプランニングを行い、5分間休憩を1セット - これを繰り返し4セットに1回15分の休憩 - スプリントプランニングには◯さん・◯さん・◯さんをランダムかつ均等にファシリテーターとして割り振る</pre> <h2 id="2ファシリテーター書記の順番交代制">2.ファシリテーター・書記の順番交代制</h2> <p>今プランニングでは議論を推進する、いわゆるファシリテーター役を定期的に交代しながら進めています。これは特定の人がずっと中心になって話し続けるといったプランニングの属人化を防ぐと言った狙いがあります。あとついでにタスクのチケットに加筆する人も順番に交代しています。</p> <h3 id="ルーレット">ルーレット</h3> <p>担当はルーレットで決めています。以前は指名もしていましたが毎度指名するのも大変なので、最近はランダムに身を任せています(笑)</p> <p>プランニングで議論をしている中での良い雰囲気づくりに一役買っているように思います。</p> <p>デメリットはルーレットにも時間がかかることです(本末転倒)。ちょっと項目が多いですね。</p> <p><figure class="figure-image figure-image-fotolife" title="最近のルーレット。名前以外は答え合わせが始まるが、それはそれで良し。"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230807/20230807173737.png" width="1200" height="698" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>最近のルーレット。名前以外は答え合わせが始まるが、それはそれで良し。</figcaption></figure></p> <p>個人的には「人志松本のすべらない話」で出てきた、あのサイコロ位のノリで決められると良いと思います。☆が出たら「最近出てないし◯◯さんやってみますかー」みたいな。</p> <h2 id="3内職を我慢する">3.内職を我慢する</h2> <p>急に当たり前な施策ですね(笑)</p> <p>「そりゃそうだろ!」と言われそうですが、オンラインだと案外難しいと感じています。</p> <p>プランニング中に開発タスクをやっちゃう、位の明確な内職だと分かりやすいし意識的に避けやすいと思います。</p> <p>しかし、議論の中で分からないことを調べてそのまま脱線してしまったり、Slackから「スッコココ」と聞こえ、返事の間に話に置いていかれるといったケースは案外多いのではないでしょうか。</p> <p>なるべく手を動かさないようにして議論に集中しようという話なんですが、そうしてると本当に疲れます。そのための定期的な休憩ですね。</p> <p>また、プランニング中はファシリテーターと書記以外は気になったことはコメントをどんどん残していくように取り決めています。オンラインMTGツールはチャット機能も付いているので、話すだけでなくそこにも書き込むことで議論を積極的に行います。</p> <p>オンラインだと話す人が一人に限られてしまったり、オフラインだとできる「小さめの声で隣の人に確認する」といったことができなかったりするため、こういったところでも補っているように思います。</p> <h2 id="4おやつを食べる">4.おやつを食べる</h2> <p>もはや施策なのかと言われそうですが施策です(断言)。</p> <p>集中はしつつも緊張しすぎないように適度におやつでもつつきながら議論しています。</p> <p>オンラインMTGなのでマイクの切り忘れなどで咀嚼音ASMRをメンバーにお届けしないように気をつけるのがポイントです。グミとかチョコとかおすすめです。</p> <h1 id="まとめ">まとめ</h1> <p>結局のところ、本質的に良いプランニングのためにすることは、メンバー全員がより仕様に詳しくなり、意思決定も滞りなく行えるようになることではないかと考えています。</p> <p>それに対して今回あげた施策は直接は影響しない、いわば付け焼き刃のような施策と言えるかもしれません。</p> <p>ですが、現状の中で最善のプランニングを行い、手戻りのない開発を進めることで、未来のより良いプランニングにつながると考えています。今回紹介したこれらの施策もそれには一役買っていると思っています。</p> <p>うまくプランニングができているかは開発が上手く回っているかの指標の一つになると思っているので、より良いプランニングを目指してこれからも振り返りつつブラッシュアップしていければと思っています。</p> smartcamp 新卒エンジニアが経験した研修とスクラムの世界 | 2023年度新卒入社エントリ hatenablog://entry/820878482954490476 2023-08-02T12:00:00+09:00 2023-08-02T19:26:39+09:00 ご挨拶 はじめまして! 2023年4月よりスマートキャンプに23卒として入社しました小宮です。 社内ではリーブスと呼ばれています。学生時代のインターンでもジェネシスと呼ばれていたので、なんかカタカナ系のあだ名が多いです。 自分について文章を書くのは苦手ですが、とりあえず書いていきたいと思います。 自己紹介 出身地は東京の蒲田で、東京の住みたくない街ランキングではいつも上位を守っています。 ネットの口コミを見ていたら、「昼間はスラム街のような雰囲気」と書かれていて、笑ってしまいました。 ですが交通の便も良く、自分的には住みやすい街だと思っています。 趣味は筋トレ(ダイエット)とサウナで、仕事が終… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230801/20230801144143.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="ご挨拶">ご挨拶</h1> <p>はじめまして! 2023年4月よりスマートキャンプに23卒として入社しました小宮です。</p> <p>社内ではリーブスと呼ばれています。学生時代のインターンでもジェネシスと呼ばれていたので、なんかカタカナ系のあだ名が多いです。</p> <p>自分について文章を書くのは苦手ですが、とりあえず書いていきたいと思います。</p> <h1 id="自己紹介">自己紹介</h1> <p>出身地は東京の蒲田で、東京の住みたくない街ランキングではいつも上位を守っています。</p> <p>ネットの口コミを見ていたら、「昼間はスラム街のような雰囲気」と書かれていて、笑ってしまいました。</p> <p>ですが交通の便も良く、自分的には住みやすい街だと思っています。</p> <p>趣味は筋トレ(ダイエット)とサウナで、仕事が終わった後は基本的にそのどちらかに行っています。</p> <h1 id="学生時代">学生時代</h1> <p>大学では文系の学部に通っていたため、エンジニアが何をする職業なのかも知らず、複数のサークルに入って典型的な大学生をやっていました。</p> <p>当時、学習塾でアルバイトをしていたのですが、同僚のアルバイトから「生徒のテスト管理が面倒」という声をよく聞くようになりました。 そこで簡単にPCで管理できる方法はないかと考え、新型コロナウイルスの影響で大学がリモート授業で時間を持て余していたということもあり、プログラミングの基礎を勉強しました。</p> <p>プログラミングの基礎を勉強することは楽しかったですが、同時に独学では作れないと感じました。 そのため当時働いていたアルバイト先の社長にお金を出してもらい、プログラミングスクールに通い、無事にアルバイト先にツールの導入できました。</p> <p>作ってみて周りからのフィードバックも嬉しかったのですが、0から1を作る楽しさを知ることができたのが収穫だったと思います。</p> <h1 id="どんなインターンに行っていたか">どんなインターンに行っていたか</h1> <p>技術力を身につけるためには実務経験が必要だと考え、アルバイト先でツールを導入した後にインターンを探し始めました。</p> <p>当時の技術力と使用言語で求人を絞っていくと、あまり求人数は多くなかったように思います。そんな中、不動産系のSaaS企業でのインターンを決めました。</p> <p>当時、僕はPHPをフレームワークを使わずに書いていました。なので会社でどんなフレームワークを使うのかワクワクしながらインターンを始めると、 聞いたこともない(Laravelしか知らないだけ)Zend Frameworkというフレームワークを使っていて、少し落ち込んだ記憶があります。</p> <p>実際にインターンを始めると、クライアントがいるサービスを作ることになったので、自分の書くコードに責任感を抱くようになりました。 そして、SaaS企業であったため、毎週のようにアップデートしていく過程で運用を意識したコーディングなども学び、独学では学べない箇所も知ることができたのは良かったと思いました。</p> <h1 id="就活">就活</h1> <p>インターンでの実務経験を経て、エンジニア職で就職活動を始めました。 SaaSであったり新規事業に挑戦している会社を中心に受けていきました。 ただ就活のやり方を全く知らなかったので、優秀なインターンの友人に何度も就活のやり方を教わった記憶があります。</p> <p>何とか第一志望だった会社から内定をいただくことができましたが、大学を卒業する直前に一身上の都合で内定を辞退してしまい、崖っぷちになりました。 新卒の募集はもう終わっていたため、中途採用で探していたのですが、実務経験3年の壁を越えることができずに断念しました。</p> <p>そこで、大学を卒業するか留年するか悩んだのですが、当時は大学にいること(就職留年)のメリットがあまりないと思い、卒業を決断しました。</p> <h1 id="フリーター生活">フリーター生活</h1> <p>学生という肩書きを失い、フリーターになったのですが、思いのほか楽しかったです。 今までの敷かれたレールの上から急に外れて、謎の解放感に包まれた記憶がありますw 就職活動をしながら、暇だったので興味があったブロックチェーン系のスタートアップでアルバイトをしたり、旅行をしたりなど、 人生でフリーターを経験してよかったなと思いました(何を言っているのだろう)。 この期間の就活中に、スマートキャンプとご縁があり、今に至ります。</p> <h1 id="スマートキャンプへの入社理由">スマートキャンプへの入社理由</h1> <p>スマートキャンプに興味を持ったきっかけは、私が選考を受ける程に関心があったSaaS業界に特化したサービスや事業を行っていたからです。 また、同社では新規事業への前向きな姿勢が強く、選考を受けてみようと思いました。</p> <p>選考を進める中で、選考に関わっていただいた方々の人柄が良く、社員の方々のnoteやテックブログを見ると、 同じ方向性を持っていてVISIONに共感している人が多く、個人としても組織としても成長していけるのではないかと感じました。 以上の理由からスマートキャンプへの入社を決めました。</p> <h1 id="入社してみて">入社してみて</h1> <h2 id="内定者インターン">内定者インターン</h2> <p>スマートキャンプの内定者インターンは半年間程行いました。</p> <p>最初は京都にある開発拠点に在籍し、スマートキャンプの技術を実践的な形で学びました。 私は東京にいたため、リモートで参加させてもらっていましたが、最初は少し不安でした。 リモートで働いたことがなく、自宅の環境も最悪で(最初はパイプ椅子に座っていました)、対面でのコミュニケーションが取れないことから関係性を築くことができるのかと思ってました。 しかし、実際に働き始めてみると、GatherやSlackなどで細かいコミュニケーションが取れることがわかり、一週間ぐらいでリモートワークは最高だと思いました。</p> <p>また、スマートキャンプではフルリモートで働くエンジニアの方も多く、コミュニケーションを取る手段がしっかり整備されている印象を受けました。</p> <p>インターンを行った所感としては、今まで触れたことのないテストやインフラ周りなどをたくさん学ぶことができ、 これまでのエンジニアのアルバイトでは得られなかった視点からの学びが多かったなと思います。</p> <h2 id="研修について">研修について</h2> <p>4月に正社員となり、最初に行ったのはマネーフォワードと合同の研修でした。 期間は約2ヶ月で、前半はビジネスと合同で社会人スキルを身につけるような研修で、後半はエンジニアの研修になりました。</p> <p>前半のビジネスと合同の研修では、FFSワークショップ(Five Factors &amp; Stress)やロジカルシンキング研修などを行いました。 FFSワークショップでは、自分の性格やタイプなどを数値化していただき、自分が思っていた自分の性格とは違う発見や相性の良い性格などを知ることができました。 また、ロジカルシンキング研修では、事実と解釈の違いや情報を整理する方法などを学び、今後の社会人生活で活用できる内容だったと思います。 ビジネス研修全体を通して、グループワークが多く、同期の数もかなりいたので、たくさんの人と話す機会をいただくことができました。</p> <p>エンジニア研修では、3人のグループで約3週間で図書館の管理システムを作りました。 基本的にはリモートで作業して、週に1回程度オフラインで進捗を確認していました。 使用技術から設計まで、自分たちで決めていくので、今までの業務などでは経験できない範囲を経験できました。 自分のチームは3人チームでしたが、配属後の使用技術が異なるため、技術選定が難しかったです。結果として、Next.js、Kotlin、GraphQLという、自分がすべて初めて触る技術スタックになりました。 ただ、新しい技術を学ぶこと自体に抵抗感はなかったので、チームの仲間と通話を繋げながら、作業してわからないところがあればすぐに聞くことができたので、苦しむことなく作業できました。 他のチームとは異なる機能を実装したくて、ChatGPTにおすすめの本をレコメンドする機能なども作っていただいたのですが、 最終的には間に合わなかったため、自分たちのリソースと向き合うことの大切さなども学ぶことができました。 最終日には発表会があり、他のチームのプロダクトを見て、マネーフォワードのレベルの高さを感じました。完成度やデザインが高いだけではなく、英語で発表しているグループもいて驚きました。 英語の方が情報量が多い点や、オフショア開発などを考えると英語でのコミュニケーションは必須なので、今後の課題として英語力を上げていきたいと思っています。 なので最近はDuolingoというサービスを使って、1日10分だけ英語の勉強をしていますw。</p> <p>研修を通じて、たくさんの同期に出会うことができたことは、良かったと感じました。 研修が始まる前から上長に何よりも友達をたくさん作るように言われていたので、プライベートで旅行に行ったり飲みに行ったりできる同期ができたのは、今回の研修の一番の収穫だったと思います。</p> <h2 id="初めてスクラムをやってみて">初めてスクラムをやってみて </h2> <p>研修が終わった後は、BALES CLOUDというインサイドセールス特化のSaaS事業部に配属となり、スクラム開発をすることになりました。 スクラム開発を始めた当初は、スクラムという単語を聞いたことあるなぐらいのレベルで、頭の中には屈強なラガーマンがたくさんいました。</p> <p>なのでまず最初に行ったことは、スクラム開発をする目的とスクラムイベントの理解から始めました。 具体的には、ドキュメントを読んだり、YouTubeで解説動画を見たり、OJT担当の方に質問したりなどをして理解を深めていきました。 ある程度流れが掴めてきたら、積極的にファシリなどに手を挙げて全体像を理解していくようにしました。</p> <p>スクラム開発をやってみて初めに思ったことは、ミーティング(スクラムイベント)が異様に多いことです。 学生時代もエンジニアのアルバイトは行っていましたが、タスクを渡されて黙々と開発するだけだったので、こんなにミーティングをする必要性があるのかと疑問を思ったこともありました。 ただミーティングがしっかり行われるからこそ、細かい仕様変更などに柔軟に対応でき、手戻りすることが減っているなと感じました。なので生産性は学生の頃に比べると明らかに上がっていると思います。</p> <p>またBALES CLOUDでは朝会(デイリースクラム)を非同期化するなど、同期的に行わなくても問題がないミーティングなどを<a href="https://tech.smartcamp.co.jp/entry/asynchronous-meeting">非同期化していく取り組み</a>をしています。 非同期化することで、ミーティングの時間を減らし、連続的な開発時間を確保する取り組みも素晴らしいと思いました。</p> <p>スクラム初心者が理解を深めるうえで、一番大事なことはしっかりタスクの完了率を追うことだと思いました。 タスクの完了率を追うことで、1スプリント内でタスクを終わらせる意識が生まれ、タスクの正しいポイント付けや考慮もれをリファイメントで話したり、 リリースまでの進捗状況を常に意識することになり、スクラム開発の目的やメリットを一番感じれるようになるのではないかと思います。</p> <h1 id="入社してからの目標">入社してからの目標</h1> <p>長期的な目標としては、新規事業などに携わって、プレイヤーではなくマネージャー的な立場になりたいと考えています。 そのため、エンジニアリングスキルを伸ばすことはもちろん、ビジネススキルも身につけ、個人の成長と事業の成長の両方を考えられる人材になることが必要だと思っています。</p> <p>短期的な目標であれば、現在所属しているBALES CLOUDはまだまだ発展途上で、多くの人々の負担を軽減できるサービスだと考えているので、 機能を充実させ多くの人々に価値を提供できるように努めたいと思います。</p> <p>まだまだ駆け出しですが、誰よりも経験を積んで成長していきたいと思っています。</p> <h1 id="まとめ">まとめ</h1> <p>ここまで読んでいただきありがとうございました!これからコミットして結果を出していこうと思いますので、よろしくお願いします!</p> smartcamp ド田舎の高専生が気づいたらWebエンジニアになっていた話 hatenablog://entry/820878482949402334 2023-07-14T13:00:00+09:00 2023-07-31T14:30:40+09:00 社内では**マリ緒**と呼ばれていますがもはや面影すら残ってないですね。あまつさえ最近は「マリ緒っち」や「マリリン」という派生形で呼ばれるようになってきたのでもう訳がわかりません。 今回は自分語りする機会を頂けたので、思う存分語りたいと思います。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230713/20230713232619.png" alt="20_years_old_eyecatch" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="マリ緒のあいさつ">マリ緒のあいさつ</h2> <p>はじめまして,4月よりスマートキャンプに23卒として入社しました那須野です。 <br> 社内では<strong>マリ緒</strong>と呼ばれていますがもはや面影すら残ってないですね。あまつさえ最近は「マリ緒っち」や「マリリン」という派生形で呼ばれるようになってきたのでもう訳がわかりません。<br> 今回は自分語りする機会を頂けたので、思う存分語りたいと思います。</p> <ul> <li>地方勢だけど上京してエンジニアとして働きたい人</li> <li>スマートキャンプに興味がある人</li> <li>就活で色々迷っている人</li> <li>高専生</li> </ul> <p>このような方々の目に留まり、参考にしていただければ幸いです。どうぞよろしくお願いします;)</p> <h2 id="自己紹介">自己紹介</h2> <p>自己紹介です。つらつらと書きます。<br> 出身は岩手県で、地元の工業系高専に通っていました。<br> 携帯の電波すら圏外になるような辺境の地に住んでいました。光回線が開通したのも最近のことです。(やばいね)<br> 現在二十歳で、高専を卒業してそのまま新卒入社した形になります。<br> ねことゲームが好きです。<br> 下の写真は実家で飼ってるねこちゃんです。かわいいね。<br> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230713/20230713094528.png" alt="neko" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="学生時代のおはなし">学生時代のおはなし</h2> <h3 id="高専知ってますか">高専、知ってますか?</h3> <p>高専、知ってますか?<br> 5年制の教育機関で専門技術を16歳からたくさん学べちゃいます。校則はだいぶ緩く、部活は任意だったり、授業は15時で終わったりと自分の時間をたくさん確保できます。<br> 最近は某呪術マンガの影響で多少認知度は上がっていると思いますがそれでもたまに「コウセン...???」みたいな反応をされることがあります。<br> ちょっと悲しいです。<br></p> <h3 id="ITなにそれ美味しいの">IT?なにそれ美味しいの?</h3> <p>学生時代からWeb開発一筋だったかというとそういう訳でもなく吹奏楽部で楽器吹いてたり、アニメ見たりと割とITとは無関係な学校生活を送っていました。(そもそもド田舎出身でPCをまともに触る機会がなかった)<br></p> <h3 id="きっかけ">きっかけ</h3> <p>そんな自分がなぜエンジニアになろうと思ったのか、きっかけはコロナ禍のリモート授業にあります。<br> 新型コロナウイルスが猛威を振るっていた2020年中頃、自分の通っていた高専もついにリモートでの授業を開始しました。そして、学校側も学生側も慣れないリモート授業のなかで友達からこのような話をされました。<br></p> <p><strong>「課題出すの...忘れるくね...????」</strong><br></p> <p>...確かに。ハッとしました。自分の通っていた高専は比較的年配の教師が多く、慣れないリモート授業で課題の周知、回収が曖昧になっていました。<br> そこで、授業で習った覚えたてのPythonと当時どっぷり沼にハマっていたTwitter(今も)を利用して、課題通知Botなるものを作成することにしました。<br> 下の写真は実際にBotが投稿したツイートです。(アイコンは自分で描いた。ブラックサンダー美味しいよね。)<br> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230713/20230713094612.png" alt="task_bot" width="599" height="473" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このBotをクラスメイトに使ってもらい、彼らから放たれた次の言葉が自分がエンジニアになりたいと思ったきっかけになりました。<br></p> <p><strong>「これめっちゃいいじゃん」</strong><br></p> <p>この一言がめっっっっっちゃ嬉しいんですよね。なにせ発案から実装まで、全部自分1人でやったことがみんなに評価されたんですよ!自分を全肯定してくれてるんですよ!嬉しくない訳ないじゃないですか!<br> この時の嬉しさが忘れられず、テクノロジーで人をたくさん喜ばせたいと思いエンジニアを志すようになりました。<br></p> <h3 id="道が決まった">道が決まった</h3> <p>その後、友人の影響でWeb技術を触り始め、Webアプリ開発を通してじっくり沼にハマっていきました。<br> そうしてなんやかんやしているうちに就活の時期になり、自分は将来Webで飯を食っていきたいと思い, Webエンジニア志望で就職活動をすることにしました。<br> それだけではなく、将来は東京で働くんだい!っていう強い想いもありました。なぜかは知らないんですけど、田舎の若者は都会に強い憧れを持ってるんですよね。異常な程に。<br>(でも実際新卒でWebエンジニアを目指すとなると企業自体がそもそも少ないので厳しい話ではあった)<br></p> <h2 id="就活のおはなし">就活のおはなし</h2> <h3 id="ポートフォリオ頑張った">ポートフォリオ頑張った</h3> <p>就活のおはなしです。<br> Webエンジニア志望で就活を始めることにした自分.<br> とは言っても一体何から始めたらいいんでしょう...<br>自分はインターンを終えると同時に内定かっさらってきたつよつよな友人に相談しました。そして「何かしらの成果物を用意しろ」とありがたいお言葉を仰せつかったので自分はまずポートフォリオの作成に取り掛かることにしました。<br> さて、何を作ろうか。<br>う〜ん何も思い浮かびません。ToDoリスト?メモ帳?Twitterクローン?否、インパクトが薄い。なぜそれを作ろうと思ったのかをしっかり説明できるようなものを作りたいと思いました。<br> そうして悩んでいるときに何とはなしに友人に「なんか困ってることない?」と聞いてみました。すると「小2の弟がいるんだけど九九を覚えられなくてねぇ」とのこと。<br> これだ。マリ緒に電流走る──!<br> さて、解決するテーマは決まった。これをどう解決していこうか。自分は「<strong>共感覚</strong>」という言葉をテレビで耳にしました。以下<a href="https://ja.wikipedia.org/wiki/%E5%85%B1%E6%84%9F%E8%A6%9A">Wikipedia</a>からの引用です。<br></p> <blockquote><p>共感覚(きょうかんかく、シナスタジア、英: synesthesia, 羅: synæsthesia)は、ある1つの刺激に対して、通常の感覚だけでなく 異なる種類の感覚も自動的に生じる知覚現象をいう。<br> 例えば、共感覚を持つ人には文字に色を感じたり、音に色を感じたり、味や匂いに、色や形を感じたりする。複数の共感覚を持つ人もいれば、1種類しか持たない人もいる。共感覚には多様なタイプがあり、これまでに150種類以上の共感覚が確認されている。</p></blockquote> <p>...これだ。マリ緒に電流走る──!(2回目)<br> 失礼しました。ということで共感覚に着目し、色と数字を連想させてあげることができれば覚えやすくなるんじゃないかと考え、<strong>KUKU</strong>と題しまして「九九を視覚的に覚えることができる教育向けWebアプリ」を開発することにしました。名前については何も聞かないでください。お願いします。<br> 長くなるので詳細は割愛しますが実際に開発したものは画像の通りです。だっさいですね。(当時はイケてると思ってたんだけどなぁ)<br> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230713/20230713094646.png" alt="kuku" width="1200" height="615" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>でも自分でデザインしたアイコン(↓)だけは今でも気に入っています.(KとU、教える人と教わる人を組み合わせてる)</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230713/20230713094700.png" alt="kuku_icon" width="512" height="512" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>デザインはともかく企業さんが技術力を判断する一番の材料になるのはこのポートフォリオの内容だと思ったので当時の自分なりにめちゃくちゃ頑張りました。<br> 今振り返ってみてもこのポートフォリオ開発のマインドは間違っていなかったなぁと思います。</p> <h3 id="ライバルは大卒-院卒">ライバルは大卒、 院卒</h3> <p>自分1人ですべて行なうのは効率が悪いと思ったので就活エージェントさんの力をお借りし、自己分析や就活対策をしてもらいました。<br> その後、自分は就活マッチングイベントに参加するようになり、そこで僕は今まで見えていなかった現実を突きつけられることになります...<br> 他の参加者、全員歳上なんですよね、大卒とか院卒とか。それに対して自分は当時19歳、まして自分はWebを触りはじめてまだ一年ちょっとだったのでめっちゃ緊張したし不安でした。<br> 選考の結果も奮わず、落ち込み、焦り、イベントを重ねていくごとに自信を失くしていきました。</p> <h3 id="頑張った結果">頑張った結果</h3> <p>そんなこんなでメンブレしていたんですがどうしても<del>東京に行きたかった</del>Webエンジニアになりたかったので頑張りました。諦めずに。<br> 今思うとよくあそこまで頑張れたな〜って思うんですが、頑張れた秘訣はちゃんと休憩の時間を取れていたからだと思います。<br> 自分はかなりのゲーマーなんですが、どんなに面接やイベントが重なって忙しくても好きなゲームをやる時間は絶対に確保していました。絶対にです。<br> ネガティブな事象が起きた後もそれを続けると生産性も落ちるし、何より心がしんどくなっちゃうんですよね。ネガティブに毒されたドロドロした心は自分が向き合うべきことまでドロドロ(?)させてしまい掴みようのないものになってしまいます。<br> だからこそ好きなことに取り組む時間を毎日必ず設け、習慣化することで毎日心に栄養を補給してあげる必要があるんじゃないかなと思います。(習慣化してあげることで「俺はこんなことをやってていいのか...;;」っていう罪悪感も薄れますし)<br> このように自分の心と向き合いながらめげずに頑張り続けた結果...<br> スマートキャンプと出会い、働かせていただくことになりました。</p> <h3 id="その後インターン">その後(インターン)</h3> <p>内定をいただいた後、スマートキャンプでインターンをすることになりました。<br> 大学とは違い、高専は必修の科目が多く就活が終わった後もそこまで時間に余裕があるわけではありませんでした。長期休暇中は週4で参加できていたのですがそれ以外は中々参加できませんでした。幸い単位に余裕があったので必修ではない科目を休んで参加することにしましたが、それでも週2回が限界でした。つらい。<br>今これを読んでいる高専生、大学生の皆さん。単位だけは余裕を持たせておきましょう。⚪︎にます。<br> そんなわけであまり長い期間インターンに参加できていたわけではありませんでしたがスマートキャンプの業務の雰囲気、文化などを感じるには十分すぎる時間だったと思います。<br> インターンはフルリモートで行ない、オンボーディングWebアプリを開発したり、実際にプロジェクトにジョインしてタスクをこなしたりとフルリモートでも十分だと思えるほど濃密な内容でした。(オフラインでできるのが一番いいんだけども地方勢にはつらい)<br> 完全に個人で行なっていたポートフォリオ作成とは違い、インターンでは学んだことがたくさんありました。他の人に見てもらうことを意識したコード、レビュアーが見やすいようなPR作成、進捗報告などのアウトプットのわかりやすい伝え方、実際の運用を考慮してのセキュリティ面、CI/CD、etc.多いですよね。個人開発では中々学べないことだと思います。忙しい中時間を削って参加した甲斐があったってもんです。実際、今インターンで学んだことを活かせてますし。<br> インターンにはもちろんオフラインで参加している方もいますが、インターンの内容としては共通のものに取り組みました。田舎人は差別されるとかそういうことはないです。安心してください。<br>また、オンラインオフィスツールの<a href="https://ja.gather.town/download">Gather</a>を使用しているため、オンラインでもオフラインのような感覚で話しかけることができます。下の写真は実際のチャットだけだとどうしても解決できないこともあるため、Gatherの存在はかなり大きかったです。<br> 下の写真は実際に集まって雑談している様子です。わちゃわちゃしてて楽しいです。<br> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230713/20230713094739.png" alt="gather" width="526" height="720" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><br> インターンに参加している人の技術レベルはまちまちでしたが、それぞれに対応した内容のタスクや課題を用意してくれるため、苦しみすぎることはありませんでしたが正直、焦りはありました。でも頑張るしかないんです。その人も同じ道を通ってきたはずだから。<br> 今振り返ると、インターンで実際の業務の流れや使用している技術を体験できていたことが今の自分にとって大きなアドバンテージになっているなぁと思います。</p> <h2 id="入社理由">入社理由</h2> <p>スマートキャンプと出会ったのは逆オファー形式のマッチングイベントでした。<br> 前述した課題通知botを作成してから作業の自動化などの効率の改善が好きになっていた自分にスマートキャンプのMISSIONである<a href="https://smartcamp.co.jp/philosophy">「<strong>テクノロジーで社会の非効率を無くす</strong>」</a>がぶち刺さりました。<br> 嬉しいことにオファーをいただきまして、そのイベント内で一次面接を通過できました。その後もオンラインランチや面接を重ね、次回最終面接というところまでたどり着くことができました。<br> そしてそのタイミングで就業体験として本社にお招きいただき、実際にメンバーの雰囲気や企業文化を肌で感じることができました。たくさんの選考を受けてきましたが選考途中で就業体験として会社にお招きいただいたのはスマートキャンプだけでした。<br> このお誘いを受けた時点でスマートキャンプは技術力云々の前に<strong>その人がどんな人物なのか</strong>をしっかり見て、丁寧に選んでいるんだと感じました。 就業体験を終えたときにはそれは確信に変わっており、同時に「<strong>絶対にここで働きたい</strong>」と思っていました。<br> 最終面接では代表の林さんとオンラインで行ないました。嬉しいことにその場で内定をいただき、思わず「マジっすか」と漏れてしまいました。普段の言葉遣いが終わってるとこういう大事な場面でボロが出るもんですね。内定取り消されなくて本当によかったです。<br> その後も何度か本社にお招きいただいて社員の皆さんと交流する機会があったのですが皆さん本当に優しくて入社してからシバき倒されるんじゃないかと不安になるほどでした。<br> もちろんそんなことはなくて本当にみんな優しいんですよ。マジで。(あまあまゆるゆるという意味ではないです)<br> これも明確な採用基準の下、その<strong>人物</strong>を見て採用しているからなのかなと思います。<br></p> <h2 id="入社してから">入社してから</h2> <h3 id="合同研修">合同研修</h3> <p>ここからは入社してからのことを書こうかなと。<br> スマートキャンプは<a href="https://corp.moneyforward.com/">株式会社マネーフォワード</a>のグループ会社なんですが研修はマネフォと合同で行なわれました。 自分は結構な人見知りなので見知らぬ大勢の人間(23卒の同期は70人近くいた)と話すのは正直大変だったのですがなんとかやりきりました。<br>お陰で話すことへの苦手意識が少し薄れてきたかもしれないと思う今日この頃です。<br> さて、気になっている方も多いかもしれないので研修の内容についても簡単に書いていきたいと思います。<br> エンジニアの研修は大きく分けて2つに分けられます。1つ目は新卒全員が参加する全社研修、2つ目が新卒のエンジニアのみが参加するエンジニア研修です。<br></p> <p>全社研修はビジネスマナーやロジカルシンキング(MECEとかSWOTとかPDCAとかロジックツリーとか、わからない人は調べてみてね)など、社会人として、マネーフォワードグループの一員として大切なことを学びました。そんな全体研修でいいな〜と思ったことが1つありまして、懇親会が定期的に開催されるんですよね。「えぇ〜〜ただでさえ疲れてるのに夜も拘束されるの...」と思った方、いると思います。そうそこの貴方です。自分も正直最初は同じ気持ちでした。だって人見知りだし、夜はゲームしたいじゃないですか。でもね、一度参加したら考えを改めざるを得ませんでした。<br>実は懇親会に参加するのは同期だけではなく、スマートキャンプ、マネフォの経営陣や各部署のリーダーなどもたくさん参加します。このような方々と歓談する機会は後にも先にも中々ありません。そういった方々の経験談や価値観、考え方を聞くことができるのは本当に貴重な機会だと思います。もちろんまだ話したことのない同期とも<del>お酒の力を借りて</del>話すことができて、とても楽しく、有意義な時間を過ごすことができました。このような体験を通して、自分のなかでの懇親会の捉え方が変わっていきました。正直、自分から話しかけることに対しての苦手意識はまだ払拭しきれていませんが...頑張っていきたいです。<br></p> <p>エンジニア研修では2~3人のグループでPBL(課題解決型学習)を行ないました。内容としては以下の通りです。 - 仕様書のみが与えられ、その仕様書を元に1つのアプリケーションを開発する - 技術選定はメンバーのトレーナー達が各々の技術スタックを考慮して決定する - 期間は2週間で週1回の進捗報告会以外はグループで自由に開発を行なっていく</p> <p>オンラインでも「ちょっといいですか」を実現できるよう作業中は常にVCを繋いたり、ふざけるところは適度にふざけたりと心理的安全性を高めることがチーム開発においてはかなり重要になってくることがわかりました。適度にふざけたいい例がありまして、自分達のチームでは猫好きが2人、カワウソ好きが1人だったのでチーム名をKawausoにしました。弊チームではマイノリティを尊重します。<br> チームで1つの共通認識みたいなキーワードがあってもいいと思います。まあ当然自分達のチームは「Kawauso」なんですが。例えばアプリケーションのアイコンをカワウソにしたり、モックデータをカワウソ関連にしたりとそのキーワードを見るときにクスッと笑えるようにするとこれもチームの心理的安全性向上につながるのかな〜と思います。<br> 最終日には制作物の発表会があり、そこで各チームが開発したアプリケーションの解説、デモを見ることができます。さすがマネーフォワードと言ったところでしょうか、皆つよつよなんですね。自分では絶対に思い付かないような実装をしているチームもあって、ただただすごいな〜と感服するばかりでした。<br> 2週間という期間は想像以上にあっという間でめちゃめちゃ楽しかったですし、同年代の人たちとチームで開発することはあまりなかったのでとても新鮮でした。<br>またやりたい。</p> <h3 id="チームへジョイン">チームへジョイン</h3> <p>研修が終わった後は<a href="https://boxil.jp/">BOXIL SaaS</a>というスマートキャンプのメインプロダクトのチームにジョインしました。<br> 1スプリント2週間のスクラム開発を導入しており、スプリント末に行なわれるスプリントレビュー(やったこと報告会みたいなやつ)では他部署の方々も参加し、ゆる〜くチャットが賑わいます。YouTubeとかニコニコみたいにコメントが流れていくのが自分は好きです。<br> また新卒メンバーのオンボーディングも充実していて、エンジニアとしてのメンター(トレーナーと呼ばれています)と社会人としてのメンターがいまして、1on1などを通じて手厚いサポートをしてくれます。本当にありがたい...<br> 勤務体系について、自分は入社する前「毎日出社するぞ!!」と意気込んでいたのですが朝の満員電車に耐えられず入社三か月にしてフルリモート状態になっています(都会怖い)。<br> ですがコミュニケーション量はMTG、ペアプロ、モブプロなどを通じ、出社していたころとほぼ変わっていません。実際に秋田からフルリモートで勤務している方もいらっしゃいますし地方の方でも働きやすいと思います。<a href="https://tech.smartcamp.co.jp/entry/ruby3-vup-from-akita">記事</a>も書かれているので見てみてください.<br> ちなみに自分が担当したタスクがプロダクトの価値向上(高評価や、売上向上など)に貢献できたときの嬉しさはマジたまらんです。これからも頑張ります。</p> <h2 id="これからのおはなし">これからのおはなし</h2> <p>これからについて語ります。<br> 20歳ということもあり自分はまだまだ未熟です。青いバナナくらい未熟。<br> タスクをサポートなしでこなせるようになることは当たり前ですしこの手の記事では書きがちのことなので省きます。それ以外について、ここではお話ししたいと思います。<br> スマートキャンプでは社内の非効率を無くすようなツールが有志によって日々開発されています。例えば日報のフォーマットを自動作成してくれるツールとか自分が今月どれくらいフレックスできるかを確認できるツールとか...いっぱいあります。 これの何がすごいって社内のプロジェクトとして開発されたわけではなく、社員が自ら非効率を見つけ自主的に開発しているところなんですよ。 もともとスマートキャンプに興味を持った理由がMISSIONである<a href="https://smartcamp.co.jp/philosophy">「<strong>テクノロジーで社会の非効率を無くす</strong>」</a>なので、それを実際に体現していることに深く感銘を受けました。僕も作りたい。<br> と、いうことでこれからは社内の非効率を無くすようなツールを作ることにチャレンジしてみたいと思います。<br> かくしてWebで飯を食っていくという夢が叶ったわけですが、これで満足できるわけもありません。周りよりも経験が少ないと卑下するのではなく、自分には周りよりも時間があるんだと、そう捉えてポジティブに生きていきたいと思います。<br></p> <h2 id="さいごに">さいごに</h2> <p>長くなってしまいすみませんmm<br> 言葉を書くのに不慣れで怪文書みたいになってしまいましたがここまで読んでくださって本当にありがとうございます。<br> この記事が少しでも皆さんの参考になれば幸いです。</p> smartcamp リモートHQでリモートワークの生産性とQOLが爆上がりした話 hatenablog://entry/820878482947901489 2023-07-10T12:00:00+09:00 2024-02-13T14:03:28+09:00 ※タイトルとアイキャッチはAIに考えてもらいました。 はじめに こんにちは。VPoEの米元です。 スマートキャンプでは2023年3月に「リモートHQ」というサービスを導入しました。 リモートHQは、在宅勤務の環境を始めとしたリモートワーク支援のためのサービスです。 hq-hq.co.jp 本稿では、スマートキャンプの働き方とその課題、リモートHQの紹介、導入後の効果、メンバーの声について紹介したいと思います。 対象読者 リモートワークでの在宅環境に課題がある方 社員のリモートワーク環境を整えたいエンジニアのマネジメント職または人事の方 導入の背景 スマートキャンプ開発組織の働き方 当社では創業… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707164855.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>※タイトルとアイキャッチはAIに考えてもらいました。</p> <h2 id="はじめに">はじめに</h2> <p>こんにちは。VPoEの米元です。 スマートキャンプでは2023年3月に「リモートHQ」というサービスを導入しました。 リモートHQは、在宅勤務の環境を始めとしたリモートワーク支援のためのサービスです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fhq-hq.co.jp%2Fremote%2F" title="リモートHQ|リモートワーク環境整備プラットフォーム" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="http://hq-hq.co.jp/remote/">hq-hq.co.jp</a></cite></p> <p>本稿では、スマートキャンプの働き方とその課題、リモートHQの紹介、導入後の効果、メンバーの声について紹介したいと思います。</p> <h3 id="対象読者">対象読者</h3> <ul> <li>リモートワークでの在宅環境に課題がある方</li> <li>社員のリモートワーク環境を整えたいエンジニアのマネジメント職または人事の方</li> </ul> <h2 id="導入の背景">導入の背景</h2> <h3 id="スマートキャンプ開発組織の働き方">スマートキャンプ開発組織の働き方</h3> <p>当社では創業間もない時期から週1回のリモートワークデーを採用していましたが、コロナ禍をきっかけに全社的にリモートワーク中心の働き方に移行しました。 その後、今年に入りコロナ禍の状況が変わってきたことで週1日以上の出社を基本としていますが、生産性を最大化するために最適な出社頻度を組織や職種ごとに選択する形をとっています。</p> <p>開発組織においては月1回以上の出社頻度を目安にしていますが、人によっては出社が中心のメンバーもいますし、関東近郊以外にお住まいのフルリモートのメンバーや週2回程度の出社を自主的に行っている<a href="https://tech.smartcamp.co.jp/entry/lets-go-kyoto-dev-center#:~:text=%E4%BA%AC%E9%83%BD%E9%96%8B%E7%99%BA%E6%8B%A0%E7%82%B9%E3%81%AF%E3%80%81%E3%83%95%E3%83%AB%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F%E4%B8%80%E5%AE%9A%E9%A0%BB%E5%BA%A6%E3%81%A7%E5%87%BA%E7%A4%BE%E3%81%99%E3%82%8B%E3%81%AE%E3%82%92%E5%89%8D%E6%8F%90%E3%81%A8%E3%81%97%E3%81%A6%E4%BD%9C%E3%82%89%E3%82%8C%E3%81%9F%E6%8B%A0%E7%82%B9%E3%81%A7%E3%81%99">京都開発拠点</a>のメンバーなど、人や部門によって多様な働き方をしています。</p> <p>全体でフルリモートに統一しない理由としては、対面でのコミュニケーションも大事にしたいという思いがあるからですが、その辺りの背景は本記事の趣旨からは外れますので省略します。気になる方はこちらの<a href="https://note.com/smartcamp_tent/n/nc4fa0e5219b4#:~:text=%E3%81%97%E3%81%9F%E3%81%84%E5%A7%BF-,1.%E3%82%AA%E3%83%95%E3%82%A3%E3%82%B9%E3%81%A8%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88%E3%81%AE%E8%89%AF%E3%81%95%E3%82%92%E3%81%84%E3%81%84%E3%81%A8%E3%81%93%E5%8F%96%E3%82%8A%E3%81%97%E3%81%9F%E3%81%84,-%E3%82%B9%E3%83%9E%E3%83%BC%E3%83%88%E3%82%AD%E3%83%A3%E3%83%B3%E3%83%97%E3%81%AE">京都開発拠点立ち上げ時のnote</a>をご覧ください!</p> <h3 id="在宅環境における課題">在宅環境における課題</h3> <p>前述したような多様な働き方をしている中で、全員に対して生産性の高い作業環境を提供することは簡単なことではありません。以前のように全員が出社する前提であればオフィスのデスクやモニタを始めとする備品を整備すれば済む話ですが、在宅での作業環境はメンバー個人の住環境や家族構成によってさまざまな制約があり、それに伴って必要な機材や金額も異なります。</p> <p>また、リモートワークが長期間続くと運動不足になってしまったり、自宅の椅子が合わず腰を痛めたりといった健康面にも懸念がありましたが、これらに対しても一律のリモートワーク手当の支給のような対応ではカバーしきれません。</p> <p>一方で個別の状況に合わせて金額や椅子・モニタなどの支給物をカスタマイズする方法も考えられますが、手間や資産管理の観点から現実的ではありません。 特にスマートキャンプでは人によっては自分で一定のレベルまで在宅環境を整えているメンバーもおり、公平性の観点からも既存の制度でのカバーが難しいと考えていました。</p> <p>このような状況の中、グループ会社の方から紹介していただいたのが「リモートHQ」でした。</p> <h2 id="リモートHQとは">リモートHQとは</h2> <p><img src="./remotehq-top.png" alt="" /><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165043.png" width="1200" height="641" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>「リモートHQ」は在宅の作業環境を整えるためのサービスです。 リモートHQを導入すると、あらかじめ設定したポイントに応じて<strong>社員自身が</strong>リモートHQのサイトから椅子・モニタ・キーボードといったさまざまな商品を選んでレンタルできます。</p> <p>また、事前に自宅の作業環境の不満点や写真を送り、それを元に<strong>専門のコンシェルジュと相談しながら</strong>自分にとって最適な作業環境を作ることができます。</p> <p>ラインナップを一部紹介するとFlexiSpotの昇降デスク・エルゴヒューマンのチェア・HHKBなどのデスク周りの商品や、ルームランナー・スマート睡眠パッド・空気清浄機などの健康増進関連など非常に幅広いラインナップから商品を選ぶことができます。</p> <p><figure class="figure-image figure-image-fotolife" title="リモートHQの資料から抜粋"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165121.png" width="1200" height="581" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>リモートHQの資料から抜粋</figcaption></figure></p> <p>さらに、既存のラインナップに無い商品についてもリクエストを出すことができ、リモートHQが承認したものは新たにラインナップに追加されます。</p> <p>商品のレンタルや返却に関するやりとりなど手間のかかる部分はリモートHQ側で対応してもらえますし、リモート手当てのような制度とは違って<strong>所得税や社会保険料がかからない</strong>ため導入企業側で増える工数はほとんどありません。</p> <p>弊社ではまだ利用していませんが、電気やネットの代金を非課税で法人負担にできるオプションもあるなど、在宅勤務やその補助で課題になりやすい点が非常にうまくカバーされたサービスだと感じています。</p> <h2 id="導入の効果">導入の効果</h2> <p>それでは実際に導入してどうだったのか、まずはサービス導入後のアンケートの結果から定量的なデータを見ていきましょう。</p> <h3 id="満足度">満足度</h3> <p>サービスの利用満足度は100%でした。(とても満足している: 60%、どちらかと言えば満足している: 40%)</p> <p>レンタルした商品自体の満足度だけでなくコンシェルジュ相談を始めとしたユーザーサポートや、欲しい商品が無かった場合でもリクエストが可能な点などサービス全体の総合的な評価もこの満足度に繋がっているようです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165207.png" width="1200" height="944" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="生産性向上の実感">生産性向上の実感</h3> <p>90%近くのメンバーが生産性の向上を実感しているとのことでした。</p> <p>すでに自分で在宅環境を整えていたメンバーも一定数いたため、組織全体としてどれくらい効果があるかは未知数だったため、この結果は(良い意味で)少し意外でした。</p> <p>また、生産性向上の実感値としては平均で19%増となっていました。 これはあくまで実感値ではあるので、仮に5〜10%程度の生産性が向上したと保守的に見積もっても十分な経済効果が出ていると判断しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165222.png" width="1200" height="877" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="健康面への影響実感">健康面への影響実感</h3> <p>健康面に関しては30%程度のメンバーが変化を実感したようです。</p> <p>満足度や生産性向上への影響と比較すると低い値になりましたが、アンケートの実施がサービスの利用開始直後だったため健康面の効果が出るまでの期間が短かったことや、椅子やその他の健康面に関わる商品をレンタルしたメンバーが多くなかったことも原因だと思われます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165246.png" width="1200" height="935" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="メンバーの声">メンバーの声</h2> <p>それでは実際に利用したメンバーの声も聞いてみましょう。今回は4人のメンバーにインタビューをしてみました。 ※括弧内はインタビューイのニックネームです。</p> <h3 id="1人目ピーターさん">1人目(ピーターさん)</h3> <p><strong>— サービス導入前の課題や気になっていたことがあれば教えてください</strong></p> <p>もともとそこまで課題感は無かったのですが、デスク環境が暗かったのでその点を改善できたらいいなというくらいでした。</p> <p><strong>— サービスを利用してみてどうでしたか</strong></p> <p>もともとの課題だった暗さに対してはコンシェルジュの方に大きめのデスクライトをおすすめしてもらいました。 また、自分では当初考えていなかった点もいくつか提案してもらいました。</p> <p>例えば「モニターアームを使ってみたらどうか」という提案や、当時はMacをデスクにそのまま置いてキーボードとしても利用していたため「Macもモニターにした方が目が疲れにくいですよ」と提案もしてもらいました。 その結果、モニターはモニターアームを利用し、Macはスタンドを使ってモニターと高さを揃えました。さらにこの形にするにあたってキーボードが必要になったので、試しにMistelの分割キーボードにしてみました。また、トラックパッドも合わせてレンタルしました。</p> <p>また、余ったポイントでトレーニングベンチを借りたのですが、思っていたより軽いものが届いてしまいこれに関しては失敗でした(笑) 総合的にはデスク周りは全体的に改善されましたし、提案していただいたデスクライトも自分では探せなかったと思うので非常に満足しています。 失敗したベンチも返却できますし、レンタルだからこそ気軽にトライできたので学びになりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165303.png" width="1200" height="771" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="2人目職人さん">2人目(職人さん)</h3> <p><strong>— サービス導入前の課題や気になっていたことがあれば教えてください</strong></p> <p>もともと在宅の環境は必要なものが揃っていて何を借りようか迷う部分がありました。 デスクはFlexiSpotの昇降デスクを持っていて、椅子もあったのであまり借りるものは無いかもしれないなと思っていました。</p> <p><strong>— サービスを利用してみてどうでしたか</strong></p> <p>デスク周りはキーボードをREALFORCE for Macに変えただけなのですが、他にエアロバイクと「<a href="https://teddyworks.co.jp/products/kensui">KENSUI -kaku-</a>」という懸垂マシンをレンタルしました。 自分は身体を動かすことが好きなのですが、エアロバイクがあれば外が雨でも有酸素運動ができますし、懸垂マシンはコンパクトな作りになっているのでデスクのすぐ横に立てていて仕事の合間にいつでも懸垂ができるようになりました。</p> <p>おかげで日常生活を豊かに過ごせるようになったと感じています。 特に懸垂マシンに関しては以前から気になっていた商品だったのですが、値段が4万円くらいするので自分で買うには少しハードルが高いです。</p> <p>もともとのリモートHQのラインナップに無かったのでダメもとでリクエスト申請してみたら承認されて嬉しかったです(笑) 実際に届いてみたらサイズも用途も自分にドンピシャでハマって、非常に満足しています。 自分はリモートHQは健康面に重点を置いて使っていきたいと思っています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165323.png" width="1200" height="925" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><figure class="figure-image figure-image-fotolife" title="省スペースな懸垂マシン1"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165356.png" width="900" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>省スペースな懸垂マシン(正面)</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="省スペースな懸垂マシン2"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165428.png" width="902" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>省スペースな懸垂マシン(横)</figcaption></figure></p> <h3 id="3人目クマノミさん">3人目(クマノミさん)</h3> <p><strong>— サービス導入前の課題や気になっていたことがあれば教えてください</strong></p> <p>私の場合は机や椅子などはすでに揃っていたためそこまで課題感はありませんでした。強いて言えばサブで使っていたモニタをもっと大きいものにしたい気持ちはありました。</p> <p><strong>— サービスを利用してみてどうでしたか</strong></p> <p>一点集中でポイントを利用し、自分では絶対に買わないであろう39.7インチ5K2Kの曲面型ウルトラワイドモニタを借りました。</p> <p>私はデータアナリストなので普段からデータ抽出業務をするも多いのですが、画面の幅が広いのでその業務が圧倒的に楽になりました。生産性が上がったと思います。 解像度も以前利用していたものよりも圧倒的に良いので、目が疲れにくくなりました。</p> <p><strong>— 今後レンタルしてみたいものはありますか</strong></p> <p>もしモニタが気に入らなかったら返却しようと思っていましたが、結構気に入ってしまいました。なので当面は残りのポイントで小物のレンタルをしようと思います。 特に電源タップなどは消耗品だと思っているものの、実際に買い換えようと思うとまだイケる!と思って買わなかったりするので電源タップとか借りてみたいです。</p> <p>また、1年に1回くらいは使いたいときがあるシュレッダーやプリンター等も低ポイントで借りられるのですが、そういった一時的にしか使わないものや置き場所に困りそうなものを借りると思います。</p> <p>あとは気軽に試して気に入らなかったら返却できるのは大きいですね。自分で購入したものは多少気に入らない点があったとしても我慢してそのまま使いがちですが、リモートHQだと躊躇なく返却して他の商品に変えられるので特に値段が高いものを試すハードルが下がりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165537.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="4人目ブラーバさん">4人目(ブラーバさん)</h3> <p><strong>— サービス導入前の課題や気になっていたことがあれば教えてください</strong></p> <p>気になっていたことは2つあって、1つはデスクです。 手軽に高さが調節できなかったため腰が痛くなりがちでした。また、天板が小さくご飯を食べながら作業がしづらかった点にも不満を感じていました。</p> <p>2つ目はモニターです。サイズが小さかったことと、スピーカーが無かったためイヤホンが必須だったことに不便さを感じていました。 いずれも学生時代に買った安いものをそのまま利用していました。そのため自宅ではあまり作業する気にならず、満員電車に揺られながらも出社することが多かったです。</p> <p>出社した際に少し残業してから帰宅するとだいたい21時過ぎになっていて、勉強に使う時間があまりとれない日々が続いていましたね。</p> <p><strong>— サービスを利用してみてどうでしたか</strong></p> <p>デスクはFlexiSpotの昇降デスクにしました。 高さが自分で手軽に設定できるので腰の痛みが少なくなりましたし、天板が大きくなったのでご飯食べながら作業したり、よく使うものもテーブルの上に置けるようになりました。</p> <p>また、モニターアームを導入したことでさらにデスク上のスペースが確保できました。</p> <p>モニターの方は31インチのものにしました。モニターから音が出るようになったので自分が喋るときだけイヤホンするようにしています。 これらのおかげで非常に快適になったため、リモートにする日がかなり増えました。(400%増)</p> <p>それによって満員電車に乗ることが少なくなったことや勉強するための時間が増えたのも良かったです。 今後はキーボードを試してみたいと思っています。</p> <p>私は今まで自宅の作業環境の整備にそこまで興味が強いわけではなかったのですが、会社が提供してくれるサービスで改善できたことが非常に良かったです。</p> <p>自分と同じように新卒で今まで自宅の環境を整えていなかった人からすると、家賃補助くらいありがたい福利厚生だと思います。エンジニア以外にも使ってもらいたいです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165552.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="マネジメントの視点から見たリモートHQ">マネジメントの視点から見たリモートHQ</h2> <p>最後に導入者側であるマネジメント(私)の視点も書いてみようと思います。</p> <h3 id="高いコストパフォーマンス">高いコストパフォーマンス</h3> <p>導入者として一番気になる費用対効果ですが、前述の「導入効果」で記載した生産性向上の効果に加えて、中長期的には健康面での効果もあるのではないかと考えています。</p> <p>業務に関する効果だけでなく「生活の質(QOL)が上がった」と話してくれるメンバーもいるなど利用者の満足度も非常に高く、定量的にも定性的にも当初の想定以上の効果が出ていると感じています。 また、利用された分だけ費用が発生する仕組みなので、もし社員にほとんど利用されなかったとしてもサービスの利用料が無駄になることはありません。</p> <p>運用に関しても負担はかなり少なく、手当や物品の支給と違って社内の手続きや資産管理が発生しないためバックオフィス側のメンバーの手をわずらわせることもありません。 サービスの利用料に対しての効果が高いため非常にコストパフォーマンスが高く、そのうえで無駄な手間やコストが発生するリスクが低いので、導入者にとってもありがたいサービスだと言えます。</p> <h3 id="考えられたユーザー体験">考えられたユーザー体験</h3> <p>初回の商品を発送する際にメッセージカードを同封してくれたり、商品を発送する際の箱を利用企業のオリジナルデザインに変更できます。</p> <p>例えば新入社員が入社したその日に、自分が選んだモニタやキーボードなどがメッセージカードと共に送られてくる・・・という体験を想像すると、それだけで会社へのエンゲージメントが高まりそうですね。</p> <p>単に物を届けるだけでなく受け取る利用者側の体験まで考え抜かれた設計になっており、サービスを作る人間としても見習いたいと思っています。</p> <p><figure class="figure-image figure-image-fotolife" title="喜んでもらったことに喜ぶ私"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230707/20230707165611.png" width="844" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>喜んでもらったことに喜ぶ私</figcaption></figure></p> <h3 id="さまざまな変化に対応できる柔軟性">さまざまな変化に対応できる柔軟性</h3> <p>例えば結婚や出産などのライフステージの変化、昇進や異動などによる業務の変化があった場合、作業環境の見直しやカスタマイズが必要になる場合もあると思います。</p> <p>ただ個人でそれらを行なうことはコストや労力の面で負担が大きく、見直したとしても微調整にとどまることが多いかもしれません。 一方でエンジニアとしては自分の作業環境をチューニングし続け、常に最高の生産性を求めるべきだとも考えています。</p> <p>リモートHQを利用することによって、これらのハードルが下がり、さまざまな変化に柔軟に対応できるため、社員の皆さんが長期的に高い生産性を発揮し続けてくれることに繋がるのではないかと考えています。</p> <h2 id="まとめ">まとめ</h2> <p>スマートキャンプにエンジニアとして入社するともれなくリモートHQが利用できます。 この記事を読んだ方が在宅環境を整えて生産性を高めることに少しでも興味を持っていただけたら幸いです。</p> <p>また先方のメディアにも弊社の記事を掲載していただいていますので、宜しければこちらもご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhq-hq.co.jp%2Fcasestudy%2Fsmartcamp" title="スマートキャンプ株式会社 |HQ|導入事例" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://hq-hq.co.jp/casestudy/smartcamp">hq-hq.co.jp</a></cite></p> <p>スマートキャンプではこれからもエンジニアが働きやすい環境作りに積極的に投資していきます。</p> <p>※PR記事のような内容になってしまいましたが、これは記事広告ではありません、念のため。</p> smartcamp プロトタイピング開発でハッピーになった話をする feat. Zoom Phone hatenablog://entry/820878482943207537 2023-06-22T13:00:00+09:00 2023-07-31T14:32:36+09:00 今回、BALES CLOUDとZoom Phoneの連携をすることになりました。 調査・実装等々行いましたので、この件についてお話ししたいと思います。 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230620/20230620194211.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>はじめまして! <a href="https://bales.smartcamp.co.jp/bales-cloud">BALES CLOUD</a>エンジニアのてぃが(光永)と申します。 今回、BALES CLOUDとZoom Phoneの連携をすることになりました。 調査・実装等々行いましたので、この件についてお話ししたいと思います。</p> <ul class="table-of-contents"> <li><a href="#前置き">前置き</a><ul> <li><a href="#Zoom-Phoneとは">Zoom Phoneとは?</a></li> <li><a href="#BALES-CLOUDとは">BALES CLOUDとは?</a><ul> <li><a href="#インサイドセールスとは">インサイドセールスとは?</a></li> </ul> </li> </ul> </li> <li><a href="#なにをするのか">なにをするのか?</a><ul> <li><a href="#BALES-CLOUDとZoom-Phoneを連携するということ">BALES CLOUDとZoom Phoneを連携するということ</a></li> <li><a href="#Zoom-Phone連携の構成">Zoom Phone連携の構成</a></li> </ul> </li> <li><a href="#なにをやったのか">なにをやったのか?</a><ul> <li><a href="#Zoom-Phoneの調査">Zoom Phoneの調査</a><ul> <li><a href="#Zoom-Phone-Smart-Embed公式リリースが今年">Zoom Phone Smart Embed公式リリースが「今年」</a></li> <li><a href="#OAuthを使うための作業が多い">OAuthを使うための作業が多い</a></li> </ul> </li> </ul> </li> <li><a href="#つくっていく">つくっていく</a><ul> <li><a href="#Zoom-Phone-Smart-Embedの開発の流れ">Zoom Phone Smart Embedの開発の流れ</a><ul> <li><a href="#1-なにはともあれ動くものをつくる">1. なにはともあれ動くものをつくる</a></li> <li><a href="#2-既存のUIと似せてみる">2. 既存のUIと似せてみる</a></li> <li><a href="#3-デザインUIを調整してみる">3. デザイン・UIを調整してみる</a></li> <li><a href="#4-機能の構成決定設計へ">4. 機能の構成決定! 設計へ</a></li> <li><a href="#5-設計ができた反映する">5. 設計ができた! 反映する</a></li> <li><a href="#6-完成まで駆け抜ける">6. 完成まで駆け抜ける</a></li> </ul> </li> <li><a href="#プロトタイピングのなにがよかったのか">プロトタイピングのなにがよかったのか?</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#プレスリリース">プレスリリース</a></li> </ul> <h2 id="前置き">前置き</h2> <p>そもそもなんのことを言っているのか? とお思いでしょうから、まずは前提説明です。</p> <h3 id="Zoom-Phoneとは">Zoom Phoneとは?</h3> <p>皆さんご存じのZoomが提供しているプロダクトの一つに<a href="https://explore.zoom.us/ja/products/zoom-phone/">Zoom Phone</a>があります。</p> <blockquote><p>クラウド VoIP 電話サービス Zoom Phone は、あらゆる規模のビジネスに向けた、機能豊富なクラウド電話システムです。</p></blockquote> <h3 id="BALES-CLOUDとは">BALES CLOUDとは?</h3> <p>一方、私の所属するチームでは<a href="https://bales.smartcamp.co.jp/bales-cloud">BALES CLOUD</a>というサービスを開発しています。</p> <blockquote><p>インサイドセールス時代に最適な営業DXツール インサイドセールスを効率的、かつ高度に行うためのツールです。</p></blockquote> <h4 id="インサイドセールスとは">インサイドセールスとは?</h4> <p>上述の通り、BALES CLOUDはインサイドセールス向けのツールです。<br> インサイドセールスについて、<a href="https://bales.smartcamp.co.jp/article/blog-32">弊社記事</a>から引用すると以下の通りです。</p> <blockquote><p>見込み顧客に対してメールや電話、Web会議ツールなどを活用しながら非対面で営業活動を行なう内勤型の営業体制です。 インサイドセールスは営業活動全体の効率化が目的です。</p></blockquote> <p>つまり、インサイドセールスでは「メールや電話、Web会議ツールなどを利用した営業活動」を「効率的」に行なうことが求められます。<br> こちらを補助するのがBALES CLOUDというわけですね。<br> また、BALES CLOUDの説明に「DXツール」ともあるとおり、インサイドセールスの行動を「データにする」こともBALES CLOUDの重要な機能です。</p> <h2 id="なにをするのか">なにをするのか?</h2> <p>さて、そんなBALES CLOUDとZoom Phoneを連携するとはどういうことでしょうか。</p> <h3 id="BALES-CLOUDとZoom-Phoneを連携するということ">BALES CLOUDとZoom Phoneを連携するということ</h3> <p>Zoom Phoneは「クラウド電話システム」であり、インサイドセールスの「電話を活用した営業活動」、いわゆる「架電業務」に活用できます。</p> <p>...と、ここまでの情報を踏まえつつ、いきなりですが、BALES CLOUDのZoom Phone連携の要求は以下の通りです。</p> <ul> <li>ワンクリックで通話が開始できる</li> <li>通話の記録が(ほぼ)自動で取れる</li> <li>通話内容の録音・再生ができる</li> </ul> <p>BALES CLOUDの提供したい「業務の効率化」という価値においては、「ワンクリックでの通話」や「自動での記録」は特に外せない機能です。</p> <h3 id="Zoom-Phone連携の構成">Zoom Phone連携の構成</h3> <p>BALES CLOUDでは、前述の要求を満たすためにZoomの以下の機能を利用することにしました。</p> <ul> <li><a href="https://marketplace.zoom.us/docs/guides/zoom-phone/smart-embed/">Zoom Phone Smart Embed</a> <ul> <li>提供されているZoom PhoneのUIをWebアプリに組み込んで使うことができる</li> </ul> </li> <li><a href="https://marketplace.zoom.us/docs/api-reference/phone/methods/#overview">Zoom Phone API</a> <ul> <li>Zoom Phoneの情報にアクセスするためのAPI</li> </ul> </li> <li><a href="https://marketplace.zoom.us/docs/guides/auth/oauth/">Zoom OAuth</a> <ul> <li>Zoom APIを使うためのOAuth(2.0)</li> </ul> </li> </ul> <p>通話機能をZoom Phone Smart Embedで実現し、<br> 録音などの情報取得をZoom Phone API×Zoom OAuthで実現する構成です。</p> <p>なお、Zoom Phone Smart EmbedのUIはこんなかんじです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230620/20230620194051.png" width="358" height="648" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="なにをやったのか">なにをやったのか?</h2> <p>いよいよ、エンジニアの具体的な行動の話をしていきましょう。</p> <p>Zoom Phone連携は以下のような流れで開発していきました。</p> <ol> <li>Zoom Phoneの調査</li> <li>Zoom Phone Smart Embedのプロトタイピング</li> <li>各種設計</li> <li>実装</li> </ol> <h3 id="Zoom-Phoneの調査">Zoom Phoneの調査</h3> <p>まずは、Zoom Phone連携のための情報を収集しました。<br> Zoom公式には下記さまざまなドキュメントやサポートが用意されています。</p> <ul> <li><a href="https://support.zoom.us/hc/en-us">Zoom Support</a>(英語Supportページ)</li> <li><a href="https://support.zoom.us/hc/ja">Zoom サポート</a>(日本語Supportページ)</li> <li><a href="https://marketplace.zoom.us/docs/guides/">Zoom Developer Platform > Document</a></li> <li><a href="https://marketplace.zoom.us/docs/api-reference/phone/methods/#overview">Zoom Developer Platform > Document > API > Zoom Phone API</a></li> <li><a href="https://devforum.zoom.us/">Zoom Developer Forum</a></li> <li><a href="https://devsupport.zoom.us/hc/en-us">Zoom Developer Support</a></li> </ul> <p>これらの文書を読み込み、時には実際に手を動かして検証してみつつ、要件を実現するための情報を集めました。<br> ある程度はエンジニアならお馴染み「ドキュメントに書いてあるとおりやれ!」のやつなのですが、苦労した点がいくつかありましたので少し紹介してみますと...。</p> <h5 id="Zoom-Phone-Smart-Embed公式リリースが今年">Zoom Phone Smart Embed公式リリースが「今年」</h5> <p>Zoom Phone Smart Embedについて調べていたてぃがさん(私)は、情報がやたらと少ないなあ、ということに気がつきました。<br> そこで更なる情報を求めてdeveloper communityの投稿などを漁っていたところ、以下の<a href="https://devforum.zoom.us/t/embedding-zoom-phone-into-web-application/67670/5">コメント</a>を見つけました。</p> <blockquote><p>Thank you for your patience. We have released Zoom Phone smart embed. To know more, please visit: <a href="https://support.zoom.us/hc/en-us/articles/11630110788877">https://support.zoom.us/hc/en-us/articles/11630110788877</a></p></blockquote> <p>その時、このコメントの投稿日時には燦然と輝く『14d』の文字がありました。<br> 「このコメントの投稿は14日前だよ」の意味です。<br> そうです、Zoom Phone Smart Embedは、公式リリースされたばかりだったのです。</p> <p>しかしBALES CLOUDチームでは、臆さず、この新しい技術の採用を決断したのでした。</p> <h5 id="OAuthを使うための作業が多い">OAuthを使うための作業が多い</h5> <p>Zoom OAuthを自社サービスで利用するためには、自社用のOAuthアプリをZoom Marketplaceのアプリとしてリリースする必要があります。<br> それに伴って、申請等々、の作業が発生しました。<br> 詳しくは、以下の公式ドキュメントを眺めて見てください。</p> <p><a href="https://marketplace.zoom.us/docs/guides/publishing/app-submission/">Zoom App Submission</a></p> <p>なかなかの物量の作業が指示されています。<br> 申請作業を受け持ってくれた弊チームの...調律者?(役割名迷子)井上さん(a.k.a.師匠)には頭が下がる思いです。</p> <p>(感謝の意を込めて師匠さんの記事を貼っておきます。よければこちらもご覧ください。→<a href="https://tech.smartcamp.co.jp/entry/asynchronous-meeting">BALES CLOUD TEAMのMTG非同期化への取り組み</a>)</p> <h3 id="つくっていく">つくっていく</h3> <p>情報をあらかた集めたので、今度は、設計や仕様について考えて作っていくフェーズです。<br> ここで、「ユーザーの使い勝手に大きく関わるUIについては、見えるものを元に検討したい!」と弊チームPO兼PdMから依頼がありました。<br> (BALES CLOUDの売りはUIの良さです。(隙自慢))</p> <p>そこで、UIを担うZoom Phone Smart Embedの実装については、プロトタイピングで行なっていくこととしました。<br> OAuthやAPIの実装はすこしわきに置いておき、ここではこの件についてお話ししたいと思います。</p> <h4 id="Zoom-Phone-Smart-Embedの開発の流れ">Zoom Phone Smart Embedの開発の流れ</h4> <p>Zoom Phone Smart Embedについては、大きく以下のような流れで開発を進めました。</p> <ol> <li>なにはともあれ動くものをつくる</li> <li>既存のUIと似せてみる</li> <li>デザイン・UIを調整してみる</li> <li>機能の構成決定! 設計へ</li> <li>設計ができた! 反映する</li> <li>完成まで駆け抜ける</li> </ol> <p>プロトタイピングは、1〜3のステップで行いました。<br> プロトタイピングで行ったことは以下のような感じです。</p> <ol> <li>「検証したい内容を確認するためのもの」を作る</li> <li>1をPdMと確認する</li> <li>PdMやチームと一緒に次のステップ(検証したい内容)を決める</li> </ol> <p>最初からステップがすべて決まっているのではなく、ステップ1からはじめて、順々に次にやることを意思決定していく形です。</p> <p>各ステップでの具体的な作業内容についても、簡単にご説明します。</p> <h5 id="1-なにはともあれ動くものをつくる">1. なにはともあれ動くものをつくる</h5> <p>Zoom Phone Smart EmbedがBALES CLOUD上で表示され、通話ができるところまでを作ってみることにしました。<br> とりあえずBALES CLOUDにZoom Phone Smart Embedを埋め込んで、動く様子を観察してみます。<br> また、Zoom Phone Smart Embedでは発着信・通話開始・終話などなど、リアルタイムに色々なイベントが取れるようなので、それらもとりあえず捕捉して様子を見てみます。</p> <p>BALES CLOUDのフロントエンドはVue.js/JavaScript/TypeScriptですので、ここの実装は完全にZoom Phone Smart Embedの「ドキュメントの通り」です。</p> <h5 id="2-既存のUIと似せてみる">2. 既存のUIと似せてみる</h5> <p>BALES CLOUDには、すでに連携しているCTIサービスがあります。<br> 今度はそのUIに挙動を似せてみることにしました。<br> Vueでcomponentを作りこみ、既存機能に埋め込んでいきます。</p> <h5 id="3-デザインUIを調整してみる">3. デザイン・UIを調整してみる</h5> <p>動かしているといくつか気になる箇所を発見。<br> ちょちょいと調整します。</p> <h5 id="4-機能の構成決定設計へ">4. 機能の構成決定! 設計へ</h5> <p>これまでに収集した情報と、プロトタイプを参照しつつ、チームで話し合ってZoomで利用する機能などの構成を決定しました。<br> 構成については前述の通りです。</p> <p>構成も決まったので、PdMが具体的な設計を開始します。<br> とはいえ、プロトタイプによってある程度は機能のイメージができています。<br> なので、ゼロからの設計ではなく、懸念点などを洗い出すためにさらに詳細な要件を具体的に書き出してもらう形です。</p> <h5 id="5-設計ができた反映する">5. 設計ができた! 反映する</h5> <p>Zoom Phone Smart Embedを利用した機能の最初のステップについて、PdMの設計がおわりました。<br> 出来上がった要件に合わせて、足りない機能の実装や違っている挙動の実装変更を行なっていきます。</p> <h5 id="6-完成まで駆け抜ける">6. 完成まで駆け抜ける</h5> <p>あとは設計が進むたびに「ものをつくる」と「確認」を何度か繰り返し...できあがりです!</p> <h4 id="プロトタイピングのなにがよかったのか">プロトタイピングのなにがよかったのか?</h4> <p>ここからはプロトタイピングをやってみた感想です。</p> <p>これは、「めっちゃアジャイルしている!」の一言に尽きると思います。<br> ...と言ってしまうと流石に不親切なので(笑)もう少し細かくしゃべってみます。</p> <p>まず、動くものをとにかく作って提供できたという点がよかったです。<br> これによってどんどんものができてくるスピード感と、ワクワク感が得られました。<br> 目に見える形ですぐに価値提供ができることで、「Zoom Phone連携、すぐできちゃうんじゃない!?」というポジティブなモチベーションが持続しました。<br> 小さく作業を切って順番に進めていくことで、作業を進めるたびに着実に完成系に近づいていく、というところもよかったです。</p> <p>また、常に目に見えるものがあることで開発者・PdMともに確認がしやすく、意思決定がスムーズに素早く行えて快適でした。<br> 合わせて、認識違いによる手戻りが発生しづらいという利点もあります。<br> 特に「3. デザイン・UIを調整してみる」のステップでは、「現状このように不都合があります」という確認、「このように修正できます」という提案をブラウザ上で実際の画面を操作しながら行えて意思疎通がスムーズでした。</p> <p>つまり、「アジャイルしている!」...言い換えると、「スピード感のある価値提供ができている!」ということになるでしょうか。<br> 以下のような、アジャイルを考えるときに3万回見る図の体現でもあったと思います。</p> <p><figure class="figure-image figure-image-fotolife" title="例の図っぽいやつ(※illustration byてぃが)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230620/20230620194124.png" width="1200" height="405" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>例の図っぽいやつ(※illustration byてぃが)</figcaption></figure></p> <h2 id="まとめ">まとめ</h2> <p>Zoom Phone連携の話はここまでです。</p> <ul> <li>新しい技術を積極的に採用できた</li> <li>アジャイルでものづくりを進められた</li> </ul> <p>ことにより、<br> BALES CLOUDはスピード感を持って開発をしている!<br> という事実をあらためて実感できた案件でした。</p> <p>また、もしBALES CLOUD、またはZoom Phoneの連携機能に興味を持って頂けましたら、<a href="https://bales.smartcamp.co.jp/bales-cloud">こちらの公式ページ</a>より資料請求・お問い合わせいただけます!</p> <p>それでは〜〜〜!</p> <h2 id="プレスリリース">プレスリリース</h2> <p>本記事で紹介されているBALES CLOUDとZoom Phoneとの連携はこちらでも紹介されています!<br></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fprtimes.jp%2Fmain%2Fhtml%2Frd%2Fp%2F000000185.000012765.html" title="「BALES CLOUD」、クラウド電話サービス「Zoom Phone」とのAPI連携を開始" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://prtimes.jp/main/html/rd/p/000000185.000012765.html">prtimes.jp</a></cite></p> smartcamp 「BOXIL SaaS」のChatGPTプラグイン開発の裏側を紹介します! hatenablog://entry/820878482943108933 2023-06-20T15:00:00+09:00 2023-07-31T14:33:31+09:00 はじめに BOXIL SaaSのChatGPTプラグインとは システム概要 開発にあたっての主な意思決定項目 開発の進め方 開発者申請 法務周りの対応 インフラ構成 カテゴリ検索APIの開発 ChatGPTプラグインのここがすごい3選 プラグインの使用を促してくれる 用意したAPI同士の連携ができる 申請から承認まで最短1日!? さいごに はじめに こんにちは。スマートキャンプでエンジニアをしている佐々木(社内ではピーターと呼ばれています)です。 2023年6月20日のプレスリリースの通り、スマートキャンプの新たな取り組みとして2023年6月15日にChatGPTプラグインの提供を開始しました… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230620/20230620120447.png" width="960" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#BOXIL-SaaSのChatGPTプラグインとは">BOXIL SaaSのChatGPTプラグインとは</a></li> <li><a href="#システム概要">システム概要</a></li> <li><a href="#開発にあたっての主な意思決定項目">開発にあたっての主な意思決定項目</a><ul> <li><a href="#開発の進め方">開発の進め方</a></li> <li><a href="#開発者申請">開発者申請</a></li> <li><a href="#法務周りの対応">法務周りの対応</a></li> <li><a href="#インフラ構成">インフラ構成</a></li> <li><a href="#カテゴリ検索APIの開発">カテゴリ検索APIの開発</a></li> </ul> </li> <li><a href="#ChatGPTプラグインのここがすごい3選">ChatGPTプラグインのここがすごい3選</a><ul> <li><a href="#プラグインの使用を促してくれる">プラグインの使用を促してくれる</a></li> <li><a href="#用意したAPI同士の連携ができる">用意したAPI同士の連携ができる</a></li> <li><a href="#申請から承認まで最短1日">申請から承認まで最短1日!?</a></li> </ul> </li> <li><a href="#さいごに">さいごに</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>こんにちは。スマートキャンプでエンジニアをしている佐々木(社内ではピーターと呼ばれています)です。</p> <p>2023年6月20日のプレスリリースの通り、スマートキャンプの新たな取り組みとして2023年6月15日にChatGPTプラグインの提供を開始しました。ChatGPTプラグインを提供するのは、SaaSおよびITサービス比較サイトとしては国内初です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fprtimes.jp%2Fmain%2Fhtml%2Frd%2Fp%2F000000184.000012765.html" title="スマートキャンプ、「BOXIL SaaS」でChatGPTプラグインの提供を開始" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://prtimes.jp/main/html/rd/p/000000184.000012765.html">prtimes.jp</a></cite></p> <p>私を含めエンジニアの正社員2名、インターン生1名、そしてプロダクトオーナー(PO)1名の合計4名で開発を進めました。</p> <p>短期間と言える約3週間で、開発開始からリリースまでを無事に達成できました。</p> <p>具体的な役割としては、私とインターン生がAPIの開発やChatGPTのレスポンスのチューニングを担当しました。一方、もう一名のエンジニアは申請関連の調査やインフラの整備、さらに、POは法務関連の調整やQAとして予想される質問のリストを作成する役割をお願いしました。</p> <p>この記事では、開発の舞台裏や得られた知見などを紹介します。</p> <h2 id="BOXIL-SaaSのChatGPTプラグインとは">BOXIL SaaSのChatGPTプラグインとは</h2> <p>今回開発したプラグインのイメージは次の画像のようになっています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sc-sasakipeter/20230620/20230620114614.png" width="619" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>BOXIL SaaSのChatGPTプラグインでは、ChatGPTに対して業務を効率化したい点について相談すると、その業務の効率化に役立つサービスの一覧が見られるページを提供します。また資料ダウンロードのリンクからBOXIL SaaSに移動することで、サービスごとの詳細な資料を取得できます。</p> <p>実は、カテゴリの一覧ページを検索することは、<a href="https://boxil.jp/">BOXIL SaaS</a>からもできます。しかし、SaaS製品のカテゴリは年々複雑性を増している中、自分が求める製品のカテゴリがどれなのか分からないといった声が寄せられています。ChatGPTを使うことで、例のように「タイムカードを切るのが面倒だ」という潜在的な悩みがどのカテゴリに属するのか判断する手助けが行えます。</p> <h2 id="システム概要">システム概要</h2> <p>実はChatGPTプラグインとして申請に必要なのはドメインだけです。</p> <p>ただし、そのドメインから次の情報にアクセスできる必要があります。</p> <ol> <li>プラグインの説明が書かれたマニフェスト(ドメイン配下の決められたパスに配置)</li> <li>OpenAPI形式で記述されたAPI定義書(マニフェストにURLを記載)</li> <li>ChatGPTから叩きたいAPI(API定義書にエンドポイントを記載)</li> </ol> <p>これにより、ChatGPTはマニフェストやAPI定義書にアクセスし、何をするプラグインなのか、どういう仕様のAPIが置いてあり、どこにAPIを叩きに行けばいいのかを把握できます。</p> <p>そしてChatGPTがユーザーとの会話の中でAPIを叩くべきだと判断したときに、APIが叩かれるという仕組みです。</p> <p>詳細は公式ドキュメントに書かれています。気になる方はご参照ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fplatform.openai.com%2Fdocs%2Fplugins%2Fintroduction" title="OpenAI Platform" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://platform.openai.com/docs/plugins/introduction">platform.openai.com</a></cite></p> <p>また、OpenAI公式からはクイックスタートとしてToDoリストのChatGPTプラグインのリポジトリが公開されています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fopenai%2Fplugins-quickstart%2Ftree%2Fmain" title="GitHub - openai/plugins-quickstart: Get a ChatGPT plugin up and running in under 5 minutes!" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/openai/plugins-quickstart/tree/main">github.com</a></cite></p> <p>今回はChatGPTに叩いてもらうAPIとして次のようなものを作成しました。</p> <p>リクエスト</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">カテゴリ名</span>&quot;: &quot;<span class="synConstant">会計</span>&quot; <span class="synSpecial">}</span> </pre> <p>レスポンス</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">カテゴリ一覧ページ</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">リンク</span>&quot;: &quot;<span class="synConstant">&lt;https://boxil.jp/sc-cloud_accounting&gt;</span>&quot;, &quot;<span class="synStatement">カテゴリ名</span>&quot;: &quot;<span class="synConstant">会計ソフト(財務会計)</span>&quot;, &quot;<span class="synStatement">概要</span>&quot;: &quot;<span class="synConstant">会計ソフトでは、会計業務に精通した方から知識が乏しく不安だという方まで、会計(買掛金台帳・売掛金台帳・賃金台帳・試算表・決算資料など)に関わる業務の効率的を実現し、生産性を飛躍的に向上させます。</span>&quot;, &quot;<span class="synStatement">資料ダウンロードページへのリンク</span>&quot;: &quot;<span class="synConstant">&lt;https://boxil.jp/downloads/confirm/?type=category&amp;ids%5B%5D=114&gt;</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">検索条件</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">カテゴリ名</span>&quot;: &quot;<span class="synConstant">会計</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p>これにより、ChatGPTはカテゴリ名を入れたリクエストをAPIに投げることで、カテゴリ一覧ページへのリンクや資料ダウンロードページへのリンクをプラグインの利用者に提供できるようになります。</p> <h2 id="開発にあたっての主な意思決定項目">開発にあたっての主な意思決定項目</h2> <h3 id="開発の進め方">開発の進め方</h3> <p>ChatGPTプラグインの開発事例は国内でも数件あるのですが、自分たちにのケースに応用できるか不明で、そもそもやりたいことができそうなのか検証するという意味合いが強かったため、エンジニア主導で開発を進めました。</p> <p>そのため、チームでの意思決定を高速に行うために、デザインドキュメントは必ず書き必要な人にレビューを依頼することで法務との調整やインフラのすり合わせ、申請周りの整備、APIの仕様決めなどをテンポ良く行ないました。</p> <p>3週間で書いたドキュメントは約50本くらいになります。</p> <p>先日デザインドキュメントについての記事も執筆したので良かったらご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.smartcamp.co.jp%2Fentry%2Fsolution-by-design-document" title="「仕様」のレビューをPRでしていませんか? 〜Design Documentが解決したスパゲッティコードとの向き合い方〜 - SMARTCAMP Engineer Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.smartcamp.co.jp/entry/solution-by-design-document">tech.smartcamp.co.jp</a></cite></p> <h3 id="開発者申請">開発者申請</h3> <p>開発を進めるにあたって、最初にChatGPTプラグインの開発者申請をする必要があります。</p> <p>開発者申請が通ったChatGPT Plusのアカウントがなければ、ローカルのAPIや申請前のAPIを登録してChatGPTから叩けるか動作検証することはできないからです。(2023年6月現在)</p> <p>私たちも、本プロジェクトが始まる際に急いで申請をしましたが、承認されるまでに16日かかりました。(5/30にChatGPT Plugin waitlistに登録、6/16にChatGPT Plugins Developer Accessが承認)</p> <p>そのため、承認が下りるまでの期間は私の個人的なアカウントで動作確認を行なっていました。</p> <p>もし開発に乗り出したい方は早めに申請するといいでしょう。</p> <h3 id="法務周りの対応">法務周りの対応</h3> <p>法務としては、主にBOXIL SaaSで利用されている情報をChatGPTに提供した際に、個人情報の第三者提供等にあたるかが一番の懸念となりました。</p> <p>そのため、BOXIL SaaSから口コミ情報などを提供する場合には個人情報をマスクすることにし、現段階では個人情報は提供しないことにしました。</p> <p>また、マニフェストの<code>legal_info_url</code>にはBOXIL SaaSの利用規約を入れることにしました。</p> <h3 id="インフラ構成">インフラ構成</h3> <p>今回は1日でも早く世に出したかったので、BOXIL SaaSで元々運用しているAPIサーバーにChatGPT用のエンドポイントを実装することにしました。ChatGPTプラグイン機能はOpenAPI形式で記述したエンドポイント以外にはアクセスしないため、このような設計でも問題ないと判断しました。</p> <p>今後アクセス数が伸びればBOXIL SaaS本体とは切り離すことになるかもしれません。</p> <h3 id="カテゴリ検索APIの開発">カテゴリ検索APIの開発</h3> <p>リリースできる最低限の機能として<a href="https://boxil.jp/">BOXIL SaaS</a>で使われている検索機能をベースに、カテゴリ一覧ページへのリンクが取得できるAPIを作成しました。</p> <p>今後、一覧ページへのリンクではなく具体的なサービスをChatGPTから提供できるようにする可能性もありますが、今回は見送りました。</p> <p>私たちは今回のプロジェクトのために結成された急拵えのチームで、普段はBOXIL SaaSの開発とはあまり関わりがありません。</p> <p>そのため、BOXIL SaaSの開発チームには、実装方針のアドバイスをいただいたり、コードレビューを手伝っていただいたりしました。</p> <p>その時に、やはり便利だったのはデザインドキュメントです。</p> <p>これにより、コードレビューを迅速に進め、滞りなく開発を進めることができました。</p> <h2 id="ChatGPTプラグインのここがすごい3選">ChatGPTプラグインのここがすごい3選</h2> <p>ここでは実際に調査・実装する中で分かったことについて紹介します。</p> <h3 id="プラグインの使用を促してくれる">プラグインの使用を促してくれる</h3> <p>プラグインを有効にしたら、必ずしもそのAPIをいきなり叩けるような質問を投げかける必要性はありません。</p> <p>下記画像のように、純粋に困っていることを相談するように話しかけても自然な流れでAPIの利用を促してくれます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sc-sasakipeter/20230620/20230620114725.png" width="579" height="755" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これは私たちがこのChatGPTに求めていた「ユーザーの潜在的な悩みを吸い上げ、具体的なSaaSサービスに紐づける」ことにおいて大きなメリットだなと感じました。</p> <h3 id="用意したAPI同士の連携ができる">用意したAPI同士の連携ができる</h3> <p>これは実際に実験して分かったのですが、特定のAPIエンドポイントから取得した情報を、別のAPIエンドポイントへの入力として使うことができました。</p> <p>これにより何が嬉しいのかといいますと、検索のマッチ度の判断をChatGPTに移譲できます。</p> <p>下記の例では、最初に「タイムカード」というキーワードでAPIが叩かれましたが、該当するカテゴリが見つからなかったために、カテゴリ一覧をChatGPTが取得し、その中から関連の高い「勤怠管理システム」というキーワードで再度APIを叩き直しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sc-sasakipeter/20230620/20230620114746.png" width="679" height="841" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このようにユーザーの知りたい内容と合致したカテゴリがどれかという判断をAPI内で行わずにChatGPTに移譲できます。</p> <p>今回、カテゴリ検索のAPIとしてBOXIL SaaS内部の検索ロジックを流用したのですが、カテゴリ検索の部分はデータベースから部分一致するカテゴリを検索するようなロジックになっており、検索ワードが長いと検索に引っかかりませんでした。</p> <p>そのため、カテゴリ一覧を別のAPIエンドポイントとして用意し、そちらから検索ワードを取得してもらった上で、ChatGPTにどの検索ワードで調べるかの判断を任せるような設計が有効でした。</p> <h3 id="申請から承認まで最短1日">申請から承認まで最短1日!?</h3> <p>たまたま運が良かったからかもしれませんが、申請した次の日には承認されました。</p> <p>参考になるかは分かりませんが、公式ドキュメントには明記されてないものの申請するにあたって注意したのは次です。</p> <ol> <li>マニフェストは英語で書く</li> <li>マニフェストの<code>description_for_human</code>には<code>Japan</code>という単語を入れる(当該プラグインは国内利用を想定しているため)</li> <li>申請項目のプロンプト例は英語で書き、やりとりを繰り返さなくても一度目の返答でAPIが叩かれる</li> </ol> <h2 id="さいごに">さいごに</h2> <p>スマートキャンプでは「Small Company, Big Business.」というVisionを掲げています。</p> <p>新卒でもこのような新しい技術を使ったサービス開発を機会をもらえたり、少人数チームが故の機動力の高さでプロジェクトを進めていけるのが魅力だと思います。</p> <p>今後も新たな技術を活用し、よりユーザーにとってぴったりなサービスを快適に探せるよう取り組んでいきます。</p> <p>最後まで読んでいただきありがとうございました!</p> sc-sasakipeter 後任者を救うための究極引き継ぎドキュメント hatenablog://entry/820878482943083116 2023-06-20T13:00:00+09:00 2023-07-31T14:33:47+09:00 ドキュメントを残さないといけないことはなんとなくわかる。 なのでNotionなりkibelaなり社内で使うツールにちょこちょこドキュメントを残していたりもする。 だけどさ、残したドキュメント見られてます?使われてます? 本当に大事なことは自分が理解できるドキュメントではなく、読者が理解できるドキュメントを残すことなんじゃないか・・・!? <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230620/20230620090907.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="はじめに">はじめに</h2> <p>ドキュメントを残さないといけないことはなんとなくわかる。</p> <p>なのでNotionなりkibelaなり社内で使うツールにちょこちょこドキュメントを残していたりもする。 だけどさ、残したドキュメント見られてます?使われてます?</p> <p>本当に大事なことは自分が理解できるドキュメントではなく、読者が理解できるドキュメントを残すことなんじゃないか・・・!?</p> <p>ちなみにタイトルはbingが考えてくれました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/m_kumanomi/20230615/20230615225633.png" width="1200" height="596" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="執筆のきっかけ">執筆のきっかけ</h2> <p>業務でAmazonPersonalizeを利用した機械学習、推論の作業を引き継ぐこととなった。</p> <p>事前知識なんてない。全くない。</p> <p>開発環境の閲覧権限のあるアカウントすら持ってなかった。</p> <p>そんな中でデータPMの方が引き継ぎドキュメントを用意してくれた。 それをみてポチポチしていけば作業を完遂できた。しかも迷うことなく。</p> <p>今まで自分が触れてきたドキュメントだと迷ったり、調べないといけなかったり・・・。 挙げ句の果てには違っているので自分で検証して実装変更しないといけなかったり。</p> <p>聞ける相手がいるならまだいいが、退職などで聞くことができなくなることもある。</p> <p>もしも自分が引き継ぎ用にドキュメントを残したとして、こんなに綺麗に引き継げるものなんだろうか?僕はこれまで何も考えず、クソみたいなドキュメントを量産するマシンだった。</p> <p>自分の課題の言語化と同時に、記事にしちゃおうと思った次第です。</p> <h2 id="引き継ぎドキュメント10ヵ条">引き継ぎドキュメント10ヵ条</h2> <p>個人的にどこを意識すると良さそうかを10項目考えました。</p> <p>本当はもっとポイントはありそうだし、気をつけなければならない優先順位などもあるのかもしれない。キリがないので10項目に絞らせて欲しい。</p> <p>ここからは実際にドキュメントを残す感じで書いていきます!</p> <h3 id="1-対象読者の明確化">1. 対象読者の明確化</h3> <p>前提条件を明記し対象の読者かどうか早期に判断できるようにする。</p> <p>これは何のドキュメントで何をして欲しいのかを短くまとめる。</p> <pre class="code" data-lang="" data-unlink> (例)前提事項 ・ このドキュメントはAmazonPersonalizeを用いてレコメンド用のCSVを作成するための手順書です ・ レコメンドロジックの利用方法   ・ 説明リンク ・ AmazonPersonalizeの概要説明   ・ 説明リンク ・ レコメンドの種類   ・ 説明リンク</pre> <ul> <li>非対象者の読者が見た場合に読まなくて良い判断を早くさせることが可能。</li> <li>意味を感じ取れない作業は苦痛にしかならないため、何のために何に利用されるのかを明確化する。</li> </ul> <h3 id="2-とにかく簡潔に">2. とにかく簡潔に</h3> <p>難しい表現、伝わりづらいものはキャプチャを入れつつ、必要な情報だけをまとめる。 その際に複数項目を並べる際には表組みも利用する。</p> <p>下記は各レコメンドロジックのインプットとアウトプットデータのディレクトリ説明の表です。</p> <table> <thead> <tr> <th>            </th> <th> 学習用データ </th> <th> 推論入力用データ </th> <th> 推論出力結果データ </th> </tr> </thead> <tbody> <tr> <td> サービスレコメンド用 </td> <td> fulldata/training/ </td> <td> fulldata/predict_input/ </td> <td> fulldata/predict_output/ </td> </tr> <tr> <td> カテゴリレコメンド用 </td> <td> category_recommend/training/ </td> <td> category_recommend/predict_input/ </td> <td> category_recommend/predict_output/ </td> </tr> </tbody> </table> <ul> <li>表組にすることでMECEに表記可能</li> <li>視覚的に理解しやすくする</li> </ul> <h3 id="3-表現方法を意識">3. 表現方法を意識</h3> <p>用語の説明を書いていく際には下記を意識し、検索性の向上させる。</p> <ul> <li>本質をついた名前をつける</li> <li>社内の用語に合わせる</li> <li>表記の揺れをなくす</li> </ul> <pre class="code" data-lang="" data-unlink>(例) ユーザー/ユーザ 売り上げ/売上</pre> <h3 id="4-肯定し断言する">4. 肯定し断言する</h3> <p><code>xxxした方が良い</code>という表現は必須項目であれば<code>xxxする</code>という表現にする。</p> <p>条件付きで実行した方が良いものに関しては<code>yyyの場合はxxxする</code>と実行条件を明記する。</p> <p><code>yyyしない</code>という否定表現ではなく<code>xxxする</code>という肯定表現を使う。</p> <p><code>yyyに注意する</code>などは<code>yyyを実行しxxxする</code>のようにすべき行動で記載する。</p> <pre class="code" data-lang="" data-unlink>(例) 誤: 条件A**でない**場合には、項目Xを選択する。 正: 条件B、Cの場合、項目Xを選択する。</pre> <ul> <li>肯定系で漏れなく書くことで具体的に作業をイメージ可能。</li> <li>多少文章が長くなる可能性もあるが、迷うことがなくなる。</li> </ul> <h3 id="5-手順を具体化する">5. 手順を具体化する</h3> <p>入力不要、選択不要も含めて、通るであろう手順に関わることはすべてもれなく具体的に記載する。</p> <pre class="code" data-lang="" data-unlink>(ポイント) ・ 別ページ参照などと本文中に書かない ・ 後から質問されないであろう粒度まで具体的にする ・ 後任者が行うであろう動作を考えたうえで説明する ・ 何かを入力していく項目画面であれば入力値を明記する ・ 注意点があれば本文中には残さずコメントに残す ・ 円滑に実行させることだけにフォーカスする</pre> <ul> <li>ドキュメントの読者がどのような動きをしているかを想定したうえで記載していくとスムーズに作業を完遂できる。</li> </ul> <h3 id="6-節目で確認可能にする">6. 節目で確認可能にする</h3> <p>途中で確認できるのであれば、確認タイミングを挟む。</p> <p>その場合は<code>実行ボタンを押下したか?(yes/no)</code>のような聞き方ではなく、押下後に起きているであろう事象を明記しておくことで確認可能になる。</p> <pre class="code" data-lang="" data-unlink>(例)実際のドキュメントより抜粋 ・ category-to-categoryレコメンドのレシピを作成(モデル学習)する。 ・ Solutions and recipesの設定画面を開く。 ・ 作成したjobが「create in progress」になっている事を確認する。</pre> <h3 id="7-道の一本化">7. 道の一本化</h3> <p>タスク1~3の処理を実行した後にタスク4を実行しなければならない場合、下記の図のような処理を想像していた。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/m_kumanomi/20230615/20230615225541.png" width="1200" height="371" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ただドキュメントに落とし込む場合にはもっとシンプルで良い。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/m_kumanomi/20230615/20230615225558.png" width="1200" height="188" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>とにかく道の一本化させる。</li> <li>途中で道が分かれるのであれば別ドキュメントに分けるなど工夫する。</li> </ul> <h3 id="8-資料は最新の状態で書くこと">8. 資料は最新の状態で書くこと</h3> <p>過去にまとめた情報からピックアップして作らずに最新の状態を元に書く。</p> <p>現状の仕様に合わせた書き方でなければ読者が勘違いする可能性は多々ある。</p> <h3 id="9-資料は後任者と確認すること">9. 資料は後任者と確認すること</h3> <p>引き継ぎ後に質問が来て場合作業が止まった経験がある。自分にとっても相手にとっても時間のロスにつながる。</p> <p>相手がどの程度理解したかを<strong>実際に試しながら</strong>確認を実施することで質問が来ない状況を作る。</p> <p>試運転に勝るものはない。通して実施した際にドキュメントの不備も見つかる。</p> <ul> <li>試運転で問題がなければ安心して業務を任せることが可能。</li> <li>後の時間のロスをなくすことが可能。</li> <li>疑問点があればドキュメントをアップデートを実施。</li> </ul> <h3 id="10-ドキュメントのオーナーを移譲すること">10. ドキュメントのオーナーを移譲すること</h3> <p>前任者のさらに前の前前任者からの秘伝のドキュメントをもらった経験が何度かある。 間違いを修正して良いのか悩んでしまいドキュメントを更新しないことも多々あった。</p> <ul> <li>ドキュメントのオーナーを移譲しメンテナンスを後任者にまかせることで最新性を保つ。</li> </ul> <h2 id="まとめ">まとめ</h2> <p>引き継ぎドキュメントは、後任者への円滑な業務引継ぎに欠かせません。</p> <p>あらかじめ引継ぎすることを考え資料を作成することで、担当業務をふりかえる機会にもなり課題を発見し、業務改善にもつなげることができます。</p> <p>自分の業務を移譲することで、新しい業務、次のステップへと円滑に進めることができ視野も広がります。</p> <p>使われるドキュメントを書いていきましょう!</p> <h2 id="雑感">雑感</h2> <p>究極とか言っておきながら書いてみたら普通のことしか書いてない。</p> <p>ただしっかりと考えたうえで日常的に実施していかないと<strong>普通</strong>はできないのかもしれない。</p> <p>ドキュメントを書くことは大事。だが一番大事なのは<strong>読者への気配り</strong>だと悟りました。</p> smartcamp 「仕様」のレビューをPRでしていませんか? 〜Design Documentが解決したスパゲッティコードとの向き合い方〜 hatenablog://entry/820878482936692611 2023-05-30T12:00:00+09:00 2023-07-31T14:33:58+09:00 はじめに プロダクトの概要と開発背景 BOXIL EVENT CLOUD について イベクラの開発背景 最初の課題 Design Documentとは GoogleのDesign Doc イベクラチームにおけるDesign Document フォーマット Design Documentがチームにもたらすもの 手戻りが発生しない 問題認識の差が埋まる 考えが整理される 応用が効く Design Documentのポイント7選 問題背景を書くことに99%の労力を割く(つもりで書く) 読者が知っているであろうことから始める 過去のPRを読みに行く PRから辿れるようにする 議論の足跡を残しておく 実… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230529/20230529125434.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#プロダクトの概要と開発背景">プロダクトの概要と開発背景</a><ul> <li><a href="#BOXIL-EVENT-CLOUD-について">BOXIL EVENT CLOUD について</a></li> <li><a href="#イベクラの開発背景">イベクラの開発背景</a></li> <li><a href="#最初の課題">最初の課題</a></li> </ul> </li> <li><a href="#Design-Documentとは">Design Documentとは</a><ul> <li><a href="#GoogleのDesign-Doc">GoogleのDesign Doc</a></li> <li><a href="#イベクラチームにおけるDesign-Document">イベクラチームにおけるDesign Document</a></li> <li><a href="#フォーマット">フォーマット</a></li> </ul> </li> <li><a href="#Design-Documentがチームにもたらすもの">Design Documentがチームにもたらすもの</a><ul> <li><a href="#手戻りが発生しない">手戻りが発生しない</a></li> <li><a href="#問題認識の差が埋まる">問題認識の差が埋まる</a></li> <li><a href="#考えが整理される">考えが整理される</a></li> <li><a href="#応用が効く">応用が効く</a></li> </ul> </li> <li><a href="#Design-Documentのポイント7選">Design Documentのポイント7選</a><ul> <li><a href="#問題背景を書くことに99の労力を割くつもりで書く">問題背景を書くことに99%の労力を割く(つもりで書く)</a></li> <li><a href="#読者が知っているであろうことから始める">読者が知っているであろうことから始める</a></li> <li><a href="#過去のPRを読みに行く">過去のPRを読みに行く</a></li> <li><a href="#PRから辿れるようにする">PRから辿れるようにする</a></li> <li><a href="#議論の足跡を残しておく">議論の足跡を残しておく</a></li> <li><a href="#実装後はDesign-Documentを更新しない">実装後はDesign Documentを更新しない</a></li> <li><a href="#タスクと紐付ける">タスクと紐付ける</a></li> </ul> </li> <li><a href="#導入による心理的な変化">導入による心理的な変化</a><ul> <li><a href="#チームへの発信がしやすい">チームへの発信がしやすい</a></li> <li><a href="#タスクを進めるときに迷いがない">タスクを進めるときに迷いがない</a></li> <li><a href="#自信を持ってPRが出せる">自信を持ってPRが出せる</a></li> </ul> </li> <li><a href="#今後の課題">今後の課題</a></li> <li><a href="#最後に">最後に</a></li> </ul> <h1 id="はじめに">はじめに</h1> <p>こんにちは!</p> <p>2022年度の新卒として入社して早一年が経ちます、ピーターこと佐々木です。</p> <p>私の配属先のプロダクトであるBOXIL EVENT CLOUDは、様々な背景(後述)から自分含め3人いる開発メンバーの誰一人既存のコードがどのような思想のもとで書かれたものか分かっていない状態からスタートしました。</p> <p>表から見るとそんなに規模の大きくない1つのサービスに見えるのですが、アプリケーションが2つに分かれており、DBも別々に管理していたりと、全体像を把握するだけでも一苦労に見えました。</p> <p>その上、ソースコード自体も詳細に分かれすぎていて、複雑に入り組んでいます。いわゆるスパゲッティコード状態です。</p> <p>そんな状態のプロダクトとの付き合いももうじき1年になります。</p> <p>そこで今回は、どのようにしてBOXIL EVENT CLOUDにチームとして立ち向かっているのかについて紹介いたします。</p> <h1 id="プロダクトの概要と開発背景">プロダクトの概要と開発背景</h1> <h2 id="BOXIL-EVENT-CLOUD-について">BOXIL EVENT CLOUD について</h2> <p>スマートキャンプでは、主にSaaSを導入したい企業様向けに、それぞれの企業様のプロダクトの紹介の場を設けることで、潜在顧客様との接点を作る手助けをさせていただいております。</p> <p>オンライン展示会を開催するプラットフォームとして、そのような接点の1つとしての役割を求めらているのが、私たちが開発している「BOXIL EVENT CLOUD(ボクシル イベント クラウド)」(以下、イベクラと言います)です。</p> <h2 id="イベクラの開発背景">イベクラの開発背景</h2> <p>イベクラは、もともとベトナムでのオフショア開発から始まったプロダクトで、スマートキャンプのエンジニアはあまり関わってこなかったプロダクトでした。</p> <p>しかしながら、開発効率を上げるために約1年前にオフショア開発から社内開発に切り替えました。そうして生まれたのが、私たちイベクラチームです。</p> <p>ですが、ある問題が浮上します。それは引き継ぎの際にあまり整理されたドキュメントなどが得られなかったことです。その結果、私たちは中身を全く知らないプロダクトをパッと渡されたところからメンテナンスを始めるという状況に等しいところから、開発を始めなければなりませんでした。</p> <h2 id="最初の課題">最初の課題</h2> <p>そのため、すでにある大量のコードがどのような思想で書かれたものなのか不明だったのが最初の課題でした。</p> <p>例えば、</p> <ul> <li>このGemはどういう目的で、いつ入れられたものなのか、今後も必要なのか</li> <li>今あるこの機能は一見不要そうなのだが、どういう経緯で作られたものなのか</li> <li>システムが複数リポジトリ、複数AWS環境に跨っているが分かれている必要はあるのか</li> <li>大量にあるテストコードが本当に意味のあるコードになっているのか</li> </ul> <p>などです。</p> <p>そして、実際に調査する中で求められているビジネス要件に対して、1+1を計算するのに量子コンピュータを持ち出しているような、過度な道具や手段を用いているところが多々見つかりました。</p> <p>いわゆる「ハンマーしか持っていなければすべてが釘のように見える」状態で作られたプロダクトと言えるやもしれません。</p> <p>そして出会ったのが、今回ご紹介したいDesign Documentです。</p> <h1 id="Design-Documentとは">Design Documentとは</h1> <h2 id="GoogleのDesign-Doc">GoogleのDesign Doc</h2> <p>一般的にはGoogleのDesign Docが有名で、それをアレンジしたものとして、私たちのチームでは活用しています。</p> <p>これは、ソフトウェアの設計を定義するためのものであり、開発者自身がコーディングタスクに着手する前に作成し、主に実装方針と設計がその際に考慮したトレードオフと共に書かれているものです。</p> <p>詳細は、こちらをご覧ください。</p> <p><a href="https://www.industrialempathy.com/posts/design-docs-at-google/">Design Docs at Google</a></p> <h2 id="イベクラチームにおけるDesign-Document">イベクラチームにおけるDesign Document</h2> <p>このDesign DocをDesign Documentとして、イベクラチームでは「仕様のレビューを行なう文章」として捉えています。</p> <p>※前提として私たちはスクラム開発で業務を進めています。</p> <p>Pull Requestを作成するとき、次のような問題背景を書くと思います。</p> <ul> <li>何が問題なのか / なぜこの変更が必要なのか</li> <li>今の仕様・設計はどうなっているのか</li> <li>変更の目的/歴史的背景</li> <li>設計上のトレードオフ</li> </ul> <p>これらをPRを出す前・コードを書く前に書いておいて、その内容をチームでレビューしてからコードを書き始めましょうというものです。</p> <p>これにより、PRを出すときには、レビュアーはすでにその変更の背景を理解しているため、実装のレビューに集中できます。</p> <p>GitHubのIssueとして書いて、PRにリンクするのも1つの手だと思いますが、私たちはNotionで行っています。レビューを行なうのに、それは特定の行、単語にコメントが書ける方が適していると考えているからです。</p> <h2 id="フォーマット">フォーマット</h2> <p>また、フォーマットは次のようにしています。</p> <ul> <li>タイトル/著者/作成日時/最終更新日時</li> <li>概要</li> <li>問題背景</li> <li>提案</li> <li>Sign off(承認)</li> </ul> <p>この他にも項目を追加することもありますが、これが基本です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230529/20230529140506.png" width="886" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>また、このDesign Documentにはレビューしやすい以外にもメリットがあります。</p> <h1 id="Design-Documentがチームにもたらすもの">Design Documentがチームにもたらすもの</h1> <h2 id="手戻りが発生しない">手戻りが発生しない</h2> <p>コードを書いてPRを出してから、「その設計どうなの?」と仕様の検討に戻ることがありません。なぜなら、その認識を合わせるのが、Design Documentの役割だからです。</p> <p>そのため、PRがマージされるまでの時間もとても短いです。</p> <p>「後からこのPRでこの修正も入れた方がいいんじゃない?」ということもありません。</p> <p>もし後から問題が見つかった場合は、別のDesign Documentとして切り出され、以前のドキュメントをリンクするようにします。</p> <h2 id="問題認識の差が埋まる">問題認識の差が埋まる</h2> <p>イベクラ開発初期は、自分がタスクにアサインされた箇所しか内部的にどのような仕様になっているかが把握できていませんでした。そのためレビューするのもなんだかとても難しかった印象があります。しかし、Design Documentを書き、あらかじめ整理されたドキュメントを読むことで新しく開発に参画した人でも問題の背景がわかり、レビューがしやすくなります。</p> <p>また、チームで設計のレビューを行なうので、技術に長けている人の知見が活かしやすくなります。逆にプロダクトに詳しくなかったり、知見があまりなかったりしても自信を持ってPRを作成できます。</p> <h2 id="考えが整理される">考えが整理される</h2> <p>チームメンバーに変更の背景を伝えようと文書化する過程で、思考が整理され無駄のない修正になります。</p> <p>おっちょこちょいな開発者は、この修正で動くからいいだろうとたくさんコードを書いてPRを出してから、よくよく調べたら今実装したクラスはすでに実装されていて、それを使えばいいだけだったということに気づくことがあるかもしれません。</p> <p>逆に、一見不要そうな機能でも実は使っていることもあります。このクラス使ってなさそうだから削除してしまえとPRを作ったら、実は使っていたなんてことは山ほどあります。</p> <p>そのようなことは、コードを書き始める前に問題背景を明らかにし、現在のコードになっている歴史的背景をまとめておけば起きないはずです。</p> <h2 id="応用が効く">応用が効く</h2> <p>Design Documentの考え方は、レビューのためだけに使うのも勿体無いです。</p> <p>リモートで働く上で、議論を進めるのにもかなり便利だなと感じています。</p> <p>周りにある全ての議題・問題に対して、Design Documentを書くことで非同期的に議論を進めることができ、会議時間が減らせると思います。</p> <h1 id="Design-Documentのポイント7選">Design Documentのポイント7選</h1> <p>実際に運用する中で、分かってきた7つのポイントについて紹介します。</p> <h2 id="問題背景を書くことに99の労力を割くつもりで書く">問題背景を書くことに99%の労力を割く(つもりで書く)</h2> <p><a href="#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88">フォーマット</a> に書いたような構成でDesign Documentを書いていますが、このドキュメントを書くのに最も意味があるのは問題背景の部分です。</p> <p>人によって何を問題と捉えているかは、実は違うことがよくあります。</p> <p>「問題だと思っていたことが仕様でした」ということや「別の問題を解決したから今のコードになっている」ということはたくさんあります。</p> <p>イベクラで例を挙げると、ジョブキューで行っている動画の視聴記録の保存処理があります。この処理は負荷が高く、他のプロセスを圧迫する可能性があるため独立させていますが、その背景を知らないと管理のしやすさを優先してまとめようという話になるかもしれません。</p> <p>したがって、自分達が使っているアーキテクチャはどのようなもので、かつ歴史的経緯はどうなっているのか、という背景を明らかにすることが重要です。そして、「確かにそれが問題だね」という共通認識を持つことが一番大事です。</p> <p>これを疎かにすると、間違った方向にプロダクトを進めてしまう可能性もあります。</p> <p>そのため、1行のバグ修正でも背景が共有できてなかったら書きます。</p> <p>Design Documentを書くのは、手間だと感じるかもしれませんが、マージされるまでの時間の短縮になるので結果的に効率は速くなると考えています。</p> <p><a href="https://speakerdeck.com/munetoshi/how-to-write-a-design-doc-ja-ver-dot?slide=17">こちら</a>とは思想が真逆ですが、今のプロダクトの状況的にこの判断が今はベターだと思っています。</p> <h2 id="読者が知っているであろうことから始める">読者が知っているであろうことから始める</h2> <p>現在の仕様を知らない、未来の開発チームが見ても話がわかるようにDesign Documentは書いておく必要があると思っています。</p> <p>そのために、現在のチーム内で周知の事実であってもなるべく省略はせず、今新しく入った人が見ても問題背景が分かるように説明しておくことが大事かなと思います。</p> <p>未来の開発者がコードを見て、なんでこういう仕様になっているのだろうと疑問に思ったときに、そのコードが書かれた時にプロダクトが抱えていた問題が正確にわかれば、どう修正していけばいいかも自ずと見えてくるはずです。</p> <h2 id="過去のPRを読みに行く">過去のPRを読みに行く</h2> <p>問題背景を調査するために、過去のPRを遡ることが重要でした。</p> <p><code>git blame</code>は欠かせません。</p> <p>今からやろうとしている修正が、実は過去に一度失敗した方法だったなんてこともここで分かります。</p> <p>この機能、このライブラリ、この変数、使ってなさそうだな、消しちゃえーっとやってしまってから、実は使ってましたなんてことが山ほどありました。</p> <p>しかし、ちゃんと事前に<code>git blame</code>してPRを見に行くと、周辺の修正が一緒に見られるので、どの機能を作るために必要だったのかが一瞬でわかります。</p> <p>この過去の経緯をしっかり把握しておくことが、スパゲッティを解くのには欠かせないと感じました。</p> <p>もう過去のPRを読み、問題背景に書かずには私はPRを作れません。</p> <h2 id="PRから辿れるようにする">PRから辿れるようにする</h2> <p>新しくプロジェクトに関わってきた人が、なんかこのコード読みにくいな、と思った時に、そのコードが書かれた経緯が遡れることはとても重要だと思っています。</p> <p>これは私たちが実際にその状況に置かれて痛感しました。</p> <p>背景を知っていれば「あ、このコードは一時的な解決策として書いたものだったけど、後でリファクタしようと思っていたんだな」とか「この機能は今後使われる見込みがないから、一時的にこの状態なんだな」と判断できます。</p> <p>昔は、こういうビジネス要件があったから、作られた機能だったが、今の要件に照らすと入らないというのも自信を持って言えます。</p> <p>しかし、過去のPRに変更背景が記載されていないことも多々あり、化石から恐竜の姿を想像するように、当時の背景を推測しかできないこともありました。</p> <p>そういうことが起こらないように、これから作るPRには、Design Documentのリンクを貼っておきたいです。</p> <h2 id="議論の足跡を残しておく">議論の足跡を残しておく</h2> <p>Design Document上では議論をします。</p> <p>そのため、行ごとに指摘がしづらいGitHubのIssue機能は使わず、Notionでドキュメントを書くようにしています。</p> <p>また、ドキュメント内で、このような感じで合意を取っています。</p> <p>チーム間、メンバー間でのすり合わせが大事なので、誰と議論したのかを残しておくことが大事です。</p> <ul> <li>[x] Aさん</li> <li>[x] Bさん</li> <li>[ ] Cさん</li> </ul> <h2 id="実装後はDesign-Documentを更新しない">実装後はDesign Documentを更新しない</h2> <p>Design Documentに詳細に問題背景を書くこと、これはそのドキュメントで果たすべき責任範囲を定義するのとほぼ同義です。</p> <p>そのため、実装後にDesign Documentを修正し、後からこの修正も入れようと考えるのは、よくないと感じます。</p> <p>もし、仕様が変わって新しいことをするときは、新しいDesign Documentとして、何が問題だと思ったのかを明らかにするとともに、歴史的経緯として前のドキュメントのリンクを入れておくといいと思います。</p> <h2 id="タスクと紐付ける">タスクと紐付ける</h2> <p>現在私たちは、バックログの管理をNotionで行なっています。</p> <p>そして、チケットをすべてDesign Document形式で書く運用にしています。</p> <p>そうすることで、管理がしやすいと思っています。</p> <h1 id="導入による心理的な変化">導入による心理的な変化</h1> <p>業務への関わり方が不慣れだった私目線での良かった点をあげます。</p> <h2 id="チームへの発信がしやすい">チームへの発信がしやすい</h2> <p>Design Documentは書くだけならタダなので、もっとここをこうした方がいいんじゃないか?という提案をチームに発信しやすくなりました。</p> <p>これにより、なんかちょっともやっとするところがあるんだよな、でも話に行って作業の邪魔をしてしまうのも悪いし…という時に、サラッとドキュメントを書いて、見ていただくようにするとお互いの好きなタイミングで議論が進められるので、気兼ねなく相談できるなと感じています。</p> <h2 id="タスクを進めるときに迷いがない">タスクを進めるときに迷いがない</h2> <p>どうやってタスクを進めようかという迷いがなくなりました。</p> <p>最初の頃は、何をどこから調べればいいのか、どういう形に持っていったら理想なのかと頭を悩ませていましたが、今は違います。</p> <p>歴史的背景を明らかにして、何が問題なのかを整理すれば自ずと解決策は分かるということに気づきました。</p> <h2 id="自信を持ってPRが出せる">自信を持ってPRが出せる</h2> <p>事前にチームと問題を共有し、自分が何をするかをすり合わせているので、後からそもそも問題解決方法がいけてないという指摘を受けることがないからです。</p> <h1 id="今後の課題">今後の課題</h1> <p>イベクラでは、全てのタスクが調査の側面を持っていて、現在の仕様確認と歴史的背景を一度整理するところから入らないことには話が進みません。</p> <p>そのため、プランニングでポイントをつけることも一切できていないです。</p> <p>対して別サービスの<a href="https://boxil.jp/">BOXIL SaaS</a> 開発チームでは、プランニングの段階で何をどのように修正するかの精度が高いです。</p> <p>誰かしらが現在の仕様について知っていて、その人を中心にみんなで話し合いながら、どういうふうに実装していくかの方針までをプランニングで決めていました。</p> <p>Figmaでどういう風にメソッドが生えているかを図を使ってわかりやすくして、問題背景の共有が同期的に行える状態にあります。</p> <p>そのため、誰が実装しても同じくらいの時間で終わるみたいな見積もりが成立すると思っています。</p> <p>しかし、イベクラチームもDesign Documentを書き続けていくことで、歴史的背景がドキュメントとして蓄積され、いずれは誰もが仕様を理解している状態に来るのではないかと思っています。</p> <p>そうなった時に、やっと私たちのスクラム開発がスタートするのです。</p> <h1 id="最後に">最後に</h1> <p>まだまだチームとしてどのように開発を行なっていくのが効率的かは模索中です。</p> <p>今後もチームでの効果的な業務の進め方の改良を続け、小さなチームでも大きな成果を出せるということを証明していきます。</p> <p>ここまで読んでいただきありがとうございました!</p> smartcamp Ruby 2.7に飽きたから秋田からRuby 3移行した話 hatenablog://entry/4207575160647714326 2023-05-11T13:00:00+09:00 2023-07-31T14:34:13+09:00 Ruby のロゴについて 自己紹介 Ruby 3への移行 脱Refile 過去の先駆者 開戦 問題その1 画像のURLがS3のエンドポイントになっている問題 問題その2 移行対象のレコードが大量問題 問題その3 画像が荒くなる問題 幾多の障害を乗り越え その他gemの更新 ついにRuby3へアップデート 1番の影響 Ruby 3へのバージョンアップを終えて 最後に 自己紹介 2023年1月1日付け入社のはかまたです。 BOXILカンパニープロダクト本部配属でBOXIL SaaSの開発エンジニアとして働いています。 スマートキャンプはニックネーム文化があり、私は「職人(しょくにん)」になりました… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230510/20230510185440.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <a href="https://www.ruby-lang.org/ja/about/logo/">Ruby &#x306E;&#x30ED;&#x30B4;&#x306B;&#x3064;&#x3044;&#x3066;</a></p> <ul class="table-of-contents"> <li><a href="#自己紹介">自己紹介</a></li> <li><a href="#Ruby-3への移行">Ruby 3への移行</a><ul> <li><a href="#脱Refile">脱Refile</a><ul> <li><a href="#過去の先駆者">過去の先駆者</a></li> <li><a href="#開戦">開戦</a><ul> <li><a href="#問題その1-画像のURLがS3のエンドポイントになっている問題">問題その1 画像のURLがS3のエンドポイントになっている問題</a></li> <li><a href="#問題その2-移行対象のレコードが大量問題">問題その2 移行対象のレコードが大量問題</a></li> <li><a href="#問題その3-画像が荒くなる問題">問題その3 画像が荒くなる問題</a></li> </ul> </li> <li><a href="#幾多の障害を乗り越え">幾多の障害を乗り越え</a></li> </ul> </li> <li><a href="#その他gemの更新">その他gemの更新</a></li> <li><a href="#ついにRuby3へアップデート">ついにRuby3へアップデート</a><ul> <li><a href="#1番の影響">1番の影響</a></li> </ul> </li> </ul> </li> <li><a href="#Ruby-3へのバージョンアップを終えて">Ruby 3へのバージョンアップを終えて</a></li> <li><a href="#最後に">最後に</a></li> </ul> <h2 id="自己紹介">自己紹介</h2> <p>2023年1月1日付け入社の<code>はかまた</code>です。</p> <p>BOXILカンパニープロダクト本部配属でBOXIL SaaSの開発エンジニアとして働いています。</p> <p>スマートキャンプはニックネーム文化があり、私は「職人(しょくにん)」になりました。 (GitHubのアカウント名を寿司職人にしていたらそうなった・・・)</p> <p>最初は職人と呼ばれることに若干の抵抗がありましたが、不思議なもので今ではもう呼ばれ慣れています。</p> <p>職人に見合った仕事を全うできるように日々奮闘中です。</p> <p>私は秋田県からフルリモートで働いています。 最初は戸惑いながらも徐々に環境を整えたり、他の社員の方々とたくさんコミュニケーションをとってようやく慣れてきた気がします。</p> <p>スマートキャンプは社員同士のコミュニケーションが本当に活発です。 あたたかく受け入れてくださった皆さまには本当に感謝しています。</p> <h2 id="Ruby-3への移行">Ruby 3への移行</h2> <p>タイトルが「Ruby 2.7に飽きたから秋田からRuby 3移行した話」となっていますが、 もちろん私1人で対応したわけではなく、BOXIL SaaS開発メンバー全員で対応しました。 (ただ私自身が秋田での勤務のため、飽きたと秋田をかけたかっただけです・・・)</p> <p>BOXIL SaaSはこれまでRuby 2.7を使用していましたが、 2023年3月31日でEOLを迎えるため、Ruby 3に移行する必要がありました。</p> <p>しかし、Ruby 3にバージョンアップするには依存ライブラリのバージョンも上げていく作業が必要でした。</p> <h3 id="脱Refile">脱Refile</h3> <p>BOXIL SaaSでは資料や画像のアップロードにRefileというgemが使われていました。 しかしRefileはもうメンテナンスがされておらず、Ruby 3に対応していません。</p> <p><a href="https://github.com/refile/refile">https://github.com/refile/refile</a> 最終コミットが3年ほど前になっています。</p> <p>そのため、Refileを剥がし、Shrineというgemに移行する必要がありました。</p> <h4 id="過去の先駆者">過去の先駆者</h4> <p>幸いなこと?に脱Refile、Shrineへの移行の作業はすでにGitHub上のPRにありました。 そのPRはRefileとShrineのデータを同期するところまでは完了しているようでしたが、それ以降音沙汰がなさそうな状況でした。 PRは2020年に作られていたようです。</p> <p>何があってこのPRが放置されてしまったのか、既存の開発メンバーに経緯を確認してみたところ、 もともとこのPRは開発体験向上のためのタスクとして、取り組んでいたそうです。 ですが、当時取り組んでいた別のタスクの方が優先度が高く、その作業をしているうちに月日が流れ、開発メンバーも入れ替わりました。 その結果、当時対応していた開発メンバーもいなくなり、対応し難い状況から放置されてしまったということらしいです。</p> <p>私たちはこれらの蓋を開けていくことになりました。</p> <h4 id="開戦">開戦</h4> <p>BOXIL SaaSはSaaSを導入したいユーザーとSaaSを提供しているベンダーをつなぐリボンモデルのプロダクトです。 そのため、SaaSのサービスロゴやSaaSに関する資料や画像のデータが多く存在します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230510/20230510181700.png" width="1200" height="759" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>BOXIL SaaSのどこにRefileが使われているのか確認していくと</p> <ul> <li>サービスのロゴ</li> <li>サービスの資料</li> <li>サービスのスクリーンショット</li> <li>ホワイトペーパー</li> <li>ライト会員用資料</li> <li>会社のロゴ</li> <li>プロフィール画像</li> </ul> <p>のアップロードと表示に使われていることがわかりました。</p> <p>ありがたいことに<a href="https://shrinerb.com/docs/refile">Shrineの公式サイト</a>には Refile移行のためのドキュメントが整備されています。 これを元に移行を進めていくことになりました。</p> <p>Shrineは1つのモデルに対して、画像をアップロードするアップローダー(実体はClass)を作成します。 そこにMIMEタイプのバリデーションや頻繁に使用される画像サイズをあらかじめ定義できます。 移行にあたり、それらを修正する必要がありましたが、その作業自体は難しくないものでした。</p> <p>ですが、問題はここからでした。</p> <h5 id="問題その1-画像のURLがS3のエンドポイントになっている問題">問題その1 画像のURLがS3のエンドポイントになっている問題</h5> <p>アップロードしたファイルはS3に保存されるような実装になっています。 画面からファイルのURLを確認するとS3のエンドポイントになっており、直接ダウンロードができてしまうという状況でした。 BOXIL SaaSはCloudflareを通し、画像を最適化しつつ表示しているので、S3から直接画像を取得してしまうと最適化も機能しません。</p> <p>この問題はShrineのプラグインである<a href="https://shrinerb.com/docs/plugins/download_endpoint">Download Endpoint</a>を使用して解決しました。</p> <p>具体的には以下のような設定を追加します。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># config/initializers/shrine.rb</span> <span class="synType">Shrine</span>.plugin <span class="synConstant">:download_endpoint</span>, <span class="synConstant">prefix</span>: <span class="synSpecial">&quot;</span><span class="synConstant">attachments/files/images</span><span class="synSpecial">&quot;</span> </pre> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># config/routes.rb (Rails) </span> <span class="synType">Rails</span>.application.routes.draw <span class="synStatement">do</span> <span class="synComment"># ... </span> mount <span class="synType">Shrine</span>.download_endpoint =&gt; <span class="synSpecial">&quot;</span><span class="synConstant">/attachments/files/images</span><span class="synSpecial">&quot;</span> <span class="synStatement">end</span> </pre> <p>これで、ファイルのエンドポイントのホストはboxil.jpになり、ファイル自体へのURLはハッシュ化された状態になりました。 実際にBOXIL SaaSのサービスロゴの画像のimgタグを確認してみると以下のようになっています。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">img</span><span class="synIdentifier"> </span><span class="synType">alt</span><span class="synIdentifier">=</span><span class="synConstant">&quot;BOXIL&quot;</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">&quot;service-logo-image&quot;</span><span class="synIdentifier"> loading=</span><span class="synConstant">&quot;lazy&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;/attachments/files/images/eyJpZCI6IjBjMGE1MzRmMWQyYjY1NjQ0Y2EyOWFkZjVjZDNhNzViLnBuZyIsInN0b3JhZ2UiOiJzZXJ2aWNlX2xvZ28iLCJtZXRhZGF0YSI6eyJmaWxlbmFtZSI6ImltYWdlX3Byb2Nlc3NpbmcyMDIzMDQxMC0xMjQtNHRtbjdpLnBuZyIsInNpemUiOjkyMjUsIm1pbWVfdHlwZSI6bnVsbH19&quot;</span><span class="synIdentifier">&gt;</span> </pre> <p>この対応をすることで、S3のエンドポイントは公開されることがなくなりました。</p> <h5 id="問題その2-移行対象のレコードが大量問題">問題その2 移行対象のレコードが大量問題</h5> <p>BOXIL SaaSでは実際のサービスを紹介しているページがあります。 そこにはサービス画面を紹介するためのスクリーンショットが表示されています。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230510/20230510181800.png" width="876" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>サービス画面のスクリーンショットは9000件のレコードがあり、このレコードをShrine用のデータに変換しつつ、元画像から各画面表示用に最適化された画像を生成する必要がありました。</p> <p>変換と画像の生成処理をバッチを作成して単純に実行したところ、実行結果が戻ってきませんでした(念の為、5〜6時間くらいは待った)。 のちのち確認したところ、件数が大量すぎて処理がロールバックされており、変換もされていませんでした。</p> <p>これはidを指定した範囲で絞り込み、少しずつ変換してあげるようにしたところ、無事最後まで変換できました。</p> <h5 id="問題その3-画像が荒くなる問題">問題その3 画像が荒くなる問題</h5> <p>画像を単純に表示すると、画質が荒くなったり、画像が大きすぎてはみ出てしまう問題がありました。</p> <p>これは縦横比を固定したまま指定された画像サイズにするメソッドresize_to_limitやCSSを駆使してなんとか解決しました。</p> <p><a href="https://www.rubydoc.info/gems/carrierwave/CarrierWave%2FMiniMagick:resize_to_limit">https://www.rubydoc.info/gems/carrierwave/CarrierWave%2FMiniMagick:resize_to_limit</a></p> <h4 id="幾多の障害を乗り越え">幾多の障害を乗り越え</h4> <p>無事にRefileからShrineへの移行が完了しました。</p> <h3 id="その他gemの更新">その他gemの更新</h3> <p>Ruby 3に対応していなかったバージョンのgemも順次アップデートを実施しました。</p> <ul> <li>unicorn</li> <li>simple_form</li> <li>rubocop</li> <li>image_processing</li> <li>mini_magick</li> <li>ddtrace</li> <li>bugsnag</li> <li>faraday</li> <li>faker</li> <li>slack-api ※自前実装して削除</li> <li>slack-notifier ※自前実装して削除</li> </ul> <p>こうして見ると細かいGemの更新が全然できておらず、プロダクトとしても不健全な状況でした。 この機会にアップデートできて良かったと思います。</p> <h3 id="ついにRuby3へアップデート">ついにRuby3へアップデート</h3> <p>BOXIL SaaSはAWS ECS上で起動しています。 Dockerfileの内容を更新し、BOXIL SaaSはついにRuby 3へとバージョンアップしました。</p> <p>もちろんすんなりバージョンアップできたわけではありませんでした。 CIを実行するとエラーが発生し、それら1つ1つを修正する必要がありました。</p> <h4 id="1番の影響">1番の影響</h4> <p>Ruby 3へのバージョンアップをするときに1番影響があった変更点はキーワード引数の仕様が変わったことです。<br> <a href="https://www.ruby-lang.org/ja/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/">https://www.ruby-lang.org/ja/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/</a></p> <p>Ruby 2.7では警告で済まされていたものが、Ruby 3ではエラーとなってしまいます。 例えば以下のようなコードです。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">def</span> <span class="synIdentifier">example</span>(<span class="synConstant">arg</span>: <span class="synConstant">1</span>) p arg <span class="synPreProc">end</span> <span class="synComment"># Ruby 2.7: 引数のHashは自動でキーワード引数に変換される</span> example({ <span class="synConstant">arg</span>: <span class="synConstant">100</span> }) <span class="synComment"># Ruby 3.0: ArgumentErrorとなる</span> <span class="synComment"># example({ arg: 100 })はNG</span> example(<span class="synConstant">arg</span>: <span class="synConstant">100</span>) </pre> <p>この変更に影響されたのかFakerもキーワード引数を受け取るような仕様に変更がされており、それも変更する必要がありました。 例えば以下のようなコードです。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># Ruby 3.0: ArgumentErrorとなる</span> <span class="synType">Faker</span>::<span class="synType">Number</span>.number(<span class="synConstant">4</span>) <span class="synComment"># キーワード引数として引数を受け取るようになった</span> <span class="synType">Faker</span>::<span class="synType">Number</span>.number(<span class="synConstant">digits</span>: <span class="synConstant">4</span>) </pre> <p>このパターンとなっているようなコードを一つずつ修正していき、やっとのことでCIが通るようになりました。 そしてようやくRuby 3へのバージョンアップができました。</p> <h2 id="Ruby-3へのバージョンアップを終えて">Ruby 3へのバージョンアップを終えて</h2> <p>今思うとかなり地味で大変な作業だったと思いますが、私はこの手の作業の経験が薄かったので、とても充実した時間を過ごせたと思っています。</p> <p>システムを定期的にバージョンアップをしていくには</p> <ul> <li>CI/CDを導入して、自動テストができている</li> <li>テストコードがある程度、網羅されている</li> </ul> <p>ということがマストだなと思い知らされました。</p> <p>これが実現できていない状況でのバージョンアップは工数もかかりますし、工数のほとんどがテストに持っていかれてしまうので作業自体も楽しいものではなくなってしまいます。</p> <p>理想としては100%全パターンを網羅したテストコードを書くべきだと思います。 しかし、ほとんどの場面では現実的ではないと思います。</p> <p>スピードを求めてサービスを展開したいという場合はテストコードを書く時間すらも惜しいということもあると思います。 さまざまな経緯があってCI/CDの環境やテストコードがないというシステムもあると思います。 しかし、テストコードを書かないことによってどのようなリスクがあるのかを把握しておくことは必要だと思います。</p> <p>今回のようなバージョンアップ作業でも大いにテストコードが役に立っていました。 自分が入社した時点でもある程度のテストコードが整備されていたので、そのおかげでバージョンアップ作業もスムーズに進めることができました。</p> <p>システムの将来を見据えた開発をしていくのであれば、完璧なテストコードではなくとも、ある程度のテストコードを整備していくことはマストだと思います。</p> <h2 id="最後に">最後に</h2> <p>今回得られた知見はドキュメントにまとめて、チームの皆さんにも共有し、定期的にBOXIL SaaSのバージョンアップをしていきたいと思います。</p> <p>もっとBOXIL SaaSを理解して、より良いシステムにしていきたい!</p> <p>最後まで読んでいただき、ありがとうございました!</p> smartcamp そうだ、京都開発拠点に行こう! hatenablog://entry/4207112889982410699 2023-04-21T13:00:00+09:00 2023-07-31T14:34:29+09:00 はじめまして! 2023年1月付でスマートキャンプ株式会社に中途入社した松下大祐です。 京都にオフィスを構える京都開発部に所属し、ソフトウェアエンジニアとして働いています。 今回は私の入社エントリとして、スマートキャンプへの入社理由や仕事内容について説明したいと思います。 自己紹介 職務経歴 スマートキャンプに入社した理由 社会に大きな影響を与えるプロダクトを開発したい 将来的なキャリアを自分の中で見つけたい 企業理念への共感 技術スタックについて 京都で働くことについて 京都開発拠点について 京都開発拠点とは 出社について 取り組んできた仕事 BOXIL SaaSの機能開発 共通ID基盤の開… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230420/20230420180438.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>はじめまして! 2023年1月付でスマートキャンプ株式会社に中途入社した松下大祐です。 京都にオフィスを構える<a href="https://tech.smartcamp.co.jp/entry/kyoto-half-birthday">京都開発部</a>に所属し、ソフトウェアエンジニアとして働いています。 今回は私の入社エントリとして、スマートキャンプへの入社理由や仕事内容について説明したいと思います。</p> <ul class="table-of-contents"> <li><a href="#自己紹介">自己紹介</a><ul> <li><a href="#職務経歴">職務経歴</a></li> </ul> </li> <li><a href="#スマートキャンプに入社した理由">スマートキャンプに入社した理由</a><ul> <li><a href="#社会に大きな影響を与えるプロダクトを開発したい">社会に大きな影響を与えるプロダクトを開発したい</a></li> <li><a href="#将来的なキャリアを自分の中で見つけたい">将来的なキャリアを自分の中で見つけたい</a></li> <li><a href="#企業理念への共感">企業理念への共感</a></li> </ul> </li> <li><a href="#技術スタックについて">技術スタックについて</a></li> <li><a href="#京都で働くことについて">京都で働くことについて</a></li> <li><a href="#京都開発拠点について">京都開発拠点について</a><ul> <li><a href="#京都開発拠点とは">京都開発拠点とは</a></li> <li><a href="#出社について">出社について</a></li> </ul> </li> <li><a href="#取り組んできた仕事">取り組んできた仕事</a><ul> <li><a href="#BOXIL-SaaSの機能開発">BOXIL SaaSの機能開発</a></li> <li><a href="#共通ID基盤の開発">共通ID基盤の開発</a></li> <li><a href="#インターン生のマネジメント">インターン生のマネジメント</a></li> </ul> </li> <li><a href="#終わりに">終わりに</a></li> </ul> <h2 id="自己紹介">自己紹介</h2> <p>まずは、自己紹介をさせてください。 私は、1992年生まれの30歳です。 簡単な経歴は下記のようになります。</p> <ul> <li>岡山県で生まれる</li> <li>高校までを岡山県で過ごす</li> <li>京都で大学生活を過ごす</li> <li>東京のソフトウェア開発企業に入社し、6年半ほど東京で働く</li> <li>スマートキャンプに転職し、京都開発部で働く</li> </ul> <p>岡山県の田園風景の中ですくすくと育ちました。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230418/20230418150548.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> 京都府にある京都大学の総合人間学部という学部に入学しました。</p> <p>学生時代にはみなさん、どんなあだ名で呼ばれていましたか? 私は大学の食堂でぱふぇを一度に3つ食べたことから、「ぱふぇ」というあだ名を友人に呼ばれていました。 スマートキャンプにもあだ名文化があり、大学時代のあだ名を採用してもらい「ぱふぇ」と呼ばれています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/smartcamp/20230418150531" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230418/20230418150531.png" width="659" height="800" loading="lazy" title="" class="hatena-fotolife hatena-fotolife-height-only" style="height:200px" itemprop="image"></a></span></p> <p>大学では、総合人間学部という学部の<a href="https://www.h.kyoto-u.ac.jp/academic/ug/division2/">認知情報学系</a>に所属し、認知言語学という言語学の一分野を専攻しました。 卒論では「ラーメンに関する言語学的表現」を題材として書いており、ウケを狙いたかった年頃だったんだろうと思います。</p> <p>上記のように厳密な専攻は情報系ではなかったのですが、単位として認められるので情報系の授業も受講していました。 SchemeというLispの方言を授業で学習しましたが、これは初めて触れるプログラムとしてはなかなか難しい部類に入るものだったと思います。 しかし、私にとってはコードを書くことが非常に面白く感じられ、これを仕事にできると楽しいだろうなだと考え、ソフトウェアエンジニアになりました。</p> <h3 id="職務経歴">職務経歴</h3> <p>職務経歴については下記です。</p> <ul> <li>1社目: パッケージソフトウェアの開発会社</li> <li>2社目: 国内大手メッセージングアプリ会社のグループ会社</li> <li>3社目: スマートキャンプ(現職)</li> </ul> <p>1社目は、インターンに参加したという縁からパッケージソフトウェアの開発会社に新卒入社しました。 Javaでのバックエンドロジックの実装や、スマートフォン向けのセキュアブラウザの保守開発をひたすら行なっていました。 システム会社ではよくある構造ですが、開発部署はただ開発をし続け、導入や運用は別の部署が受け持つという構造です。 よりユーザーと関わりを持ってフィードバックループを回して良いソフトウェアやサービスを作りたいという気持ちが強くなり、2社目の企業に転職しました。</p> <p>2社目は、国内大手のメッセージングアプリ開発会社のグループ会社で働いていました。 社内で非エンジニアが利用するLP作成ツールの開発、エンジニア向けイベントのシステム開発、サービス協業社様向けのCMSの開発など色々な経験を積みました。 そのグループ会社は、親会社が運営している多くのサービスを裏から支えることをミッションとしています。 しかし、数年働く中で、裏から支えるのではなく自分の手でサービス・プロダクトそのものを作って、社会に大きな影響を与えたいという気持ちが強くなり、転職活動を開始しました。</p> <h2 id="スマートキャンプに入社した理由">スマートキャンプに入社した理由</h2> <p>次に私がスマートキャンプに入社した理由について説明したいと思います。 主な理由については下記になります。</p> <ul> <li>社会に大きな影響を与えるプロダクトを開発したい</li> <li>将来的なキャリアを自分の中で見つけたい</li> <li>企業理念への共感</li> </ul> <h3 id="社会に大きな影響を与えるプロダクトを開発したい">社会に大きな影響を与えるプロダクトを開発したい</h3> <p>前項でも触れたとおり、自分の手でサービス・プロダクトを開発して、社会に大きな影響を与えたいという気持ちが自分の中にはあります。 前職入社時点では、社内のサービスを裏から支えることで間接的に社会に影響を与えることができると考えていました。 しかし、実際に働く中で間接的ではなくより直接的にサービス・プロダクトそのものを作っていきたいという気持ちが強くなっていきました。</p> <p>転職活動の中で、スマートキャンプでは新規のプロダクト開発へのチャレンジを加速させていきたいという思いがあり、それを京都開発拠点で推進していきたいと聞きました。 実現のためにはまだまだ決まっていないことも多く、自分が介在する余地が大きいと感じ、そこに関わっていきたいと強く感じました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/smartcamp/20230418175937" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230418/20230418175937.png" width="550" height="404" loading="lazy" title="" class="hatena-fotolife hatena-fotolife-height-only" style="height:200px" itemprop="image"></a></span></p> <h3 id="将来的なキャリアを自分の中で見つけたい">将来的なキャリアを自分の中で見つけたい</h3> <p>また、前職で働いている時点では、ソフトウェアエンジニアとしての自分のキャリアについて迷っている部分が大きかったです。 アプリケーションの設計をすることが好きなので、ソフトウェアアーキテクトになろうかな…?となんとなく考えていました。 しかし、その他のたとえばマネジメントといった業務の経験があるわけではなく、自分が何を楽しいと感じるかはあまりわかっていない状態でした。</p> <p>スマートキャンプの京都開発拠点では、人数も少なく、インターン生も多く採用しており、マネジメントの分野も経験できるという話を聞きました。 また、マネジメント以外の分野も、平たく言えば何でもやる機会があるという話も聞いて、チャレンジしてみようという気持ちになりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/smartcamp/20230418180000" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230418/20230418180000.png" width="400" height="370" loading="lazy" title="" class="hatena-fotolife hatena-fotolife-height-only" style="height:200px" itemprop="image"></a></span></p> <h3 id="企業理念への共感">企業理念への共感</h3> <p>上記のような理由で、スマートキャンプに興味を持ったうえで、選考に進むにあたって、カジュアル面談で聞いた企業理念にも共感しました。 スマートキャンプの<a href="https://smartcamp.co.jp/philosophy">MISSION</a>は「テクノロジーで社会の非効率をなくす」というものであり、社会の非効率をなくすというのはまさに自分がやりたい、社会に大きな影響を与えることだと感じています。</p> <h2 id="技術スタックについて">技術スタックについて</h2> <p>前述のようにスマートキャンプは私にとって魅力的に感じられたものの、技術スタックがあまり噛み合っていないという問題もありました。 たとえば、自分は言語の面ではJavaやKotlinの経験が多いのに対して、スマートキャンプでは主にRubyを利用しています。 インフラ構成などもオンプレミスやプライベートクラウド上での構築が多く、スマートキャンプで主にAWSのようなパブリッククラウドを利用しています。</p> <p>ごく基本的な部分のキャッチアップは当然自分で行なう必要がありましたが、応用的な部分や社内の文脈に依存した部分はドキュメントが多く存在しており、あまりキャッチアップは苦になりませんでした。 スマートキャンプではNotionにドキュメントを書く文化が強くあり、その文化に助けられたと言えます。</p> <h2 id="京都で働くことについて">京都で働くことについて</h2> <p>転職活動時に声をかけてもらったときは、スマートキャンプの京都開発拠点での採用という形で、京都に転居して働くことになるという前提がありました。 私は、大学時代を京都で過ごしており、京都に関しては郷愁を感じており、魅力的に感じました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230418/20230418180022.png" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>声をかけてもらうまでに、具体的に京都に戻りたいという希望が自分の中にあったわけではありませんでした。 しかし、前職ではフルリモートで働いており、東京都心で暮らす必要性はあまり感じていませんでした。</p> <p>都会で働いていた人が、その他の地方に引っ越しすることを、○ターンと表現することがあります。 一度実家がある都市を離れた後に、実家がある都市に戻るUターン、縁がない都市に行くIターンといったものもありますが、故郷の岡山にやや近い京都に引っ越したことから私の場合はJターンに分類されるようです。 郷愁を感じていた京都に戻るという面以外でも、実家の岡山により近い場所に転居するというのは、高齢になってきている両親に会いやすくなるという面でも好ましかったです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/smartcamp/20230418180039" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230418/20230418180039.png" width="409" height="600" loading="lazy" title="" class="hatena-fotolife" style="width:200px" itemprop="image"></a></span></p> <p>学生生活ぶりに京都に引っ越してあらためて感じたのは、東京よりも街自体がコンパクトで便利であるということです。 スマートキャンプでは家賃補助制度があり、オフィスの1.5km圏内に住むことで、家賃補助があります。 京都開発拠点のオフィスは京都の繁華街の中にあるため、私も繁華街近くに転居したため、生活も非常に便利になりました。</p> <h2 id="京都開発拠点について">京都開発拠点について</h2> <p>ここまでは、私の転職理由について説明してきました。 次は、スマートキャンプや京都開発拠点について説明していきたいと思います。</p> <h3 id="京都開発拠点とは">京都開発拠点とは</h3> <p>京都開発拠点は、フルリモートワークではなく一定頻度で出社するのを前提として作られた拠点です。 出社を前提とするのは下記のような理由からです。</p> <ul> <li>新規事業などのコミュニケーションを多く必要とする業務をスピード感を持って実施する</li> <li>京都という学生が多い都市でインターン生とのコミュニケーションを多くとる</li> </ul> <p>京都拠点でのインターン生は現在3名が働いています。 全員関西の大学に所属している学生です。 学生でありながら非常に優秀なメンバーたちで、彼らと働くことができるのは刺激的です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/smartcamp/20230418180103" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230418/20230418180103.png" width="362" height="400" loading="lazy" title="" class="hatena-fotolife hatena-fotolife-height-only" style="height:200px" itemprop="image"></a></span></p> <h3 id="出社について">出社について</h3> <p>京都開発拠点では曜日を合わせて現在週2回の出社するようにしています。 インターン生の稼働日もそれに合わせて設定し、コミュニケーションをとるようにしています。 社会一般では、感染症の関係で出社に関してハードルを感じている人も多いと思いますが、私個人としてはたとえばミーティングといった特定の業務は、出社を行なって対面で話した方が効率が良いと感じています。</p> <p>また、週3回はリモートワークをするような取り決めになっています。 リモートワークの方がより集中しやすい環境を整えやすいということあり、コードを書くといった比較的自分の中で完結する作業に関してはこちらの方が効率が良いと感じています。</p> <p>極端に出社とリモートワークのどちらに振り切るのではなく、ハイブリッドな働き方にするのは、個人的には肌に合っていると思っています。</p> <h2 id="取り組んできた仕事">取り組んできた仕事</h2> <p>次に私が入社してから取り組んできた仕事について紹介します。</p> <ul> <li>BOXIL SaaSの改修</li> <li>共通ID基盤の開発</li> <li>インターン生のマネジメント</li> </ul> <h3 id="BOXIL-SaaSの機能開発">BOXIL SaaSの機能開発</h3> <p><a href="https://boxil.jp/">BOXIL SaaS</a>はスマートキャンプのプロダクトで、SaaSを比較できるサービスです。 BOXIL SaaSのメインの開発メンバーは、京都開発拠点以外に所属しているメンバーです。 しかし、後述する共通ID基盤をBOXIL SaaSから利用するという観点から、キャッチアップを行なう必要があり、その一環でBOXIL SaaSそのものの機能開発を行いました。 エンジニアとしては実際にコードを読んで開発することで、ドメイン知識の理解が大きく進みました。</p> <p>BOXIL SaaSでは、社内全体でスクラムのスプリントレビューを実施しており、自分の開発した機能が多くの人に褒められたのも嬉しかったです。 また、ビジネスメンバーと開発メンバーの距離が近いのは、スマートキャンプの良いところだと思いますし、個人的にも大好きです!</p> <h3 id="共通ID基盤の開発">共通ID基盤の開発</h3> <p>共通ID基盤は、現在京都開発拠点でメインで開発しているプロダクトです。</p> <p>スマートキャンプで現在運営しているサービスでは、サービスそれぞれでユーザー情報を保持しており、複数のサービスを同一のユーザーが利用する場合でもそれぞれアカウントを作成する必要があります。 複数のサービスを利用するユーザーにとってこういった状況は不便なので、共通してログインができる基盤を作ろうとしています。</p> <p>まずは、BOXIL SaaSのユーザーが共通ID基盤を通じてログインができるように開発を進めています。 いきなり認証部分を切り出すのではなく、データベースを内部的に分割して読み替えを進めていくといった感じで、段階的に実装を行なっている状態です。 ユーザーの利便性を向上させるために実装しているものなので、できるだけユーザーにとって特別なアクションが必要にならない形を意識しています。</p> <p>認証はWebアプリケーションにとって非常に重要な部分であり、それを切り出すという作業は非常に難易度が高いですが、だからこそやりがいがあります。</p> <h3 id="インターン生のマネジメント">インターン生のマネジメント</h3> <p>前述の通り、京都開発拠点では学生のインターン生を3名採用しており、インターン生のマネジメントも業務として存在しています。 学生個々人がインターンを通して実現したいことと、現状やってもらっている業務がマッチしているかどうかのすり合わせや、日々の困ったことを1on1で拾い上げるといったことを行なっています。 業務経験が少ない学生だからこそ、業務ではこうやるといった知識を伝えていくといったことも意識しています。 「学生という業務経験の少ない相手だからより丁寧な伝え方をした方がよいな」というように相手の特性を考えながら自分の行動を変えるといった工夫をしながら取り組んでいます。 ソフトウェア開発とはまた違う難しさですが、スキルを新しく身につけている感じがして非常に楽しいです!</p> <h2 id="終わりに">終わりに</h2> <p>以上、この記事では入社エントリとして、私がスマートキャンプに入社した理由や、現在の仕事内容について説明しました。 入社から3ヶ月が経過しましたが、入社前に期待していたことからのギャップはありません。 共通ID基盤の開発などではキャッチアップ的な要素を含むタスクも多い状態でしたが、これからはがっつりコミットして成果を出していきたいと考えています。 頑張っていきます!</p> smartcamp 後で楽できるTerraformの書き方(※ただし書くときは辛い) hatenablog://entry/4207112889979797947 2023-04-10T12:00:00+09:00 2023-04-10T12:00:01+09:00 はじめに ざっくりしたシステム構成の紹介 全体の構造 設計のポイント コーディング規約 上の階層を見に行かない 変数名は全体でユニークにする 変数のデフォルト値は設定しない main, outputs, variables 以外のファイルを原則置かない ポリシードキュメントはJSONファイルのまま管理する 変数で処理を変える仕組みを極力使わない 値のハードコードをためらわない コードが冗長であることをためらわない 残っている課題 AWSアカウント単位でしか用意しないものの扱い ECSのタスク定義の扱い 最後に はじめに はじめまして。スマートキャンプのおにまるです。 2022年10月に入社し、… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230410/20230410104414.jpg" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#ざっくりしたシステム構成の紹介">ざっくりしたシステム構成の紹介</a></li> <li><a href="#全体の構造">全体の構造</a></li> <li><a href="#設計のポイント">設計のポイント</a></li> <li><a href="#コーディング規約">コーディング規約</a><ul> <li><a href="#上の階層を見に行かない">上の階層を見に行かない</a></li> <li><a href="#変数名は全体でユニークにする">変数名は全体でユニークにする</a></li> <li><a href="#変数のデフォルト値は設定しない">変数のデフォルト値は設定しない</a></li> <li><a href="#main-outputs-variables-以外のファイルを原則置かない">main, outputs, variables 以外のファイルを原則置かない</a></li> <li><a href="#ポリシードキュメントはJSONファイルのまま管理する">ポリシードキュメントはJSONファイルのまま管理する</a></li> <li><a href="#変数で処理を変える仕組みを極力使わない">変数で処理を変える仕組みを極力使わない</a></li> <li><a href="#値のハードコードをためらわない">値のハードコードをためらわない</a></li> <li><a href="#コードが冗長であることをためらわない">コードが冗長であることをためらわない</a></li> </ul> </li> <li><a href="#残っている課題">残っている課題</a><ul> <li><a href="#AWSアカウント単位でしか用意しないものの扱い">AWSアカウント単位でしか用意しないものの扱い</a></li> <li><a href="#ECSのタスク定義の扱い">ECSのタスク定義の扱い</a></li> </ul> </li> <li><a href="#最後に">最後に</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p>はじめまして。スマートキャンプのおにまるです。<br> 2022年10月に入社し、SRE兼インフラエンジニアとして働いています。</p> <p>今回は、あるプロダクトの再スタートにあたって新しく作った、AWSのTerraformについてお話したいと思います。</p> <p>再スタートにあたってアプリケーションが大きく変わるため、インフラも再構築する必要がありました。<br> もともとのインフラもTerraformで管理されていたのですが、アプリケーションの変更にあわせてインフラも大きく変えなければならず、Terraformのコードも大改修が必要だと分かりました。しかしあまりにも変更する箇所が多く、その範囲も広かったので、いっそ書き直したほうがよい、ということになったのです。<br> 汎用性の高い設計にすれば、新しいプロダクトを立ち上げるときに使い回しが効くのでは、という目論みもありました。</p> <p>しかし……いやあ、大変でした……。</p> <p>ゼロから作る、という判断は間違っていなかったと思います。しかし汎用性の高いものを作る、という判断は間違っていたかもしれない、と途中で弱気になるぐらいトライ&エラーを繰り返すことになったのでした。</p> <p>今回はそんなトライ&エラーの結果を紹介します。<br> これが少しでもみなさんのお役に立てば幸いです。</p> <h2 id="ざっくりしたシステム構成の紹介">ざっくりしたシステム構成の紹介</h2> <p>これまでのアプリケーションは、かなり複雑な構成になっていました。表向きは1つに見えるのですが、GitHubのリポジトリが2つ、AWSの環境も2つあり、裏でこの両方を繋げて動作していたのです。これが開発する際の大きな障壁になっていました。</p> <p>見た目や動作を大きく変えないまま統合する、というのが再スタートの目的の1つです。壮大なリファクタリングと言えるかもしれませんが、応答速度を改善する、信頼性を上げる、といったこの先の改善業務をするためにも避けて通れない道でした。</p> <h2 id="全体の構造">全体の構造</h2> <p>まず最初に、全体のディレクトリ構成をお見せします:</p> <pre class="code" data-lang="" data-unlink>. ├── environments │ ├── _common │ ├── lt │ ├── mig │ ├── prod │ └── stg ├── modules │ ├── acm │ ├── alb │ ├── ec2 │ ├── ecr │ ├── ecs │ ├── es │ ├── iam │ │ └── policy-documents │ ├── kms │ ├── rds │ ├── redis │ ├── s3 │ ├── ses │ ├── sg │ └── vpc ├── scripts ├── tmp └── utils</pre> <p>ちなみに、過去のものはこういった感じでした:</p> <pre class="code" data-lang="" data-unlink>├── terraform │ ├── modules │ │ ├── (app_1) │ │ │ ├── cdn │ │ │ ├── chatbot │ │ │ ├── datadog │ │ │ ├── ecs │ │ │ ├── efs │ │ │ ├── es │ │ │ ├── instances │ │ │ ├── kinesis-firehose │ │ │ ├── load_balancers │ │ │ ├── networks │ │ │ ├── rds │ │ │ ├── redis │ │ │ ├── s3 │ │ │ ├── ses │ │ │ └── site │ │ └── (app_2) │ │ ├── cdn │ │ ├── ecs │ │ ├── efs │ │ ├── instances │ │ ├── load_balancers │ │ ├── networks │ │ ├── rds │ │ ├── redis │ │ ├── s3 │ │ └── site │ └── workspaces │ ├── (app_1)-prod │ ├── (app_1)-test │ ├── (app_2)-prod │ ├── (app_2)-test │ └── tf-constant └── terraform-aws-ecs ├── cluster └── service_load_balancing</pre> <p>ご覧のようにモジュールが <code>app_1</code> と <code>app_2</code> で分けて管理されていて、重複しているものもありました。</p> <p>ECSの部分が別ディレクトリにあるのも気になります。どうしてこういう形になったのかは、すでに退職された方が書いたものなので、はっきりとは分かりません。<code>app_1</code> と <code>app_2</code> が別々に管理されていたのが原因かもしれません。</p> <p>次に、新しく作った方の <code>environments</code> ディレクトリの中身について説明したいと思います。<br>ファイルはこういった構成になっています:</p> <pre class="code" data-lang="" data-unlink>environments ├── README.md ├── _common │ ├── main.tf │ └── variables.tf ├── lt │ ├── locals.tf │ ├── main.tf -&gt; ../_common/main.tf │ └── variables.tf -&gt; ../_common/variables.tf ├── mig │ ├── locals.tf │ ├── main.tf -&gt; ../_common/main.tf │ └── variables.tf -&gt; ../_common/variables.tf ├── prod │ ├── locals.tf │ ├── main.tf -&gt; ../_common/main.tf │ └── variables.tf -&gt; ../_common/variables.tf └── stg └── locals.tf</pre> <p>ご覧のように、<code>environments</code> ディレクトリ以下に環境ごとの設定を置くディレクトリがあり、その中に <code>locals.tf</code> と <code>main.tf</code>、<code>variables.tf</code> があります。このうち <code>main.tf</code> と <code>variables.tf</code> は、どの環境でも同じということを明示するために <code>_common</code> のファイルへのシンボリックリンクにしています。つまり、環境ごとに違うのは <code>locals.tf</code> だけです。</p> <p>共通の値を使う場合でも、<code>main.tf</code> に値をハードコードすることは避けています。これは、環境ごとに値を変えることになっても対処しやすいようにする、という理由もありますが、社外秘情報を <code>locals.tf</code> に集めることで、これ以外の部分は誰に見られても大丈夫なようにしたい、と思ったからです。</p> <h2 id="設計のポイント">設計のポイント</h2> <p>「はじめに」で書いたように、汎用的に使えるものを作るのが目的の1つです。どうすれば実現できるか、と考えたとき、読みやすいコードを書くのが一番大事なのでは、という考えに至りました。</p> <p>汎用的に使えるといっても、完全にそのまま使えるわけではありません。違いを埋めるために書き換えが必要ですが、そうしやすいかどうか、そこが汎用的に使える、使ってもらえるときのポイントになると思ったのです。</p> <p>そこで、</p> <ul> <li>読む人にとって、見なければならない場所を減らす</li> <li>読む人にとって、考えさせる場所を減らす</li> <li>そのために書く人は(すごく)がんばる</li> </ul> <p>を念頭にコーディング規約を作りました。</p> <h2 id="コーディング規約">コーディング規約</h2> <h3 id="上の階層を見に行かない">上の階層を見に行かない</h3> <p>モジュールの呼び出しやファイルの読み込みのときに、親ディレクトリを参照しないようにしました。こうすることでモジュールが使っているものはすべてそのディレクトリと、そのサブディレクトリにあることが保証され、ファイルの中を見ることなく、見なければならない範囲が明らかになります。</p> <p>ただし環境別の <code>main.tf</code> は別です。<code>environments/prod/main.tf</code> は、<code>../../modules</code> 以下のディレクトリを参照します。これ以外の例外を作りません。</p> <p>(全体のディレクトリ構造は、ページの上部で説明しています)</p> <h3 id="変数名は全体でユニークにする">変数名は全体でユニークにする</h3> <p>変数名は、どのファイルの中でも同じものを意味するようにしました。たとえばvpcモジュールの <code>id</code> という変数はVPCのIDなのは明らかですが、あえて <code>vpc_id</code> とします。こうしておくと、VPCのIDがどこでセットされ、どこで参照されているか、全文検索すれば一目でわかるようになります。</p> <p>これだけだと、それほど必要ないと思われるかもしれません。しかし、たとえばログ出力のバケット名が入る変数だと効果があります。ECS用のALBなら <code>log_bucket_alb_ecs</code>、CloudFrontなら <code>log_bucket_cloudfront</code> といった名前にしておくと、<code>log_bucket</code> という変数を見て「中身は何だろう?」と調べるような状況にならずに済みます。</p> <p>この延長で、すべてをユニークにしておくと、今後いつか役に立つ……かもしれません。</p> <p>(変数の名前のつけ方はよく議論になります。すべての場面で優れているものはない、ということですが、Terraformに関してはわかりやすく、長い名前をつけることに利があるように感じます)</p> <h3 id="変数のデフォルト値は設定しない">変数のデフォルト値は設定しない</h3> <p>一般に、プログラミング言語では変数にデフォルト値を代入(初期化)するものです。しかしTerraformでは、ロジックが書かれている <code>main.tf</code> とは別の <code>variables.tf</code> で変数が定義されています。</p> <p>デフォルト値を使う可能性があると、変数に何が入っているか知りたいときは <code>variables.tf</code> を確認しなければなりません。しかし使わないと決まっていれば、<code>variables.tf</code> を見る必要はありません。</p> <h3 id="main-outputs-variables-以外のファイルを原則置かない">main, outputs, variables 以外のファイルを原則置かない</h3> <p>ファイルが少なく、それぞれの役目がはっきりしていれば、見なければならない場所が減ります。</p> <p>モジュールは <code>modules</code> 以下にあり、こういったファイル構成になっています:</p> <pre class="code" data-lang="" data-unlink>modules ├── acm │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── alb │ ├── main.tf │ ├── outputs.tf │ └── variables.tf : (省略) : ├── sg │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── vpc ├── main.tf ├── outputs.tf └── variables.tf</pre> <p>1つのモジュールのロジックはすべて <code>main.tf</code> にあり、出力は <code>outputs.tf</code>、入力は <code>variables.tf</code> にある、と決まっていれば、ロジックを知りたいときは <code>main.tf</code> だけ見れば済む、というのが見る前から把握できます。</p> <p>また、2つ前で書いたように、変数名は全体でユニークです。<code>main.tf</code> に出てくる変数がどう使われているかは、 <code>outputs.tf</code> や <code>variables.tf</code> を見なくても分かるということです。</p> <p><code>locals.tf</code> は、原則として置きません。ローカル変数を使わざるを得ない場合は仕方ありませんが、その場合でも変数に何が入っているかはっきりわかるように書きます。</p> <h3 id="ポリシードキュメントはJSONファイルのまま管理する">ポリシードキュメントはJSONファイルのまま管理する</h3> <p>Terraformを理解できる人より、JSONを理解できる人の方が多いからです。<br> ポリシードキュメントを変更することは多くありません。しかし、変更しなければならないときTerraformを使えない人でも編集できれば、より早く変更を反映させることができます。</p> <p><code>templatefile</code> という関数が用意されているのは、それを見越してのことではないかと思います。</p> <h3 id="変数で処理を変える仕組みを極力使わない">変数で処理を変える仕組みを極力使わない</h3> <p>使わない <code>resource</code> を <code>count = 0</code> にして使わないようにする方法があります。</p> <p>たとえば本番環境とステージング環境、どちらか片方でしか使わない <code>resource</code> を、もう片方ではそもそも作らないようにする、といった使い方をすることが多いようです。</p> <p>これはとても便利で、使わない理由はありません。ですが <code>resource</code> に渡すものによって違うものを作る、違う動作をする、ということだと話は別です。渡される変数がどういう値なのか、モジュールを呼び出している側を意識しなければ大事に至る可能性があるからです。</p> <h3 id="値のハードコードをためらわない">値のハードコードをためらわない</h3> <p>値をハードコードするのは悪だと、プログラマーは教わってきたと思います。私もそうですし、普通のコードを書くときはハードコードを避けています。<br> しかし今回のTerraformでは、状況によってはハードコードするようにしました。</p> <p>具体的には、以下の条件をすべて満たす場合です。</p> <ol> <li>環境ごとで値が変わらない</li> <li>普段の運用で変更する可能性が(ほぼ)ない</li> <li>他のプロダクトで使うときでも変更しない可能性が高い</li> </ol> <p>たとえばALBでは、</p> <ul> <li>ターゲットグループの、ヘルスチェック対象のパスやステータスコード</li> <li>SSLポリシー</li> <li>リスナーのポート番号</li> </ul> <p>などがその一例です。</p> <p>これらを環境ごとの <code>locals.tf</code> で定義すると膨大な数になってしまいます。前の項目で書いたように、<code>variables.tf</code> を意識せずに済むよう、変数のデフォルト値を使わないようにしましたが、その代わりに <code>main.tf</code> の中で(ハードコードして)定義しているわけです。設定を変えることが滅多にないなら問題にならないでしょう。</p> <p>Webアプリケーションの場合、似たようなインフラ構成になることが多いと思います。新しくWebアプリケーションを作るときは、設計段階でインフラを考慮してもらえればいいと考えています。(もちろん、無理して寄せなくてもいいことをちゃんと伝えたうえで、です)</p> <h3 id="コードが冗長であることをためらわない">コードが冗長であることをためらわない</h3> <p>参照する場所を減らすということは、それだけ共通化をしないということです。どうしても冗長になるところが出てきますが、ある程度は仕方ないと割り切ります。</p> <p>これは、読む人に、ほんの少し違うコードを同じものと思わせてしまう危険があります。それでもTerraformでは見る場所を減らす方が有効だと、私は考えています。そういった箇所にはコメントを書くことで、危険をかなり抑えることができるからです。</p> <h2 id="残っている課題">残っている課題</h2> <h3 id="AWSアカウント単位でしか用意しないものの扱い">AWSアカウント単位でしか用意しないものの扱い</h3> <p>ECRは、環境毎に分けてもいいですが、別にそうしなくても問題ありません。Dockerイメージのキャッシュを活かす点では共有した方がいいような気がします。しかしTerraformでは、環境をまたいで共有するのはそれなりに面倒ですし、完全に分かれていた方が見かけもスマートな気もします。</p> <p>今のところ環境別に分けていますが、今後どうすべきか、まだ悩んでいます。</p> <h3 id="ECSのタスク定義の扱い">ECSのタスク定義の扱い</h3> <p>ECSのタスク定義をTerrafromとアプリケーションのどちらで管理するか、という部分はいまだに悩んでいます。コンテナインスタンスの管理という点では、Terraformで扱うのが普通のような気がします。しかしアプリケーションの動作を変えるため、よく変更するものでもあるので、そのたびにapplyするのは、あまり合理的ではないようにも感じます。</p> <p>今はすべてTerraformで管理するようにしていますが、頻繁に変更する部分だけはアプリケーションで管理し、全体のベースとなるものはTerraformで管理する、というのを試してみようと思っています。ただ、それがベストかどうかは自信が持てないので、この先も試行錯誤が続くでしょう。</p> <h2 id="最後に">最後に</h2> <p>Terraformで管理しているインフラは、手でいじってしまうと管理下に戻すのが大変だ、と感じている方は多いと思います。ですから運用中に手でいじってしまいたくなる要素を、Terraformでどれだけ簡単に変えられるか、と考えながら設計するのがとても大事だと感じました。</p> <p>見通しがよく、シンプルで、他人がメンテナンスしやすいものが理想的、というのは一般的なプログラミング言語と変わりありません。<br> しかしTerraformはインフラを扱う分、いろいろな部分で「これはなんなんだ…!」と驚かされる、理不尽にも思える制約に振り回されます。変数やモジュールの呼び出しなどはその一例で、制約の理由が分からないままだと力業で解決しがちで、結果として読みにくいコードになってしまいます。そうならないよう、コーディング規約をきちんと作る、読む人が楽になるよう書く人が面倒をする、ということを普段より強く意識する必要があると感じました。</p> <p>理想を目指すのは大変です。しかし逆に言うと腕の見せ所でもありますし、やり甲斐を感じられる部分でもあります。</p> <p>今回の記事が、少しでも皆さんの参考になれば幸いです。フィードバックも大歓迎です!</p> smartcamp SES企業出身者が競技プログラミングで転職して頑張ってる話 hatenablog://entry/4207112889977506067 2023-04-03T13:30:00+09:00 2023-07-31T14:34:50+09:00 はじめまして。ビジネス向けのSaaS比較サイト『BOXIL SaaS』のエンジニアをしていますJinJin(三浦)です。昨年12月にSESをメインで行っている企業からスマートキャンプに転職しました。 この度、テックブログの執筆を担当させていただけることになりましたので、競技プログラミングの成績をアピールポイントにして転職活動を行なった経験や、スマートキャンプに入社してみて気付いたことなどをメインに、入社エントリを書いていこうと思います。 スマートキャンプのエンジニアポジションに興味がある方だけでなく、競技プログラミングに興味がある方や、SES企業・SIerからWeb系への転職を考えている方にと… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230403/20230403124459.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>はじめまして。ビジネス向けのSaaS比較サイト『<a href="https://boxil.jp/">BOXIL SaaS</a>』のエンジニアをしていますJinJin(三浦)です。昨年12月にSESをメインで行っている企業からスマートキャンプに転職しました。</p> <p>この度、テックブログの執筆を担当させていただけることになりましたので、競技プログラミングの成績をアピールポイントにして転職活動を行なった経験や、スマートキャンプに入社してみて気付いたことなどをメインに、入社エントリを書いていこうと思います。</p> <p>スマートキャンプのエンジニアポジションに興味がある方だけでなく、競技プログラミングに興味がある方や、SES企業・SIerからWeb系への転職を考えている方にとって参考になるような記事にできればと思っております。</p> <ul class="table-of-contents"> <li><a href="#転職活動について">転職活動について</a><ul> <li><a href="#きっかけ">きっかけ</a></li> <li><a href="#スマートキャンプを見つけた経緯">スマートキャンプを見つけた経緯</a></li> <li><a href="#どうしてスマートキャンプにしたか">どうしてスマートキャンプにしたか</a></li> </ul> </li> <li><a href="#入社してみて">入社してみて</a><ul> <li><a href="#カルチャー面">カルチャー面</a></li> <li><a href="#業務面">業務面</a></li> </ul> </li> <li><a href="#これから">これから</a></li> </ul> <h2 id="転職活動について">転職活動について</h2> <p>はじめに、転職をしようと思ったきっかけやスマートキャンプを選んだ経緯について書かせていただきます。</p> <h3 id="きっかけ">きっかけ</h3> <p>もともと愛知県にあるSESをメインで行っている企業に務めており、自動車メーカーの関連企業に客先常駐しながらアプリケーション・エンジニアとして働いていました。そこでは主に自社フレームワークを用いた社内向けの経理システムの開発に携わっていました。</p> <p>入社6年目に差し掛かる頃に自分の中で心境の変化があり、「よりモダンな環境・手法で開発をしてみたい」、「自分のスキルやキャッチアップ力が他の環境で通用するのか試してみたい」と思うようになり、その頃から業務を続ける傍らひっそりと転職活動を始めました。</p> <h3 id="スマートキャンプを見つけた経緯">スマートキャンプを見つけた経緯</h3> <p>趣味で競技プログラミングを行っていたため、そのスキルをアピールできないかと、求人サイトは『<a href="https://paiza.jp/">paiza</a>』をメインで利用しました。競技プログラミングのコンテストで有名なAtCoderの行っている転職サービスも選択肢としてあったのですが、求人の多さや企業へのアプローチのしやすさから、paizaをメインで利用しました。</p> <p><figure class="figure-image figure-image-fotolife" title="paizaのページ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230403/20230403123134.png" width="1200" height="687" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>出典:プログラミングスキルチェック | ITエンジニア専門の転職サイト【paiza転職】<a href="https://paiza.jp/challenges">https://paiza.jp/challenges</a></figcaption></figure></p> <p>paizaでは求人を出している企業へアピールできる材料として、レーティングシステムが用意されています。その数値は、サイト内で用意されているプログラミングの課題を解くことで上げることができました。</p> <p>もともと競技プログラミングの入門書として有名な『<a href="https://book.mynavi.jp/ec/products/detail/id=22672">蟻本</a>』で勉強していたり、AtCoderの週末のコンテストに継続的に参加して水色のレーティングを獲得していたりしたこともあり、本格的にして開始して約半月で最高のSランクの中でも上位の赤色のレートを取ることができました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230403/20230403123207.png" width="1200" height="687" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> レートの推移。</p> <p>そして、「どうせだったら、上位のランクを募集条件にしているところから探そう」と思い、条件指定して検索をかけた際に目に留まったのがスマートキャンプでした。</p> <h3 id="どうしてスマートキャンプにしたか">どうしてスマートキャンプにしたか</h3> <p>カジュアル面談から選考を始めてもらい、数回の面接とリファレンスチェックがあった後に、内定をいただくことができ、その数日後に行われた内定者向けのWeb面談にてEMの入山さんと会話する機会がありました。</p> <p>その場で、前職とはカルチャーも必要なスキルセットもギャップがあるけども、それを踏まえたうえで三浦さんでしたらキャップアップできると思うので、挑戦して欲しいという気持ちを伝えてもらえたのが、入社する決め手になりました。</p> <h2 id="入社してみて">入社してみて</h2> <h3 id="カルチャー面">カルチャー面</h3> <ul> <li><p><strong>利用するSaaSが多い</strong></p> <p> SaaS比較サイトを運用しているだけあってか、社内で利用しているSaaSの種類も多いです。入社した初日には、多くのツールの利用登録を行なった記憶があります。(社内チャットツール、バーチャルオフィスツール、勤怠管理ツール、経費精算ツール、人事評価・社員名簿ツール、ナレッジ共有ツール、短文メッセージツール、など)</p> <p> 実際にそれぞれ有用に用いられており、Missionとして掲げる「テクノロジーで社会の非効率をなくす」を体現しているように思えました。</p> <p> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230403/20230403123526.png" width="1200" height="751" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> BOXIL SaaSエンジニアの雑談チャンネル『# boxil_dev_心』は心の癒し。</p> <p> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230403/20230403123418.png" width="758" height="866" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> キャンプスペースでの開発朝会中のスクリーンショット。Gatherについての記事は<a href="https://tech.smartcamp.co.jp/entry/discord-to-gather">こちら</a>。</p></li> <li><p><strong>Microsoft製品をあまり使わない</strong></p> <p> 前職では「ドキュメントといえばとりあえずExcel」という文化でしたが、スマートキャンプに入ってからは、NotionでMarkdown形式で記述することが多く、スプレッドシートを使う場合でもGoogle Spreadsheetを利用するようになりました。</p> <p> 地味ですが自分の中では結構大きな変化でした。</p> <p> また、開発PCもWindowsではなくMacを利用するようになりました。(Visual Studio Codeは愛用しています。)</p> <p> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230403/20230403123654.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> 支給されたMacBook</p></li> <li><p><strong>あだ名文化</strong></p> <p> スマートキャンプではあだ名をつける文化があり、本名で呼ぶよりもメンバー同士の距離感を縮めやすく、ありがたいと感じています。(あだ名に慣れすぎて本名が思い出せなくなるのはスマートキャンプあるあるだと勝手に思っています。)</p> <p> ちなみに自分は、下の名前を音読みすると『ジン』となることから『JinJin(ジンジン)』と呼ばれています。</p></li> <li><p><strong>みんなコミュ力高い</strong></p> <p> 心理的安全性を保てるようにしつつも、フランクに接していただける方が多いように感じます。自分も見習いたいと思う面が多いです。</p></li> </ul> <h3 id="業務面">業務面</h3> <ul> <li><p><strong>Ruby on Rails</strong></p> <p> 前職ではJava(Struts系)をメインを使っていたため、スマートキャンプに入社が決まってから初めてRuby on Railsに触りました。 フレームワークの方でいい感じにやってくれるが故に、どこがどう動いているのかわからなくて初めの頃は戸惑うことが多かったです。最近になってようやく勘所がついてきました。(と思いたい。。)</p></li> <li><p><strong>GitHub</strong></p> <p> Git・GitHubでの開発を本格的に行ったのもスマートキャンプに入社してからになります。前職では資産管理ツールとしてずっとSVNを用いていたため、ようやくデファクトスタンダードな資産管理ツールで開発ができる嬉しさがありました。</p> <p> また、前職ではコードレビューの指摘事項はExcelに記述して共有していたのですが、GitHubのページ上でソースコードの具体的な箇所にレビューコメントを残すことができるようになり、レビューがGitHub上で完結するようになったのもありがたかったです。</p> <p> また、『<a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">GitLens</a>』(VSCodeの拡張機能)を用いて、ソースコードから過去のPull Requestを簡単に辿ることができ、書かれた背景などを簡単に調べられるのも便利でした。</p></li> <li><p><strong>アジャイル開発</strong></p> <p> BOXIL Magazineを運営しているメディアチームやセールスチームから次々と開発要望が上がってきて、それを、Sprintのタスクとして次々積んでいき消化する。それが、早いサイクルで回っていくのが楽しいです。 毎週金曜日にあるSprint Reviewでは、エンジニアチームの行った開発の成果を発表する場があり、その場で反応がいただけたりしてモチベーション向上にも繋がります。</p></li> </ul> <h2 id="これから">これから</h2> <p>チームやプロダクトに対してリスペクトがあるので、今以上にドメイン知識やシステム構成について理解して、機能追加や開発改善などをゴリゴリにできるようになっていきたいと思ってます。</p> smartcamp Vue3にアップグレードしてフロントエンドを改善した話 hatenablog://entry/4207112889964870080 2023-02-28T13:00:00+09:00 2023-07-31T14:35:20+09:00 vue3-migration-improve-frontend はじめまして! BALES CLOUDエンジニアのえーす(井上)です。この度、BALES CLOUDで長年使ってきたVue2から卒業し、Vue3を導入した状態でリリースできました。今日はこれについてお話できればと思います。 やったこと なぜVue3移行をしたか TypeScriptサポート 各ライブラリが古い Vue2のEOLが近い 具体的なVue3移行ステップ Vuetify卒業 Vue3導入 Vue3完全移行 移行にあたって問題だったこと ライブラリのアップグレード Vuetify卒業 ElementUI -> Element… <p><figure class="figure-image figure-image-fotolife" title="vue3-migration-improve-frontend"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230227/20230227191558.png" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>vue3-migration-improve-frontend</figcaption></figure></p> <p>はじめまして! <a href="https://bales.smartcamp.co.jp/bales-cloud">BALES CLOUD</a>エンジニアのえーす(井上)です。この度、BALES CLOUDで長年使ってきたVue2から卒業し、Vue3を導入した状態でリリースできました。今日はこれについてお話できればと思います。</p> <ul class="table-of-contents"> <li><a href="#やったこと">やったこと</a></li> <li><a href="#なぜVue3移行をしたか">なぜVue3移行をしたか</a><ul> <li><a href="#TypeScriptサポート">TypeScriptサポート</a></li> <li><a href="#各ライブラリが古い">各ライブラリが古い</a></li> <li><a href="#Vue2のEOLが近い">Vue2のEOLが近い</a></li> </ul> </li> <li><a href="#具体的なVue3移行ステップ">具体的なVue3移行ステップ</a><ul> <li><a href="#Vuetify卒業">Vuetify卒業</a></li> <li><a href="#Vue3導入">Vue3導入</a></li> <li><a href="#Vue3完全移行">Vue3完全移行</a></li> </ul> </li> <li><a href="#移行にあたって問題だったこと">移行にあたって問題だったこと</a><ul> <li><a href="#ライブラリのアップグレード">ライブラリのアップグレード</a></li> <li><a href="#Vuetify卒業-1">Vuetify卒業</a></li> <li><a href="#ElementUI---ElementPlus">ElementUI -&gt; ElementPlus</a></li> <li><a href="#巨大PRによるレビュー負荷">巨大PRによるレビュー負荷</a></li> <li><a href="#チーム体制">チーム体制</a></li> <li><a href="#マイグレーションビルドと他ライブラリの相性">マイグレーションビルドと他ライブラリの相性</a></li> </ul> </li> <li><a href="#よかったこと">よかったこと</a></li> <li><a href="#課題感">課題感</a></li> <li><a href="#これから">これから</a></li> </ul> <h2 id="やったこと">やったこと</h2> <ul> <li>Vue2を卒業し、Vue3を導入</li> <li>依存している各ライブラリのバージョンアップグレード</li> <li>適宜コードの書き直し</li> </ul> <p>Vue3導入と書きましたが、より詳細には<a href="https://v3-migration.vuejs.org/migration-build.html">マイグレーションビルド</a>を使ってのVue3導入ができている状態です。つまりマイグレーションビルドによってVue2とVue3が共存しているような状態です(Vueのバージョンとしては3系です)。まだ完全にVue3へ移行できたわけではないのでやることは残っていますが、現状までに多くの知見を得られたので記事としてまとめることにしました。</p> <h2 id="なぜVue3移行をしたか">なぜVue3移行をしたか</h2> <p>BALES CLOUDではVueを使っています。バージョンは2.6.10となっていて、このバージョンは4年前から更新されていませんでした。4年前というと体感的にも相当古いと思います。</p> <p>古いとはいえ動いているならそのままでステイするのもまた正義だとは思うのですが、BALES CLOUDではVue3移行を決断しました。</p> <p>以下、理由です。</p> <h3 id="TypeScriptサポート">TypeScriptサポート</h3> <p>フレームワークとしてVue2を使い続けていることによって、やはり辛いと思う部分が多くなってきました。その最大のものが「型」です。</p> <p>やはりVue3といえばより良いTypeScriptサポートがされるようになった点が非常に大きな進歩だと思います。</p> <p>Vue2でもVue.extendを使ってTypeScriptを利用できますが、型推論がうまく動かなくて明示的に指定する必要があったり、Vueインスタンスの型があまり使えなかったりしていました。「ああ、Vue2とTypeScirptの相性って微妙なんだなあ」と思うことが多く、積極的に型を使おうと言う気になれませんでした。 そのせいもあり、BALES CLOUDのVueコンポーネントでも、全体として型があまり使われていませんでした。</p> <p>もっと型を活用しようと考えたのは、フロントエンドでバグがよく発生していた時期があったからです。型がないから起きたバグというわけではないのですが、むしろバグの原因となったものから短期的な改善を目指すのではなく、長期的に品質を上げ、結果として効率よく開発ができる状態を目指すことにしました。そこから型を活用しようという考えに至り、そのための環境があるVue3へ移行することにしました。</p> <p>このあたり短期的な視点でなく長期的な視点で改善することを決断できるのがBALES CLOUDチームの良いところかなと思います。</p> <h3 id="各ライブラリが古い">各ライブラリが古い</h3> <p>Vue以外にも、Vueに依存する各ライブラリも非常に古い状態でした。例えばElementUI(UIライブラリ)、vue-i18nなどコンポーネントで使っているものや、vue-cliやsass-loaderなどのdevDependenciesなどです。</p> <p>ライブラリを古いまま放置しておくと、そのうちEOLとなってセキュリティリスクにもなったりしますし、開発者体験としてもよくないので、刷新をしたいと考えました。</p> <h3 id="Vue2のEOLが近い">Vue2のEOLが近い</h3> <p>皆さん<a href="https://v2.vuejs.org/lts/">Vue2のEOL (End of Life) が今年末</a>までであることはご存じでしたでしょうか。EOLとなるからといって、<a href="https://v2.vuejs.org/lts/#Stay-on-Vue-2">セキュリティのサポートやブラウザの互換性サポートは引き続きされる</a>ようですので、Vue2でステイするという判断も妥当だと思います。が、それも遅かれ早かれ終了するとは思うので、この機会に移行することにしました。</p> <h2 id="具体的なVue3移行ステップ">具体的なVue3移行ステップ</h2> <p>以下が具体的なVue3移行ステップです。</p> <ol> <li>Vuetify卒業</li> <li>Vue3導入</li> <li>Vue3完全移行</li> </ol> <p>一つずつ詳しく説明します。</p> <h3 id="Vuetify卒業">Vuetify卒業</h3> <p>当時、BALES CLOUDではUIフレームワークを2種類利用していました。ElementUIとVuetifyです。</p> <p>Vue3移行するにあたって最大の問題となったのがVuetifyでした。現在は<a href="https://vuetifyjs.com/ja/introduction/roadmap/#v2-7-nirvana">VuetifyはVue3に対応しております</a>が、移行を決めた当時はVue3にまだ対応しておらず、Vue3へ移行するのに障害となっていました。</p> <p>ElementUIについてはVue3対応版としてElementPlusがリリースされていました。かつ、BALES CLOUD全体を見たときに、Vuetifyを利用している割合がElementUIに比べて低かったということもあって、Vuetifyを使うのを辞めてElementPlusに一本化することに決めました。</p> <p>ElementPlusに一本化する前段のプロセスとして、まずVuetifyを依存ライブラリから抜いて、ElementUIに統一したものをリリースすることで、のちのVue3導入での変更量を減らすようにしました。</p> <h3 id="Vue3導入">Vue3導入</h3> <p>Vue3導入フェーズでは、ゴールを下記に定めました。</p> <ul> <li>Vueのバージョンが3系であること</li> <li>vue-compat(マイグレーションビルド)を利用して、Vue2の機能も使える状態であること</li> </ul> <p>つまり、マイグレーションビルドを使った状態でのリリースを目標としました。</p> <p>これはVue3で使えなくなる機能まですべて修正するより、まずマイグレーションビルドを使った状態(つまりVue2の機能もある程度使える状態)でリリースすることで、コード修正量を減らし、ビッグバンリリースとなることを避けるためです。</p> <p>こうすることで、このフェーズでやるべきことは下記に絞ることができました。</p> <ul> <li>Vueのアップグレード</li> <li>マイグレーションビルドの導入</li> <li>Vue3 + マイグレーションビルド環境では動かないライブラリのアップグレードとコード修正 <ul> <li>ElementUI, vue-router, Vuex etc.</li> </ul> </li> <li><a href="https://v3-migration.vuejs.org/migration-build.html#incompatible">マイグレーションビルドで互換性がないVue2の機能</a>への対応</li> </ul> <h3 id="Vue3完全移行">Vue3完全移行</h3> <p>ここでのゴールは下記のとおりです。</p> <ul> <li>Vueのバージョンが3系であること</li> <li>マイグレーションビルドが削除されていること</li> </ul> <p>主にやることは下記の通りです。</p> <ul> <li>マイグレーションビルドが出しているWarningへの対処</li> <li>マイグレーションビルドを抜いたことによって動かなくなる部分への対処</li> </ul> <p>ここまでやって初めて「Vue3へ移行した」ことが言えると思います。</p> <p>ちなみに「マイグレーションビルドですでにVue3にバージョンアップしているのだから、無理に抜く必要なくね?」と思いましたが、<a href="https://v3.ja.vuejs.org/guide/migration/migration-build.html#%E6%9C%9F%E5%BE%85%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8">マイグレーションビルドは将来のマイナーバージョンでリリースされなくなる</a>ので、やっぱり必要です。</p> <h2 id="移行にあたって問題だったこと">移行にあたって問題だったこと</h2> <h3 id="ライブラリのアップグレード">ライブラリのアップグレード</h3> <p>当然ですが、Vueに依存するライブラリはほとんどすべてアップグレードしました。以下、一部ですが紹介します。実際にはもっとあります。</p> <ul> <li><code>@vue-compat</code>の導入 <ul> <li>マイグレーションビルド</li> </ul> </li> <li><code>element-plus</code>の導入 <ul> <li>element-uiはVue2までのため</li> </ul> </li> <li><code>vue-i18n</code>を9系にアップグレード</li> <li><code>vuex</code>を4系に</li> <li><code>vue-router</code>を4系に</li> <li><code>@vue/cli-service</code>を5系に</li> <li><code>sass-loader</code>を10系に</li> <li><code>core-js</code>を3系に</li> <li><code>@vue/cli~</code>をすべて5系に <ul> <li><code>vue/cli-service</code>のアップグレード(webpack5)に合わせるため</li> </ul> </li> <li><code>@vue/compiler-sfc</code>の導入 <ul> <li><code>@vue-compat</code>のため</li> </ul> </li> <li><code>node-polyfill-webpack-plugin</code>の導入 <ul> <li>webpack5系がnodeのpolyfillを自前でやってくれなくなったので</li> </ul> </li> </ul> <p>devDependenciesはとりあえず動いていればよいだろうと判断しました。その他は一つずつ公式のBreaking Changesを読み解き、自プロダクトのコードが移行対象でないかを確認しました。Breaking Changesに未記載だったり、単純に確認漏れもあるので、なかなか一筋縄ではいきませんでした。</p> <h3 id="Vuetify卒業-1">Vuetify卒業</h3> <p>前述の通りBALES CLOUDはElementUIとVuetifyの両方に依存していますが、このうちVuetifyを抜くとなるとシステムのUIが一部変わることにならざるを得ません。例えば一番顕著だったのがページネーションのデザインの変更でした。こちらは常にユーザーに見えているものなので、デザイン変更の影響が大きいです。</p> <p>このようなデザイン変更に伴ってPdMを合意を取るべく、変更予定のコンポーネントを一つずつ洗い出して、Figmaに起こしました。それを確認してもらいつつ、かつ実際の環境でも触れるようにして、PdMとデザイン変更の合意形成を行いました。</p> <p><figure class="figure-image figure-image-fotolife" title="一つずつUIの対照リストを作成"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230220/20230220170508.png" width="1200" height="160" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>一つずつUIの対照リストを作成</figcaption></figure></p> <h3 id="ElementUI---ElementPlus">ElementUI -&gt; ElementPlus</h3> <p>ElementUIからElementPlusへの移行は、一言でいうと苦労の連続でした。おそらくここがVue3移行において一番つらかったと思います。</p> <p>まず<a href="https://github.com/element-plus/element-plus/discussions/5658">Breaking Changes</a>にも載っていないような本当に些細な変更がところどころあったことが大きかったです。例えばDatePickerなどでのBlurイベントで渡ってくるものが<a href="https://element.eleme.io/#/en-US/component/date-picker#events">コンポーネントインスタンス</a>から<a href="https://element-plus.org/en-US/component/date-picker.html#events">FocusEvent</a>になっていたりなどです。もともとこの動きに依存していたので結構困りました。</p> <p>このような些細な変更に対しては、一つずつコンポーネントを見て動きをチェックする他ないです。かつ問題があったとしても元の仕様を再現するには非常に難しくなっているものも多く、かなり工数がかかりました。</p> <p>地味にウッとなるのが<a href="https://github.com/element-plus/element-plus/issues">GitHubのissue</a>などを漁っていると高頻度で中国語が出てくることです。Elementに中国企業のスポンサーが入っていることが大きいと思います。ページ翻訳すればいいと思うのですが、やっぱりさっと英語で読めるのと比べると少し手間だったり、開発者としても疎外感のようなものを覚えますね。</p> <p><figure class="figure-image figure-image-fotolife" title="ElementPlusのissueは半分ぐらい中国語で記載されている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230220/20230220170604.png" width="1200" height="455" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ElementPlusのissueは半分ぐらい中国語で記載されている</figcaption></figure></p> <h3 id="巨大PRによるレビュー負荷">巨大PRによるレビュー負荷</h3> <p>この手の移行はインクリメントとしてリリースできないので、どうしても巨大なPRとならざるを得ません。巨大なPRはそれだけでリスクですし、そもそもエンジニアのレビュー負担も大きいです。</p> <p>レビュー負荷を下げるべく行ったのは、まずPRの分割です。出てきたバグに対して一つずつアイテム化し、それに対してトピックブランチに向けてPRを作成しました。これによって変更内容と変更理由が一つずつ明確になります。特に移行による変更は複線的なので、一つのPRでまとめてしまうと、どの変更がどのような意図で行われているのか全体像が掴みづらいところがあります。修正を分割することで、そのあたりの負荷を減らすことができたと思います。</p> <h3 id="チーム体制">チーム体制</h3> <p>Vue3移行は、通常の機能開発をストップせずに行いました。プロダクトロードマップとして達成するべき機能開発があるなかで、開発改善に全リソースを割くだけの余力はないからです。</p> <p>そのためチームの体制をあらためて考える必要がありました。そして最終的にVue3移行の実行責任者として私が就き、主にVue3移行の主開発やその他開発体制を整える仕事(各ドキュメントやマイルストーンの作成など)を行いました。</p> <p>このような体制にしたことで、責任範囲が明確になり、自分としては動きやすかったように思います。プロジェクトをドライブする人が一人いることで、「いつまで経っても移行が進まない」みたいな状態を作らなかった点も良かったかなと思います(規模が大きめの開発改善だとあるあるですよね)。</p> <h3 id="マイグレーションビルドと他ライブラリの相性">マイグレーションビルドと他ライブラリの相性</h3> <p>マイグレーションビルドは便利なのですが、これをそのまま他のライブラリのVue3対応版で動かすとちゃんと動かないケースが何回かありました。</p> <p>例えばElementPlusはマイグレーションビルド下だとDatePickerなどの日時入力系コンポーネントにvalueが入らない問題がありました。</p> <p>最終的な原因としては、Vue3のAPIを利用しているのにマイグレーションビルドによってVue2でコンパイルされていたことでした。これは<code>compatConfig</code>というオプションをDatePicker系が依存しているElementPlusのコンポーネントに指定することで解決しました。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink> <span class="synStatement">import</span> <span class="synIdentifier">{</span> CommonPicker <span class="synIdentifier">}</span> from <span class="synConstant">'element-plus'</span> CommonPicker.compatConfig = <span class="synIdentifier">{</span> MODE: 3 <span class="synIdentifier">}</span> </pre> <p>マイグレーションビルドを経由せず一気に移行すれば起きなかった問題ではありますが、今回の場合は仕方ないかなと思います。ちなみにマイグレーションビルド + ElementUIで動かないかとも探ったのですが、それはそれでエラーが出て全く動かず、丸一日試して駄目だったので素直にElementPlusに移行することにしました。</p> <h2 id="よかったこと">よかったこと</h2> <p>Vue3を導入してみて、あらためて実感したのが型サポートの充実です。</p> <p>もともと「VueとTypeScriptの相性微妙じゃね?」みたいな空気がVue2では漂っていたと思うのですが、アップグレードして実際にVue3を使ってみると、VueとTypeScriptの相性が微妙という感想がなくなりました。コンポーネントが持っているpropsに対する型も効きますし、綺麗に型推論が使えていたり、<code>this</code>の型が充実していたり、とにかく型をフルに使える環境が整っています。</p> <p><figure class="figure-image figure-image-fotolife" title="充実のthis"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230220/20230220170631.png" width="1082" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:200px" itemprop="image"></span><figcaption>充実のthis</figcaption></figure></p> <p>これによって、より安全に、より高速に開発できる環境が整ったように思います。この環境だけでも導入した価値があるように思います。</p> <h2 id="課題感">課題感</h2> <p>BALES CLOUDはElementPlusに大きく依存したUIなのですが、Vue3移行をしていて、あらためて「依存していること」自体に課題感を覚えました。</p> <p>BALES CLOUDはその特徴として第一に「UIが使いやすい」ことを挙げています。これは顧客からもよくフィードバックをいただきますし、開発チームとしても非常に大事にしている価値です。</p> <p>そのようなプロダクトの価値の根幹を成すUIが、完全にライブラリに依存していることで、移行時に不都合がありました。例えば「ElementUIからElementPlus移行すると、ElementPlusの仕様上、元の挙動にはどうしてもできない」というような事象がありました。</p> <p>一時はリポジトリをフォークしてくるというようなアイデアも出たのですが、それはそれでアップグレード時に手間だったりして大変だという話にもなりました。最終的には、顧客の体験を大きく損なうことはないと判断し、仕様を少し変えて提供するという形になりました。</p> <p>ですがこれからも同じような事象が発生する可能性があることを考えると、「UIが使いやすい」ということを標榜するプロダクトである以上、いずれ向き合う必要がある課題ではないかとBALES CLOUDチームでは考えています。</p> <h2 id="これから">これから</h2> <p>今の状態としては上記で言うところの「Vue3導入(マイグレーションビルド込みでのリリース)」が終わっている状態で、まだマイグレーションビルドからの卒業ができていません。このあたりはこれからまだBALES CLOUDでやっていくところだと思います。ただVue3へのアップグレード自体は終えられたので、大きな山は超えたと言えると思います。</p> <p>ここからマイグレーションビルドを抜いてVue3に完全対応するまで、BALES CLOUDチームは走っていきたいと思います。</p> smartcamp WebのOSを目指す新しい体験のWebブラウザ「Arc」を紹介したい hatenablog://entry/4207112889961236105 2023-02-10T12:00:00+09:00 2023-07-31T14:35:44+09:00 こんにちは!スマートキャンプエンジニアの中田です。 みなさんWebブラウザには何を使われてますか? Chrome, Firefox, SafariにEdgeなど多くの選択肢があるWebブラウザですが、私は2ヶ月ほど前に長らく使ってきたChromeから移行し、現在は Arc というbeta版が公開されている新しいWebブラウザを使ってみています。 この Arc がとても便利で楽しいものだったので、本記事ではそんな Arc の紹介 (※普及活動) をしていきます! Arcとは 概要 インストール方法 ※ 公式から情報を辿りたい方向けの補足 特徴 機能 1. タブ/ブックマークの管理 2. Note… <p>こんにちは!スマートキャンプエンジニアの中田です。</p> <p>みなさんWebブラウザには何を使われてますか? Chrome, Firefox, SafariにEdgeなど多くの選択肢があるWebブラウザですが、私は2ヶ月ほど前に長らく使ってきたChromeから移行し、現在は <code>Arc</code> というbeta版が公開されている新しいWebブラウザを使ってみています。</p> <p>この <code>Arc</code> がとても便利で楽しいものだったので、本記事ではそんな <code>Arc</code> の紹介 (※普及活動) をしていきます!</p> <ul class="table-of-contents"> <li><a href="#Arcとは">Arcとは</a><ul> <li><a href="#概要">概要</a></li> <li><a href="#インストール方法">インストール方法</a></li> <li><a href="#-公式から情報を辿りたい方向けの補足">※ 公式から情報を辿りたい方向けの補足</a></li> </ul> </li> <li><a href="#特徴">特徴</a><ul> <li><a href="#機能">機能</a><ul> <li><a href="#1-タブブックマークの管理">1. タブ/ブックマークの管理</a></li> <li><a href="#2-Note--Easel機能">2. Note / Easel機能</a></li> <li><a href="#3-Boosts機能">3. Boosts機能</a></li> </ul> </li> <li><a href="#実装の方向性">実装の方向性</a><ul> <li><a href="#WebのOS的な立ち位置のアプリ">WebのOS的な立ち位置のアプリ</a></li> </ul> </li> </ul> </li> <li><a href="#推し機能">推し機能</a><ul> <li><a href="#Note--Easelのキャプチャ機能">Note / Easelのキャプチャ機能</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#参考サイト">参考サイト</a></li> </ul> <h2 id="Arcとは">Arcとは</h2> <h3 id="概要">概要</h3> <p>Arcは <a href="https://thebrowser.company/">The Browser Company</a> という、創立2019年、New Yorkが拠点の企業で開発されているそうです。</p> <p>企業サイトの <a href="https://thebrowser.company/values/">Values</a> ページが個性的なので興味のある方はぜひご一読ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Farc.net%2F" title="Arc from The Browser Company" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://arc.net/">arc.net</a></cite></p> <p>ArcはブラウザエンジンにChromiumを使用したWebブラウザです。</p> <p>現在はbeta版のみ公開されており、OSはmacOSにのみ対応しています。 (2023年中にはWindowsOSやモバイルのOSもサポート予定とのこと)</p> <h3 id="インストール方法">インストール方法</h3> <p>現在はbeta版公開のみのため、アプリを即時インストールできない仕様になっており、インストールには以下の手順を踏む必要があります。</p> <ol> <li><a href="https://browserco.typeform.com/to/l9lYbJtU?typeform-source=arc.net#source=arcnet">登録ページ</a>から情報を入力</li> <li>待機リストに入る</li> <li>1~2週間ほどで招待メールが届く</li> <li>インストール</li> </ol> <p>また、上記以外のフローとして、すでに導入済みのユーザーからの招待を受ける方法もあります。</p> <ol> <li>導入済みのユーザーの招待機能で招待リンクを発行してもらう</li> <li>招待リンクからインストール</li> </ol> <h3 id="-公式から情報を辿りたい方向けの補足">※ 公式から情報を辿りたい方向けの補足</h3> <p>執筆に際して、Arcの公式サイトを再訪してみたものの、全体を通して情報が少量かつ抽象的で、具体的に「何ができるWebブラウザなのか」がサイト内の情報からは掴みづらい印象でした。</p> <p>Arcの具体的な機能に関しては、Arcをインストールした後、アプリ内のヘルプから説明ページにアクセスできる導線になっており、企業サイトに記載のある価値観と照らし合わせて考えると、「まずは感覚的に使ってみてね!」という方針での設計なのかもしれません。</p> <p>(私も説明を流し目に待機リストに飛び込んだ一人ですが、実際に使ってみて困ることは少なかった気がします)</p> <p>また、<a href="https://www.youtube.com/@TheBrowserCompany/featured">YouTube</a> にて公式が公開している解説動画も豊富にあるので、使うより先に情報が欲しい方は本ブログと共にそちらもチェックしてみるのをオススメします。</p> <h2 id="特徴">特徴</h2> <p>Arcが他のWebブラウザと異なる点を「機能」と「実装の方向性」の2つの観点で挙げてみます。</p> <h3 id="機能">機能</h3> <h4 id="1-タブブックマークの管理">1. タブ/ブックマークの管理</h4> <p><figure class="figure-image figure-image-fotolife" title="Arc利用時の全体画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kiki_ki/20230207/20230207210646.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Arc利用時の全体画面</figcaption></figure></p> <p>Arcを利用する際、アプリのコントロールは基本的に上図左のサイドバーから行います。</p> <p><figure class="figure-image figure-image-fotolife" title="サイドバーについて"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kiki_ki/20230207/20230207213222.png" width="719" height="865" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>サイドバーについて</figcaption></figure></p> <p>サイドバーは大きく3つのセクションに分かれています。 各セクションはざっくり以下のような使い分けができます。</p> <ul> <li>①: Favorites <ul> <li>よく使うタブを置いておくセクションです</li> <li>(噂によるとSpotify等特定のアプリを置いておくと連動していい感じのアニメーションが出たりするそう)</li> </ul> </li> <li>②: Pinned tab <ul> <li>Favoritesほどではないがよく使うなというタブを置いておくセクションです</li> <li>(Chromeで言うブックマークにはこのセクションが一番近そうです)</li> </ul> </li> <li>③: Today tab <ul> <li>Temporaryなタブが置かれるセクションです</li> <li>(デフォルトの設定だと、12時間が経過するとこのセクションの内容はリセットされます)</li> </ul> </li> </ul> <p>①と②の特徴として、これらはブックマークでありながらタブのようにも機能します。</p> <p>最初にアプリを開くときにはブックマークとして機能し、その後はサイドバー上の同位置に固定されたアプリのタブとして使えます。 これにより特定アプリのタブへの出戻りがしやすく、同アプリのタブが複数乱立するような状況が生まれづらくなっています。</p> <p>これはChromeのブックマーク機能と大きく異なる点だと感じました。</p> <p>①と②の使い分けが曖昧ですが、本記事では紹介しないSpace機能利用時の差分など、他にも若干の差分があるはずです。 が、私もまだ使い分け方を模索中なのと、正しいことが言える自信がないので曖昧のままに留めます。</p> <p>個人的には③のタブが一定時間でリセットされる仕様も気に入ってます。</p> <p>仕事を終えて次の日の朝、昨日のタブの残骸に脅かされる心配がなく快適です!</p> <h4 id="2-Note--Easel機能">2. Note / Easel機能</h4> <p><figure class="figure-image figure-image-fotolife" title="Note機能"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kiki_ki/20230209/20230209111505.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Note機能</figcaption></figure></p> <p>Noteはマークダウン形式で書けるメモ機能です。</p> <p>ちょっとしたメモに別アプリを起動しなくても良いのは便利かもしれません。</p> <p><figure class="figure-image figure-image-fotolife" title="Easel機能"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kiki_ki/20230207/20230207222315.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Easel機能</figcaption></figure></p> <p>Easelは画像を編集したり、ダッシュボードを作ることのできる機能です。</p> <p>ダッシュボードとしての使い方は後ほどご紹介します!</p> <h4 id="3-Boosts機能">3. Boosts機能</h4> <p><figure class="figure-image figure-image-fotolife" title="Boosts機能"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kiki_ki/20230207/20230207223612.png" width="731" height="682" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Boosts機能</figcaption></figure></p> <p>Boostsは任意のWebサイトにカスタムCSS,JSを適用できる機能です。</p> <p>(特定のサイトor全サイトで適用のスコープを選択できます)</p> <p>エンジニア心をくすぐられる機能ですし、Chrome拡張のように作成 -> 申請のような過程を踏む必要なく、サクッと作ったスクリプトを適用できるのは嬉しいですね。</p> <p>※ 中身がChromiumなのでChrome拡張も利用可能です。</p> <p>Arcの機能に見えるような何かを自分の環境にだけ配置してオレオレArcを作ってみたりも楽しそうです。</p> <h3 id="実装の方向性">実装の方向性</h3> <p>The Browser Company CEO(Josh Miller)への <a href="https://www.makingproductsense.com/p/the-future-of-arc-with-josh-miller">インタビュー記事</a> を読むと、以下の1点においてArcの特徴的な実装の方向性がうかがえます。</p> <h4 id="WebのOS的な立ち位置のアプリ">WebのOS的な立ち位置のアプリ</h4> <blockquote><p>Arcはまさにこれを目指しているのです! Meta-OS Layer の新しいパラダイムを、グローバルノートのようなメタレイヤーのツールで実現することです。 ネイティブアプリケーションからブラウザベースのアプリケーションにワークフローを自然に移行させるような体験を提供するため、Meta-OS Layerの下にすべてのアプリケーションを少しずつ統合しているのだそうです。 (DeepLによる翻訳)</p></blockquote> <p>※ 引用:<a href="https://www.makingproductsense.com/i/67773152/enter-arc">https://www.makingproductsense.com/i/67773152/enter-arc</a></p> <p>上記の通り、ArcはWebのOS的な立ち位置を目指しているそうです。</p> <p>Note, Easelのような機能も本来OSにも搭載されている機能に思えますし、書類や画像などPCのストレージに保存されているコンテンツへのアクセスがFinderを開かずにArc上からできるのもWebのOSを目指しての機能なのかも知れません。</p> <p><figure class="figure-image figure-image-fotolife" title="Arc上で直アクセス可能"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kiki_ki/20230207/20230207234831.png" width="782" height="702" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Arc上で直アクセス可能</figcaption></figure></p> <p>また、Arcはアプリをブラウザベースで管理するApp Launcherのようにも利用しやすく、その点もWebのOS的な思想が反映されていると言えそうです。</p> <p>例えばChromeをお使いのみなさんは、一度は以下のような経験をされたことがあるのではないでしょうか?</p> <ol> <li>アプリをChromeでブックマークから開いて使用</li> <li>アプリを離れて別の作業をする</li> <li>しばらくして、さっき開いていたアプリを開きたくなる</li> <li>タブが乱立しておりどれが目当てのアプリタブか分からなくなる</li> </ol> <p>"機能"セクションでも紹介したとおり、Arcはタブとブックマークの管理方法に特徴があり、1度開いたアプリタブを見失いづらい仕様になっています。 Arcを使うと以下のような体験に改善されそうです。</p> <ol> <li>アプリをArcのFavoriteから開いて使用</li> <li>アプリを離れて別の作業をする</li> <li>しばらくして、さっき開いていたアプリを開きたくなる</li> <li>Favoriteからアプリを開く (もう迷わない!)</li> </ol> <p>個人的に、これまではなるべくデスクトップアプリを入れるようにしており、その理由は上記のタブ問題が主でした。</p> <p>タブ問題の改善されたブラウザは、アプリウィンドウの数も増えづらく、各アプリへのアクセスもしやすいため、App Launcherに向いているなと感じました。</p> <p>実際私は、Arcを使い始めてから2ヶ月ほどが経過した今、これまでデスクトップから開いていたほとんどのアプリにはWebブラウザ(Arc)からアクセスするようになりました。</p> <h2 id="推し機能">推し機能</h2> <p>最後に、Arcの個人的な推し機能を1つご紹介します。</p> <h3 id="Note--Easelのキャプチャ機能">Note / Easelのキャプチャ機能</h3> <p>「Easelはダッシュボードとして使える」と先述しましたが、このキャプチャ機能を利用することでダッシュボード化が可能です。</p> <p><figure class="figure-image figure-image-fotolife" title="キャプチャ機能紹介"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kiki_ki/20230208/20230208000034.png" width="1200" height="616" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>キャプチャ機能紹介</figcaption></figure></p> <p>上図は私物の「朝ボード」ですが、配置されている各コンテンツはキャプチャ機能を利用したもので、特定のWebページの特定の位置のコンテンツが投影されているような状態になっています。</p> <p>(正確にはコンテンツをホバーすると同期ボタンが表示され、実行すると最新の状態になる仕様)</p> <p>色んなサイトから手軽に情報を集めて自分用のダッシュボードを作れるのは相当便利です。</p> <p>また、作成したEaselはWebに公開可能なので、複数人での共用ダッシュボードとしても利用できそうです。</p> <p>この機能について興味本位で少し実装を探ってみると、キャプチャされたコンテンツはHTML上ではimgタグの要素となっていました。 Webページとページ内での位置情報をArc内部で持ちコンテンツと紐付けておき、同期の実行時に最新のキャプチャを撮影して置き換えるような実装なのかもしれません。</p> <p>(同期はArc上でのみ可能かつArc上でEaselに対してはChromeのDevツールが禁止だったため、同期処理の内容についてはあくまでも想像です)</p> <p>イチオシの機能なので、Arcを使われている/今後使われる方はぜひ試してみてください!</p> <h2 id="まとめ">まとめ</h2> <p>いかがでしたでしょうか?</p> <p>簡単にですが、WebのOSを目指す新しい体験のWebブラウザ <code>Arc</code> についてご紹介しました。</p> <p>本記事では触れていないArcの魅力も多分にあると思うので、記事を通して少しでも興味を持たれた方はぜひ一度使ってみてください。</p> <p>個人的に用法をユーザーで拡張できるようなツールが好きなので、Arcはツボでした。</p> <p>直近にも高頻度でアップデートが来ており、今後リリースされる機能も楽しみです。 (betaでこのクオリティ...!と驚くばかりです)</p> <p>最後までお読みくださりありがとうございました。それではまた!</p> <h2 id="参考サイト">参考サイト</h2> <ul> <li><a href="https://thebrowser.company/">https://thebrowser.company/</a></li> <li><a href="https://arc.net/">https://arc.net/</a></li> <li><a href="https://www.youtube.com/@TheBrowserCompany/featured">https://www.youtube.com/@TheBrowserCompany/featured</a></li> <li><a href="https://www.makingproductsense.com/p/why-everyone-is-losing-their-minds">https://www.makingproductsense.com/p/why-everyone-is-losing-their-minds</a></li> <li><a href="https://www.makingproductsense.com/p/the-future-of-arc-with-josh-miller">https://www.makingproductsense.com/p/the-future-of-arc-with-josh-miller</a></li> </ul> kiki_ki エンジニア採用サイトをリニューアルした話 hatenablog://entry/4207112889961333102 2023-02-08T13:30:00+09:00 2023-07-31T14:36:10+09:00 挨拶 こんにちは!私はBOXIL SaaS開発エンジニアのハヤシ(ぱずー)です。 前回、私がスマートキャンプで成長したエピソードを紹介しましたが、今回はエンジニア採用サイトのリニューアルに携わったので、それについて紹介します。 最後まで読んでいただけると嬉しいです! 今回、リニューアルした採用サイトです。こちらも見ていただけると嬉しいです! https://engineer.smartcamp.co.jp/ 想定読者 これから採用サイトをリニューアルしたい人 エンジニアでスマートキャンプに興味がある人 目次 挨拶 想定読者 目次 TL;DR なぜ採用サイトをリニューアルしたか どう実装したか … <p><figure class="figure-image figure-image-fotolife" title="eyecatch_renewal_recruiting_site"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208123228.png" alt="&#x30A2;&#x30A4;&#x30AD;&#x30E3;&#x30C3;&#x30C1;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure></p> <h2 id="挨拶">挨拶</h2> <p>こんにちは!私はBOXIL SaaS開発エンジニアのハヤシ(ぱずー)です。</p> <p>前回、私がスマートキャンプで成長したエピソードを紹介しましたが、今回はエンジニア採用サイトのリニューアルに携わったので、それについて紹介します。</p> <p>最後まで読んでいただけると嬉しいです!</p> <p>今回、リニューアルした採用サイトです。こちらも見ていただけると嬉しいです!</p> <p><a href="https://engineer.smartcamp.co.jp/">https://engineer.smartcamp.co.jp/</a></p> <h2 id="想定読者">想定読者</h2> <ul> <li>これから採用サイトをリニューアルしたい人</li> <li>エンジニアでスマートキャンプに興味がある人</li> </ul> <h2 id="目次">目次</h2> <ul class="table-of-contents"> <li><a href="#挨拶">挨拶</a></li> <li><a href="#想定読者">想定読者</a></li> <li><a href="#目次">目次</a></li> <li><a href="#TLDR">TL;DR</a></li> <li><a href="#なぜ採用サイトをリニューアルしたか">なぜ採用サイトをリニューアルしたか</a></li> <li><a href="#どう実装したか">どう実装したか</a><ul> <li><a href="#開発にあたっての方針">開発にあたっての方針</a><ul> <li><a href="#1-モダンな技術を使う">1. モダンな技術を使う</a></li> <li><a href="#2-独自実装を極力避ける">2. 独自実装を極力避ける</a></li> <li><a href="#3-シンプルな構成を目指す">3. シンプルな構成を目指す</a></li> <li><a href="#4-最初から完璧を目指さない">4. 最初から完璧を目指さない</a></li> </ul> </li> </ul> </li> <li><a href="#技術構成">技術構成</a><ul> <li><a href="#スタイリングにCSS-in-JSではなくCSS-Modulesを使用した理由">スタイリングにCSS in JSではなくCSS Modulesを使用した理由</a></li> </ul> </li> <li><a href="#実装で工夫したところ">実装で工夫したところ</a><ul> <li><a href="#作業しやすいディレクトリ構成にする">作業しやすいディレクトリ構成にする</a><ul> <li><a href="#メリット">メリット</a></li> <li><a href="#デメリット">デメリット</a></li> </ul> </li> <li><a href="#コンポーネントのテンプレートを作成するスクリプトを組んで開発効率を上げる">コンポーネントのテンプレートを作成するスクリプトを組んで開発効率を上げる</a></li> <li><a href="#next-seoを使ってSEO対策でラクをする">next-seoを使ってSEO対策でラクをする</a></li> <li><a href="#headタグ内にAAを置く">headタグ内にAAを置く</a></li> </ul> </li> <li><a href="#ハマったところ">ハマったところ</a><ul> <li><a href="#アニメーションなんもわからん">アニメーションなんもわからん</a><ul> <li><a href="#CSSを極力使わない">CSSを極力使わない</a></li> <li><a href="#とにかく実装イメージをつける">とにかく実装イメージをつける</a></li> </ul> </li> <li><a href="#最下部にスクロールするときに画面がチラついてしまう">最下部にスクロールするときに画面がチラついてしまう</a></li> </ul> </li> <li><a href="#開発してみての感想">開発してみての感想</a></li> <li><a href="#最後に">最後に</a></li> </ul> <h2 id="TLDR">TL;DR</h2> <ul> <li>アニメーションの実装経験があまりなかったため、なんもわからん状態だったけど気合いで乗り切った</li> <li>採用サイトをリニューアルすることでエンジニア組織の方向性が定まった。</li> </ul> <h2 id="なぜ採用サイトをリニューアルしたか">なぜ採用サイトをリニューアルしたか</h2> <p>主に以下の理由で採用サイトがリニューアルするモチベーションになったと認識しています。</p> <ul> <li>エンジニア採用サイトの更新が2020年で止まっており、単純に情報を更新したい</li> <li>テクノロジーを活用する姿勢を求職者に見せたい</li> </ul> <p>また、リニューアルにあたっての想いを企画担当者からいただきましたので、掲載します。</p> <pre class="code lang-markdown" data-lang="markdown" data-unlink>スマートキャンプはIPOを目指すことになり、そのためにもよりテクノロジーを活用していく必要性があります。 そこで私たちの思いに共感し、それを推進したいと言っていただけるようなエンジニアを広く募りたくリニューアルを開始しました。 これから弊社に必要なのは、AI技術の進歩を始めテクノロジーが劇的に進歩している中で、経験したことのない未知の領域でもおもしろさを感じ飛び込んでいく力がある人だと考えています。 なので採用ページを見ていただいた方に「スマートキャンプがテクノロジーに投資をする意思があること」と「ワクワクやおもしろさを大切にしていること」が伝えたいということでなかなかない趣向にしてほしいとデザインも依頼し、自分たちエンジニアで開発・運用していくことに決めました。 </pre> <p>※IPOを目指すことについての想いは以下の記事をご覧ください。</p> <p><a href="https://note.com/smartcamp_tent/n/nc9ad8887a5af">https://note.com/smartcamp_tent/n/nc9ad8887a5af</a></p> <h2 id="どう実装したか">どう実装したか</h2> <p>さまざまな想いが詰まった採用サイトを実装することになり、実装を担当する自分も技術的チャレンジしながら開発できる!とワクワクしていました。</p> <p>企画担当者から「斬新なデザインを希望」とのメッセージ通りに、今まで見たことのないようなデザインが上がってきました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208123634.png" alt="" width="301" height="155" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>↑一番最初にデザインを見たときのslack上でのやりとり。</p> <p>複雑なアニメーションだったので、実装しなくて済む方法も模索する道はありましたが、せっかくならばチャレンジしてみようと自力で実装することを決めました。</p> <p>今まで凝ったアニメーション作成の経験はなかったので、かなりチャレンジングな開発でしたが、その中で何を考えて実装したのかをご紹介します。</p> <h3 id="開発にあたっての方針">開発にあたっての方針</h3> <p>技術選定は以下を念頭において進めていきました。</p> <h4 id="1-モダンな技術を使う">1. モダンな技術を使う</h4> <ul> <li>完成させることだけ目的であれば、HTML/CSS、素のJavaScript(jQuery)などでも実装は可能でしたが、今回は技術的にチャレンジしたいという思いもあり、モダンな技術を用いて実装することに決めました。</li> </ul> <h4 id="2-独自実装を極力避ける">2. 独自実装を極力避ける</h4> <ul> <li>ライブラリを最大限活用して実装する方針にしました。</li> </ul> <h4 id="3-シンプルな構成を目指す">3. シンプルな構成を目指す</h4> <ul> <li>今後、チームで採用サイトを運用していくにあたり、JavaScriptに不慣れなメンバーがコードに触れる可能性があるため、極力わかりやすい技術を採用することを目指しました。</li> </ul> <h4 id="4-最初から完璧を目指さない">4. 最初から完璧を目指さない</h4> <ul> <li>最初から完璧な状態でリリースすることはせず、まずはリリースを先行させて、コードのリファクタは徐々に行なっていく方針にしました。</li> </ul> <h2 id="技術構成">技術構成</h2> <p>上記を念頭に以下のような技術を使うことにしました。</p> <ul> <li>フレームワーク <ul> <li><p>React.js (Next.js v13 + SG)</p> <p>理由:今までフロントエンドはVue.jsで書くことが多かった弊社でも、最近のプロジェクトでは、React.jsを使用した実績が増えてきており、知見が溜まっていたため、React.jsを採用しました。</p></li> </ul> </li> <li><p>TypesScript</p> <p>理由:言わずもがなですね。</p></li> <li>状態管理 <ul> <li><p>jotai</p> <p>理由: 特に理由はないですが、状態管理には<a href="https://github.com/pmndrs/jotai">jotai</a>を採用しました。<a href="https://github.com/facebookexperimental/Recoil">Recoil</a>とAPIが似ているのですが、更にシンプルにしたような感じで特に不満なく使えています。</p></li> </ul> </li> <li><p>スタイリング</p> <ul> <li><p>CSS Modules + scss</p> <p>理由: 詳しく書きたいので<a href="#%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%ABcss-in-js%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8Fcss-modules%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E7%90%86%E7%94%B1">この項</a>で説明します。</p></li> </ul> </li> <li>アニメーション <ul> <li>framer-motion</li> <li><p>イイ感じのスクロールはframer-motionをラップしたパッケージであるscroller-motionを使用。</p> <p>理由: 最初はcssだけで作ろうと思ったのですが、全く自信がなかったのもあり、良い感じにアニメーションを作れるライブラリを探していたところ、framer-motionを見つけ採用しました。 例えば、アニメーションを使用して要素を表示・非表示したい場合は以下のようにすることで簡単に実現できます。</p></li> </ul> </li> </ul> <pre class="code lang-javascript" data-lang="javascript" data-unlink> &lt;motion.div initial=<span class="synIdentifier">{{</span> opacity: 0 <span class="synIdentifier">}}</span> <span class="synComment">// 初期表示は要素は非表示</span> animate=<span class="synIdentifier">{{</span> opacity: 1 <span class="synIdentifier">}}</span> <span class="synComment">// アニメーションすると要素が表示される</span> exit=<span class="synIdentifier">{{</span> opacity: 0 <span class="synIdentifier">}}</span> <span class="synComment">//要素が非表示になるときに要素を見えなくする</span> /&gt; </pre> <ul> <li>ホスティング <ul> <li>Cloudflare Pages <ul> <li>弊社のドメイン管理はCloudflareを使用しており、親和性があると感じたため。</li> <li>また、Node.jsv18は執筆時点では未対応だったため、v17.9.1で動かしてます。</li> <li>GitHubと簡単に連携できて、Preview環境をすぐに作れるので成果物をスムーズにチェックしてもらえたのでかなり良かったです。</li> </ul> </li> </ul> </li> </ul> <h3 id="スタイリングにCSS-in-JSではなくCSS-Modulesを使用した理由">スタイリングにCSS in JSではなくCSS Modulesを使用した理由</h3> <p>CSS Modulesを採用した主な理由は以下になります。</p> <ul> <li>チームで採用サイトを更新していくことを考えると、JavaScriptを詳しく知らなくてもスタイリングできるほうがよさそう。</li> <li>結局、CSS in JSは、CSSで実現できる以上のことはできないため、あえて採用するモチベーションがないと個人的に感じた。</li> <li>Tailwind CSSも検討したが、独自に書き方を覚える必要があるので、今回は避けた。</li> </ul> <p>また、Next.jsはCSS Modulesをビルトインサポートしていて、特別な設定が不要なため、そのこともCSS Modulesを選択した理由となりました。</p> <p>CSS Modulesについては、Next.js v13でも継続してサポートしていることから、意外と今後も使われ続けるのではないかと思っています。</p> <h2 id="実装で工夫したところ">実装で工夫したところ</h2> <h3 id="作業しやすいディレクトリ構成にする">作業しやすいディレクトリ構成にする</h3> <p>フロントエンド開発では一般的にAtomic Designなどを用いてディレクトリを構成することが多いと思いますが、コンポーネントを分類する手間が個人的に負担だと感じていたため、今回はAtomic Designの採用を見送りました。</p> <p>そこで、ページ固有のものか、共通のものかを明確に分類できるようなディレクトリ構造を採用することに決定しました。</p> <p>以下のような構成にして、作業しやすい構成を目指しました。</p> <pre class="code lang-markdown" data-lang="markdown" data-unlink>app/ ├── assets/ # 画像、フォント、アイコンなどのアセットを格納する ├── components/ # 共通で使用するコンポーネントを格納する ├── constants/ # 全ページ共通で使用する定数を格納する ├── features/ # ページや特定の機能に関するコンポーネントやhooksを格納する │ └── [page名] │ ├── components/ # 特定のページでのみ使うコンポーネントを格納する │ ├── hooks/ # 特定のページでのみ使うhooksを格納する │ ├── constants/ # 特定のページ単位でのみ使う定数を格納する │ ├── index.tsx # エントリーポイント │ └── style.module.scss # index.tsxで使用するスタイルファイル ├── hooks/ # 全ページ共通で使用するhooksを格納する ├── pages/ # 各ページを表すcomponentを格納する ├── utils/ # すべてのユーティリティ関数を格納する └── styles/ # すべてのCSSを格納する </pre> <p>ポイントとしては、featuresというディレクトリを作ってページごとにコンポーネントやhooksを配置したことです。</p> <p>こうすることで、どこでそのコンポーネントが使われるかが明確になり、コードの見通しが良くなることを期待しました。</p> <p>実際にこの構成にしてみて、以下のようなメリットがありました。</p> <h4 id="メリット">メリット</h4> <ul> <li>コンポーネントを分ける負担の軽減 <ul> <li>Atomic DesignであったMoleculesやOrganismsとかの括りがなくなったので、確実にコンポーネントを分類する際の工数は減ったように思えます。</li> </ul> </li> <li>フォルダを行ったり来たりすることが少なくなった。 <ul> <li>ページ単位でcomponentやhooksがまとまってて参照しやすくなり「あれこのhooksどこだっけな」みたいなのがなくなりました。</li> </ul> </li> </ul> <h4 id="デメリット">デメリット</h4> <ul> <li>コンポーネントを分ける負担は相変わらず存在する <ul> <li>Atomic Designよりは負担は減ったのですが、それが共通・ページ固有のコンポーネントなのかを考えながら作業しないといけないのは変わらず存在します。</li> </ul> </li> <li>ディレクトリ一式(componentsとかhooksとか)を都度作らないといけない</li> </ul> <p>また、featuresをどの粒度で切るかを考えるのはかなり重要だと思いました。</p> <p>今回は採用サイト作成だったので、ページ単位でfeaturesディレクトリを作成しましたが、業務アプリケーションの開発では、同じようなコンポーネントが複数のページで使用される可能性があります。 その場合はページ単位ではなく、ドメイン単位(ユーザーとか)でのfeaturesディレクトリを分割した方がより適切な構成になると思っています。</p> <h3 id="コンポーネントのテンプレートを作成するスクリプトを組んで開発効率を上げる">コンポーネントのテンプレートを作成するスクリプトを組んで開発効率を上げる</h3> <p>上記のディレクトリ構成に従って、コンポーネントを量産していく際に、対応するindex.tsxとstyle.module.scssを自動生成できるスクリプトを作成することで、効率化を図りました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> fs <span class="synStatement">from</span> <span class="synConstant">'fs'</span><span class="synStatement">;</span> <span class="synStatement">function</span> capitalizeFirstLetter<span class="synStatement">(</span><span class="synType">string</span>: <span class="synType">string</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synType">string</span>.charAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">)</span>.toUpperCase<span class="synStatement">()</span> + <span class="synType">string</span>.slice<span class="synStatement">(</span><span class="synConstant">1</span><span class="synStatement">);</span> <span class="synIdentifier">}</span> <span class="synStatement">function</span> lowerCaseFirstLetter<span class="synStatement">(</span><span class="synType">string</span>: <span class="synType">string</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synType">string</span>.charAt<span class="synStatement">(</span><span class="synConstant">0</span><span class="synStatement">)</span>.toLowerCase<span class="synStatement">()</span> + <span class="synType">string</span>.slice<span class="synStatement">(</span><span class="synConstant">1</span><span class="synStatement">);</span> <span class="synIdentifier">}</span> <span class="synType">const</span> component <span class="synStatement">=</span> <span class="synStatement">(</span>name: <span class="synType">string</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synConstant">`import style from './style.module.scss';</span> <span class="synConstant">export const </span><span class="synSpecial">${</span>capitalizeFirstLetter(name)<span class="synSpecial">}</span><span class="synConstant"> = () =&gt; {</span> <span class="synConstant"> return (&lt;div className={style.</span><span class="synSpecial">${</span>lowerCaseFirstLetter(name)<span class="synSpecial">}</span><span class="synConstant">}&gt;&lt;/div&gt;)</span> <span class="synConstant">}</span> <span class="synConstant">`</span><span class="synStatement">;</span> <span class="synType">const</span> style <span class="synStatement">=</span> <span class="synStatement">(</span>name: <span class="synType">string</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synConstant">`.</span><span class="synSpecial">${</span>lowerCaseFirstLetter(name)<span class="synSpecial">}</span><span class="synConstant"> {</span> <span class="synConstant">}</span> <span class="synConstant">`</span><span class="synStatement">;</span> <span class="synType">const</span> handler <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> filePath <span class="synStatement">=</span> <span class="synSpecial">process</span>.argv<span class="synIdentifier">[</span><span class="synConstant">2</span><span class="synIdentifier">]</span> <span class="synType">const</span> componentPath <span class="synStatement">=</span> <span class="synConstant">`</span><span class="synSpecial">${process</span>.cwd()<span class="synSpecial">}</span><span class="synConstant">/src/</span><span class="synSpecial">${</span>filePath<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synStatement">;</span> <span class="synType">const</span> fileName <span class="synStatement">=</span> filePath.split<span class="synStatement">(</span><span class="synConstant">&quot;/&quot;</span><span class="synStatement">)</span>.slice<span class="synStatement">(</span><span class="synConstant">-1</span><span class="synStatement">)</span><span class="synIdentifier">[</span><span class="synConstant">0</span><span class="synIdentifier">]</span> <span class="synComment">// フォルダがない場合は再帰的に作成する</span> <span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>fs.existsSync<span class="synStatement">(</span>componentPath<span class="synStatement">))</span> <span class="synIdentifier">{</span> fs.mkdirSync<span class="synStatement">(</span>componentPath<span class="synStatement">,</span> <span class="synIdentifier">{</span> recursive: <span class="synConstant">true</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synIdentifier">}</span> fs.writeFileSync<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">${</span>componentPath<span class="synSpecial">}</span><span class="synConstant">/index.tsx`</span><span class="synStatement">,</span> component<span class="synStatement">(</span>fileName<span class="synStatement">));</span> fs.writeFileSync<span class="synStatement">(</span><span class="synConstant">`</span><span class="synSpecial">${</span>componentPath<span class="synSpecial">}</span><span class="synConstant">/style.module.scss`</span><span class="synStatement">,</span> style<span class="synStatement">(</span>fileName<span class="synStatement">));</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> handler<span class="synStatement">();</span> </pre> <pre class="code bash" data-lang="bash" data-unlink>npm run create:component components/Image</pre> <p>npm scriptsに登録して、こんな感じで実行してあげることですぐにコンポーネントの雛形を作れるようになったので、意外と開発体験が上がってよかったです。</p> <h3 id="next-seoを使ってSEO対策でラクをする">next-seoを使ってSEO対策でラクをする</h3> <p>next-seoという素晴らしいライブラリを利用することで、複雑になりがちなSEO周りを簡単に設定できました。 やったことはシンプルで、プロジェクトルートにnext-seo.config.tsを作成し、必要な設定を記述するだけです。</p> <p>今回は必要最低限の設定に留めましたが、ページ毎に使用する情報を変更するなど、他にももっと細かい設定があるので、詳しくはこちらをチェックしてください。<a href="https://github.com/garmeeh/next-seo">next-seo</a></p> <p>next-seo.config.ts</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> seoConfig <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synComment">// メタタイトル</span> defaultTitle: <span class="synConstant">&quot;スマートキャンプ株式会社 エンジニア採用&quot;</span><span class="synStatement">,</span> <span class="synComment">// メタディスクリプション</span> description: <span class="synConstant">&quot;スマートキャンプ株式会社のエンジニア採用ページです。スマートキャンプはエンジニアが活躍して社会の非効率をなくすテックカンパニーを目指しており、そのための取り組みやチーム体制、採用情報などを公開しています。&quot;</span><span class="synStatement">,</span> <span class="synComment">// faviconの設定</span> additionalLinkTags: <span class="synIdentifier">[</span> <span class="synIdentifier">{</span> rel: <span class="synConstant">&quot;icon&quot;</span><span class="synStatement">,</span> href: <span class="synConstant">&quot;favicon.ico&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">]</span><span class="synStatement">,</span> <span class="synComment">// OGPの設定</span> openGraph: <span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">&quot;website&quot;</span><span class="synStatement">,</span> locale: <span class="synConstant">&quot;ja_JP&quot;</span><span class="synStatement">,</span> url: <span class="synConstant">&quot;https://www.example.com/page&quot;</span><span class="synStatement">,</span> site_name: <span class="synConstant">&quot;スマートキャンプ株式会社 エンジニア採用&quot;</span><span class="synStatement">,</span> description: <span class="synConstant">&quot;スマートキャンプ株式会社のエンジニア採用ページです。スマートキャンプはエンジニアが活躍して社会の非効率をなくすテックカンパニーを目指しており、そのための取り組みやチーム体制、採用情報などを公開しています。&quot;</span><span class="synStatement">,</span> images: <span class="synIdentifier">[</span> <span class="synIdentifier">{</span> url: <span class="synConstant">&quot;/ogp.png&quot;</span><span class="synStatement">,</span> width: <span class="synConstant">1200</span><span class="synStatement">,</span> height: <span class="synConstant">630</span><span class="synStatement">,</span> alt: <span class="synConstant">&quot;Og Image Alt&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">]</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synStatement">export</span> <span class="synStatement">default</span> seoConfig<span class="synStatement">;</span> </pre> <p>_app.tsx</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> DefaultSeo <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'next-seo'</span><span class="synStatement">;</span> <span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synStatement">function</span> App<span class="synStatement">(</span><span class="synIdentifier">{</span> Component<span class="synStatement">,</span> pageProps <span class="synIdentifier">}</span>: AppProps<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> SeoContent <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synStatement">&lt;</span>DefaultSeo <span class="synIdentifier">{</span>...seoConfig<span class="synIdentifier">}</span> /<span class="synStatement">&gt;</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synStatement">&lt;</span>Provider<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>SeoContent/<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>Component <span class="synIdentifier">{</span>...pageProps<span class="synIdentifier">}</span> /<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/Provider<span class="synStatement">&gt;</span> <span class="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <h3 id="headタグ内にAAを置く">headタグ内にAAを置く</h3> <p>エンジニア採用サイトらしいことをやりたいなと思い、headタグ内にアスキーアートを設置することにしました。</p> <p>これも↓の感じで簡単にできたので、やってみると面白いかもしれません。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> logo <span class="synStatement">=</span> <span class="synConstant">`&lt;!-- .=+:</span> <span class="synConstant"> -**##+. :+*##*=: :**+ :**+ :. .******+= -********* :+*##*=. - +**: ***. .******+:</span> <span class="synConstant"> -****####=. =##*==###- :###= *##+ *= .##*===###.:==+##*=== =##*==###. -*. +##* =###. .##*==+##=</span> <span class="synConstant"> -******######= ##* :##+ :####: +###+ -**. .##+ -##= :##+ *## -##- .**= +###= :####. .##+ +##</span> <span class="synConstant"> :+*******########- ###. :####* -####+ .***+ .##+ :##= :##+ *## =***. +####: .#####. .##+ +##.</span> <span class="synConstant"> :+*********##########- :###*+-. :##+*#=.##+##+ =****: .##*---*##. :##+ *## .****+ +##+#* +#**##. .##*..:*#*</span> <span class="synConstant"> .+****###****###########*: :-+*##*. :##+:##*#*.##+ :****** .###*###= :##+ *## +*****: +##.*#*##:+##. :#######+.</span> <span class="synConstant">.=*****####****#############+: -##= :##+ +###:.##+ +**+***- .##+ .##+ :##+ *## :***+*** +##.:###= +##. :##+::.</span> <span class="synConstant">.-+**######****############*=: .++= :##+ :##+ .##= .##+ :***.-*** .##+ -##- :##+ *## -++: ***=.***- *##. +#* +##. :##+</span> <span class="synConstant"> ::::::*****#########+-. *##:.:+##- :##+ :- .##+ ***= ***= .##+ *##. :##+ +##-.:*##: -***. -*** +##. .-. +##. :##+</span> <span class="synConstant"> +****#####*=: .+#####*= :##+ .##+ -***. :***..##+ :##+ :##+ .+#####*: .***- ***= +##. +##. :##+</span> <span class="synConstant"> :=+*#*+- .::. ... ... .... .... ... ... ... .::. ... .... ... ... ...</span> <span class="synConstant"> :-</span> <span class="synConstant">--&gt;`</span> <span class="synType">const</span> RawHtml <span class="synStatement">=</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> html <span class="synStatement">=</span> <span class="synConstant">&quot;&quot;</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span> <span class="synStatement">&lt;</span>script dangerouslySetInnerHTML<span class="synStatement">=</span><span class="synIdentifier">{{</span> __html: <span class="synConstant">`&lt;/script&gt;</span><span class="synSpecial">${</span>html<span class="synSpecial">}</span><span class="synConstant">&lt;script&gt;`</span> <span class="synIdentifier">}}</span> /<span class="synStatement">&gt;</span> <span class="synStatement">);</span> <span class="synComment">//_app.tsx内のHeadに入れてあげる</span> <span class="synStatement">&lt;</span>Head<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>RawHtml html<span class="synStatement">=</span><span class="synIdentifier">{</span>logo<span class="synIdentifier">}</span> /<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/Head<span class="synStatement">&gt;</span> </pre> <p>こんな感じで表示されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208125602.png" alt="" width="998" height="287" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="ハマったところ">ハマったところ</h2> <h3 id="アニメーションなんもわからん">アニメーションなんもわからん</h3> <p>アニメーションを作った経験がほとんどなかったため、どうやって実装したらいいのか全くわかりませんでした。</p> <p>そこで以下の二つの方針で実装を進めることにしました。</p> <ul> <li>CSSを極力使わない</li> <li>とにかく実装イメージをつける</li> </ul> <h4 id="CSSを極力使わない">CSSを極力使わない</h4> <p>CSSでアニメーションの実装を調べていたら時間がかかるので、ライブラリに頼りまくって実装する方向に決めました。</p> <p>今回は、framer-motionを使用しましたが、CSSをあまり書かなくていいのと、コンポーネントにどうアニメーションするかの設定を書くことができるので、直感的に使うことができたので、作業が非常に捗りました。</p> <h4 id="とにかく実装イメージをつける">とにかく実装イメージをつける</h4> <p>幸いにもアニメーションのイメージ動画は頂いていたので、まずとにかくアニメーションを繰り返し見て、流れを整理してから実装していきました。</p> <p>以下のような整理をすると、実際にコードに落とし込んだときも、どの部分の実装しているのが明確になり、スムーズに進めることができました。</p> <ol> <li>画面が表示される <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208123733.png" alt="" width="1200" height="666" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> <li>タイトルが落ちる <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208124851.png" alt="" width="1200" height="665" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> <li>タイトルと背景がぶつかる <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208124931.png" alt="" width="1200" height="667" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> <li>ぶつかった衝撃でタイトルが弾むと同時に背景が傾く <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208125052.png" alt="" width="1200" height="665" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> <li>傾斜によってタイトルが滑る <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208125147.png" alt="" width="1200" height="670" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> <li>マウスを上にスクロールすると背景が上昇し、傾斜が逆になり、同時にタイトルが上に吹き飛ぶ <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208125218.png" alt="" width="1200" height="672" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> <li>かっこいいメッセージが表示される <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208125308.png" alt="" width="1200" height="661" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> </ol> <p>特に難しかったのが、複数の要素を同時に動かして連動しているように見せることです。</p> <p>②では、タイトルをバウンドさせながら傾斜を滑らせる処理がはいるのですが、ここにかなり苦戦しました。(なーんだ簡単じゃんと思われた方は、ぜひ自分の師匠になってください...!)</p> <p>具体的な例を挙げると、framer-motionにはuseAnimationControlsというhooksが提供されていて、任意のタイミングでアニメーションを発火させることができます。</p> <p>そのオプションにどれぐらい要素を落とすかを設定する値をハードコードするなど、かなり強引な実装で回避しました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink> titleControl.start<span class="synStatement">(</span><span class="synIdentifier">{</span> x: <span class="synConstant">&quot;calc(50% - 50vw)&quot;</span><span class="synStatement">,</span> y: <span class="synIdentifier">[</span><span class="synType">null</span><span class="synStatement">,</span> y - animationOptions.bounce<span class="synStatement">,</span> y<span class="synStatement">,</span> y<span class="synStatement">,</span> y<span class="synIdentifier">]</span><span class="synStatement">,</span> <span class="synComment">// animationOptionsはディスプレイに応じてハードコードしてしまっている</span> rotate: <span class="synIdentifier">[</span><span class="synType">null</span><span class="synStatement">,</span> <span class="synConstant">0</span><span class="synStatement">,</span> animationOptions.rotate<span class="synIdentifier">]</span><span class="synStatement">,</span> transition: <span class="synIdentifier">{</span> duration: <span class="synConstant">1.4</span><span class="synStatement">,</span> delay: <span class="synConstant">3</span><span class="synStatement">,</span> ease: <span class="synIdentifier">[</span><span class="synConstant">1</span><span class="synStatement">,</span> <span class="synConstant">1.6</span><span class="synStatement">,</span> <span class="synConstant">0.28</span><span class="synStatement">,</span> <span class="synConstant">0.71</span><span class="synIdentifier">]</span><span class="synStatement">,</span> x: <span class="synIdentifier">{</span> delay: <span class="synConstant">3</span><span class="synStatement">,</span> duration: <span class="synConstant">0.7</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> rotate: <span class="synIdentifier">{</span> delay: <span class="synConstant">2.65</span><span class="synStatement">,</span> duration: <span class="synConstant">1</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> </pre> <p>本当は、どのディスプレイでも同じ表示になるようにしたかったのですが、複雑な計算が必要になりそう &amp; 実装期間との兼ね合いもあり、今回は見送りました。(すぐ分かる方いたら教えて欲しいです...!お待ちしています。)</p> <h3 id="最下部にスクロールするときに画面がチラついてしまう">最下部にスクロールするときに画面がチラついてしまう</h3> <p>特徴としてトップページは最下部から上に向かってスクロールするような仕組みになっています。</p> <p>そのため、画面ロード時にJavaScriptを使用して最下部まで移動する必要があったのですが、どうしても一瞬移動前のパーツが見えてしまう問題がありました。</p> <p>そこで、ローディング画面を2秒間見せ、その間に最下部に移動することで違和感を与えないように変更しました。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208125429.png" alt="" width="1200" height="585" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="開発してみての感想">開発してみての感想</h2> <p>複雑なアニメーションを実装したのは初めてだったので、試行錯誤しながらどう実装していくかを考えるのは非常に勉強になり、純粋に楽しかったです。一部強引な実装をしてしまった部分があったので、そこは反省しつつも、全体的には技術的なチャレンジもできたので、良いものが出来上がったのかなと思っています。</p> <p>ただ、作って終わりではないので、リファクタを継続しながら、もっともっとエンジニアドリブンで開発していける組織を目指していきたいなと開発してみて思いました。</p> <p>ちなみに↓のセクションでは、表示するメッセージを各メンバーに考えてもらったのですが、皆が向かっていきたい方向やビジョンが垣間見えて凄くお気に入りです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230208/20230208125506.png" alt="" width="1200" height="832" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="最後に">最後に</h2> <p>ここまで読んでくださってありがとうございました!</p> <p>今回は、更新が止まっていた弊社の採用サイトをチームで更新していくことを考えつつリニューアルしました。 複雑なアニメーションを実現が特に難しかったですが、試行錯誤した結果、<code>斬新なデザイン</code>を実現できました。</p> <p>そして、採用サイトをリニューアルする意義とは何かを考えてみると、あらためて会社の方向性、会社の制度、伝えたいメッセージを考えるきっかけになって、組織の方向性がより明確に定まることなのかなと思いました。</p> <p>弊社はMISSONに「テクノロジーで社会の非効率を無くす」を掲げていますが、 そういった意味合いでもエンジニアが中心となって採用サイトをリニューアルするという取り組みに携われたことは非常に良かったと個人的には思いました!</p> <p>これからもその想いの実現に向けて頑張っていきたいと思います!</p> <p>今後は以下のアップデートを予定しているので、引き続きチェックしていただけると嬉しいです。</p> <ul> <li>新たに対談ページを追加</li> <li>アニメーションを追加してもっとリッチにする</li> <li>新メンバーの画像とメッセージを追加する</li> </ul> smartcamp BERTとSageMakerによる検索アルゴリズムの実装とデプロイ例の紹介 hatenablog://entry/4207112889955574707 2023-01-20T12:00:00+09:00 2023-01-20T12:59:55+09:00 概要 スマートキャンプでエンジニアをしている佐々木です。 本記事では、自然言語処理モデルを用いて新規サービスを作れないか試行錯誤した話をしようと思います。 今回は精度の良い検索はうまく実装できませんでしたが、機械学習モデルをインフラで動かす流れは学ぶことができました。 実際に実装したコード例ともに紹介します。 概要 経緯 検索の仕組み MLモデルのトレンド 採用した文章の類似度計算のアルゴリズム 類似度計算モデルのデプロイ 実際のコードの紹介 モデルの取得とデプロイ 必要なライブラリのimport 事前学習済みモデルの取得 モデルの保存とS3へのアップロード エンドポイントへのモデルのデプロイ… <h2 id="概要">概要</h2> <p>スマートキャンプでエンジニアをしている佐々木です。</p> <p>本記事では、自然言語処理モデルを用いて新規サービスを作れないか試行錯誤した話をしようと思います。</p> <p>今回は精度の良い検索はうまく実装できませんでしたが、機械学習モデルをインフラで動かす流れは学ぶことができました。</p> <p>実際に実装したコード例ともに紹介します。</p> <ul class="table-of-contents"> <li><a href="#概要">概要</a></li> <li><a href="#経緯">経緯</a></li> <li><a href="#検索の仕組み">検索の仕組み</a></li> <li><a href="#MLモデルのトレンド">MLモデルのトレンド</a></li> <li><a href="#採用した文章の類似度計算のアルゴリズム">採用した文章の類似度計算のアルゴリズム</a></li> <li><a href="#類似度計算モデルのデプロイ">類似度計算モデルのデプロイ</a></li> <li><a href="#実際のコードの紹介">実際のコードの紹介</a><ul> <li><a href="#モデルの取得とデプロイ">モデルの取得とデプロイ</a><ul> <li><a href="#必要なライブラリのimport">必要なライブラリのimport</a></li> <li><a href="#事前学習済みモデルの取得">事前学習済みモデルの取得</a></li> <li><a href="#モデルの保存とS3へのアップロード">モデルの保存とS3へのアップロード</a></li> <li><a href="#エンドポイントへのモデルのデプロイ">エンドポイントへのモデルのデプロイ</a><ul> <li><a href="#通常のインスタンスの場合">通常のインスタンスの場合</a></li> <li><a href="#サーバーレスの場合">サーバーレスの場合</a></li> </ul> </li> <li><a href="#エンドポイントの動作確認">エンドポイントの動作確認</a></li> </ul> </li> <li><a href="#エンドポイントでの推論処理">エンドポイントでの推論処理</a><ul> <li><a href="#ライブラリのimport">ライブラリのimport</a></li> <li><a href="#model_fn">model_fn</a></li> <li><a href="#input_fn">input_fn</a></li> <li><a href="#predict_fn">predict_fn</a></li> <li><a href="#output_fn">output_fn</a></li> </ul> </li> </ul> </li> <li><a href="#学び">学び</a><ul> <li><a href="#SageMakerMLに何でも押し付けない">SageMaker(ML)に何でも押し付けない</a></li> <li><a href="#料金">料金</a></li> </ul> </li> <li><a href="#最後に">最後に</a></li> </ul> <h2 id="経緯">経緯</h2> <p>スマートキャンプでは毎年プロダクトチーム合宿をしています。(<a href="https://tech.smartcamp.co.jp/entry/training-camp-2022">前回の様子</a>)</p> <p>その機会を活用し、普段の業務ではあまり触れない技術の探索をしました。</p> <p>その中で、自然言語処理モデルを用いた検索システムの開発を試みた話をします。</p> <p>なぜ、このシステム開発をすることにしたのか、理由は2つあります。</p> <ol> <li>最新の自然言語処理(NLP)の動向にキャッチアップするきっかけが欲しかった</li> <li>個人で実験するには、学習にお金がかかるので試行錯誤しにくい</li> </ol> <p>1について、まず、個人的に学習したい分野であったことが大きいです。また、実務でも利用できる段階にあるという話は聞いていたので、会社としてもNLP周りの知見があれば使い所はありそうだと感じました。</p> <p>2について、MLを行なううえでネックになるのが大体計算リソースとデータ量ですが、その片方のコストがこの機会なら浮くと思ったことが理由です(膨大なコストがかかるのはNGですが、ある程度はOKです)。</p> <h2 id="検索の仕組み">検索の仕組み</h2> <p>通常、検索エンジンを構築するには次のような手順を踏む必要があります。</p> <ol> <li>インデックスを作成する: Webサイトから文章を抽出し、検索用のインデックスを作成する</li> <li>クローラを作成する: インターネット上のさまざまなWebサイトを巡回し、新しいコンテンツを収集するプログラムを作成する</li> <li>ランキングアルゴリズムを実装する: インデックスを元に、検索クエリに対して適切な結果を返すためのアルゴリズムを実装する</li> </ol> <p>今回は、1の検索用のインデックスに関しては、slackなどのチャットツールやKibelaなどの社内ドキュメントツールから作成しました。</p> <p>また、2のクローラは作成せず、定期的にAPIを介して定期的に検索対象の文章を取得するバッチ処理を作成しました。</p> <p>今回、主に話したいのは3のランキングアルゴリズムについてです。</p> <p>検索クエリと検索対象の文章の類似度を計算することで、検索クエリに対する適切な結果を含む文章を探し出すことができます。</p> <p>そして、その2文章間の類似度計算の前段階としてSentenceBERTの日本語事前学習モデルを用いました。</p> <p>ここについて次から詳しく説明します。</p> <h2 id="MLモデルのトレンド">MLモデルのトレンド</h2> <p>自然言語処理(NLP)の分野では、BERTやGPTといったTransformerモデルがここ数年のトレンドとなっています。</p> <p>先々月にはOpenAIがChatGPTを発表し、弊社でもよくこういう会話をしてみたという投稿が盛んに行われていました。(私は現在でもこちらを活用しています)</p> <p>こちらもTransformerモデルであるGPTを改良したものであることが知られています。</p> <p>このようなモデルを1から作成するには、膨大なデータの用意と豊富な計算資源と運用費が必要になるため、資金力に余裕がある一握りの企業や大学でしか行なうことができません。</p> <p>しかし、それらの組織が作成し公開してくれた事前学習モデルをfine-tuningすることによって、個別のタスクに応用し、資源の少ない組織でも扱うことができます。</p> <h2 id="採用した文章の類似度計算のアルゴリズム">採用した文章の類似度計算のアルゴリズム</h2> <p>その中でもSentenceBERTを用いることが文章の類似度を計算するのに適しています。</p> <p>SentenceBERTは、文章を入力として受け取り、その文章を特徴量ベクトルに変換するTransformerモデルです。これは先のBERTと呼ばれる大規模な言語モデルを拡張して作られており、文章のsemantics(意味的な情報)を正確に捉えることができます。</p> <p>このように文章を「意味的な情報を含んだ特徴量ベクトル」に変換するというのが肝で、このベクトルがどのくらい似ているか調べることにより文章の類似度計算ができます。それ以外にも文章のタグ付けや、文章の分類なども行えます。</p> <p> よく使われる2つのベクトル間の類似度計算にコサイン類似度というものがあります。</p> <p>2つのベクトルが完全一致する場合、1になり、完全に異なる場合に-1になります。</p> <p>具体的には、次の式を使います。</p> <p><img src="https://chart.apis.google.com/chart?cht=tx&chl=%20similarity%20%3D%20%5Cfrac%7Bv_1%20%5Ccdot%20v_2%7D%7B%5Cleft%7Cv_1%5Cright%7C%20%5Ccdot%20%5Cleft%7Cv_2%5Cright%7C%7D" alt=" similarity = \frac{v_1 \cdot v_2}{\left|v_1\right| \cdot \left|v_2\right|}"/></p> <p>ここで、<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20v_1" alt=" v_1"/>、<img src="https://chart.apis.google.com/chart?cht=tx&chl=v_2" alt="v_2"/>は2つのベクトルを表します。<img src="https://chart.apis.google.com/chart?cht=tx&chl=%5Ccdot" alt="\cdot"/>は内積を表す記号です。<img src="https://chart.apis.google.com/chart?cht=tx&chl=%5Cleft%7Cv_1%5Cright%7C" alt="\left|v_1\right|"/>は、ベクトル<img src="https://chart.apis.google.com/chart?cht=tx&chl=v_1" alt="v_1"/>の大きさを表します。<img src="https://chart.apis.google.com/chart?cht=tx&chl=%5Cleft%7Cv_2%5Cright%7C" alt="\left|v_2\right|"/>も同様です。</p> <p>すなわち類似度計算の流れは次のようになります。</p> <ol> <li>検索対象の文章を「意味的な情報を含んだ特徴量ベクトル」に変換する</li> <li>検索クエリを「意味的な情報を含んだ特徴量ベクトル」に変換する</li> <li>2つのベクトルの類似度を計算する</li> <li>それらを類似度が高いもの順に並べる</li> </ol> <p>こちらを元に、検索におけるランキングアルゴリズムを実装し、デプロイしてみました。</p> <p>次にデプロイについて話します。</p> <h2 id="類似度計算モデルのデプロイ">類似度計算モデルのデプロイ</h2> <p>デプロイには、Amazon SageMakerを用いました。Amazon SageMakerはAmazon Web Services(AWS)が提供する機械学習を手軽に行えるサービスです。機械学習では統計的な分析や予測モデルの構築、訓練、デプロイなどさまざまな作業が必要になりますが、それらをすべて管理できます。</p> <p>Amazon SageMakerにはユースケースに応じていくつかに機能が分かれています。例えば、ビジネスアナリスト用のSageMaker Canvasというノーコードインターフェースや、機械学習エンジニア用のSageMaker MLOpsといった機能があります。その中で、今回はAmazon SageMaker Studioというデータサイエンティスト向けのIDEを活用しました。</p> <p>こちらを用いることで、データの前処理からGBDT系のモデルやPyTorchやTensorFlowを用いたディープラーニング系のモデルの開発からデプロイまで一気通貫して行えます。</p> <p>今回はその中でも次の機能を利用しました。</p> <ol> <li>Notebookインスタンス <ul> <li>Jupyter NotebookやJupyter Labが活用できます。</li> </ul> </li> <li>エンドポイント <ul> <li>エンドポイント用のMLインスタンスです。作成したMLモデルをこちらへデプロイしAPIとして推論結果を取得できます。</li> </ul> </li> </ol> <p>1について、データ前処理や、モデルの保存や、推論用のインスタンスを作成するのに用いました。こちらからAmazon S3バケットを作成したり、作成したMLエンドポイントへのアクセスのデモを行なうこともできます。</p> <p>2について、今回は、推論結果として、文章の類似度ランキング情報を取得できるようにしました。(※あとで詳しく紹介しますが、検索として、MLモデルの中で類似度計算をするのは速度の面で好ましくありません。)</p> <p>また、エンドポイント機能には、サーバーレスモードと常駐モードがあります。サーバーレスモードでは、エンドポイントへのアクセスに応じて推論用インスタンスが立てられるため、料金を安く抑えることができます。そのため、開発段階では、サーバーレスモードを活用し、デモとして行なう際には常駐インスタンスを用いました。</p> <p>このように、SageMakerではデータの前処理、可視化などを行なうインスタンスと、学習インスタンス、そして、推論をするインスタンスの3つを必要に応じて扱えるようになっています。</p> <p>すなわち、「学習の際は、GPUを積んだ高性能なインスタンスを短時間使用し、推論の際は、サーバーレス、もしくは低予算のインスタンスを起動させ、開発用のノートブックインスタンスは必要なときのみ起動する」のように使い分けられます。</p> <p><figure class="figure-image figure-image-fotolife" title="用いた構成例"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/smartcamp/20230119/20230119180505.png" width="1200" height="477" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>用いた構成例</figcaption></figure></p> <h2 id="実際のコードの紹介">実際のコードの紹介</h2> <h3 id="モデルの取得とデプロイ">モデルの取得とデプロイ</h3> <p>実際には、学習を挟む必要がありますが、デプロイを行なうだけなら次のようにできます。</p> <h4 id="必要なライブラリのimport">必要なライブラリのimport</h4> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># import libraries</span> <span class="synPreProc">import</span> boto3, re, sys, math, json, os, sagemaker, urllib.request <span class="synPreProc">from</span> sagemaker <span class="synPreProc">import</span> get_execution_role <span class="synPreProc">import</span> numpy <span class="synStatement">as</span> np <span class="synPreProc">import</span> pandas <span class="synStatement">as</span> pd <span class="synPreProc">import</span> matplotlib.pyplot <span class="synStatement">as</span> plt <span class="synPreProc">from</span> IPython.display <span class="synPreProc">import</span> Image <span class="synPreProc">from</span> IPython.display <span class="synPreProc">import</span> display <span class="synPreProc">from</span> tqdm <span class="synPreProc">import</span> tqdm </pre> <pre class="code lang-python" data-lang="python" data-unlink>!pip install transformers[torch] sentencepiece !pip install fugashi ipadic </pre> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> transformers <span class="synPreProc">import</span> BertJapaneseTokenizer, BertModel <span class="synPreProc">import</span> torch </pre> <h4 id="事前学習済みモデルの取得">事前学習済みモデルの取得</h4> <pre class="code lang-python" data-lang="python" data-unlink>MODEL_NAME = <span class="synConstant">&quot;sonoisa/sentence-bert-base-ja-mean-tokens-v2&quot;</span> </pre> <p>今回使用させていただいたモデルはこちらで詳しく紹介されています。</p> <ul> <li><a href="https://qiita.com/sonoisa/items/1df94d0a98cd4f209051">【日本語モデル付き】2020年に自然言語処理をする人にお勧めしたい文ベクトルモデル</a></li> </ul> <pre class="code lang-python" data-lang="python" data-unlink>tokenizer = BertJapaneseTokenizer.from_pretrained(MODEL_NAME) model = BertModel.from_pretrained(MODEL_NAME) </pre> <h4 id="モデルの保存とS3へのアップロード">モデルの保存とS3へのアップロード</h4> <pre class="code lang-python" data-lang="python" data-unlink>SAVED_MODEL_DIR = <span class="synConstant">'transformer'</span> os.makedirs(SAVED_MODEL_DIR, exist_ok=<span class="synIdentifier">True</span>) </pre> <pre class="code lang-python" data-lang="python" data-unlink>tokenizer.save_pretrained(SAVED_MODEL_DIR) model.save_pretrained(SAVED_MODEL_DIR) </pre> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment">#zip the model in tar.gz format</span> !cd transformer &amp;&amp; tar czvf ../model.tar.gz * </pre> <pre class="code lang-python" data-lang="python" data-unlink>sagemaker_session = sagemaker.Session() role = get_execution_role() </pre> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment">#Upload the model to S3</span> inputs = sagemaker_session.upload_data(path=<span class="synConstant">'model.tar.gz'</span>, key_prefix=<span class="synConstant">'model'</span>) inputs </pre> <p>※ このinputsの値にはS3上のモデルへのパスが入っています[1]。</p> <h4 id="エンドポイントへのモデルのデプロイ">エンドポイントへのモデルのデプロイ</h4> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> sagemaker.pytorch <span class="synPreProc">import</span> PyTorch, PyTorchModel <span class="synPreProc">from</span> sagemaker.predictor <span class="synPreProc">import</span> Predictor </pre> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">class</span> <span class="synIdentifier">StringPredictor</span>(Predictor): <span class="synStatement">def</span> <span class="synIdentifier">__init__</span>(self, endpoint_name, sagemaker_session): <span class="synIdentifier">super</span>(StringPredictor, self).__init__(endpoint_name, sagemaker_session, content_type=<span class="synConstant">'text/plain'</span>) </pre> <p>こちらは、エンドポイントへの入力を単一の文字列にしたかったため作成しました。デフォルトではエンドポイントへ渡すデータは、特徴量が入った配列です。</p> <pre class="code lang-python" data-lang="python" data-unlink>pytorch_model = PyTorchModel( model_data = <span class="synConstant">'s3://xxxxx/model/model.tar.gz'</span>, role=role, entry_point =<span class="synConstant">'inference.py'</span>, source_dir = <span class="synConstant">'./code'</span>, py_version = <span class="synConstant">'py3'</span>, framework_version = <span class="synConstant">'1.7.1'</span>, predictor_cls=StringPredictor ) </pre> <p>※ <code>model_data</code>へは[1]の値を入れてください。 ※ <code>inference.py</code>はデプロイされたエンドポイントへのアクセス時に実行される関数が集まったファイルです。のちに紹介します[2]。</p> <h5 id="通常のインスタンスの場合">通常のインスタンスの場合</h5> <p>インスタンスタイプは<a href="https://aws.amazon.com/jp/sagemaker/pricing/">こちら</a>から適宜必要なものを選択してください。 アクセスが多いと自動でスケールしてくれるようです。</p> <pre class="code lang-python" data-lang="python" data-unlink>predictor = pytorch_model.deploy( instance_type=<span class="synConstant">'xxx'</span>, initial_instance_count=<span class="synConstant">1</span>, endpoint_name = <span class="synConstant">'ml-endpoint'</span>, ) </pre> <p>注意点として、デプロイに応じて最低1時間分の料金が請求されるので注意してください。(高額インスタンスで無闇矢鱈にデプロイして削除するを繰り返すと、実際の稼働時間が少なくてもたくさんの請求がきます。)</p> <p>なので、開発時は次のサーバーレスでのデプロイが特におすすめです。</p> <h5 id="サーバーレスの場合">サーバーレスの場合</h5> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> sagemaker.serverless <span class="synPreProc">import</span> ServerlessInferenceConfig </pre> <pre class="code lang-python" data-lang="python" data-unlink>serverless_config = ServerlessInferenceConfig( memory_size_in_mb=<span class="synConstant">5120</span>, max_concurrency=<span class="synConstant">5</span> ) </pre> <pre class="code lang-python" data-lang="python" data-unlink>predictor = pytorch_model.deploy( serverless_inference_config=serverless_config, initial_instance_count=<span class="synConstant">1</span>, endpoint_name = <span class="synConstant">'ml-endpoint'</span> ) </pre> <h4 id="エンドポイントの動作確認">エンドポイントの動作確認</h4> <p>実際にはLambdaからエンドポイントを叩いてレスポンスを取得していますが、ノートブックインスタンスから同様にエンドポイントを叩く際のコードを紹介します。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> json <span class="synPreProc">import</span> boto3 client = boto3.client(<span class="synConstant">'sagemaker-runtime'</span>) </pre> <pre class="code lang-python" data-lang="python" data-unlink>ENDPOINT_NAME = <span class="synConstant">'ml-endpoint'</span> payload = <span class="synConstant">&quot;検索Query&quot;</span> response = client.invoke_endpoint( EndpointName=ENDPOINT_NAME, ContentType=<span class="synConstant">'text/plain'</span>, Accept=<span class="synConstant">'application/json'</span>, Body=payload ) result = json.loads(response[<span class="synConstant">'Body'</span>].read().decode()) <span class="synIdentifier">print</span>(result) </pre> <pre class="code shell" data-lang="shell" data-unlink>[{&#39;articles&#39;: [{&#39;score&#39;: 0.5378279958997847, &#39;note_id&#39;: &#39;xxxxxxxxxxx&#39;, &#39;title&#39;: &#39;記事のタイトル&#39;, &#39;text&#39;: &#39;記事本文&#39;, &#39;timestamp&#39;: &#39;2022-04-15T09:45:35.633Z&#39;},...], &#39;score_sum&#39;: 1.6076672689827232, &#39;profile&#39;: {&#39;username&#39;: &#39;執筆者の名前&#39;, &#39;dm_url&#39;: &#39;slackのDMのURL&#39;, &#39;role&#39;: &#39;役職&#39;, &#39;division&#39;: &#39;部署&#39;}, &#39;slack_id&#39;: &#39;xxxxxxxxx&#39;, &#39;score_ave&#39;: 0.5358890896609078},... ]</pre> <p>※ 実は今回は単純な検索ではなくて、関連する記事をたくさん書いている人をレコメンドするシステムにしているので、1レコードに対して複数の記事が紐づくような構成になっています。</p> <h3 id="エンドポイントでの推論処理">エンドポイントでの推論処理</h3> <p><a href="####%20%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%B8%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">エンドポイントへのモデルのデプロイ</a>の[2]で触れた<code>inference.py</code> について説明します。</p> <p>こちらのファイルでは少なくとも次の関数が必要です。</p> <blockquote><ul> <li>model_fn(model_dir)—モデルを Amazon SageMaker PyTorch モデルサーバーに読み込みます。</li> <li>input_fn(request_body,request_content_type)—データを推論のオブジェクトに逆シリアル化します。</li> <li>predict_fn(input_object,model)—逆シリアル化されたデータは、読み込まれたモデルに対して推論を実行します。</li> <li>output_fn(prediction,content_type)—応答コンテンツタイプに従って推論をシリアル化します。</li> </ul> <p>引用元: <a href="https://aws.amazon.com/jp/blogs/news/building-training-and-deploying-fastai-models-with-amazon-sagemaker/">Amazon SageMaker を使用した fastai モデルの構築、トレーニング、およびデプロイ</a></p></blockquote> <p>実装例を紹介します。</p> <h4 id="ライブラリのimport">ライブラリのimport</h4> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> argparse <span class="synPreProc">import</span> logging <span class="synPreProc">import</span> sagemaker_containers <span class="synPreProc">import</span> requests <span class="synPreProc">import</span> boto3 <span class="synPreProc">import</span> os <span class="synPreProc">import</span> io <span class="synPreProc">import</span> time <span class="synPreProc">from</span> shutil <span class="synPreProc">import</span> unpack_archive <span class="synPreProc">from</span> collections <span class="synPreProc">import</span> defaultdict <span class="synPreProc">import</span> simplejson <span class="synStatement">as</span> json <span class="synPreProc">from</span> scipy.spatial <span class="synPreProc">import</span> distance <span class="synPreProc">import</span> pandas <span class="synStatement">as</span> pd <span class="synPreProc">from</span> tqdm <span class="synPreProc">import</span> tqdm <span class="synPreProc">import</span> torch <span class="synPreProc">from</span> transformers <span class="synPreProc">import</span> BertJapaneseTokenizer, BertModel logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) </pre> <h4 id="model_fn">model_fn</h4> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">def</span> <span class="synIdentifier">model_fn</span>(model_dir): logger.info(<span class="synConstant">'START model_fn'</span>) device = torch.device(<span class="synConstant">&quot;cuda&quot;</span> <span class="synStatement">if</span> torch.cuda.is_available() <span class="synStatement">else</span> <span class="synConstant">&quot;cpu&quot;</span>) tokenizer = BertJapaneseTokenizer.from_pretrained(model_dir) nlp_model = BertModel.from_pretrained(model_dir) nlp_model.eval() nlp_model.to(device) model = {<span class="synConstant">'model'</span>: nlp_model, <span class="synConstant">'tokenizer'</span>: tokenizer} logger.info(<span class="synConstant">'END model_fn'</span>) <span class="synStatement">return</span> model </pre> <h4 id="input_fn">input_fn</h4> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">def</span> <span class="synIdentifier">input_fn</span>(request_body, content_type=<span class="synConstant">'text/plain'</span>): logger.info(<span class="synConstant">'START input_fn'</span>) <span class="synStatement">try</span>: data = [request_body.decode(<span class="synConstant">'utf-8'</span>)] <span class="synStatement">return</span> data <span class="synStatement">except</span>: <span class="synStatement">raise</span> <span class="synType">Exception</span>( <span class="synConstant">'Requested unsupported ContentType in content_type: {}'</span>.format(content_type)) </pre> <h4 id="predict_fn">predict_fn</h4> <p>こちらの<code>predict_fn</code>内でやっていることだけざっくり紹介すると、下記の流れになります。</p> <ol> <li>S3から検索対象の文章の意味ベクトルをダウンロード・解凍</li> <li>検索クエリとの意味ベクトルのコサイン類似度を計算</li> <li>ランキング形式に整形</li> </ol> <p>また、この部分に関しては実際に用いたものではなく、単純に関連度順の記事が10件得られるように改変してます。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">def</span> <span class="synIdentifier">mean_pooling</span>(model_output, attention_mask): token_embeddings = model_output[<span class="synConstant">0</span>] <span class="synComment"># last_hidden_state</span> input_mask_expanded = attention_mask.unsqueeze( -<span class="synConstant">1</span>).expand(token_embeddings.size()).float() sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, <span class="synConstant">1</span>) sum_mask = torch.clamp(input_mask_expanded.sum(<span class="synConstant">1</span>), <span class="synIdentifier">min</span>=<span class="synConstant">1e-9</span>) <span class="synStatement">return</span> sum_embeddings / sum_mask <span class="synStatement">def</span> <span class="synIdentifier">embed_tformer</span>(model, tokenizer, sentences): encoded_input = tokenizer( sentences, padding=<span class="synConstant">&quot;max_length&quot;</span>, truncation=<span class="synIdentifier">True</span>, max_length=<span class="synConstant">256</span>, return_tensors=<span class="synConstant">'pt'</span>) device = torch.device(<span class="synConstant">&quot;cuda&quot;</span> <span class="synStatement">if</span> torch.cuda.is_available() <span class="synStatement">else</span> <span class="synConstant">&quot;cpu&quot;</span>) encoded_input.to(device) <span class="synStatement">with</span> torch.no_grad(): model_output = model(**encoded_input) sentence_embeddings = mean_pooling( model_output, encoded_input[<span class="synConstant">'attention_mask'</span>]) <span class="synStatement">return</span> sentence_embeddings <span class="synStatement">def</span> <span class="synIdentifier">get_result</span>(df, query_embedding, sentence_embeddings_path): cols = [ df.note_id, df.text, ] result = search_documents(query_embedding, cols, sentence_embeddings_path, batch_size=<span class="synConstant">4</span>) <span class="synStatement">return</span> result <span class="synStatement">def</span> <span class="synIdentifier">search_documents</span>(query_embedding, cols, sentence_embeddings_path, batch_size=<span class="synConstant">4</span>): note_id, sentences = cols <span class="synComment"># メモリに負荷がかかるので、バッチごとに意味ベクトルを読み込み、</span> <span class="synComment"># 類似度計算した結果だけを配列に持っておきます</span> s_scores = [] <span class="synStatement">for</span> idx, batch_path <span class="synStatement">in</span> tqdm(<span class="synIdentifier">enumerate</span>(sentence_embeddings_path)): sentence_embeddings = torch.load(batch_path).detach().numpy() <span class="synComment"># 検索クエリとそれぞれの文書に対するcos類似度を一度に計算</span> cos_sim = <span class="synConstant">1</span> - distance.cdist([query_embedding], sentence_embeddings, metric=<span class="synConstant">&quot;cosine&quot;</span>)[<span class="synConstant">0</span>] <span class="synComment"># ここの実装はあまり良くありません(参考程度に)</span> s_scores.extend(<span class="synIdentifier">zip</span>(sentences[idx*batch_size:(idx+<span class="synConstant">1</span>)*batch_size], cos_sim, <span class="synIdentifier">range</span>(idx*batch_size, (idx+<span class="synConstant">1</span>)*batch_size))) sorted_score = <span class="synIdentifier">sorted</span>(s_scores, key=<span class="synStatement">lambda</span> x: x[<span class="synConstant">1</span>], reverse=<span class="synIdentifier">True</span>) n_docs = note_id.shape[<span class="synConstant">0</span>] upper_10 = n_docs//<span class="synConstant">10</span> <span class="synComment"># 例としてスコアの高いもの10件を返すことにします</span> ret_val = [] <span class="synStatement">for</span> s, score, i <span class="synStatement">in</span> sorted_score[:upper_10]: ret_dict = { <span class="synConstant">'score'</span>: score, <span class="synConstant">'note_id'</span>: note_id[i], } ret_val.append(ret_dict) <span class="synStatement">return</span> ret_val <span class="synStatement">def</span> <span class="synIdentifier">read_csv</span>(s3_client, bucket, path): s3_object = s3_client.get_object(Bucket=bucket, Key=path) csv_data = io.BytesIO(s3_object[<span class="synConstant">'Body'</span>].read()) csv_data.seek(<span class="synConstant">0</span>) df = pd.read_csv(csv_data, encoding=<span class="synConstant">'utf8'</span>) <span class="synStatement">return</span> <span class="synStatement">def</span> <span class="synIdentifier">predict_fn</span>(input_object, model): logger.info(<span class="synConstant">'START predict_fn'</span>) start_time = time.time() sentence_embeddings = embed_tformer(model[<span class="synConstant">'model'</span>], model[<span class="synConstant">'tokenizer'</span>], input_object) <span class="synComment"># 文章に紐づいた情報が入ったテーブルの取得</span> s3_client = boto3.client(<span class="synConstant">'s3'</span>) data_bucket = <span class="synConstant">'bucket-name'</span> supplementary_table_path = <span class="synConstant">'sagemaker/data/supplementary-table.csv'</span> supplementary_table = read_csv(s3_client, data_bucket, supplementary_table_path) <span class="synComment"># S3から圧縮された検索対象の意味ベクトルが入ったフォルダを取得(後述しますが、これはアンチパターンだと思います)</span> s3 = boto3.resource(<span class="synConstant">'s3'</span>) bucket = s3.Bucket(data_bucket) semantic_tensor_gz_path = <span class="synConstant">'sagemaker/data/semantic_tensor.tar.gz'</span> bucket.download_file(semantic_tensor_gz_path, <span class="synConstant">'/tmp/semantic_tensor.tar.gz'</span>) unpack_archive(filename=<span class="synConstant">'/tmp/semantic_tensor.tar.gz'</span>, extract_dir=<span class="synConstant">'/tmp/semantic_tensor'</span>, <span class="synIdentifier">format</span>=<span class="synConstant">'gztar'</span>) <span class="synComment"># tmp以下に配置した検索対象文章の意味ベクトルファイルのパス</span> sentence_embeddings_path = <span class="synIdentifier">sorted</span>(<span class="synIdentifier">map</span>( <span class="synStatement">lambda</span> x: os.path.join(<span class="synConstant">'/tmp/semantic_tensor/tensor'</span>, x), os.listdir(<span class="synConstant">'/tmp/semantic_tensor/tensor'</span>))) responce = get_result(supplementary_table, sentence_embeddings[<span class="synConstant">0</span>].tolist(), sentence_embeddings_path) <span class="synIdentifier">print</span>(<span class="synConstant">&quot;--- Inference time: %s seconds ---&quot;</span> % (time.time() - start_time)) <span class="synStatement">return</span> response </pre> <h4 id="output_fn">output_fn</h4> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">def</span> <span class="synIdentifier">output_fn</span>(prediction, accept=<span class="synConstant">'application/json'</span>): logger.info(<span class="synConstant">'START output_fn'</span>) logger.info(f<span class="synConstant">&quot;accept: {accept}&quot;</span>) <span class="synStatement">if</span> accept == <span class="synConstant">'application/json'</span>: output = json.dumps(prediction, ignore_nan=<span class="synIdentifier">True</span>) <span class="synStatement">return</span> output <span class="synStatement">raise</span> <span class="synType">Exception</span>( <span class="synConstant">'Requested unsupported ContentType in Accept: {}'</span>.format(accept)) </pre> <h2 id="学び">学び</h2> <h3 id="SageMakerMLに何でも押し付けない">SageMaker(ML)に何でも押し付けない</h3> <p>これは、そもそも検索エンジンの仕組みを全くわかっていなかったというお恥ずかしい話でもあるのですが、今回のこの類似度検索の手法はbi-encoderといい、文章の意味ベクトルを推論結果として返し、その類似度の比較は別で行なうことによって、高速に検索ができるようになっています。</p> <p>しかしながら、実装の時間も限られており、Amazon ElasticSearch KNN indexなどの別のアーキテクチャを導入し、類似度計算を他で行なう実装ができなかったため、推論内で類似度検索を無理くり実装するという愚行を犯してしまいました。</p> <p>また、検索対象の文章のデータの持ち方も問題がありました。最初は、1文1文の意味ベクトルを1つずつファイルにして、S3にあげようとしていましたが、それだと取得に時間もかかるしお金もかかるということで、すべての検索対象をtar.gzでまとめ、推論インスタンスの中で解凍することで動かしています。</p> <h3 id="料金">料金</h3> <p>推論に関しては、サーバーレスなエンドポイントがあって、試すにはちょうどいいなと感じました。</p> <p>ただ、やはり学習にはそれなりにかかるので、オンプレGPU環境があれば、それを使ってモデルだけ引っ張ってこれるようにしてみるのも実験段階ではアリかもしれません。</p> <h2 id="最後に">最後に</h2> <p>今回のタスクは、私が合宿という機会にチームに提案して受け入れられたものですが、スマートキャンプではテックカンパニー化が現在推進されており、通常業務と並行して新しい技術の開発やビジネスモデルの創出に力を入れています。</p> <p>気になった方は、ぜひ一度カジュアル面談にお越しください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsmartcamp.co.jp%2Frecruit%2Fengineer" title="開発体制とエンジニア|スマートキャンプ株式会社" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://smartcamp.co.jp/recruit/engineer">smartcamp.co.jp</a></cite></p> smartcamp