株式会社ココナラ様、ニフティ株式会社様とフロントエンド合同勉強会を開催しました

レバレジーズ株式会社 システム本部の田中です。
2023/7/11に、ココナラさん、ニフティさんと、フロントエンドをテーマに合同勉強会を開催しました。 以前にエイチームさんと開催した合同勉強会に続きいて今回もconnpassで一般公開し、ココナラさん、ニフティさんを弊社の渋谷本社にお招きしてオフラインでの開催でした!

この記事では、弊社のエンジニアの発表内容を簡単にご紹介したいと思います。

発表内容

弊社からは、私を含めて3人が発表しました。


speakerdeck.com

フロントエンドをリードするエンジニアの育成を目的としたPJで取り組んだことについての発表です。


speakerdeck.com

AWS CognitoとAmplify SDKを使ってフロントエンドの認証処理を爆速で実装したという発表です。


speakerdeck.com

Next3、TypeScript、urqlを使ってmonorepo構成で新規プロダクトの開発に取り組んだことについての発表です。


We are hiring

レバレジーズ株式会社ではフロントエンドエンジニアも、それ以外のエンジニアも幅広く募集中です。 もし今回の勉強会の発表を見てご興味を持っていただけたなら、こちらのページからご応募ください。

勉強会の開催も随時受け付け中です

今回はフロントエンドをテーマに合同勉強会を開催しましたが、レバレジーズはオールインハウスで開発しておりフロントエンド、バックエンド、インフラ、機械学習と幅広い領域をカバーしているので、フロントエンドに限らず様々なテーマで合同勉強会を開催してくれる企業さまを募集中です。ご興味ある企業さまがいらっしゃいましたらこちらまでご連絡いただけると嬉しいです。

新人エンジニアが振り返る、学生・インターン・正社員におけるエンジニアリングの違いと3つの学び

はじめに

こんにちは、2022年新卒の河原です。

突然ですが、皆さんは今までどのような開発体験をされてきましたか?
学生時代からゴリゴリに開発していた人もいれば、全く違う職種から独学やプログラミングスクールで学んでエンジニアリングに携わっている人など、人によってキャリアは多種多様だと思います。

私の場合はこれまで、学生・インターン・正社員としてシステムの開発に携わってきました。まだ社会人になってから1年と少ししか経っておらず、学生時代の経験もある程度は新鮮に覚えています。

この記事では、そんな私が学生・インターン・正社員でのエンジニアリングを通して痛感した『開発体験の差』と、今後に活かしたい『3つの学び』をご紹介したいと思います。

日々の業務に追われ、過去を振り返ることを忘れがちになっている方々が「私もこんな経験があったな」「僕はどう成長してきただろうか」と思い出す・考えてみるきっかけになるような記事になれば嬉しく思います。

自己紹介

まずはじめに、自分の経歴を軽く紹介させていただきます。

学生時代、高専でコンピューターサイエンスを広く浅く学び、情報系の大学・大学院へ進学して情報系に特化したことを学んできました。
研究は、自然言語と音声といった複数の感覚次元を機械学習させるマルチモーダル感情認識を題目として扱っていました。当時はまだ最近話題のGPTの「GPT-2」が論文として発表され、様々なベンチマークを更新し、GeneralなLLMの台頭にワクワクしていた思い出があります。

その後レバレジーズに内定し、「早くから社会で通用する本格的な開発経験を積みたい」と思い、2021年の8月からエンジニアとして早期のインターンを始めます。
インターンでは、未経験のエンジニアが中心で構成された教育目的のチームにて、小さめの社内システムを要件定義・技術選定から設計・開発まで一通り経験させてもらえました。

そして、去年4月に正社員として入社し、現在は正社員として、若年層領域の事業を展開する『ハタラクティブ』や『キャリアチケット』というサービスで、営業職が使用するSFA(Sales Force Automation)開発に携わっています。
今のチームに移動してきて1年ほど経った状態です。

学生と社会人の違い

開発の『目的』

私が学生と社会人(インターン・正社員含む)で感じた開発体験の最も大きな違いは、開発の『目的』です。
どのように目的が違うのでしょうか?私の経験と合わせて、具体的に説明していきたいと思います。

私の学生時代は趣味や学業の一環で、簡単な

  • 2Dゲーム
  • 組み込みシステム
  • Webシステム
  • AIモデル構築

など様々な開発をしていました。
これらは、学業で必要になったり、興味関心が沸けば「開発してみる!」といった具合で、特に大きな目標はなく『開発を楽しむ目的』でいろんな開発をしてみていました。

しかし、どれも『システムが完成して目的を達成すれば、開発も終わり』のような拡張性がなく、モチベーション頼りの単発な開発でした。
結果として、作り上げたシステムとしての品質は高いとは言えず技術的な深掘りもあまりできておらず、また自身の能力的が成長した実感もあまりありませんでした

それに対して、社会人での開発目的は『利益を生み出す目的』へと変わりました。
社会人は全員「ビジネス的な視点」を持つ必要があります。
それはエンジニアも例外ではなく、開発したシステムを含めて、最終的には市場において競合優位性を確保する「サービス」を作り上げて『利益を生み出す目的』に合致したシステムを開発していく必要があります。

観点の多さと責任範囲

目的に付随して『責任』や『観点』にも違いが生まれてきました。
学生時代の開発観点は、システムの機能性ばかりに注目し、「とりあえず動けばOK!」のような考えが先行していた記憶です。

対して、社会人では利益を生み出すために正確で安全なシステムが、迅速にリリースされる必要があります。
そのため、開発観点はさらに多角的になり、

  • コードの可読性
  • 保守性
  • スケーラビリティ
  • アジリティ
  • セキュリティ

などの品質を満たした開発が必要になりました。

責任の変化なども加えて、感じた特徴の違いをまとめると以下になります。

学生 社会人
目的 開発を楽しむため 利益を生み出すため
モチベーション ・興味のあるものを作ってみたい
・学業で必要になったから
・システム、サービスをグロースさせたい
・社会に役立つシステムにしたい
開発物 いろいろ Webシステム
開発期間 完成すれば終了 継続的な利用がされる
開発観点 ・機能性
・正確性
(上記も品質は低い)
・機能性
・正確性
・保守性
・スケーラビリティ
・アジリティ
・セキュリティ
などなど…
責任 基本自分のみにかかる システム、サービス、会社全体へ影響することもあるため大きい

開発に向かう姿勢

ここまでで、違いを紹介してみましたが、決して学生時代の開発が良くなかったわけではありません。

学生時代の自由な開発がなければ、そもそもエンジニアリングに携わってみようというモチベーションも発生していなかったかもしれません。また、どんなシステムでも作ってみることで、わかることはたくさんありました
現在も、興味のある技術があれば、上手く開発できるかどうかはさておき触ってみたりしています。それは、個人開発の自由さと楽しさを知っているからだと思っています。

しかし、私個人としては作成したシステムが利益につながりより多くの役割を果たすことで、社会の役に立っていると実感できる社会人での開発の方が楽しく感じています。
これは、元々アルバイトなどの仕事が好きであった私の性に合っていたのかもしれないですが、システム開発を通して利益を生み出し、サービスをグロースさせるのは社会人ならではの醍醐味だと感じています。

インターンと正社員の違い

続いて、インターンと正社員で感じた開発体験の違いについてです。はじめに、それぞれの業務内容を説明します。

インターン時代は、小さめの社内システムを要件定義・技術選定から設計・開発まで一通り行いました。新規システムであり、かつ規模も小さいことから採用した技術はモダンなものが中心でした。

そして現在は、正社員としてSFA開発に携わっています。 SFAは、営業プロセスの自動化・効率化を行い、業務上の顧客の流入から売上の確定に至るまでの全てのデータを一元化することで、

  • 営業業務の効率向上
  • データ分析による戦略の立案
  • 施策の効果検討

など多方面に役立てられています。 営業・マーケティング・企画・データ戦略室といった関係者が存在し、システム自体も8年ほど前から存在するため、非常に高機能かつ規模も大きめなシステムです。

開発はスクラム体制で、新規機能開発とCakePHP→Laravelへのフレームワークリプレイス作業がメインです。
技術選定やインフラに触れる機会は減りましたが、複雑な要件を把握して適切なソリューションを考案し、設計・開発を通して解決していくことは、良いエンジニアリング経験になっていると感じています。

それぞれの特徴をまとめると以下のようになります。

インターン 正社員
特徴 社内で利用目的の新規システム 約8年前から開発・運用しているSFAシステム
目的・要件 シンプル 複雑で多種多様
規模 小さめ 大きめ
機能 少なめ 多め
関係者 ・主にエンジニア
・関係者数が少ない
・営業、企画、マーケティングなど
・関係者数が多い
技術 モダンなものが中心 新旧混在
担当業務 ・要件定義、技術選定
・設計
・開発
・運用保守業務(フレームワーク移行・インシデント対応・法務対応)
・新規機能設計・開発
開発 FE・BE・インフラ全て BE中心
開発体制 ウォーターフォール スクラム

システムの『役割』の理解の深さ

特徴や規模、言語・技術が全く違う2つですが、ここで感じたことは「システムの役割を理解する必要がある」ということです。

インターン時代は、『新規社内向けシステム』ということもあり、純粋にエンジニアとして開発に必要な知識や経験を培い『Webシステムの開発』に注力することができました。
これは当時いたチームの目的が「手を動かしながら学習して、PDCAを回し、システム開発に必要な基礎を身につける」ことにあったためです。
作成していたシステムを振り返ると、未経験中心のチームであったこともあり、決して品質が高いとは言えませんでしたが、チームの目的を達成するための役割は果たしていたと思っています。
私個人としても、当時未経験でありながら、包括的な開発体験をさせていただけたことはとても良いファーストキャリアになったと思っています。この時の

  • 業務で知識不足を痛感する
  • 知識不足と感じたことを学習する
  • 学習した内容を業務に活かす

というエンジニアにとっては当たり前のサイクルが、開発体験上大きな成長実感をくれました。

対して、現在の『SFA開発』は、サービスの根幹を支える基幹システムです。メリットである機能の多さやデータの一元化の対価として、デメリットは、

  • 8年前から存在する古いコードと新規のコードが混在すること
  • 複雑な要件・関係者が多いといった特徴による開発の鈍化

などが挙げられます。 これは、例えば、新規機能開発にてテーブル構造を変えれば、そのテーブルを分析に利用しているマーケティング職の業務に影響が生じたりします。

そんなSFAの役割は何に当たるでしょうか?
これは明確で、SFAの利点が上げられると思います。開発した機能や蓄積したデータ活用によってビジネス的な利益に結びついているためです。 そのため、『機能性・拡張性・保守性・セキュリティが比重重めに重要視される』反面、相対的に『UI/UXといった使用性は軽視される傾向になる』ことはあります。

システムの役割が明確で、開発に軸があることで、数々の開発タスクの優先度や重要度を振り分け、サービスや組織全体にとってより良い開発を進めることができています。

システム品質特性は様々な観点がありますが、優先・重要視される観点は『システムの役割』によって多面的に変わると思います。皆さんが携わるシステムはどんな特性を重要視されていますでしょうか?

3つの学び

これまでで学生時代から、インターン、正社員での開発体験の違いを説明してきました。

開発のポートフォリオも目的も技術もそれぞれ違いますが、これらを通して痛感した、以下の3つの学びがあります。

  • 新しい技術が最適とは限らない
  • 負債を生む可能性を常に考慮する
  • 基礎が大事

新しい技術が最適とは限らない

インターン時代は、frourioと呼ばれるフルスタックフレームワークで用意したTypeScript・Next.js・Material-UIなどを用いた開発を行っていました。
対してSFAは、BEはphpのフレームワークであるCakePHP・Laravel、FEはscss、jQueryとReact・Graph-ql(JavaScriptとTypeScript)が混在しています。

インターン時代は新しめの技術が中心でしたが、当時は自分やチームのメンバー含めて、Webシステム開発の知識や経験が浅かったこともあり、以下のようなことが起きていました。

  • インターネット上に参考となる事例が少ないことによる開発のつまづき
  • 発展途上の技術は、カバーされてない箇所もあること
  • 扱う人の知識不足で各技術の利点を活かしきれていないこと

あるあるだと感じる人も多いのではないかと思いますが、特に3つ目はエンジニアにとって無くしていきたいことだと思っています。

対して、SFA開発ではどうでしょうか。

  • 現在主にフレームワーク移行中であるLaravelは、非常に高機能で、開発者としての使い勝手の良さがある
  • FEで利用しているjQueryは現代の主流とは言えないですが、複雑な業務的な要件を満たす機能は果たしてくれています

これらを通して僕が感じたことは、新しい技術のみが万能ではないということです。

「古い技術の方が良い!」というわけではありませんが、どんな技術も

  • 扱う開発者のスキル
  • システム(≒サービス)の性質

によってその価値は変わると思っています。

例えば、エンジニアとして、現在のSFAのFEをjQueryからReactなどのモダンなフレームワークへリプレイスさせたいなという気持ちはあります。
しかし、現在のSFAの特徴として、

  • システムが多機能なことから、リプレイスに要する時間は多大なこと
  • その他の観点と比べると、UI/UXといった使用性が軽視される傾向にあること

があります。

そんなシステムにおいて、FEコードのリプレイスを行うビジネス的な価値は果たしてどのくらいあるのでしょうか?これはとても難しい判断だと思います。

この疑問は、営業職やマーケティング職が数値を根拠に仮説を立てて業務を行う職種の働き方を間近で感じられるレバレジーズで培われた視点かもしれません。
エンジニアはしばしば手段と目的を逆にしてしまうこと(「この技術を使いたいから、これをしよう!」など)がありますが、その行動の価値を証明する根拠を正しく策定しなければ、この後に書く「負債」につながると思います。

負債を生む可能性を常に考慮する

1年前の自分が書いたコードを見ると恥ずかしい気持ちになる方は、経験が浅い方であれば誰でもあると思います。
私自身もそれは痛感しており、それは自身が成長しているという実績でもあると言えますが、意図しない将来的な技術的負債を残しているという事実にも繋がります。

現在のSFAは古めのシステムであるため、

  • 設計思想が今と合わなくなっていた
  • 機能拡張を繰り返した

ことが要因で、コード品質が低下していました。また、業務要件が複雑で綺麗な設計が難しい場合も多いです。
多くのテスト駆動開発や開発アーキテクチャの文献でもそう述べられている通り、将来的な技術的負債を軽減させることは重要です。これがシステムの保守性や拡張性に大きく関わってくることは、フレームワーク移行・新規機能拡張の業務を通して痛感しています。
自分のコードの品質が高いかどうかの判断は難しいですが、品質向上のための学習や仕組みによる解消、適切な指摘が行えるチームビルディング・コミュニケーションを怠らないことが必要だと感じています。

また負債とは、特に技術だけではありません。それは、例えばドキュメンテーションや開発ルール、会議など様々です。

  • 作成したはいいものの、保守されていなくて結局使われていない資料
  • なぜこうなっているのかわからないけど、みんなで守っている運用・開発ルール
  • どの立場として参加はしているかわからず、意見のまとまらない会議

開発だけでなく、仕事をする上では様々な将来的な負債が発生している可能性はあり、それは自分だけでなく、チームや組織にも当てはまります。

  • 本当に解決したい課題は何なのか?
  • その機能はスケールできるのか?アジリティは高いのか?
  • 品質のスコープはどこまでなのか?また、それはなぜなのか?

といった目的と手段の整合性や効果を明確にすることが重要であり、それを正しく行うためには、賛同と同程度に建設的な批判ができるチームや組織が理想であると思っています。

これは開発職以外と関わることが多い自社開発企業におけるメリットであるとも感じます。他職種においても、仮説立てやロジカルシンキングを用いて施策を提案しています。
その姿勢から学べることもたくさんあります。

基礎が大事

全ての体験で共通して感じたことですが、結局基礎が最も大事だなと痛感します。

現代は便利なフレームワークやインフラサービスが豊富な時代ではありますが、最終的にはHTMLやCSS、JavaScript、SQLなど基礎的なWebシステムの技術に帰結すると思います。

インターンやSFA開発、どの開発段階においても、基礎となる部分の知識不足の壁にぶつかり、バグや障害発生の問題の根本を掴めず、適切な対応ができない・対応が遅れることがありました。ですが、逆に基礎を理解していると、新しいフレームワークや技術に対する理解のハードルも下がりました。

つまりは、新しい技術の習得や興味を持つこともとても大事ですが、それ以上に基礎的なスキルが必要不可欠だと感じます。学生時代の勉強や開発が、この基礎の学習において役立っていることは言うまでもありません。

私の場合、最近はBE周りに携わることが多いため、データベースそのものの仕組みや、SQL・データ指向のアプリケーションのようなBEに関わることを

  • 達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ
  • 達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ
  • データ指向アプリケーションデザイン - 信頼性、拡張性、保守性の高い分散システム設計の原理

等の書籍やWeb上の事例を用いて、基礎から学びなおして応用まで学習の範囲を広げてみています。データ指向アプリケーションデザインはまだ読書中ではありますが、この3冊は非常に有益であったので、BEに関わる方におすすめしたい書籍です。

最後に

学生・インターン・正社員での開発体験の違いと今後に活かしたい3つの学びをご紹介させていただきました。

今回の記事で、自分の体験を忘れないための備忘録と、今後も意識したい学びを書き留めることにより、将来の自分が過去を振り返るきっかけになったと思います。
今後も学んだことをチームや組織に活かせる社会人になるために、BEを中心としてまだまだコンピュータサイエンスを深く学んでいきたいと思います。

読者の皆様も、日々の業務に追われ、新しいことを立て続けに学んでいる日常であると思います。
そんな日々で、過去を振り返る機会が減ってしまってはいないでしょうか?
自分の過去や現在の開発経験のふりかえり、得られた学びをまとめてみることで新たに見えてくるものや自分の目指す方向性を再確認するきっかけになると思っています。

レバレジーズでは、様々な体験ができるシステム開発を行っています。ぜひ、みなさんも一緒にサービスを作っていきませんか?レバレジーズに少しでも興味を持っていただけた方は、是非下記リンクを覗いてみてください!

(詳しくはこちら

株式会社エイチーム様とフロントエンド合同勉強会を開催しました

レバレジーズ株式会社 テクノロジー戦略室室長の竹下です。
2023/3/24に、エイチームさんと、フロントエンドをテーマに合同勉強会を開催しました。 以前もプレイドさんと合同勉強会を開催したことがあるのですが、前回は2社クローズだったところを今回はconnpassで一般公開して開催しました!
また、弊社の開発拠点が東京の渋谷で、エイチームさんの開発拠点が名古屋なので、リモートでの開催でした。

この記事では、弊社のエンジニアの発表内容を簡単にご紹介したいと思います。

発表内容

弊社からは、4人が発表しました。


speakerdeck.com

AWS Amplify Studioと Figmaを組み合わせて、HTML,CSS + Reactを自動生成し、ランディングページの開発効率を爆上げしたという発表です。


speakerdeck.com

代数的データ型(Algebraic data type, ADT)とGraphQLの組み合わせて、TypeScriptのUnion TypeやGraphQLのコード生成を使用したエラーハンドリングの定義方法のご紹介です。


speakerdeck.com

Nuxt2からNuxt-bridgeを経てNuxt3への移行を完了した奮闘記です。


speakerdeck.com

React + Vercelを組み合わせて、Markdownの記述を補助するツールを短期間で作ってみた!という発表です。


We are hiring

レバレジーズ株式会社ではフロントエンドエンジニアも、それ以外のエンジニアも幅広く募集中です。 もし今回の勉強会の発表を見てご興味を持っていただけたなら、こちらのページからご応募ください。

勉強会の開催も随時受け付け中です

今回はフロントエンドをテーマに合同勉強会を開催しましたが、レバレジーズはオールインハウスで開発しておりフロントエンド、バックエンド、インフラ、機械学習と幅広い領域をカバーしているので、フロントエンドに限らず様々なテーマで合同勉強会を開催してくれる企業さまを募集中です。ご興味ある企業さまがいらっしゃいましたらこちらまでご連絡いただけると嬉しいです。

振る舞い駆動開発

speed

はじめに

レバレジーズ株式会社でフロントエンドエンジニアとして勤めている森山です。

今回は振る舞い駆動開発(Behavior Driven Development、通称BDD)をご紹介します。 ここでの振る舞い駆動開発とはDan North氏が提唱しているBDDの要点を掴み、自チーム流に昇華させたものです。

振る舞い駆動開発を導入して最も良かったことは、アジャイルにおいても開発スピードを保ちつつ一定の品質を担保できたことです。 一定の品質というのは機能適合性が担保されているということです。

また振る舞い駆動開発をすすめていく中で副次的に開発者のプロダクトへの理解度や視座が高まり、より良いプロダクトを作ろうという意思が一人ひとりに芽生えたと感じています。

概要

振る舞い駆動開発の目的は以下です。

  • 開発スピードを維持しながら一定の品質担保

振る舞い駆動開発の手順は以下になります。

  • ユビキタス言語の定義
  • ユーザーストーリーの定義
  • ユースケースの定義
  • テストケースの定義
  • プロダクト実装
  • テスト実装

振る舞い駆動開発ってなに?

そもそも私が振る舞い駆動開発をどう定義しているのか説明します。

観点としては以下になります。

  • 目的はなに?
  • 振る舞い駆動開発ってなに?
  • テスト駆動開発とは何か違うの?
  • 振る舞いってなに?

目的はなに?

開発スピードを維持しながら一定の品質担保です。

振る舞い駆動開発ってなに?

端的に言うと「システムの振る舞いに焦点を当てたテスト駆動開発」です。
一般的に振る舞い駆動開発はテスト駆動開発の1つの流派として捉えられています。

テスト駆動開発とは何が違うの?

テストの対象が異なります。

イメージとしては、テスト駆動開発よりも振る舞い駆動開発の方がテスト粒度が荒く、網羅性が低いです。
振る舞い駆動開発はその名の通りシステムの「振る舞い」のみをテストの対象としています。

一方、テスト対象以外はテスト駆動開発と振る舞い駆動開発もテストファーストな開発手法としては同じです。

テストファーストとある通りテストコードを先に書いてから、
そのテストコードをパスするような実装とリファクタリングを繰り返します。

テストファーストは以下のメリットがあります。
致命的な欠陥を開発終盤に持ち越さない。
開発者がシステム要件を深く理解できる。
デグレードを恐れずリファクタリングができる。

ただテストファーストな開発はメリットだけではないと思っています。

以下のような難点も存在します。

  • プロダクトコードのみを書く場合よりも開発が遅い。
  • テストすべき内容や粒度をどのように判断するのか難しい。
  • 仕様が変更された時の影響が大きい。

上記はウォーターフォール開発であればさほど目立ちません。
そもそも短期間で成果を出すことは求められていないですし、
テストで担保したい基準も最初に決定するはずです。
また手戻りがあるとしても要因は考慮漏れくらいで、外的要因による仕様の変更も少ないはずです。
つまりテスト駆動開発の恩恵を存分に享受できます。

しかし上記の難点は、アジャイルな開発だと顕著になります。
特に課題に上がるのは以下です。

  • スプリント期間内でアウトプットを出す難易度が高い。
  • 機能毎にテストの粒度を判断するコストが高い。
  • 仕様変更による手戻りの影響範囲が大きい。

プロダクトコードと合わせてテストコードも書く必要があるので、短期的に見ると開発スピードは落ちます。
そのためスプリント期間内に動くものをアウトプットする難易度が上がります。

またテストはどの粒度で定義するのかという迷いも生じます。
最初に決めることができればよいですが、それも難しいはずです。
なぜなら、システムの最終ゴールが分からないからです。
そもそも開発当初にシステムの最終ゴールが見えないために、アジャイルな組織体制で市場の変動等に適応していく手段を選択していたりするのかなと思います。
テスト粒度の線引が完璧にできないので、機能毎に都度どこまで網羅的にテストをするのか判断していくことになります。
例えば、「データを描画するだけならこの程度で良いか。」、「決済機能は重大なインシデントに繋がるからカバレッジをできる限り高めよう」などです。
また過去に実装した時は適切な厳密さでテストができていたが、機能拡張で再度判断が必要になることもあります。
スプリントの中で都度テストの厳密さを適切に判断していくというのはコストが高いです。

さらに手戻りが発生した時の影響範囲も大きいです。
改修範囲がプロダクトコード + テストコードになるので当然ではありますが、手戻りの改修がしんどくなるのはアジャイルの良さが失われてしまいます。

とはいえテストファーストで一定の品質を保ちつつ開発者のシステム要件の理解度を上げていきたいので、アジャイルで開発しつつもテスト駆動開発の旨味を享受するためにテスト対象として「振る舞い」に着目します。

振る舞いってなに?

振る舞いとは、ユーザに価値を提供できるシステムの動きです。

極端な例ですがECサイトを考えます。
ユーザーが実現したいことはオンラインで欲しい物を購入できることです。
その過程として、「商品をカートに入れる」はユーザが実現したいことを叶えるためのシステムの動きです。
しかし「ボタンを連打できない制御」が施されていたとしてもそれはユーザーが実現したいことに密接には紐付きません。

「商品をカートに入れる」ことができれば、ボタンの連打制御は無くてもいいです。
あれば嬉しいなくらいです。
端的に言えば機能要件よりも非機能要件を重視したいということになります。

またこれは私の持論ですが、困りごとが解決できる手段があれば多少使い勝手が悪くても嬉しいです。
確かに入念なテストをパスしたソフトウェアは安心感があります。
しかし多少レイアウトが崩れていたり、フォームにバリデーションがない状態になっていたとしても課題を解決できる手段があることの方が圧倒的に嬉しいです。

ソフトウェアが提供する価値の種類にもよるので一概にも言えませんが、アジャイルな開発で求められるソフトウェアは上記がほぼ当てはまるのかなと思っています。
つまりテストをする際にもまずは動くことを担保するテストを優先させたいということです。

ただ単純にテストという言葉に引っ張られると大抵「xxxであるべき」という文言を使って理想状態を定義します。
そしてそれを満たしているかテストコードを書いていきます。
「xxxであるべき」となると各テスト項目はビジネスインパクトに関わらず、どれも等しく満たしている必要があるように見えます。
そしてテストを書いていく中でも「xxができていない。」、「yyは担保しなくていいのか?」と不安にも駆られます。

開発者としても「システムとしてあるべき状態を担保するテストを書け」と言われると際限がないように思えます。
しかし「ユーザが実現したいこと(振る舞い)を担保するテストを書け」と言われると自然と機能要件に着目しそれ以上を闇雲に目指すことは無いはずです。

もう一度、この章の冒頭の言葉を引用すると「振る舞いとは、ユーザに価値を提供できるシステムの動き」です。
この振る舞いにフォーカスすることで、アジャイルで開発していく中でも特に必要な機能をテスト対象として選定できます。

また「振る舞い」はコード上ではシステマチックに定義されます。

振る舞いひとつの構成は以下です。

  • Given: システムがこの状態の時に
  • When: この動作をすると
  • Then: システムはこう振る舞う

詳しくは後述しますが、振る舞いは1つのユーザーの1つのアクションに対してシステムがどう動くのかを定義します。
そしてこの振る舞いが複数集まることでユーザーがどの順番で、何を成し遂げたいのかも見えてきます。

振る舞い駆動開発の手順

振る舞い駆動開発の手順は以下になります。

  • ユビキタス言語の定義
  • ユーザーストーリーの定義
  • ユースケースの定義
  • テストケースの定義
  • プロダクト実装
  • テスト実装

上記の手順が生まれた背景・思考プロセスはざっくり以下です。

  • 一定の品質を保ちながら開発をすすめるためにテストファーストな開発をしたいな。
  • テストファーストな開発をするためには、何をテストすべきか決まっている必要があるな。
  • 何をテストすべきか決めるにはシステムがユーザーに提供したい価値や手段を明確にする必要があるな。
  • ユーザーに提供したい価値を明確にするためにはユーザーストーリー、ユースケースの定義が必要だな。
  • ユーザストーリーやユースケースを開発者やPDM等、誰が読んでも一律同じ解釈ができるためにはユビキタス言語の定義が必要だ。

上記の背景・思考プロセスを逆転させて振る舞い駆動開発の手順となっています。

次章から各手順でどんなことをしているのか説明します。

ユビキタス言語の定義

ユビキタス言語とは

開発者やPDMを含むチーム全体の共通言語のことです。

例えば、会社を表現する時にそのまま「会社」という表現を使うのか、「企業」という表現を使うのかをチームの中で決めておくということです。 またプロジェクト独自の概念や造語も含まれます。

ユビキタス言語を定義する目的

目的は以下の3つです。

  • コードやドキュメントを誰が読んでも一律同じ解釈ができる状態にすること
  • システム上での表記揺れを無くすこと
  • 新規参入者のオンボーディング

ユビキタス言語の定義方法

定義方法は簡単で、テーブル形式で記載しています。
以下がその一例です。

ドキュメント 類義語 プログラム単数 プログラム複数 意味
企業 会社、組織 corporation corporations 利益を得ることを目的にして事業を行う組織体のこと。
引き直し 再登録 paymentReload - サブスクリプションサービスを解約していた人が再契約した時に決済予定が記録されること。

各カラムについて説明します。

ドキュメント

開発者やPdMが会話上やドキュメント上で活用していく表現です。

類義語

端的に言うとNGワードのような扱いです。
定義されたユビキタス言語が「表記ゆれを起こすとしたらこんな表現だろう」という言葉が類義語というカラムに記録されます。
なので類義語を見つけたら開発者やPdM間で指摘し合います。

プログラム(単数/複数)

コード上で変数名やテーブルのカラム名として活用されます。
プログラム上でもユビキタス言語が無いと企業idをフロントエンドではcorporationIdと定義しているのに、
バックエンドではcompanyIdという命名で定義しているということが起きてしまいかねません。

念の為複数形も定義しておくと良いです。
基本的には英語の複数形のルールに従えばOKですが、プロジェクト独自の概念や造語があるとブレそうになる場面があります。
概念として複数形が存在しない時はハイフンを入れています。

意味

その単語の意味です。

ユビキタス言語の恩恵

ユビキタス言語の恩恵としては、コードやドキュメント上で顕著に発揮されています。

特にコード上で、企業idをフロントエンドではcorporationIdと定義しているのに、バックエンドではcompanyIdという命名で定義しているというような場面を稀に見かけます。

これの悪いところは、変数名を変更するという改修が顧客に対して価値を生み出さないところです。
ビジネスインパクトが弱いからという理由で修正が後回しにされていくことが多いです。
しかし個人的にこれは大きな技術的負債です。
見るたびに脳内でcorporationId = companyIdという変換でメモリが食われて本当に考えたいことに100%のリソースを避けないことが永遠に続きます。
たった1つであれば問題ないと考えているとどんどん増えるので見かけたら即直してユビキタス言語を更新する方が良いと思っています。

ユビキタス言語は新規開発者のオンボーディングでも役に立ちます。
ユビキタス言語の一覧表を眺めるだけでそのシステムが何をしたいシステムなのか雰囲気をさっと掴むことができます。

ユーザーストーリーの定義

ユーザーストーリーとは

ユーザーストーリーとは、ユーザーができるようになりたいことをまとめたものです。 システムが何を提供すべきかではなく、ユーザーが何をしたいかに焦点を当てます。

ユーザーストーリーの目的

ユーザーストーリーの目的は開発するシステムに引っ張られずにユーザーに提供したい価値を見える化することです。 ユーザーストーリーがあることで開発者は「振る舞い」を実装・テストする際にそもそもその振る舞いは何を実現したいのか?を見極め自分自身でシステムとして適切な機能なのか有効なテストなのか判断できます。

ユーザーストーリーの構成

ユーザーストーリーは以下の要素から構成されています。

  • アクター
  • エピック
  • ユーザーストーリー

それぞれ何を意味するのか説明します。

アクター
ユーザーストーリーに登場する人物の列挙と各人の説明です。

エピック
ユーザーストーリーの骨子です。
エピックはユーザーストーリーよりも1段抽象度が高い概念です。
複数のユーザーストーリーのまとまりがエピックです。

ユーザーストーリー
ユーザーのニーズと背景が2つセットで表現されます。
ユーザーストーリーは箇条書きで記載しています。

ユーザーストーリーの具体例 例としてフリマサイトのユーザーストーリーを書いてみます。

アクター

  • 出品者
    • 商品の出品者
  • 購入者
    • 商品の購入者

エピック

  • 出品者は、商品を出品する。
  • 購入者は、商品を検索する。
  • 購入者は、商品の詳細を確認する。
  • 購入者は、商品をお気に入り登録する。
  • 購入者は、商品を購入する。

ユーザーストーリー

  • 出品者は、商品を出品する。
    • 出品者は、出品する商品の画像をアップロードする。
      • なぜなら、商品がどんな商品なのか購入者に画像で知らせたいから。
    • 出品者は、出品する商品の価格を設定する。
      • なぜなら、商品をその価格で販売したいから。
    • 出品者は、出品する商品の詳細情報を設定する。
      • なぜなら、商品の画像で伝わらない商品の情報を購入者に伝えたいから。
    • 出品者は、商品の出品を完了する。
      • なぜなら、設定した内容で商品を出品したいから。
    • 出品者は、自身の出品リストを確認する。
      • なぜなら、先ほど出品した商品が出品できているか確認したいから。
      • なぜなら、出品した商品の設定内容に誤りが無いか再確認したいから。
  • 購入者は、商品を検索する。
    • 購入者は、
    • …etc

上記のように箇条書きで羅列する形式でユーザーストーリーを書いています。

ユーザーストーリーの定義方法

定義方法の要点は3点あります。

  • フォーマット
  • 行動はシステムを介さないユーザー視点
  • いきなりユーザーストーリーを書かない

1.フォーマット
フォーマットとしては、以下の体裁を守っています。

  • [アクター]は、xxxする。
    • なぜなら、yyyしたいから。

ユーザーのニーズと背景が必ず一対一もしくは一対多で紐付く形式にするためです。

2.行動はシステムを介さないユーザー視点
商品の出品完了に関しては「商品の出品ボタンをクリックする。」ではなくわざと「商品の出品を完了する。」のようにやや抽象度の高い表現になっています。
些細な違いですが、「出品ボタンをクリックする。」になっているとこの時点で出品ボタンがある前提のUIが設計されます。
「完了する。」にしてUI決定の余地を残しておくことで、もしかしたら上にスワイプすることでリッチなアニメーションと共に出品が完了するようなUIになるかも知れません。

また場合によってはシステムが介入しないユーザーストーリーも存在しえます。
例えばシステム上のデータをプリンターでプリントアウトする。等です。
労務系の業務システムであれば帳票を印刷して行政に提出するようなアナログな動きが業務の中に含まれることもあり得ます。

3.いきなりユーザーストーリーを書かない
ユーザーストーリーをいきなり箇条書きで列挙するとユーザーにはそのニーズ以外を満たす余地を与えないタスク指向のシステムになってしまいやすいです。
事前にユーザーストーリーマッピングやオブジェクト思考UIを絡めて定義していくのが良いです。

ユースケースの定義

ユースケースとは

ユースケースとは、端的に言うとユーザーストーリーを具体化したものです。
ユーザーストーリーを実現するためのユーザーとシステムのインタラクティブな動きを表現するものです。
ユーザーストーリーとの違いは、システムが介入するかどうかです。

ユースケースの目的

ユースケースの目的は、ユーザストーリーを実現するための画面上での遷移や操作を表現することです。

単にシステムがどう振る舞うかのドキュメントではなく、「ユーザーストーリーを実現するための」というところが重要です。
システムの振る舞い1つ1つがユーザーストーリーを実現するために適切な動きなのか、適切な導線設計なのか、開発者からも深ぼって確認することができます。

ユースケースの構成

ユースケースは以下の要素から構成されています。

  • アクター
  • エピック
  • ユースケース

それぞれ何を意味するのか説明します。

アクター
ユースケースに登場する人物の列挙と各人の説明です。

エピック
ユースケースの骨子です。
エピックはユースケースよりも1段、抽象度が高い概念です。
複数のユースケースのまとまりがエピックです。

ユースケース
ユーザーのアクションとシステム挙動が2つセットで表現されます。

ユースケースは箇条書きで記載しています。

ユースケースの具体例

例としてフリマサイトのユースケースを書いてみます。

アクター

  • 出品者
    • 商品の出品者
  • 購入者
    • 商品の購入者
    • システム

エピック

  • 出品者は、商品を出品する。
  • 購入者は、商品を検索する。
  • 購入者は、商品の詳細を確認する。
  • 購入者は、商品をお気に入り登録する。
  • 購入者は、商品を購入する。

ユースケース

  • 出品者は、商品を出品する。
  • 出品者は、フリマアプリにアクセスする。
    • システムは、ログイン画面を表示する。
  • 出品者は、ログイン情報を入力する。
    • システムは、トップページを表示する。
  • 出品者は、「出品ボタン」をクリックする。
    • システムは、出品画面に遷移する。
  • 出品者は、「商品の画像アップロードボタン」をクリックする。
    • システムは、OSのファイルシステムを起動する。
  • 出品者は、出品したい画像を選択する。
    • システムは、アップロードされた画像を表示する。
  • 出品者は、商品の価格に3000と入力する。
    • システムは、商品の価格を3000と表示する。
    • システムは、商品の手数料を300と表示する。
    • …etc
  • 購入者は、商品を検索する。
    • 購入者は、
    • …etc

上記のように箇条書きで羅列する形式でユースケースを書いています。

ユースケースの定義方法

定義方法の要点は3点あります。

  • フォーマット
  • ユーザーストーリーと紐ついていること
  • ケースの網羅は不要

1.フォーマット
フォーマットとしては、以下の体裁を守っています。

  • [アクター]は、xxxをする。
    • システムは、yyyする。

2.ユーザーストーリーと紐ついていること 基本的にユースケースは何かしらのユーザーストーリーと一対一で紐ついています。
また、エピックも同様です。
ユーザーストーリーで定義されたエピックは、ユースケース上にも存在します。

3.ケースの網羅は不要 基本的にユーザーストーリーを実現する手段は細かく見ると複数存在します。
例えば、「出品者は、画像をアップロードする」というユーザーストーリーでも以下の3種類のアクションが考えられます。

  • デバイスのカメラを起動して撮影してアップロード
  • OSのファイルシステムを起動してアップロード
  • ドラッグ&ドロップでアップロード

ユースケースはユーザーストーリーを実現するための詳細な挙動なので3パターンのユースケースが想定できますが、
基本的には最も発生頻度が高そうなケースで1パターン書かれていれば良いです。
ユースケースの主たる目的としてユーザーストーリーという1つのフローを実現するための大まかな動きを確認することだからです。

テストの定義

テストケースとは

ここでのテストケースとは、振る舞いをシステマチックにテストケース化したものです。

振る舞いはユースケースの一つ一つから以下のようなフォーマットで作成されます。

  • Given: システムがこの状態の時に
  • When: この動作をすると
  • Then: システムはこう振る舞う

上記の様にGherkin記法と呼ばれる書き方で記載します。

  • Given = 前提条件
  • When = ユーザーのシステムに対する動作
  • Then = ユーザーの動作に対するシステムの挙動

Given、When、ThenのそれぞれをStepと呼びます。
またこれらのStepの塊をScenarioと呼びます。

そして1つのユースケースが1つのScenarioとしてテストケース化されます。

テストケースの具体例

テストケースの具体例としては以下になります。

Feature: 商品の出品

    Background: 前提条件
        Given ログインしていること

    Scenario: 出品の開始
       Given トップページにアクセスしていること
        When 「出品」ボタンをクリック
        Then 商品の出品画面に遷移していること

    Scenario: 商品価格の入力
       Given 商品の出品画面にアクセスしていること
        When 商品の価格フォームに3000と入力
        Then 商品の価格が3000と表示されていること
           * 商品の手数料が300と表示されていること

    Scenario: 商品詳細の入力...

Gherkinではまず上記の様にFeatureとしてユースケースのエピックに該当する大項目を記載します。
その後、各Scenarioに共通する処理があればBackgroundに表現します。
(BackgroundはJestで言うところのBeforeEachのような役割です。)

そこから各Scenarioを列挙していきます。
振る舞いのテストケースでは1アクションnレスポンスの形式になっています。
なので各ScenarioにおけるWhenは1つになるのが一般的です。
1つのアクションに対して複数の動作が発生することはありえるのでThenは複数になることもあります。

テストの実装

Gherkin記法で定義したテストケースに紐付くテストを実装していきます。
テスティングライブラリとしてCypressを活用しているのでcypress-cucumber-preprocessor(以下、CCP)というライブラリを用いています。

テストの実装自体はCypressを用いた実装になりますが、CCPの関数を間に挟むことでテストケースと紐付くテストが存在していることを機械的に担保できるようになります。

例として、先ほどのユースケースに紐付くテストを書くと以下になります。

import { Given ,When, Then } from '@badeball/cypress-cucumber-preprocessor';

Given("ログインしていること",()=>{
  cy.login();
});

// 出品の開始
Given("トップページにアクセスしていること",()=>{
  cy.visit("http://toppage")
});
When("「出品」ボタンをクリック",()=>{
  cy.get('[data-cy="ExhabitButton"]').click();
});
Then("商品の出品画面に遷移していること",()=>{
  cy.url().should("equal", "http://exhabit");
});

CCPは、各Step(Given、When、Then)の関数を提供してくれています。
テストファイルの中ではGherkin記法で書いた各Stepの文面を第一引数に取ってCCPのStep関数を呼び出します。

各Step関数の中では、Cypressのテスト関数がそのまま使えるので対応するユーザーのアクションを定義していきます。
イメージとしてはCypressのE2EテストをCCPのStepでラップしてカテゴライズしているような感覚です。

テストファイル内での各Step関数の第一引数はGherkinファイルで定義した各Stepの内容と完全一致している必要があります。

CCPのアルゴリズムとしては、まずテストケースの中身を上から順番に読み込みます。
その中でStep(Given、When、Then)があればそのStepに紐付く文面をテストファイルの中から探しに行きます。
テストファイルの中でStep関数の第一引数が完全一致したStepの中身を実行します。
完全一致する文面が存在しなければエラーを返します。

以下は「商品の出品画面に遷移していること」というテストが存在しなかった時のerrorです。

そもそもテストが存在しないときには、紐付くテストを定義するようにサジェストされます。
Gherkin記法で記載したテストケースとそれに対応するテストが実装されていない場合は上記の様にテストがfailするので担保したい振る舞いがテストできていることを機械的に検知できます。

サジェストにあるように”pending”という文字列をStep関数で返却するとテストをスキップすることができます。

Then(("商品の出品画面に遷移していること")=>{
  return "pending";
});

テスト結果ではPendingの件数が表示されます。
なのでプロダクトコード実装前に動作を担保したい振る舞いはテストの中身をPendingで全て仮定義します。
プロダクトコードの実装が進むと同時にテストの中身を実装しPendingのテストを潰していきます。
実装結果にPendingの件数が表示されるので担保したい振る舞いのテストが未実装のまま放置されてしまうことも無いはずです。

振る舞い駆動開発の恩恵

最も実感しやすかった恩恵は実装スピードを落とさずに一定の品質を担保できているところです。

元々は、開発者とPdMの目線があっていないことが原因で手戻りが発生していました。

しかし以下がシステマチックに管理されることでその手戻りは激減しました。

  • システムはどう振る舞うべきか?
  • その振る舞いはユーザーにどんな価値を提供するのか?

またテストの実装漏れも少なくなり、実装段階から品質を担保することができるようになりました。

そして開発者が機能の背景を理解していることで「ユーザーがこのニーズを満たしたいのであればxxの機能があったほうが良いのでは?」等の新たな提案も生まれやすくなりました。

振る舞い駆動開発の副次的な恩恵として開発者がやらされ仕事で実装するのではなく、視座を高くもって開発に取り組むことが実現できたことも良い点でした。

終わりに

いかがでしたでしょうか。 私の所属する事業部は発足してまだまだ成長段階でプロダクトの状況も0-1フェーズにあります。なのでこういったどういう構成で開発を進めるべきかなどを検討・決定していける機会が多いです。この記事をご覧になり、この事業部を一緒に盛り上げてみたいなと思う方が1人でも増えれば嬉しく思います!(詳しくはこちら

レバレジーズで歩む、ユーザーファーストなエンジニアとしてのキャリア

はじめに

こんにちは、レバレジーズ株式会社の髙橋です。2020年に新卒エンジニアとして入社して以降、エンジニア向けQ&Aサイト「teratail」のメンバーとして働いています。最初の2年弱を企画メンバーとして過ごした後に、開発メンバーとしてのキャリアを今日まで進めてきました。

本記事では、新卒から2年弱を開発せずに過ごした結果、

  • どのような経験をし、
  • どのような能力を身につけ、
  • どのようなエンジニアへと成長して行ったのか

を、時系列に沿って以下の7章綴りでお送りいたします。

本記事(私のキャリアパス)を通して、なかなか知る機会のないレバレジーズでのエンジニアキャリアをお伝えし、少しでもレバレジーズの魅力をお伝えできればと思います。

また、本記事では私がキャリアを歩む上で抱えた苦悩も紹介しています。 エンジニアでありながら、開発をせずに過ごす上で、本記事で紹介するような苦悩はつきものと思っております。全てが良いことだけで歩めるキャリアではなかったため、同じキャリアを歩まれている方に向けて、同じ悩みを抱える私を知ってもらうことでエールを届けられたらと思っております。

※ お忙しい方は、後半2章をご覧ください。現在の姿はここに詰まっています。

対象読者

対象読者は以下になります。

  • レバレジーズでのエンジニアキャリアに興味がある方
  • エンジニアとしての仕事に興味がある方
  • エンジニア経験が浅く、今後のエンジニアとしてのキャリアパスが定まっていない方、あるいは、悩んでいる方

入社前から変わらない、私のキャリア目標

私のキャリアの目標は「あったらいいな」を実現できるエンジニアとなり実現していくことです。「あったらいいな」とは、自分だけでなく周囲のさまざまな人が抱えている悩みを解消するものであったり、あるいは、夢や空想上の欲しいと思うものやサービスのことです。私がこのキャリアの目標を立てたときは、ソードアートオンラインという作品のナーヴギアというゲーム機に憧れがあり、こうした憧れを実現できるような人になりたいと思っていました。そのためには、自分を含め、人が持つ「あったらいいな」を分析し、システムとして表現できる能力をつける必要がありました。

こうして、エンジニアとしての就活をはじめ、レバレジーズのエンジニアとなり、キャリアをスタートさせていきます。

エンジニアの課題解決を促進していく。これからのteratailを考える

入社後の研修を経て2020年7月にエンジニアとしてteratailチームに配属されました。最初は、teratailのサービスとシステムの理解を深めるために、過去のドキュメントの確認や簡単なバグの修正を通してオンボーディングを進めました。

当時のteratailは開発と企画の2チームがあり、開発はサービスの運用や機能追加・修正などを担当し、企画はteratailのユーザーを分析し、どのようにサービスを拡大するかを企画することをメインで行っていました。

エンジニアとして配属された私は当初、エンジニアとして開発スキルを伸ばすために開発をすべきであると考えていましたが、ユーザーを分析しサービスを改善・拡大するという企画チームの業務内容を知ったことをきっかけに、自分の目指すエンジニアになるためには企画での経験値を積んだ方が良いのでは? と思い始め、teratailでの企画に興味を持つようになりました。

さて、なぜエンジニアである私が、開発ではなく企画に興味を持ったのでしょうか。

理由は大きく二つあります。

一つは、自身のキャリア目標を踏まえた時に、ユーザーの目線に立って企画を進める経験は重要で、必要な能力であると考えたからです。「あったらいいな」を考えるためには、

  • 誰をターゲットとして
  • どのようなことに悩んでいて
  • 何を必要としていて
  • なぜそれを必要としているのか

といった視点で考察できる必要があると考えており、このような視点で考察する能力を養ううえで、企画することは学びを得られる場であると考えました。

もう一つは、teratailでの企画の仕事はやりたくてもなかなかできない仕事だと考えたからです。teratailでの企画は、機能の改善のための企画だけでなく、サービスの今後を考え、どのように拡大していくのかを決めることまで担っていました。こうしたタスク(特に後者)に入社まもないうちから着手し経験を積めることも、自身のキャリアにとってプラスになると考えました。

そして、企画のタスクに手を挙げ、teratailを利用するユーザーの課題解決に向けた企画を進めることになります。

抱える苦悩、同期の成長とベクトルが違う自分

企画の仕事に携わるようになりしばらくした頃でした。

新卒のエンジニアとして入社した同期が皆、それぞれの配属先でエンジニアとしての成果を出すようになりました。同期との交流が盛んだったこともあり、互いが今どのような開発をしているのかを話す機会も多く、同期の活躍が自然と耳に入ってきます。

最初はこうした話を嬉しく思っていたのですが、次第にある不安を抱えるようになります。

それは、同期と比較した時の自身の「開発力の低さ」と「成果を実感しにくい」ことです。

配属されて間もない頃から企画に取り組んできていたため、開発はほとんどできていませんでした。同期が業務を通して開発力を伸ばす一方で、私の開発力は停滞したままでした。エンジニアであるのに開発力が伸びていない状況は、エンジニアとしての自信をなくす要因となりました。

そして、企画を通して実感できる成果のなさが、さらに自信を無くさせることになりました。開発では、作ったものをリリースすることですぐにエンドユーザーに届き、利用してもらえるため、目に見えた成果を実感しやすいです。一方で企画の仕事は、ただ企画をするだけでは目に見える成果とし現れず、そこから要件定義や開発を通して初めて形となります。また、企画としての完成度が高くなければ開発に動き出すことができず、何度も企画をやり直し、終わりの見えない仕事に成果を実感することはありませんでした。

こうして、同期との開発力の差の広がりや実感できる成果の少なさから、自分の仕事に自信をなくしていきました。

大規模なプロジェクトが動き出す

企画をしている頃、開発チームでは、CakePHPをTypeScriptを使用したマイクロサービスへとリプレイスするプロジェクトが始まります。

※ 本記事では技術的な内容は記載しません。もし興味がありましたら、これに関する記事を公開しておりますので、以下からご覧ください。

tech.leverages.jp tech.leverages.jp

1年以上をかけてリプレイスを行うことになるのですが、私はリプレイスプロジェクトへはほとんど参画しません。これまで通り企画に従事しつつ、開発メンバーが行っていた保守運用の仕事を引き受けることになります。ここで、ようやく開発に少しずつ着手することになりました。

そして、リプレイスプロジェクトも終盤に差し掛かった頃に、サイトの動作テストを担当することになります。

比較的規模の大きいサイトでもあったため、一時的なテストチームを組むことになるのですが、エンジニアは私のみでした。限られた時間でテストを完了するには、非エンジニアの方でもスムーズにテストを進められるテスト項目書を作成する必要がありました。

このテスト項目書の作成の際に、企画で培ったユーザーやサービスを見る力が活きることとなります。それは、

  • サービスを利用するカスタマー
  • テスト項目書を利用するテストチームのメンバー

の2者のユーザーを考えてテスト項目書を作成することです。

サービスを利用するカスタマーが、普段、teratailをどのように利用しているのかを考えることで、サービスとしてあるべき姿をテスト項目として抽出し、 テスト項目書を利用するデバッグチームのメンバーが、どのような属性を持っているのか(どれくらいITリテラシーがあるかなど)を考えることで、テスト手順を書く際に理解しやすく表現することを自然とできるようになっていました。

このような形で企画の時に培った能力を発揮できていたことで、過去に感じられなかった成長を感じることができました。

こうしてデバッグを終え、リプレイスのプロジェクトが一段落しました。  

そしてエンジニアへ

大規模なリプレイスプロジェクトが一段落したころに、本格的に開発をすることになります。バックエンド開発に始まり、次第にフロントエンドに寄っていき、現在に至るまで、フロントエンド開発に携わっています。

開発をする上でも企画の経験が活きる瞬間があり、また、過去に感じていた不安が払拭し始めます

開発メンバーとして開発を始めたての頃は、設計や言語仕様への理解が浅く一つ一つのタスクをこなすだけで精一杯なことが多く、とてもエンジニアとはいえないレベルでした。それでも、実装したものをすぐにユーザーに利用してもらえ、Twitterやサービスのお問い合わせを通して多くのフィードバックを受けられるようになったため、成果を実感しやすくなりました。開発を通したこの体験が、過去に抱えていた不安を次第に解消してくれました。

そして、開発に慣れてからは、協働するメンバーが読みやすいコードとなるように意識したり、作業時間の短縮による時間的な余裕が生まれ、機能追加と併せてリファクタリングできる箇所を併せて修正したり、簡単に直せるバグや機能追加であれば対応することができるようになっています。

ここまでの開発経験を通して、ある程度自走したエンジニアとしてチームに貢献ができるようになりエンジニアとして自信が持てるようになっていきました。

自分で企画し、要件を定義し、開発できるエンジニア

バックエンド・フロントエンド両方の開発に慣れ、開発に自信がつき始めたころ、小規模な機能追加プロジェクトを立ち上げることになりました。このプロジェクトでは、企画、要件定義、プロジェクトマネジメント、開発全てに携わる形で、これまでに得た能力を掛け合わせて遂行しました。

企画や開発ではこれまでに体験し得た能力をそのまま活かし、要件定義やプロジェクトマネジメントでは、企画や開発を通して得たユーザーファーストな思考から、

  • その機能を通してサービスのユーザーにどのような体験を生み出し、実現するか
  • プロジェクトを遂行する上で、いかにしてメンバーの働きやすさを実現するか

を自然と意識して取り組むことができるようになりました。

また、「企画、要件定義、プロジェクトマネジメント、開発全てに携わりながらプロジェクトを遂行する」ことは、私のキャリア目標である「「あったらいいな」を実現できるエンジニアとなり実現する」ことでもありました。

3年目にして、必要な基礎能力を一通り揃えることができ、こうして一つの機能として実現できたことで、「あったらいいな」を実現できるエンジニアとしてのキャリアを始められた気がします。

レバレジーズで歩む、ユーザーファーストなエンジニアとしてのキャリア(まとめ)

これまでのまとめです。ここまでお読みいただいた方、本当にありがとうございました。拙く長い文章で読みにくかったことと思いますが、あと少しだけお付き合いください。

今日に至るまで、多くのことに挑戦させていただきました。サービスの企画を通してユーザー志向を身につけ、設計や開発の際に応用することで、サービスのユーザーや協働するメンバーに価値を提供することができるようになりました。(道中で苦悩もありましたが、、、

とはいえ、まだまだ至らぬ部分も多く成長できる余地が多いため、今後も「あったらいいな」を実現するエンジニアとして、より多くの価値を提供していけるように努めて参ります。

新卒からの3年間をこのように歩み、「あったらいいな」を実現するエンジニアをスタートさせることができたのは、「エンジニア = システム開発をする人」という枠にとらわれずに、さまざまな職域に挑戦できるレバレジーズの文化が大きく関わっています。レバレジーズには、こうした挑戦をするメンバーを支えてくれる上司やメンバーが多くいます。私の他にも開発以外の職域を経験しているエンジニアは多く、それぞれが違ったスキルを持ち、関わり合うことでより大きな価値を生み出しています。

本記事を読んで、レバレジーズのエンジニアとしてのキャリアに興味を持ってくださった方は、ぜひ、採用フォームよりご応募ください。

最後までお読みいただき、ありがとうございました。

https://recruit.jobcan.jp/leveragesrecruit.jobcan.jp

【Architect New World on AWS 2022】ITエンジニア特化型Q&Aサイトteratailを 言語、DB、クラウドなど フルリプレイスした話

レバレジーズ株式会社 テクノロジー戦略室室長 兼 テラテイルチームリーダーの竹下です。

Architect New World on AWS 2022のイベントで発表した際のスライドです。 teratailのリプレイスのインフラ面に注目した発表になっています。

現在レバレジーズでは一緒に働いてくれる仲間を募集しています。ご興味のある方は、以下のリンクからご応募ください。
https://recruit.jobcan.jp/leverages/

テックフェス2022秋レポート React Suspense ~ v18から見えること ~

はじめに

こんにちは。レバレジーズ株式会社の松本です。普段はHRテック事業部で主にフロントエンドの開発を担当しております。 今回の記事では、入社2ヶ月の私がテックフェス全体のレポートと、弊社フロントエンジニアの小林がReact18について発表した「Parce que j’aime la React.」を受けて、追加で調べたり試したりしたことをご紹介したいと思います!

テックフェスとは?

レバレジーズグループに所属するエンジニアを対象に社内で半年に一度行われている技術の祭典です。エンジニアが新しい技術に興味を持ち、勉強をするきっかけを作ることを目的とし、グループ全体の技術力向上を目指します。

11月22日行われたテックフェス2022秋では、「まずは知ろう。次に学ぼう。そして明日使おう」というテーマで開催されました。

テックフェス全体について

当日の流れは、有名な「テスト駆動開発」の日本語訳をされた和田卓人氏をお招きし、「質とスピード - ソフトウェア開発の典型的な誤解を解く(2022年秋版)」の基調講演から始まり、Sessionが2時間弱、LTが30分程、最後にパネルディスカッションが1時間程というタイムスケジュールでした。

オープニング

基調講演(和田卓人氏)

Sessions(30分 × 4名の弊社エンジニア)

LT(5分 × 5名の弊社エンジニア)

パネルディスカッション(1時間 4名の弊社ベテランエンジニア)

パネルディスカッションでは、「ITエンジニア35歳定年説について語る」というテーマで、意見を交わしていました。

・面白かった発表
特に面白かった発表は、個人的には和田卓人氏の発表と最後のパネルディスカッションでした。 和田卓人氏の「質とスピード」についての発表では、スピードのために犠牲にしようとしている品質とは何を指すのか、品質とスピードはトレードオフではないことを経験談やデータに基づいてお話ししていただきました。 スピードおよび質とトレードオフなのは教育、成長、多様性への投資であるというお話もあったので、内部品質(スピードは落とさない)への投資はエンジニアだけでなく事業会社全体にとって重要であると感じました。

また、最後のパネルディスカッションも個人的には非常に面白かったです。 一般的に、「ITエンジニア35歳定年説」というのはネガティブな意味で広まっていると思います。 調べてみると一般的に35歳定年説と言われているのは、学習能力の低下や体力の低下が理由とされているようです。

ただ、今回はそれとは反対にポジティブな印象を受けました。 登壇された方々もキャリアを積むにつれてコードを書くことが減ってきているとのことでしたが、前述したネガティブな理由ではなく、できることが増えたことで業務の幅が広がったことが大きな理由の一つとなっているようでした。 エンジニアに関わらず、年齢に関するトピックではネガティブに語られることが多いイメージだったので、今回はポジティブな話を聞くことができて非常に良かったなと感じました。

・関心があった発表
SessionsやLTの発表も非常に面白かったです。また、個人的には弊社フロントエンジニアの小林によるReactについての発表(30分のSesssion)「Parce que j’aime la React.」は、普段触れている技術領域ということもあり非常に関心がありました。 今回は、この発表から何を知って、何を学んで、何を使ったのかについても共有できればなと思います!

何を知って、何を学んで、何を使ったのか?

・知ったこと
小林の発表ではReact18で提供されるものとして、並行処理レンダリング、Suspense(正しくはアップデート)の紹介がありました。 例えば、以下のような実装だと、FetchComponent内でPromiseがthrowされている場合(サスペンドした場合)は、fallbackで指定されているComponentが返されます。このPromiseが解決された時はFetchComponentが返されます。

function App() {
 return (
     <Suspense fallback={<Spinner />}>
       <FetchComponent />
     </Suspense>
 );
}

また、Reactの日本語公式ドキュメントには、以下のような記述があります。

サスペンスにより、「UI ロード中状態」というものが、React のプログラミングモデルで宣言的に記述可能な主要コンセプトに昇格します。

今まではuseQuery等を利用してloadingの状態を実装者が管理し、それを用いてUIを分岐させるような実装をしていた方も多いと思います。私も同じような実装をしておりました。

この分岐のロジックをReact側が担ってくれるようになるため、我々はloadingの状態やそれを利用した分岐に関心を持つ必要がなくなり、ロード中とロード完了後のUIを宣言するだけで良くなったということだと解釈しています。


・学んだこと
React18で提供される機能の一つとしてTransitionというものがあります。今回はこの機能について学んでみました。

Transitionは更新処理に優先順位をつけたり、中断するための機能です。この機能では、更新処理を「緊急性の高い更新」と「トランジションによる更新(緊急性の低い更新)」で区別しています。緊急性の高い更新は即座にUIを更新します。一方、トランジションによる更新では緊急性の高い更新が発生したら更新処理を中断し、最終的な結果のみを返します。これはSuspenseと合わせて利用することもできるようです。

また、React18について学んでみて面白かったのは、この更新処理に優先度をつけたり更新処理を中断するような機能は ”React Fiber” というアーキテクチャによって実現可能になったということです。

Fiberについて調べていた中で、こちらの記事がとても面白かったです。 ”React Fiber”はReact16から採用されたもので、React16以前とは仮想DOMを更新してUIに反映するアルゴリズムが変更されているとのことです。今までは仮想DOMを上から再帰的に更新していたものが、各ReactElement一つ一つに対して更新処理が用意されるため、非同期的に仮想DOMを更新することが可能になったとのことみたいです。

また、少し古いものですが、こちらにも以下のような記載があります。

The goal of React Fiber is to increase its suitability for areas like animation, layout, and gestures.

“suitability”という単語が使われているように、FiberはUIの動きを絶対的に早くするものではなく適性を高めること、つまりUXの向上を目的にしていると考えられそうです。 この目的を達成するための基盤にFiberが必要で、今回のReact18で導入された機能はまさにこの目的を達成するための具体的な方法の一つと言えそうだなと感じました。


・使ったこと
今回学んだ内容は業務ですぐに使える内容ではないこともあり、ここではサンプルを触ってみた感想のみにしようと思います。公式のReact Docsに載っているuseTransitionのサンプルコードを手元で動かしてみました。 所感としてはSuspenseは比較的理解しやすく感じた一方で、useTransitionは思っていたよりも腰を据えて学んでいく必要があるものだなと感じました。

SuspenseもuseTransitionも背景には並行処理レンダリングがあって、私たちが見えている画面の状態とは別でもう一つ状態をもっていることが少し理解できました。 今後遠くない未来で実務での利用もありそうなので、引き続きインプットを続けていこうと思います。

最後に

今回の記事では、テックフェスで知った内容からReact18について自分なりに学んだことを書かせていただきました。ReactはUX改善のためのアップデートを続けているというのが非常に面白かったです。また、テックフェスは非常に魅力的な発表ばかりで、話を聞いていてとても楽しかったです。 私は今年の9月に中途で入社したばかりですが、このように組織内での技術の共有があるのは、ナレッジ共有の観点からも非常に有意義だと感じています。 皆さんもレバレジーズで一緒に働いてみませんか? レバレジーズに少しでも興味を持っていただけた方は、こちらからエントリーをお願いします!

recruit.jobcan.jp

データサイエンティストの皆さん不安よな。MLOps動きます。

はじめに

こんにちは、テクノロジー戦略室の新城です。私は2022年6月に中途入社し、入社してまだ半年ですがMLOpsの推進という仕事を担当しています。
現在レバレジーズではレバテックダイレクトの求人マッチ度判定のような、レコメンドシステムを多くのサービスで活用しています。
一方で機械学習モデルを本番運用したはいいが、データサイエンティストがモデルの改善フローを回しにくく、再トレーニングしたモデルのデプロイが頻繁にできないといった問題が顕在化してきました。
そこで、今回は社内営業支援ツールにおいてMLOpsを導入し実際にデータサイエンティストがモデルの改善プロセスを回せるようになったその事例を紹介できればと思います。

背景

レバレジーズのMLシステムはMLOps レベル0と言われる状態に近く、データサイエンティストがモデルを作成し、プロダクトへモデルを一度組み込むとその後の改善は行いにくく、プロダクトでのモデル品質も充分に確認できない状態でした。
あるプロダクトでは、図中のModel Servingの部分でプロダクションエンジニアがプロダクトの要求レベルに沿うように前処理の高速化等を行っており、トレーニング時と推論時に前処理が異なっている状態(Training-Serving Skewが起こっている状態)でモデルの更新に双方のエンジニアに工数がかかってしまう状況でした。
このような状況では、データサイエンティストは他のエンジニアの手を借りないとデプロイが出来ないという制約から、軽微なモデルの改善を手軽に行うことができません。加えて、レコメンドにおけるMLモデルはその品質を保つために、定期的に再学習を行い、トレンドを学習する必要があります。しかし、再学習時のデプロイ毎に双方のエンジニアに工数がかかる状態は好ましくありません。
そこで今回はデータサイエンティストが主導してモデルの改善・デプロイを行うことができる環境を整えることで、プロダクトエンジニアの工数を削減し、モデルの改善に積極的に取り組めるような仕組みづくりを目指しました。

出典: https://cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning?hl=ja#mlops_level_0_manual_process

MLプラットフォームの導入

上記の状態を改善するため、今回AWS SageMakerやGCP Vertex AIに代表されるようなMLプラットフォームの導入を検討しSageMakerを採用することになりました。データの分析から学習、モデルデプロイを一手に担ってくれるプラットフォームを導入することでデータサイエンティスト主導の機械学習モデル開発・デプロイができるようになることが狙いです。
またMLパイプラインを用途ごとに設けることでMLOps レベル1の状態に少しでも近づけることを目標としています。

構成図

今回はAWS SageMakerとStep Functions DataScience SDKを用いてMLパイプラインを構築しました。グレーアウトしている部分は今後の実装を予定している部分です。
図中の棒人間がいる箇所をデータサイエンティストに実装してもらい、後はパイプラインを実行していけばプロダクトに使用する図中左下のエンドポイントまでデプロイが完了します。
全体構成だけではわかりにくいと思うので各パイプラインの役割をそれぞれ見ていければと思います。

1. データ取り込みパイプライン

まずはデータ取り込み処理部分について順を追って説明をしていきます。

1.1. Event Bridgeの起動
学習・推論に使用するデータをBigQueryから定期的に読み込むためにEvent BrigeでStep Functionsの起動をしています。

1.2. データ取得と前処理
レバレジーズではデータウェアハウスとしてBigQueryを採用しており、データサイエンティストはここから任意のデータを取得・前処理・データフレーム化して使用します。
少しややこしいのですが、ここでの前処理と後述する学習パイプラインの前処理は異なっており、統計的な特徴量(平均値や分散値など)等の推論毎に作成する必要のない特徴量を予め作成し、推論時にこれらのDataFrameを予め読み込んでおくことで推論処理の高速化を図っています。

1.3. データの品質チェック(将来実装)
取得するデータの特徴量ドリフト検知といった品質チェックや定期実行毎の特徴量の推移等を出力できるとデータサイエンティストに気づきを与えることができるのではないかと検討中です。

2. 学習パイプライン

S3に用意されたデータを元に学習から評価までを行っています。Lambda Stepではモデルとその評価値をSageMaker Model Registryへ登録しています。
ここでの処理は一般的に紹介されている学習パイプラインとほぼ同等と思って頂けたらと思います。

3. デプロイパイプライン

ModelRegistryへ登録されたモデルをデプロイするパイプラインです。

3.1. Model情報取得(Lambda)
Step Functionsへの入力となるモデルパッケージ情報とバージョン情報を元にModel Registryから必要な情報を取り出しています

3.2. Endpoint新規作成・更新判定
このLambdaは単にEndpointの新規作成か更新かを判定するものです。入力されたEndpointNameを元に判定しています。

3.3. 推論Endpointの立ち上げ
Endpoint立ち上げ時に前述のデータ取り込みパイプラインで作成したDataFrameを読み込んでおり、推論時に都度作成する必要のない特徴量はこちらを活用しています。
またデータキャプチャ設定で保存された推論結果をS3からBigQueryへ日次で転送しておりデータサイエンティストが推論結果をBigQuery上で解析できるようにしています。

3.4. エンドポイントに対しての自動テスト(未実装)
データサイエンティストがプロダクトに関わるエンドポイントの実装とデプロイを行うということで、心理的安全性の担保ためにここの実装は必要だと考えています。

改善できたこと

データサイエンティスト主導のモデル改善フローの実行

データサイエンティストが用意したパイプライン実行用のPython Notebookを使用して、モデルの改善を任意のタイミングでより容易に行うことができました。実際にSageMakerを導入してから短期間で複数回モデル変更のデプロイをデータサイエンティスト主導で行っておりその効果はあったのかなと思います。
一方で実行が手動なのは変わりなく、どのファイルを変更したらどのパイプラインを実行すれば良いといったフローはかなり煩雑なので、現在は変更に対応するパイプラインが自動実行されるといった自動化に着手中です。

Training-Serving Skewの改善

こちらのルールの通り、コードを再利用する実装を事前にデータサイエンティストと進めたことで、基本的にはデータサイエンティストの変更のみでモデルのデプロイを行う仕組みを整えることができました。一方でエンドポイントのレスポンス速度は常に保証する必要があるため、前述した自動テストでのチェックや後述の前処理高速化に取り組んでいきたいと考えています。

試したが今回は盛り込めなかったもの

前処理の高速化(ApachBeam等の導入)

今回はリアルタイム推論だったのですが、比較的前処理の量も少なく、特に工夫せずともプロダクト要件を満たす処理時間に収めることができたため学習コストの観点から導入はしませんでした。
但し、今後時間のかかる前処理やストリーミングで捌かないといけないデータが必要となった際にはApachBeamといったバッチとストリーム処理が可能なデータ処理パイプラインを導入しなければいけないと考えています。導入の際には推論時のみだけでなく、学習側もきちんと共通化できるように考えて実装していきたいです。

Feature Storeの導入

S3へ特徴量データを保存している部分をFeature Storeに置き換えたかったのですが、定期実行時にFeature Storeへ書き込むと今回のデータ量では書き込み量が多く、想定以上の料金がかかってしまうことがわかりました。データの差分のみを登録する仕組みを作る案も出ましたが、定期実行ごとにDataFrameを作りバージョン管理できた方が便利なため今回は採用を見送っています。上記の前処理高速化と関連してストリーミングデータを扱う際にはまた必要だと考えています。

今後の施策

MLOps レベル1への移行やより良いモデル開発体験を目指して以下のことを行っていきたいです。

  • パイプラインや運用プロセスの自動化
    • データサイエンティストがGitHub上で改修ブランチを切ったら前処理・学習・評価(ABテスト)まで自動で行われる仕組みを目指したい
  • モデル評価機構の確立
    • 特徴量や特徴量寄与度に違いがないか監視(ドリフト検知)
    • モデルレジストリへ登録しているモデルへのリアルタイム評価の反映
      • もしこのモデルがデプロイされたらこのくらいのパフォーマンスを出すだろうといった予測を与えられたら
  • 前処理高速化やFeature Storeの導入
    • 今回は必要性がなかったため断念したが、今後に向けて実装を検討していきたい

最後に

今回の記事ではSageMakerを導入しデータサイエンティストがモデルの改善プロセスを回せるようになったその事例を紹介しました。MLOpsの導入と言い切るにはまだまだ改善すべきことが多いですが、まずは初めの一歩を踏み出すことができたのかなと思います。

また、これらの実装には入社してまだ半年の私を支えてくれた、データ戦略室のデータサイエンティストの理解と協力無くしては実現できませんでした。 このように、レバレジーズには年次や経験を問わず、主体的に取り組める環境が整っています。ご興味のある方は、以下のリンクから是非ご応募ください!

recruit.jobcan.jp

SREから始める、関係者全員の幸福

はじめに

 レバレジーズ株式会社 テクノロジー戦略室室長の竹下です。レバレジーズでは現在SRE(Site Reliability Engineering)チームを立ち上げ、全社を巻き込んでSRE化を推進しています。
 SREを提唱したGoogleを筆頭に、日本でもメルカリやサイバーエージェントなど数多くのテック企業がSREチームを立ち上げ、インフラ基盤を維持管理するだけではなく、サイトの信頼性を担保することでサービス価値を最大化しようとしています。レバレジーズでも、1年ほど前からSREチームを立ち上げ、インフラからもお客様に最高のサービスを届ける事を目指しています。
 この記事ではSREチーム立ち上げの背景と、レバレジーズがどのように顧客に寄り添ったSREチームを目指しているか、そしてSREで使用している技術のご紹介をしていきたいと思います。

背景

 レバレジーズ株式会社では、チームをサービス単位で分けており、それぞれが大きな裁量を持ってサービス運営をしています。 これまでは、チーム毎にインフラ担当者を置きサービスに合わせた構築、運用を行ってきていました。そのため、チームによって使用技術やインフラ構成が異なっており、チームの統廃合や異動、退職などによりインフラ担当が変わる際には、高い学習コストがかかっていました。また、担当者に寄ってインフラのレベルもまちまちなので、CI/CDやIaCを組み込んで自動化出来ているチームもあれば、手作業でインフラ変更をしているためインフラ作業で手一杯になっているチームもあったりしました。このままでは、今後事業分野も拡大してより多くのチームが出来た時に、チーム間の流動性と、全体の効率が低下してしまい組織規模の拡大のブレーキとなることが予期され、また、レバレジーズではマイクロサービス化を進めておりその観点からもインフラ周りの効率化が必要となってきていました。
 そこで、インフラ関連の知識を集約し標準化を図るとともに全体最適を行うため、プロジェクト横断チームとして、SREチームを立ち上げることになりました。

SREチームのミッション

 SREチームのミッションとして
SRE化を通して、Developer Experienceの改善、事業の拡大への対応、お客様に信頼されるサービスの提供を実現する
を掲げています。
 ただのインフラ屋としてタスクをこなすだけでは、レバレジーズのミッションである「関係者全員の幸福を追求する」ことの達成は難しく、また、インフラは顧客から少し離れた所で開発を行うため、誰のためのサービスなのかの意識が薄くなりがちです。そのため弊社のSREチームでは、インフラの上で動くサービスの更にその先に顧客がいることを忘れず、常にチームとしても個人としても成長を続けられることを目指し上記のミッションを設定しています。

目指すSRE組織

 上記のミッションを達成するにあたってどのようなSREチームを構築すればよいか調べましたが、いくつかの型が紹介されていたり、他社事例を調べても各社それぞれSREチームの形は違っていました。そのため、レバレジーズの文化にもっとも適したSREの形を模索し、Embedded SREとEvangelist SREのハイブリッドを目指すことにしました。

Embedded SREとは

 レバレジーズのエンジニアの使命は、世の中に価値あるサービスを届けるところにあると考えています。最大限の価値を届けるためには、Embedded SREは、ドメイン知識を持ち事業を理解した人材でないといけません。そのため、Embedded SREは開発チーム内のアプリエンジニアから選出してもらい、サービスのインフラ運営、監視、トイル撲滅、SLOの策定など、SREの実務を行いDevOpsの実現をしてもらいたいと考えています。

Evangelist SREとは

 しかし、Embedded SREだけではチーム内に知識が閉じてこれまでと同様に局所最適に陥ってしまう可能性もあります。また、アプリエンジニアに任せることになるため技術レベルもチーム毎に違って来てしまいます。そこを補うためにEvengelist SREを置き、SREプラットフォームの構築、SREのトレーニング、ノウハウやベストプラクティスを各チームから吸い上げ、全社に浸透させるなどの動きをしていきます。今回立ち上げたSREチームは、このEvangelist SREの集団になります。

使用技術

 SREチームで標準となる技術を策定し全社に広めることで、ノウハウやベストプラクティスの共有を効率的に行おうとしています。現在使っている or これから使おうと考えている主要な技術を紹介したいと思います。

Infrastructure as a Code (IaC)

  • CDK
  • Terraform
  • Ansible

 AWSでの新規のインフラ構築には主にCDKを利用しています。既存のリソースのインポートだったり、DataDogやGCPの場合はTerraformを利用しています。新しく立ち上げるサービスやリプレース時には、CDK,Terraformだけで構築できるようにしていますが、既存のEC2などでセットアップが必要な場合にはAnsibleを利用しています。

監視

  • Datadog
  • Datadog RUM(Real User Monitoring)
  • Datadog APM(Application Parformance Monitoring)

 メトリックスやログは現在Datadogへ集約を進めています。また、DatadogもダッシュボードをIaC化し、全てのサービスで標準的な監視をすぐに始められるようにしています。またDatadog RUMやAPMを導入により、より詳細なメトリックスの取得し、効率的なパフォーマンスチューニングを目指しています。

サーバレス

  • AWS Fargate
  • Cloud Run
  • Docker
  • kubernetes

 新しく立ち上げるサービスは全てサーバーレス化を進めています。SREチームの前身になるインフラチームの際には人数が少なくkubernetesクラスターの維持管理が難しと考えて、フルマネージドサービスである、AWS Fargateを採用しています。GCPではCloud Runを主に採用しています。ただ、SREチームの人数も増え管理を、より自由な構成を行うためにkubernetesの導入検討も続けてはいます。

マイクロサービス

  • gRPC
  • GraphQL
  • AWS App Mesh

 サーバレス化とともにマイクロサービス化も進めています。サーバー間の通信はgRPCを主に採用しており、一部のサービスではGraphQLも利用しています。また、サービス数も増えてきたため現在はAWSのフルマネージドのService MeshであるAWS App Meshの導入も進めています。

最後に

 いかがでしたでしょうか?SREチームもシステムのその先にいるユーザーを視野に入れ、より良いユーザー体験を届けるために日々奮闘しています。また、ただ闇雲に頑張るのではなく、適切な技術を選定しかつ組織に浸透させるところまでを考えながらSRE化を進めています。

 一緒にSREのチームひいてはSREの文化を創っていきたいという方や、今はまだインフラ知識が無いけどフルスタックエンジニアになりたいという意欲のある仲間を募集中です。ご興味のある方は、以下のリンクからご応募ください。

 一緒にSREのチームひいてはSREの文化を創っていきたいという方や、今はまだインフラ知識が無いけどフルスタックエンジニアになって行きたいという意欲のある仲間を募集中です。ご興味のある方は、以下のリンクからご応募ください。 https://recruit.jobcan.jp/leverages/

おまけ

 カバー画像にもしているのですが、AIに「Site Reliability Engineering」で描いてもらったらなんとなくそれっぽいのになりました

【Next.js】Server Side RenderingでABテスト(Google Optimize)を実装した話

はじめに

初めまして、レバレジーズ株式会社の小林といいます。
私は2022年4月に開発未経験でエンジニアとして中途入社し、teratailというサービスのフロントエンド開発とマーケティング周りの業務に携わっています。

teratailでは、昨年末にリプレイスを行ったこともあり分析基盤がきちんと整備されておらず、各種分析ツール(Google Analytics4やBigQuery、Google Optimizeなど)を導入する必要がありました。

中でも今回は、導入に特に苦労した「Next.jsのServer Side RenderingでABテストを実装した方法」について紹介したいと思います。

使用技術

今回の実装で使用する主な技術は以下の通りです。

  • Next.js
  • TypeScript
  • Recoil, React Context (状態管理)
  • Google Optimize (ABテスト)

背景

■なぜこの記事を書いたのか

  • 参考文献が少なかった
    通常のReactアプリ(SPA)のOptimize導入については参考文献が豊富ですが、Next.jsのServer Side Rendering時などにサーバー側からABテストを制御する方法に関しては文献が多くありませんでした。公式リファレンスを読んでも「???」な部分が多くあり、予想以上に詰まったため、同じような苦労をされている方は多いのではないかと思い記事にしました。

  • 今回の苦労を残しておきたかった
    このタスクを担当したときは入社3〜4ヶ月目の頃でしたが、開発経験ほぼゼロで入社した私にとっては難易度が高かったです。入社早々に今回のような貴重な経験ができたので、この苦労をここぞとばかりに共有したく、記事にしました。

■なぜ分析基盤を整備するのか

プロダクト自体は月間PV数が1,000万を超えており十分なデータが存在するにも関わらず、分析基盤が整っていないために、データドリブンに施策の立案や効果の検証が行えていないという課題がありました。 今後teratailが更にグロースしていく上で、データドリブンに意思決定を行える体制を作ることが必須だと考え、そのための分析基盤構築の一環としてABテスト環境を整えることとなりました。

■対象の読者

特に以下のような方々の参考となれば嬉しいです。

  • Next.js(SSR)でABテストを実装したいエンジニア
  • フロントエンドエンジニア
  • ABテスト担当者、マーケター等

前提知識

■Next.jsについて

プリレンダリング(ページにアクセスがあったときにサーバー側でJavaScriptを実行してHTMLを生成する)機能の一つで、SEO対策や高速なコンテンツ表示が可能となります。

  • 通常のReactアプリ(SPA)との違い
    通常のReactアプリ(Single Page Application)はブラウザ側でHTMLを生成するのに対し、SSRではサーバー側でHTMLを生成します。後述しますが、サーバー側で生成されるという点が、本記事の大きなポイントとなります。

■Google Optimizeについて

  • Google Optimizeとは
    Googleが提供する無償のABテストツールです。専用のタグをサイトに埋め込むだけで簡単にエクスペリエンス(=ABテスト)を実施することできます。エクスペリエンスやデータの集計は_gaexpというcookieで管理されます。

  • 管理画面でエクスペリエンスの実施が可能
    Google Optimizeの特徴として、ブラウザの管理画面で各種設定を行うことで簡単にABテストを実施することができる、という点が挙げられます。ノーコードでテストパターンの要素変更ができるため、非エンジニアでも気軽に操作することが可能です。

今回の問題点

通常、Google OptimizeのABテストはクライアント側で処理される仕様のため、サーバー側で機能を切り替えることができません。クライアント側でページの読み込み後に処理されるため、ABテストの実行(要素の変更)時にはどうしてもちらつき(ページフリック)が発生してしまいます。

今回、このちらつきを避けることや、管理画面側では設定できないようなABテストをプログラム側で実装することを考慮し、通常の方法ではなくサーバー側でOptimizeを使用する環境を整える必要がありました。

それではここから、本題の「Server Side RenderingとGoogle OptimizeでABテストを実装する方法」について説明していきます。

Server Side Rendering×OptimizeでABテストを実装する方法

■結論

Server Side RenderingでGoogleOptimizeのABテストを実装するためには、Optimizeのサーバーサイドテストの機能を使い、プログラム側とOptimize管理画面側でそれぞれ以下の項目を実装・設定する必要があります。

【プログラム側での実装】
1) テストパターンの割り当て
2) Optimize(GA)側にテストパターンを送信
3) テストパターンのグローバル管理、取得
4) コンテンツの出しわけ

【Optimize管理画面側での設定】
1)テストパターンの作成
2)ページターゲティング
3)その他

それぞれ説明していきます。

■プログラム側での実装

1) テストパターンの割り当て
まず、ABテストのseed値がCookieにあればCookieから取得、なければ生成する関数を定義します。
(※seed値にはuserIdを用いてもOKですが、今後「ログアウト⇨ログイン状態」をまたいだABテストを行うことも考慮し、userIdを利用せずにABテスト用にseed値を生成してパターンの振り分けを行うように実装しています。)

【abTestSeed.ts】

import { NextPageContext } from 'next';
import nookies, { parseCookies } from 'nookies';
 
export interface AbTestSeed {
 seed: string;
}
export const COOKIE_KEY_AB_TEST_SEED = 'abTestSeed';
 
const genRandomString = () => {
 return Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
};
 
export const getOrCreateAbTestSeed = async (context: Pick<NextPageContext, 'req' | 'res'>): Promise<AbTestSeed> => {
 const cookies = parseCookies(context);
 
 if (COOKIE_KEY_AB_TEST_SEED in cookies && cookies[COOKIE_KEY_AB_TEST_SEED]) {
   return {
     seed: cookies[COOKIE_KEY_AB_TEST_SEED],
   };
 }
 
 const seed = genRandomString();
 nookies.set(context, COOKIE_KEY_AB_TEST_SEED, seed, {
   maxAge: 5 * 365,
   path: '/',
   httpOnly: false,
   sameSite: 'lax',
 });
 return {
   seed,
 };
};

次に、今回のエクスペリエンスをSAMPLE_TESTとし、以下の項目を設定します。

  • テストを開始するページ: トップページ
  • テストパターンの数: 2
  • テスト対象の割合: 全てのユーザー

【abTestSetting.ts】

export interface AbTestSetting {
 experimentId: string; // OptimizeのエクスペリエンスID
 triggerPaths?: RegExp[]; // テスト開始をトリガーするPathを指定
 numOfVariants?: number; // テストパターンの数
 targetRatio?: number; // 対象とするユーザーの割合。0~1で設定
}
 
// 全てのクエリを含む、トップページが対象
const TopPageRegex = /^\/(\?.*)?$/;
 
export const SAMPLE_TEST = {
 experimentId: 'XXXXXXXXXXXXXXXX',
 numOfVariants: 2,
 targetRatio: 1,
 triggerPaths: TopPageRegex;
};
 
export const ABTests: AbTestSetting[] = [SAMPLE_TEST];

今後複数のエクスペリエンスを実行することを考慮し、ABTestsという変数に配列で格納しておきます。

次に、abTestSeedを受け取ってテストパターンを返す関数を定義します。
OptimizeABテスト用のReact Contextも作成しておきます(詳細は後述します)。

【abTestState.ts】

import { createContext } from 'react'; 
import { ABTests, AbTestSetting } from './ABTestSettings';
import * as crypto from 'crypto';
import * as hashSha256 from 'sha256-uint8array';
 
export type ABTestVariant = {
 experimentId: string;
 variant: number;
};
 
// テストパターンを決定する
export const determineVariantsForPath = (path: string, seed: string): ABTestVariant[] => {
 const abTests = ABTests.filter((setting) => {
   if (!setting.triggerPaths) {
     return true;
   }
   return setting.triggerPaths.some((pattern) => pattern.test(path));
 });
 return _determinVariants(abTests, seed);
};
 
const _determinVariants = (abTests: AbTestSetting[], seed: string): ABTestVariant[] => {
 const variants = abTests
   .map((setting) => {
     return {
       experimentId: setting.experimentId,
       variant: determineVariant(setting, seed),
     };
   })
   .filter((variant) => variant.variant !== NO_VARIANTS);
 return variants;
};
 
const UINT_MAX = 4294967295;
export const NO_VARIANTS = -1;
 
export const determineVariant = (setting: AbTestSetting, seed: string) => {
 //Seed値をハッシュ化させてパターンを割り振る
 const digest = hashSha256.createHash().update(`${setting
.experimentId}-${seed}`).digest().buffer;
 const patternBase = new DataView(digest.slice(0, 4)).getUint32(0);
 const numOfPatterns = setting.numOfVariants || 2;
 const targetRatio = setting.targetRatio || 1;
 const pattern = patternBase % numOfPatterns;
 
 if (targetRatio >= 1) {
   return pattern;
 } else {
   // 比率が1ではない場合は、4~8byte目を使用する
   const ratioBase = new DataView(digest.slice(4, 8)).getUint32(0) / UINT_MAX;
   if (ratioBase < targetRatio) {
     return pattern;
   } else {
     return NO_VARIANTS;
   }
 }
};
 
//グローバルで状態管理するためにReact Contextを使用
export const ABTestContext = createContext({
 seed: 'empty',
});
 
//テストパターンを取得。実際にABテストを実施するコンポーネントで使用する
export const getVariant = (setting: AbTestSetting): number => {
 const seed = useContext(ABTestContext).seed;
 const variant = determineVariant(setting, seed);
 if (variant !== NO_VARIANTS) {
   return variant;
 } else {
   return undefined;
 }
};

2) Optimizeにテストパターンを送信
abTestSeedを取得する関数を_app.tsx内で実行し、メタ要素を管理するMetadataコンポーネントに渡します。

【_app.tsx】

import { NextPageContext } from 'next';
import { useRouter } from 'next/router';
import Metadata from './Metadata';
import { determineVariantsForPath } from './ABTestState';
 
const App = ({ Component, pageProps, abTestSeed }) => {
 const router = useRouter();
 return (
   <>
     <Metadata
       abTestVariants={determineVariantsForPath(router.asPath, abTestSeed)}
     />
     <Component {...pageProps} />
   </>
 );
};
 
App.getInitialProps = async (appContext: AppContext) => {
 const abTestSeed = await getOrCreateAbTestSeed(appContext.ctx);
 
 return { abTestSeed: abTestSeed.seed };
};
 
export default App;

【Metadata.tsx】

import React from 'react';
import Script from 'next/script';
import { ABTestVariant } from './ABTestState';
 
const googleAnalyticsTrackingId = 'YYYYYYYYYYYYYYY'
const googleOptimizeTrackingId = 'ZZZZZZZZZZZZZZZ'
 
interface MetadataProps {
 abTestVariants: ABTestVariant[];
}
 
const Metadata: React.FC<MetadataProps> = (props) => {
 return (
   <>
     <Script src={`https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsTrackingId}`}
     />
     <Script
       id="gtag-snippet"
       strategy="afterInteractive"
       dangerouslySetInnerHTML={{
         __html: `
           window.dataLayer = window.dataLayer || [];
           function gtag(){dataLayer.push(arguments);}
           gtag('js', new Date());
           gtag('config', '${googleAnalyticsTrackingId}', {
            page_path: window.location.pathname,
            experiments: [${props.abTestVariants
       .map((variant) => {
         return `{ id:'${variant.experimentId}', variant:'${variant.variant}' 
      }`;
       })
       .join(',')}],
     });
       `,
       }}
     />
   </>
 );
};
 
export default Metadata;

これで、トップページにアクセスがあったタイミングでOptimizeにデータが送信されるようになります。

3) テストパターンのグローバル管理、取得
(2)で記述した_app.tsxのコンポーネントをReact Contextでラップすることで、テストパターンをグローバルで状態管理します。

【_app.tsx】

import { ABTestContext} from './ABTestState';
 
//...省略...
 
return (
   <ABTestContext.Provider value={{ seed: abTestSeed }}>
     <GoogleAnalyticsMetadata
       abTestVariants={determineVariant(abTestSeed)}
     />
     <Component {...pageProps} />
   </ABTestContext.Provider>
 );
};
 
//...省略...

私たちのプロダクトでは状態管理にRecoilを使用していますが、Recoilの場合、RecoilRootごと(各ページごと)に状態が分割しているためテストパターンを全体で共有できず、React Contextを使用するようにしました。

4) コンテンツの出し分け
最後は実際の表示部分です。
テストパターンを取得し、表示を出しわけます。

【SampleComponent.tsx】

import React from 'react';
import { getVariant } from './ABTestState';
import { SAMPLE_TEST } from './ABTestSettings';
 
const pattern = ['オリジナル','パターンA']
 
const SampleComponent: React.FC = (props) => {
 const variant = getVariant(SAMPLE_TEST);
 
 return (
   <>
     <p>{pattern[variant]}</>
   <>
 );
};
 
export default SampleComponent;

上記のコンポーネントでは、テストパターンが0だと「オリジナル」、1だと「パターン1」が表示されるようになります。

※実施するABテストの内容によっては、コンポーネント自体を別で作って表示を出し分けるような実装でもいいかと思います。テスト内容に応じて調整ください。

以上がプログラム側で必要な実装となります。

■Optimize側での実装

Optimize側で必要な設定は主に以下の2つです。

1) テストパターンの作成
ABテストに必要な数だけパターンを作成します。 テストの実装は全てプログラム側で行っているため、編集ボタンでの操作は一切不要です。パターンを作成するだけでOKです。

2)ページターゲティングの設定
存在しないサイトのURLを入力してください。正式なURLを入力してしまうとエクスペリエンスがOptimize側で自動的に開始されてしまい、プログラム側で実装した処理よりも優先して開始されてしまいます。ここでは公式リファレンスを参考にして「SERVER_SIDE」と入力しています。画像のようなWarningが出ますが、無視でOKです。

その他
それ以外に必要な設定は特にありません。管理画面にはトラフィックの割り当てやアクティベーションイベントなどを設定する項目がありますが、今回のサーバーサイドでのABテストの場合、それらは全てプログラム側で設定することになるため管理画面側での設定は不要です。

あとは、GAとの連携やOptimizeのインストールが済んでいればエクスペリエンスの開始が可能なので、「開始」ボタンをクリックしてエクスペリエンスを開始させてください。プログラム側からデータが正常に送られているとデータが集計されます。

設定は以上となります。

■ハマった点

  • gtag.jsでのデータ送信
    Googleの測定サービスにデータを送信するにはgtag.jsというフレームワークを使いますが、公式のサンプルコードではgtag.jsの’set’ディレクティブが記述されているものの、今回の実装だとデータが送信されませんでした。結局’config’ディレクティブに変更して上手くいきましたが、かなり詰まりました。

  • 管理画面側で必要な設定項目
    通常のReactアプリ(SPA)でのOptimize実装の文献を読むと「アクティベーションイベントを設定してプログラム側でuseEffectで呼び出す〜」といった説明がありますが、サーバーサイドでのテストの場合はこれらの設定は一切不要です。 上述の通り、管理画面側で必要な設定は主に2つのみです。管理画面はあくまでABテストを行うためだけの土台作りのイメージ、ということを認識する必要がありました。

  • 状態管理
    状態管理にはRecoilを使用していますが、私たちのプロダクトでは各ページごとにRecoil Rootで状態を分割しているため、テストパターンの値を全体で共有できませんでした。React Contextを使うことで解決しました。

  • ABTestSeed生成のタイミング
    該当ページにアクセスした時にseed値を生成するようにすると、複数ページにまたがるABテストを実行する場合に矛盾が生じてしまいます。
    例)/hogeと/fugaで同一のテストを行いたい場合、/hogeにアクセス(オリジナル) ⇨ /fugaに遷移(テスト発火) ⇨ /hogeにアクセス(パターンA) のように、同じユーザーに対して/hogeの表示内容が変わってしまう可能性があります。
    なのでABTestSeedの生成を_app.tsxで実行することで、全てのページにアクセスがあったタイミングでseed値の生成をするようにしています。

■懸念点

  • ブラウザ(Optimize管理画面)側でテストの稼働を制御できない
    ABテストを中断するにはプログラムを修正する必要があります。管理画面でエクスペリエンスを停止すると結果の集計はされなくなりますが、テスト自体はプログラム側で実装しているため、プログラムを修正しない限り継続して稼働し続けてしまいます。 対策として、DBにテストの稼働を管理するためのデータを持たせて制御するような仕組みを検討していく必要があると考えています。

最後に

いかがでしたでしょうか。

Server Side RenderingでのABテストの実装は参考文献が少なく、詰まった部分がとても多かったですが、今回無事に実装できてよかったです。これでABテストを行う基盤が整ったので、今後は実際にテストを沢山まわしてプロダクトの分析・グロースに繋げていければと思います。

レバレジーズでは、私のようにエンジニア未経験で入社した社員でも幅広くチャレンジできる環境が整っています。ご興味のある方は、以下のリンクから是非ご応募ください!

recruit.jobcan.jp

Reactにおける責務(UI/ロジック)の切り分け

はじめに

初めまして、レバレジーズ株式会社FEエンジニアの森山です。
今回は、React開発におけるコンポーネントの定義方法の1つの解をご紹介します。

結論

結論を簡潔に記載すると以下です。
  • ロジックをUIロジックと業務ロジックに切り分ける。
  • UIとUIロジックは密結合させて再利用性を高める。
  • UIと業務ロジックは疎結合にして再利用性を高める。
背景や具体的な例は後述します。

なぜこの記事を書いたのか

私が調べた限りではReactにおけるコンポーネントの定義方法におけるベストプラクティスが存在しないからです。

Reactの公式ドキュメントにおいてもコンポーネントの定義方法の方針は記載が無いかと思います。これはReactの思想として開発者がプロジェクトの規模や特性を考慮してある程度の自由度を持って開発できることを尊重したのではないかと考えています。

自由度が高いと開発方法の手段が増えます。手段が多いのでどの開発方法がベストなのかReactの開発者も日々、様々な開発手段を試行錯誤しブログを執筆したり、議論しています。

プロジェクトに合わせて最適な手段を選択できる余地があるのは良いことです。
しかし、その自由度の高さをそのままにしておく。

つまりコンポーネントの定義ルールが曖昧な状態でプロジェクトが進むと以下のようなことが起きる可能性があります。
  • 開発者が各々、自由に開発を進める。
  • システム内に様々なコンポーネント実装の流派が生まれ、自分の実装が正しいか判断できなくなる。
  • 実装方法の正解が分からず、何をどこに書くべきか自信がないため実装スピードが落ちる。
  • 修正を加える度に複雑さが増し、再利用性も低下する。
  • 最終的にプロジェクト当初では1時間と見積もっていた規模の修正タスクに3時間かかってしまう。
上記を払拭するためにも、FEエンジニアが納得感を持ってコンポーネント実装ができるルールを作る必要がありました。

コンポーネント実装における課題はUIとロジックの関係性

よく議論されるのは以下です。
  • コンポーネントのUIとロジックを密結合にするのか、疎結合にするのか。
  • 疎結合の場合はHooksを活用するのか、コンポーネント自体を分けるのか。

そもそもReactにおけるロジックとは何か?

そもそもこの記事においてのReactにおけるロジックとはreturnでDOMを返す前の処理やUIの表示分岐のことです。
const Component = () => {

  const [state, setState] = useState(true);
  const data = fetch('https://api.endpoint');
  // ...
  // ここから上がロジック
  return (
    <>
      <h1>{data.title}</h1>

      <!-- UI表示の分岐ロジック -->
      <p>{state ? 'on' : 'off' }</p>
    </>
  )
}
一言でロジックと言っても、以下の2種類に分類できると考えています。
  • UIロジック
  • 業務ロジック

UIロジック

UIロジックとは、UIの制御のみを目的としたロジックです。

具体例としては以下の様な挙動が挙げられます。
  • トグルボタンのon/offの制御
  • プルダウンの開閉の制御
const Toggle = ({ isActive }) => {
  return isActive ? <ActiveToggle/> : <InActiveToggle/>
}
UIロジックは、「業務に依存しない」純粋なUIのふるまいの制御になります。

業務ロジック

業務ロジックとは業務ルールを実現するためのロジックです。

表示する数値に単位を付ける等の業務独自の「表示データの整形」や、「APIの発行」等が該当します。
const Price = () => {
  const { data } = fetch('https://api.endpoint');

  const { quantity, unitPrice ,taxRate, shippingCost } = data;
  const basePrice = quantity * unitPrice;
  const price = (basePrice + shippingCost) * taxRate;
  return (
    <>
      <h1>価格: {price}円</h1>
      <p>内訳</p>
      <ul>
          <li>個数: {quantity}個</li>
          <li>単価: {unitPrice}円</li>
          <li>税率: {taxRate}%</li>
          <li>送料: {shippingCost}円</li>
      </ul>
    </>
  )
}

UIとロジックの密結合、疎結合の使い分け

UIとロジックは必ずしも「密結合が良い」とか「疎結合が良い」とは言い切れません。

前述したロジックの分類(UIロジックと業務ロジック)によって変わるかと思います。 コンポーネント単体で見るときれいに切り分けられている記事も見かけます。

しかしその1部品をルール化してプロジェクト全体に適応しようとすると、プロジェクトの規模や業務ロジックの複雑さも起因してどうしてもルールを守るのが苦しい場面に遭遇します。そして苦しいながらもルールを守るために、意味もなく複雑度の高いコンポーネントが生み出されます。

上手くUIとロジックの関係性をルール化するためにはロジックの分類を考慮する必要があります。

私の考えでは以下の使い分けが望ましいです。
  • UIとUIロジックは密結合
  • UIと業務ロジックは疎結合
  • UIロジックと業務ロジックは疎結合
  • 具体的な実装例

    具体的にどのように実装すべきか一般的なAtomic designに沿った分類のコードで説明します。

    まず各コンポーネントの区分けは以下になります。
    ※ templatesとpagesはここでは省きます。
    • atoms
      • コンポーネントの最小単位。
      • ロジックは持たない。
      • ステートレスなコンポーネント。
      • システム全体で流用できる。
    • molecules
      • atoms、moleculesの複合コンポーネント。
      • UIロジックを持つことがある。
      • ステートレスなコンポーネント。
      • システム全体で流用できる。
    • organisms
      • atoms、moleculesの複合コンポーネント。
      • 業務のドメイン情報をDOMに持つことがある。
      • そのためシステム全体で流用できるとは限らない。
      • APIとの疎通や業務ロジックを持つことがある。
      • そのためステートレスとは限らない。

    atoms

    // アクティブなラジオボタン
    const ActiveRadio = () => {
      return (
        <div style={{
          width: 15px;
          height: 15px;
          backgroundColor: white;
          border: 1px solid black;
          borderRadius: 10px;
          display: flex;
          alignItems: center;
          justifyContent: center;
        }}>
          <span style={{
            width: 5px;
            height: 5px;
            backgroundColor: blue;
            borderRadius: 10px;
          }} />
        </div>
      )
    }
    const Title = ({ text }) => {
      return <h1 style={{ fontSize: 20px }}>{text}</h1>
    }

    atomsの特徴

  • コンポーネントの最小単位。
  • ロジックは持たない。
  • ステートレスなコンポーネント。
  • システム全体で流用される。
  • atoms実装の注意点

    atomsでは基本的にreturnの上にロジックが書かれることはありません。

    というのも意識的にロジックを書かないというよりかは、「最小単位」のコンポーネントなので責務も小さく自然と書く必要がなくなるイメージです。

    ロジックを書く必要がある場合はそのatomsは「最小単位」として扱うべきでない可能性があります。
    更に最小のコンポーネントになり得ないかを疑う余地があるかと思います。

    molecules

    import { ActiveRadio, InActiveRadio } from '~/atoms/radio';
    
    // on/offができるラジオボタン
    const Radio = ({ isActive }) => {
      return isActive ? <ActiveRadio /> : <InActiveRadio />
    }
    import { Button } from '~/atoms/button';
    
    // ファイルアップロードフォーム & ボタン
    const FileUpload = ({ name, text }) => {
      const ref = useRef(null);
      const onClickInput = () => ref.current.click();
    
      return (
        <>
          <Button onClick={onClickInput} >{text}</Button>
          <input hidden name={name} ref={ref} type="file" />
        </>
      );
    }

    moleculesの特徴

  • atoms、moleculesの複合コンポーネント。
  • UIロジックを持つことがある。
  • ステートレスなコンポーネント。
  • システム全体で流用される。

  • 上記のラジオボタンのようにコード量が少なくても「他のコンポーネントに依存」していればmoleculesに該当します。
    ここがatomsとmoleculesの一番の違いです。

    molecules実装の注意点①

    moleculesにおけるUIロジックとUIは前述の通り「密結合」にするのが望ましいです。

    なぜなら、依存関係が簡潔になりやすいためです。

    例えば、以下のようにUIロジックをCustom hookで共通化したとします。

    全てのpulldownに共通する処理
    // usePulldown.ts
    export const usePulldown = () => {
        return ...
    }
    一般的なプルダウン
    // Pulldown.tsx
    const Pulldown = () => {
      const pulldownProps = usePulldown()
      return <Pulldown {...plldownProps}/>
    }
    検索可能なプルダウン
    // SearchPulldown.tsx
    const SearchPulldown = () => {
      const pulldownProps = usePulldown()
      // ... SearchPulldown独自のロジック
      return (
        <>
          <!-- other component -->
          <Pulldown {...plldownProps}/>
        </>
      )
    }
    複数選択可能なプルダウン
    // MultiPulldown.tsx
    const MultiPulldown = () => {
      const pulldownProps = usePulldown()
      // ... MultiPulldown独自のロジック
      return (
        <>
          <!-- other component -->
          <Pulldown {...plldownProps}/>
        </>
      )
    }
    ここでusePulldownの処理の中でSearchPulldown.tsxのみに例外的な処理の追加が必要になったとします。

    根本的な対処としてはusePulldownの中から例外的な処理が入る部分を切り出して他のPulldownMultiPulldownにも変更箇所のコードを移植することになるかと思います。

    しかし元々一元管理されていたコードをわざわざ重複させる書き方はDRY原則にも反します。

    大抵は以下のようにif文を一行入れて例外的な処理を走らせるような直感的な対処をしてしまいます。
    // usePulldown.ts
    export const usePulldown = (type) => {
    +    if(type === 'searchPulldown') doSomething();
        return ...
    }
    依存される側のusePulldownが依存する側のコンポーネントの情報を保持することになります。

    こうなると双方向に依存が発生し依存関係がおかしなことになります。

    これはReactによらずプログラミング全般として良くない実装かと思います。

    しかしReact開発で上記のように似たようなふるまいのコンポーネントが複数生成されて開発規模が大きくなると自然とやってしまいがちです。

    そもそもこういった修正が発生しないためにも「UIロジック」と「DOM」は密結合に実装し、コンポーネント同士の独立性を高める方が各コンポーネントの拡張性も高くなります。

    molecules層で複雑なUIのふるまいを持つことは少ないので1ファイルに「UIロジック」と「DOM」を密結合に実装しても50行前後に収めて可読性を担保することも可能なはずです。

    molecules実装の注意点②

    moleculesコンポーネントはシステム全体で活用できるレベルの再利用性を持つことが望ましいです。

    システム全体で活用できるとは特定の業務ドメインに縛られないということです。

    例えば以下のファイルアップロードボタンは汎用性が高く基本的にどんな場面でも活用できるかと思います。
    import { Button } from '~/atoms/button';
    
    // ファイルアップロードフォーム & ボタン
    const FileUpload = ({ name, text }) => {
      const ref = useRef(null);
      const onClickInput = () => ref.current.click();
    
      return (
        <>
          <Button onClick={onClickInput} >{text}</Button>
          <input hidden name={name} ref={ref} type="file" />
        </>
      );
    }
    逆に特定の業務ドメインに縛られているとは以下のような状態です。

    違いはボタンの名前が固定値になっただけです。
    import { Button } from '~/atoms/button';
    
    // ファイルアップロードフォーム & ボタン
    const FileUpload = ({ name }) => {
      const ref = useRef(null);
      const onClickInput = () => ref.current.click();
    
      return (
        <>
          <Button onClick={onClickInput} >プロフィール画像のアップロード</Button>
          <input hidden name={name} ref={ref} type="file" />
        </>
      );
    }
    どんなシステムを開発しているかの前提もないですが、 上記のファイルアップロードボタンはユーザ情報の登録や編集を担う画面でしか活用できないコンポーネントになることが想像できます。

    もしシステムが商品登録等の機能を持っていたらそちらでもファイルアップロードボタンは必要になりそうです。

    しかし上記のように「特定の業務ドメイン」に限定されたコンポーネントだと再利用はできません。

    再利用性の低いコンポーネントがmoleculesレベルで存在しているとシステムの規模が大きくなった時に以下のどちらかの苦しい対処が発生します。

  • 利用箇所が限定的なコンポーネントが極端に増える
  • 無理くりatomsをpages内で組み合わせて実装

  • なのでmoleculesでは特定の業務ドメインに縛られないコンポーネントであるべきです。

    特定の業務ドメインとは何か

    特定の業務ドメインとは何かについてさらに詳しく触れます。

    単純に「業務ドメイン」とは言わずに「特定の業務ドメイン」と呼んでいるのには意味があります。

    極端な例ですが、開発するシステムが商品管理システムでそれ以外の情報を扱わなかったとします。
    • 商品管理システム
      • 商品登録機能
      • 商品編集機能
      • 商品削除機能
    この場合には、特定の業務ドメインは「商品登録機能、商品編集機能、商品削除機能」が該当します。

    なので以下のようなコンポーネントが実装されてもシステム全体で活用できるのでmoleculesとして成立します。
    import { Button } from '~/atoms/button';
    
    // ファイルアップロードフォーム & ボタン
    const FileUpload = ({ name }) => {
      const ref = useRef(null);
      const onClickInput = () => ref.current.click();
    
      return (
        <>
          <Button onClick={onClickInput} >商品画像のアップロード</Button>
          <input hidden name={name} ref={ref} type="file" />
        </>
      );
    }
    ただシステムが以下のような構成だと上記のコンポーネントはmoleculesとして成立しません。
    • フリーマーケットシステム
      • ユーザ管理機能
        • 登録、編集、削除
      • 商品管理機能
        • 登録、編集、削除

    molecules実装の注意点まとめ

    長くなりましたが特に注意すべきは以下の2点です。

  • UIとUIロジックが密結合になっていること
  • 特定の業務ドメインに縛られないこと
  • organisms

    import { Title, Paragraph } from '~/atoms'
    import { PriceList } from '~/molecules/PriceList'
    
    // 価格
    const Price = () => {
      const { data } = fetch('https://api.endpoint')
    
      const { quantity, unitPrice ,taxRate, shippingCost } = data;
      const basePrice = quantity * unitPrice;
      const price = (basePrice + shippingCost) * taxRate;
      return (
        <>
          <Title>価格: {price}円</Title>
          <Paragraph>内訳</Paragraph>
          <PriceList
            quantity={quantity}
            taxRate={taxRate}
            shippingCost={shippingCost}
          />
        </>
      )
    }

    organismsの特徴

  • atoms、moleculesの複合コンポーネント。
  • 業務のドメイン情報をDOMに持つことがある。
  • APIとの疎通や業務ロジックを持つことがある。
  • organismsの実装の注意点

    業務ロジックとUI(DOM)を疎結合にすることです。

    organismsは上記の例のように業務ロジックや業務のドメイン情報をDOMに持ちます。

    ただ上記のコード例は悪い例です。

    以下のように業務ロジックとUIが密結合していると悪い点が3点あります。
    1. 再利用性の低下
    2. データ構造が見えない
    3. 業務ロジックをDOM上で実装できてしまう
    1. 再利用性の低下
    業務ロジックとUIが密結合しているのでUIを再利用したくても限られた場面でしか活用できません。

    2. データ構造が見えない
    コンポーネントにpropsが存在しないので最終的にどんな構造のデータをレンダリングされるのか読み取るためにはDOMを全て読み解く必要があります。上記のコードはまだシンプルですがDOMの中で条件分岐が発生したりコード量が増えるとかなりしんどくなります。

    3. 業務ロジックをDOM上で実装できてしまう
    業務ロジックがDOM上に実装されると、ロジックの記載場所が以下の2パターンになります。
  • returnより上
  • DOMの中

  • 書く場所が複数ありルールも存在しないと開発する時にどちらに書くべきか迷いが生まれます。

    例えば、先程提示したコードにおいて業務上の仕様が変わって会員の時は「価格が10%off」になるという業務ロジックが加わったとします。

    一番手っ取り早くしようとすると最終的にレンダリングされている価格に計算処理を加える実装になりそうです。
    import { Title } from '~/atoms/Title'
    import { PriceList } from '~/molecules/PriceList'
    
    // 価格
    const Price = () => {
      const { data } = fetch('https://api.endpoint')
    
    -  const { quantity, unitPrice ,taxRate, shippingCost } = data
    +  const { quantity, unitPrice ,taxRate, shippingCost, isMember } = data
      cons t basePrice = quantity 
  • cons unitPrice t price = (basePrice + shippingCost)
  • taxRate return ( <> - <Title>価格: {price}円</Title> + <Title>価格: {isMember ? price * 0.9 : price} <p>内訳</p> <PriceList quantity={quantity} taxRate={taxRate} shippingCost={shippingCost} /> </> ) }
    この例ではコードの行数が短いですが、実際のorganismsコンポーネントになるとコードの行数が長くなりがちで自然とこういった楽な実装に流されることが多いです。
    そして何より厄介なのは上記の例のように三項演算子1つだと対して悪くなさそうに見えることです。

    これに更にもう一種類、ゴールド会員が追加されるとどうでしょうか?
    import { Title } from '~/atoms/Title'
    import { PriceList } from '~/molecules/PriceList'
    
    // 価格
    const Price = () => {
      const { data } = fetch('https://api.endpoint')
    
    -  const { quantity, unitPrice ,taxRate, shippingCost, isMember } = data
    +  const { quantity, unitPrice ,taxRate, shippingCost, isMember, isGoldMember } = data
      cons t basePrice = quantity 
  • cons unitPrice t price = (basePrice + shippingCost)
  • taxRate return ( <> - <Title>価格: {isMember ? price * 0.9 : price} + <Title>価格: {isGoldMember + ? price * 0.8 + : isMember + ? price * 0.9 + : price + }円 <p>内訳</p> <PriceList quantity={quantity} taxRate={taxRate} shippingCost={shippingCost} /> </> ) }
    元々のコードでDOMに三項演算子が実装されていると自然とそこに追加する実装をしてしまうことがあると思います。

    こうなると業務ロジックがreturnの上だけでなくDOMの中にも存在することになります。

    ロジックの実装箇所がルールもなく複数箇所になることで可読性が一気に落ちます。

    DOMを再利用するために別ファイルに切り出そうとしてもロジックを含むことで単純にDOMだけを切り出すこともできなくなります。

    打開策

    上記の良くない2点を打開するためには、業務ロジックとUIを疎結合にする必要があります。

    具体的には、organismsの中でもコンポーネントを2つに分けます。
    organisms/
        ├ Presentations/
        │  └ Price.tsx (UIを責務としたコンポーネント)
        │
        └ Containers/
            └ PriceContainer.tsx (業務ロジックを責務としたコンポーネント)
    

    今後、2つのコンポーネントは以下の呼び方をします。
  • UIを責務としたコンポーネント: Presentational Components
  • 業務ロジックを責務としたコンポーネント: Container Components
  • PresentationalComponents

    // organisms/Price.tsx
    
    import { Title } from '~/atoms/Title'
    import { PriceList } from '~/molecules/PriceList'
    
    // 価格
    const Price = ({
      price,
      quantity,
      taxRate,
      shippingCost
    }) => {
      return (
        <>
          <Title>価格: {price}円</Title>
          <p>内訳</p>
          <PriceList
            quantity={quantity}
            taxRate={taxRate}
            shippingCost={shippingCost}
          />
        </>
      )
    }

    PresentationalComponentsの特徴

    ステートレスなコンポーネントで「どのようにデータを表示するか?」を責務とします。

    UI表示のみが責任範囲として切り出されているので別の場所で再利用も可能になります。
    PresentationalCompornentsは業務ロジックを持たないため、storybook等のUIのライブラリ化も容易にできます。 propsが表示すべきデータの構造そのものになります。

    PresentationalComponents実装の注意点

    propsをAPIのデータ構造に依存させないことです。

    あくまでも「どのようにデータを表示するか?」に注目します。
    APIのデータ構造に依存するとUIの構造とデータ構造が乖離して苦しい実装を強いられたりします。 APIのデータ構造の変更が頻発し、FEの改修工数が増えるとBE側がFEの負荷を軽減するためにUIドリブンなAPI定義を意識したりします。

    そうなってしまうとBE実装がUIに引っ張られて業務ドメインに沿った実装ができずFEもBEも拡張性の低い実装になったりしてしまいます。

    ContainerComponents

    // organisms/PriceContainer.tsx
    const PriceContainer = () => {
      const { data } = fetch('https://api.endpoint')
    
      const { quantity, unitPrice ,taxRate, shippingCost } = data;
      const basePrice = quantity * unitPrice;
      const price = (basePrice + shippingCost) * taxRate;
      return (
        <Price
            price={price}
            quantity={quantity}
            taxRate={taxRate}
            shippingCost={shippingCost} />
      )
    }

    ContainerComponentsの特徴

    業務ロジックの実装に着目したコンポーネントになります。

    どう表示されるかの責任はすべてPresentationalComponentsに委ねます。
    ContainerComponentsでは位置の調整や余白のためにスタイルを当てることもありません。

    Presentational/Container Componentsの実装方針

    organismsの中でもPresentationalComponentsContainerComponentsはディレクトリも区分けして明確に違うものとして定義しておくのが良いです。

    そうすることで修正が必要な場合にどちらのコンポーネントを修正すべきかも明確に判断が付きます。
    ただコンポーネントの実装を始める前からPresentational/Container Componentsをどういった構造にすべきか設計することは難易度が非常に高いです。 まずはPresentational Componentsをひたすら実装していき、画面まで実装するのが望ましいです。 その後APIをpagesコンポーネントで繋ぎこみます。 pagesでAPIを発行するとorganismsのPresentational Componentsを辿って末端のatomsコンポーネントまでデータをバケツリレーしていくことになります。 この時にpropsとして受け取ったデータを無加工でそのまま更に末端のコンポーネントに渡す場面が発生するはずです。 その場面でContainer Componentsの実装を検討していくというのが手戻りを少なくできます。

    結論

    まとめですが、結論以下のような構成・ルールでコンポーネントを分割しました。

    • atoms
      • コンポーネントの最小単位。
      • ロジックは持たない。
      • ステートレスなコンポーネント。
      • システム全体で流用できる。
    • molecules
      • atoms、moleculesの複合コンポーネント。
      • UIロジックを持つことがある。
      • ステートレスなコンポーネント。
      • システム全体で流用できる。
    • organisms
      • atoms、moleculesの複合コンポーネント。
      • 業務のドメイン情報をDOMに持つことがある。
      • そのためシステム全体で流用できるとは限らない。
      • APIとの疎通や業務ロジックを持つことがある。
      • PresentationalComponentsとContainerComponentsを分ける。

    得た恩恵

  • Atomic designの区分が明確化
  • atoms/moleculesの再利用性・拡張性の担保
  • organismsのUIと業務ロジックの責務をコンポーネント単位で分離

  • 全体を通してどこに何を実装すべきかが判断できるようになり無理な実装が生まれない仕組みを実現できました。

    おわりに

    いかがでしたでしょうか。

    上記のコンポーネント開発の構成がどのプロジェクトでも上手く行くとは限らないですが、何か1つでも参考になる箇所があれば幸いです。私の所属するHRテック事業部は発足してまだ日が浅く、プロダクトの状況も0-1フェーズにあります。なのでこういったどういう構成で開発を進めるべきかなどを検討・決定していける機会が多いです。この記事をご覧になり、この事業部を一緒に盛り上げてみたいなと思う方が1人でも増えれば嬉しく思います!(詳しくはこちら

    エンジニアが事業戦略の実現のために認証基盤システムを構築した話

    はじめに

     こんにちは、レバレジーズ株式会社エンジニアの住村です。 現在、私が開発に関わっているレバテックでは、IT人材向けのフリーランス事業を開始してから成長フェーズを経て、2022年現在では市場のトップシェアを獲得しています。

    レバテックでは、今後も市場のトップシェアを取り続け、トップであることを活かしてエンジニア業界を主導して流れを作り、業界全体を良い方向に動かしていくことを目指しています。そのためには、エンジニアも目先の開発だけに囚われず、職種も超えて協力して事業を創っていく必要があります。

    今回は、認証基盤システムの開発を通じて、エンジニアが今後の事業戦略を支えるためのシステム開発をどのように進めたかの事例を紹介していきたいと思います。

    導入背景

    レバテックの事業戦略

     レバテックでは、フリーランス・正社員・未経験からのスタートなど、さまざまな形で職を探しているエンジニア・クリエイターと企業のマッチングを支援しています。

    レバテックのサービス一覧

     一度サービスを利用したら終わりではなく、プログラミングを学ぶフェーズ(カレッジ)から、就職(ルーキー)・転職(キャリア)といったサービスを継続的に受けることが可能です。過去の蓄積データを元にエンジニアはより充実したサービスを受けることができます。エンジニアをターゲットとしたサービスを提供している関係上、ユーザーの行動に合わせて当初とは異なるサービスを提供することもあります。

    サービスの利用イメージ

     また、継続して顧客に利用してもらうことでファン化を促進し、顧客生涯価値(LTV)を高めるだけでなく、サービスの情報を知り合いのエンジニアに紹介してもらう、新規に展開する別サービスの潜在顧客になるなど、今後のマーケティング活動にも繋げることができます。 今回は、この「ユーザーに継続してサービスを使い続けてもらう」事業戦略を実現するために、根幹となる認証基盤システムを構築していきました。

    システム課題

     前述の事業戦略の実現にあたり、1つのサービスを利用したユーザーがその後も別のサービスをシームレスに利用し続けるためには、システムに大きな課題が存在しました。

    それは、これまでのレバテックでは各種サービスが使い切りのビジネスモデルであったため、システムもサービスの開始に合わせて都度構築していたことです。

    既存システム構成

     その際、認証機能も新規システムを構築する度に作成し、そのシステム内でのみ管理しているため、ユーザーはレバテックの別のサービスを利用する度にアカウントを作成しなければなりませんでした。 また、同じ処理を別々のシステムで開発していたために無駄な開発コストがかかっていたのも事実です。それ以外にも、あるサービスではSNSログインが使えるが別のサービスではEmail/PWでしかログインできない、パスワードに使える文字列の長さや種類が異なる、といったように認証要件も統一されていませんでした。

    既存システム課題

     今後の事業戦略を実現するため、シームレスにサービス間を利用できるようにするにはこれらの問題点も解消する必要がありました。 そのため、レバテックでは共通の認証基盤を作成し、既存サービスの認証方法を統一する判断に至りました。

    今回の構成

    技術選定理由

     今回のシステムを構築するにあたり、前述の事業戦略からサービス間を横断的にアクセスできる必要性があるため、まずサービス間でのSSOログインを実現する必要がありました。 SSOログインの実現にあたり、大枠の方針としてはAuth0やCognitoやFirebaseといった外部サービスを利用するか、各システムで認証プロセス管理するライブラリを用意するなどの自社で開発するかの2つが選択肢として挙がりました。 SSOログイン機能を開発するには大きく分けて認証プロセスの管理と認証情報の管理の2つの機能が必要です。

     今回は認証プロセスに独自仕様がないことと、実装コストを削減するために外部サービスのALBの機能を利用しました。 その上で、認証情報の管理は既存システムからの移行やアカウントの統合といった独自仕様があったため、独自で実装する選択を取りました。 また、認証プロトコルにはALBでサポートされておりセキュリティ信頼性の高いOpenID Connect(OIDC)を採用しました。

    ALB

    認証プロセス

    認証プロセス

     今回の構成では、 ・クライアントのログインセッションの管理 ・セッションの状態に応じたログイン画面への振り分け ・認証成功時のアクセストークンの管理 ・認証が完了したセッションへのJWTの発行 などの認証プロセスの管理をALBへ任せています。

    ALBを利用しない場合はこれらをサイトで管理する必要がありましたが、今回の構成ではALBが管理するため、共通認証基盤の利用に必要な対応が大幅に削減できます。 そのため、既存のサイトはJWTの有無で認証状態を、JWTを検証してどのユーザーがアクセスしているかチェックするだけで良くなります。認証の仕様はOpenID Providerが共通で持っているため、認証仕様を一元管理することで全てのサイトで認証のセキュリティレベルを統一できます。

    新規サイト構築時

     他にも新規にサイトを構築する場合、共通の認証基盤を利用するにはALBの設定を追加すれば、後はJWTの検証の仕組みを入れるだけで済みます。 これにより、今後の事業展開でサービスを増やす場合のハードルを大きく下げることができました。

    思わぬ落とし穴

     今回構築を行い、分かった点ですが、ALBを利用する場合は認証プロセスを15分以内に完了しない場合はALBから401エラーがクライアントに返却される仕様がありました。 これに関してはAWSの公式Docにも記載がありますが、このタイムアウトの期間は変更も削除もできない仕様になっています。 今回はアプリ側でタイムアウト期限を過ぎる場合の挙動を制御することで対応しましたが、ALBでタイムアウト時の遷移先が制御できると嬉しいですね。

    おわりに

     今回の記事では、レバテックの今後の事業戦略と、実現に必要なSSOログインの認証基盤システムをどのように構築したかについて紹介しました。 レバレジーズでは、今回の事例のように短期的な開発だけでなく事業の長期戦略に必要なシステムをエンジニアが主導して進めることができます。 それは、事業の方向性議論にエンジニアも参画するためです。

     レバレジーズでは、エンジニアとしてシステムの開発だけでなく、事業の未来を考えサービスを創っていくことに意欲のある仲間を募集中です!ご興味のある方は、以下のリンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/

    第一回テックフェス 〜テックトーク編〜

    はじめに

    こんにちは、2022年度新卒でレバレジーズ株式会社に入社した河原です。

    現在私は、若年層領域の事業を展開する『ハタラクティブ』や『キャリアチケット』というサービスで、営業職が使用するSFA開発*1に携わっています。 機能の多いSFAでの開発業務や業務・業界理解など難しいことが多いですが、営業職の社員が同じ職場にいて、日々私たちが開発したシステムを使って仕事しているところを見かけると、とてもやりがいを感じられます!

    本記事では、先日行われたレバレジーズエンジニア組織テックフェスでのテックトークセッションの内容をご紹介します!また、私が従事しているSFA開発にどうすれば活かせるかを考え、クラウドコンテナサービスの調査・検証してみました。

    テックフェス

    テックフェスは、レバレジーズのエンジニア組織メンバー全員を対象としたテクノロジーの祭典です。弊社エンジニアの技術力を向上させ、より良いサービスを世の中に提供できるようにするために企画されました。

    今回、記念すべき第一回目のテックフェスは、以下のようなタイムスケジュールで行われました。

    テックトーク

    今回は、テックフェス後半で行われたテックトークでの内容をご紹介したいと思います。

    テックトークのコーナーでは、各事業部のシステム開発を行う開発チームリーダークラスのエンジニアが、開発を通じて得られた経験のふりかえりや技術の活用法についての話を5〜10分の時間でプレゼンしました。テックトークのタイトルは以下の通りです。

    テックトーク - トークテーマ一覧

    インフラリソースや関数型言語など技術にフォーカスした話から、事業部側と密に連携する開発体制についてなどレバレジーズの開発組織ならではの話もあり、短い時間ながらもとても役に立つ内容でした!

    テックトークの内容を開発に活かしてみたい!

    今回のテックトークに刺激を受け、私のチームのシステム開発に活かしてみたい!という気持ちが生まれました。私は様々なトークテーマの中から、現在の私の開発に活かせるものがないかを模索しました。

    開発チームリーダーにSFA開発の現状を確認したところ、以下のようなインフラの構想があることがわかりました。

    • 現状:開発環境のみオンプレミスサーバーで作業している
    • 今後:Docker・Github Actions・クラウドコンテナサービスを利用した自動デプロイ環境を構築したい

    しかし、この環境構築の担当エンジニアは決まっておらず技術調査もあまり進んでいない状況でした。

    そこで、テックトークでの『Cloud Runへのデプロイ自動化が簡単で脳汁が出てしまった話』の内容をさらに深ぼり、クラウドのコンテナリソースについて調査・検証してみました!

    技術調査・比較

    テックトークは主に以下のような内容でした。 Cloud Runを使えばコンテナの自動デプロイが簡単に行えるそうです。 コンテナはベンダーロックインされない特徴を持ち、昨今のマイクロサービスアーキテクチャの実現には不可欠な仮想化技術です。

    テックトーク - GCP Cloud Runの説明抜粋

    本当に簡単にデプロイやCI/CDが設定できるのでしょうか?また、他にはどんなコンテナリソースがあり、それぞれの違いは何があるのでしょうか? その違いを知っておきたく思い、クラウドサービスを調査してみました!

    テックトークの内容はGCPであり、普段の開発ではAWSを使っていることから、2つのクラウドサービスに対して調査を行います(ちなみに私はコンテナリソースに関してはほぼ無知な状態でした…)。調査した内容をカテゴリ分けすると以下のようになります。

    AWS*2 , GCP, OSS*3の比較

    上記のように、同じサーバーレスコンテナリソース同士にも細かな違いが存在するそうですが、それはどのような箇所なのでしょうか? 調査を一通り終え、それぞれのコンテナリソースの違いが大まかにわかってきたところでGCP・AWSのコンテナリソースを簡単に検証してみます!

    コンテナ自動デプロイ環境構築

    今回は、以下の2つのクラウドサービスを用いて検証を行います。

    • GCP:Cloud Run + Cloud Build
      • フルマネージド型コンテナデプロイサービス
    • AWS:ECR + ECS + Fargate
      • フルマネージド型コンテナオーケストレーションサービス

    GCPのCloud Runは今回のテックトークで登場し、その手軽さを実感したいと思ったため選択しました。AWSにはCloud Runに似たサービスとしてApp Runnerがあります。App Runnerの設定や機能はCloud Runとほぼ同様のため、コンテナリソース間の違いをあまり実感できないのではないかと思い、今回はApp Runnerより少し複雑な設定や調節が可能な『ECR + ECS + Fargate』を使って検証してみます。

    また、今回はクラウドサービスの比較検証がメインであるため、WebサーバーであるNginxを使った簡単なコンテナ環境で動作確認を行います!下記構成でGithubに登録した状態で検証スタートです!

    .├── Dockerfile
     └── html
          └── index.html
    

    【GCP】Cloud Run + Cloud Build

    まずは、テックトークでも登場したCloud Runを使ってみます。GCPのコンソール上の簡単な操作で自動デプロイ設定が可能なようです。実際に試してみましょう!

    GCPにProjectを作成し、Cloud RunとCloud BuildのAPIを有効にします。テックトークで説明された項目を作成したGithubのリポジトリで設定していきます。

    今回は、master branchのコンテナをデプロイする設定にしてみました

    次に作成するコンテナの設定をしていきます。今回は検証なのでユーザー認証の設定はせず、リクエストの処理中のみコンテナを起動する設定にしてみます。

    設定が終わり、GCPがコンテナの準備を始めます。

    数分待つと、Cloud Runでコンテナが起動

    作成されたコンテナはhttpsで自動にルーティングされます。 URLにアクセスするとGithubにpushしたコードがデプロイされていることが確認できました。本当に簡単ですね…。

    作成したNginxコンテナ

    次にCD(Continuous Deployment:継続的デプロイ)の設定をしていきます。 コンソールの「継続的デプロイの編集」を押し、以下のように編集していきます。今回はmasterブランチへpushされたイベントでコンテナがデプロイされる設定を行ってみました。

    試しにコードに、簡単なcssと画像を追加し、Githubにpushしてみます。 すると、そのイベントを検知し、デプロイが始まります。

    しばらくするとコンテナが起動し、変更を確認することができました。CDの設定まで非常に簡単にできてしまいます。

    【AWS】ECR + ECS + Fargate

    次は、AWSのコンテナ用サーバーレス環境のFargateを使って、ECRとECSでコンテナをデプロイしてみます。

    ECSはフルマネージド型コンテナオーケストレーションサービスと呼ばれており、Cloud Runよりも設定や導入は複雑ですが、その分高度な設定やコンテナ同士の連携が可能になります。FargateはECSやEKS用のサーバーレス環境であり、AWSユーザーはEC2のようなホストマシンの複雑な設定が必要なくコンテナを運用できます。

    GithubとECRを連携させるために、Github Actionsの設定を行います。

    初めにAWSのIAMで『AmazonEC2ContainerRegistryPowerUser』のポリシーを設定したユーザーを作成し、アクセスキーを発行します。発行したアクセスキーペアと、あらかじめ作成したECRのリポジトリ名をGithub>Setting>Secrets>Actionsに設定しておきます。

    次に作業ディレクトリにて、.github/workflows/main.ymlを以下のような内容で作成します。今回は「tech*」というtagのpushイベントをトリガーにECRにデプロイされるように設定しました。

    name: Push and Deploy ECR
    
    on:
      push:
        tags:
          - tech*
    
    jobs:
      build-and-push:
    
        runs-on: ubuntu-18.04
        timeout-minutes: 300
    
        steps:
        - uses: actions/checkout@v1    
    
        - name: Configure AWS Credentials
          uses: aws-actions/configure-aws-credentials@v1
          with:
            aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
            aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
            aws-region: ap-northeast-1
    
        - name: Push to Amazon ECR
          id: login-ecr
          uses: aws-actions/amazon-ecr-login@v1
    
        - name: Build, tag, and push image to Amazon ECR
          env:
            ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
            ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }}
          run: |
            IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")
            docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
            docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
    

    tagをつけてGithubにpushしてみます。するとGithubでBuildされ、AWSへのデプロイが開始します。

    Github ActionによりAWS ECRへデプロイ
    ECRへイメージがデプロイされていることがわかります

    ECRにあるリポジトリのイメージURLをECSでタスク定義として設定し、クラスターで作成したサービス内でタスクを実行してみます。

    デプロイされたECRのイメージを設定
    ここでFargateを選択

    ECRにデプロイされたイメージを、自動でECSコンテナとして起動させるためには、別でさらにAWS CodePipelineを使用して設定する必要があります。今回はそちらは省かせていただきます。

    起動したタスクで発行されたパブリックIPにアクセスすると、無事Github Actionsのイベントでpushされたimageのコンテナを確認することができました。

    比較

    ここまで技術調査・検証をしてきましたが、クラウドサービスにはたくさんのリソースがある中で、結局、どのリソースを使えば良いのでしょうか?

    私の感想としては、今回検証に使用したような非常に簡単なアプリケーション構成で「とりあえずコンテナのデプロイやCI/CDを試してみたい!」というニーズにはCloud Runが適しているなと思いました。 しかし、本来の開発のような、複数のアプリケーションが連携しているシステムで詳細な設定がしたい場合は、FargateやEC2が選択できるECSも選択肢になると感じました。

    また、この疑問に対して、GCPの公式の記事では、以下の図でまとめています。

    参考:https://cloud.google.com/blog/ja/topics/developers-practitioners/where-should-i-run-my-stuff-choosing-google-cloud-compute-option

    管理工数の少ないコンテナの場合、インフラへの考慮を最小限にとどめることで属人化を防ぎ、エンジニアはアプリケーション開発に集中できる利点により、モダンな開発に浸透している理由がわかります。しかし、サーバーレスで管理の少ない環境であるばあるほど、利用料金は高くなり、カスタマイズの幅が小さくなるといったトレードオフの関係にあります。

    開発現場における多種多様なシステム・開発体制に対して、それぞれのケースに適切なリソースを選定するためには、保守運用の人的コストや可用性・移植性等の品質面、また料金面などの複数の観点を考慮し、チームで意見を交え、リソースを選定していく必要があります。

    最後に

    今回は、新卒3ヶ月目の私が、先日のテックフェスで行われたテックトークの内容を現在の開発に活かせないか考え、クラウドサービスのコンテナリソースの調査・検証してみました!

    私も今回の調査・検証を経て、クラウドサービスのコンテナリソースやCI/CD・マイクロサービス化についての理解が深まりました。 今後、開発環境をクラウドでの運用に切り替えるタイミングが来れば、積極的に手をあげて環境構築に携わりたいなと思います。

    このようにレバレジーズでは年齢や経験に寄らずに、様々な開発の知識・経験が吸収できる環境があります。 若いうちからスキルをつけたい方や、熱意があるチームで毎日学びのあるチーム開発を行いたい方にはこれ以上ない環境だと思います。

    ぜひ、みなさんも一緒にサービスを作っていきませんか? レバレジーズに少しでも興味を持っていただけた方は、是非下記リンクを覗いてみてください!

    leverages.jp

    *1:営業業務の効率化を図り、売上や利益の向上を実現する営業支援システム、Sales Force Automationの略称

    *2:Amazon Web Services、AWS、Powered by AWS ロゴ、および資料で使用されるその他の AWS 商標は、Amazon.com, Inc. またはその関連会社の商標です

    *3:DockerおよびDockerのロゴは、米国およびその他の国におけるDocker、Inc.の商標または登録商標です。

    大盛況!? 第一回テックフェス 〜テックバトル編〜

    はじめに

    みなさんこんにちは!現在、内定者インターン中の内藤です。
    本日は、先日レバレジーズで行われ予想以上に(失礼)大盛況だった「第一回テックフェスのテックバトル」についてお話しします。

    この記事を通して、レバレジーズのエンジニア組織の風通しの良さを少しでも感じてもらえると嬉しいです!

    そもそもテックフェスとは?

    テックフェスとは何なのか簡単にお話しします。
    テックフェスは、レバレジーズのエンジニア組織メンバー全員を対象としたテクノロジーの祭典です。弊社エンジニアの技術力を向上させ、よりよいサービスを世の中に提供できるようにするために企画されました。

    今回、テックフェスは記念すべき第一回を迎えました🎊
    その記念すべき第一回目は、以下のようなタイムスケジュールで行われました。

    テックバトル

    テックバトルでは、3人1組チームに分かれ、コーディングの「正確さ」と「速さ」を競うコーディングバトルを行いました。 コーディングバトルってやっぱりエンジニアならではで楽しいですよね!

    では、早速今回のテックバトルで扱った問題を見ていきたいと思います!

    問題概要

    今回のテックバトルの問題は、以下の3つの実装を各人が担当し3人で協力して一つのものを作るという内容でした。

    ※ 今回、企業数と候補者数は同じ( n=1000)であるとします
    ※ この安定マッチングとは、企業と候補者、共に現在組んでいるペアよりも希望順位が高いペア(以降では、ブロッキングペアと呼びます)が存在しない状態を指します

    テックバトルの問題に挑戦!

    上記でも少し触れた通り、この問題の肝はCさんであり、Cさんが如何にこのプログラムを実装するかにチームの命運がかかってます笑

    今回、企業(候補者)数  n に対して、全組み合わせを力任せで調べるBrute-Forceアルゴリズムを用いると計算量は  n ! になります。 n が小さい値の時はまだマシですが、 n=1000 ともなると計算量があまりに膨大になってしまうので、このような時はアルゴリズムの知識が重要になりますね!

    今回の問題で活躍するのは、Gale-Shapleyアルゴリズムというもので、このアルゴリズムを用いると計算量は  O(n^2)で済ませる事が出来ます。恐ろしいほど強力ですね!😲

    Gale-Shapleyアルゴリズム

    今回の問題で重要なのは、如何にしてブロッキングペアの存在しない安定マッチングを実現するかという点です。
    Gale-Shapleyアルゴリズムは以下のようにして、安定マッチングを実現します。

    では、アルゴリズムを確認したところで、実際にコードを書いてみましょう!

    実際に書いてみた!

    今回は、Aさん, Bさんの実装は置いておいて(ごめんなさい笑)、CさんのGale-Shapleyアルゴリズムに該当する部分のプログラムをpythonで書いてみました。
    (pythonにしたのは、自分が日頃業務とは別に使うことが多いからです)

    今回使う変数は以下の7つです。

    # 初期に与えられている変数
    n : 企業(候補者)数  ... 今回、企業数と候補者数は同じ数とします
    corps : 企業のリスト ... ['A', 'B', 'C', 'D', 'E'] のような形
    candidates : 候補者のリスト ... ['a', 'b', 'c', 'd', 'e'] のような形
    corp_desire_order : 企業の希望順位リスト ... {'A': ['b', 'e', 'd', 'c', 'a'], 'B': ['c', 'a', 'e', 'd', 'b'],}...のような形
    candidate_desire_order : 候補者の希望順位リスト ... {'a': ['B', 'D', 'A', 'E', 'C'], 'b': ['C', 'A', 'D', 'E', 'B'], ...}のような形
     
    # 最終的に出力する変数
    corp_to_candidate : 企業と候補者のマッチングリスト ... {'a': 'D', 'b': 'C', ...}のような形
    candidate_to_corp : 候補者と企業のマッチングリスト ... {'D': 'a', 'C': 'b', ...}のような形
    

    この7つの変数を基に、プログラムを書くと以下のようになります。

    def gale_shapley(corps, candidates, corp_desire_order, candidate_desire_order, corp_to_candidate, candidate_to_corp):
       # 1. 候補者とマッチングしてない企業1社が、希望順位が一番高い候補者にマッチングを申し込む
       for corp in corps:
           if corp not in corp_to_candidate: free_corp = corp
       candidate = corp_desire_order[free_corp][0]
     
       # 2-B. 1で選ばれた候補者がマッチング中であった場合、現在マッチング中の企業とマッチングを申し込まれた企業の希望順位を比較する
       if candidate in candidate_to_corp:
           another_corp = candidate_to_corp[candidate]
          
           # 2-B-b. もし、マッチングを申し込まれた企業の方が希望順位が上なら、候補者は現在のマッチングを解消し、マッチングを申し込まれた企業とマッチングをする
           if candidate_desire_order[candidate].index(free_corp) < candidate_desire_order[candidate].index(another_corp):
               del corp_to_candidate[another_corp]
               corp_to_candidate[free_corp] = candidate
               candidate_to_corp[candidate] = free_corp
               corp_desire_order[another_corp].pop(0)
     
           # 2-B-a. もし、現在マッチング中の企業の方が希望順位が上なら、候補者はマッチングを申し込まれた企業を断る
           else:
               corp_desire_order[free_corp].pop(0)
      
       # 2-A. 1で選ばれた候補者がマッチングしていなかった場合、候補者は企業からの申し込みを承諾する
       else:
           corp_to_candidate[free_corp] = candidate
           candidate_to_corp[candidate] = free_corp
    
    # 3. 1~2を全ての企業と候補者がマッチングするまで繰り返す
    while len(corp_to_candidate) < n:
       gale_shapley(corps, candidates, corp_desire_order, candidate_desire_order, corp_to_candidate, candidate_to_corp)
    

    書いてみると意外とシンプルですよね。
    皆さんも是非他の言語でチャレンジしてみてください!

    最後に

    今回、テックバトルを通して、「新しいアルゴリズムを知れたから良かった!」というよりは、エンジニア同士の交流が何よりも貴重で楽しかったです笑

    レバレジーズのエンジニア組織は、技術力だけではなく、今回のイベントのようなエンジニア同士の交流も大事にする組織です。日頃からこのようなイベントを行うことで部署間の壁がなく、気軽に悩みを相談出来る環境が常に整っており、とても働きやすいと日々感じています!

    皆さんもこんなレバレジーズで一緒に働いてみませんか? レバレジーズに少しでも興味を持っていただけた方は、是非下記リンクを覗いてみてください!


    leverages.jp

    新卒1年目のエンジニアがUX改善のPDCAを構築し、1年間でCVRを13%改善した話

    はじめに

    こんにちは。21年新卒の益子です。現在私は、看護業界に特化した人材サービス「看護のお仕事」において、求人サイトのUX改善やそれに伴うシステム開発、ディレクションに携わっています。

    看護のお仕事では、サイト改善において指標となる定量データだけでなく、「看護師さんにとっての使いやすさ」などの定性データも重視して改善を進めています。

    このようなサイト改善の取り組みに対しては、事業部としても知見が浅く、常に手探りの状況でしたが、私はユーザビリティテスト・デプスインタビューなどのUXリサーチやその結果を元にした改善施策の立案・実装・効果検証までを一貫して担当し、1年間でCVR(※1)を13%改善しました。

    (※1). Webサイトへのアクセス数、またはアクセスしたユーザー数のうち、コンバージョンに至った割合を示す指標。転職エージェントサービスが運営する求人サイトの場合、求人応募やサービス登録に至ったユーザーの割合を示す。

    この記事ではエンジニアとして新卒入社してからUX改善に取り組み始めたきっかけ、取り組みの中でぶつかった壁、どのように問題を解決しCVR改善という成果に結び付けたかについてまとめたいと思います。


    きっかけは「ユーザーが迷いながら求人を探している」現場に遭遇したこと

    私が求人サイトのUX改善に取り組み始めたきっかけは、ユーザビリティテストに同席した際に希望する求人を探せずに戸惑っているユーザーの様子を目の当たりにしたことでした。

    ユーザビリティテストでは被験者に対し「東京都渋谷区で、希望する条件に一致する求人を検索して表示して下さい」といったタスクをお願いし、モデレーターの目の前でタスクを実行してもらいます。被験者の実際の操作を通して、スムーズに求人を検索できるか、希望条件に一致する求人を絞り込めるかなど、ターゲットユーザーにとってのプロダクトやサイトの使いやすさを評価していきます。

    初めて同席したユーザビリティテストで見たものは想像と大きく異なり、「希望条件に対して、サイト上のどの絞り込み項目が適切なのか迷っている」「絞り込み条件を増やしすぎてしまい希望する求人を見つけることができない」というユーザーの様子でした。

    私はこの体験を通し「どれだけ努力して実装してもユーザーにとって使いにくいようでは無意味。看護師さんにとって使いやすい求人サイトを作るために開発業務以外もできるようになりたい。」と思うようになりました。


    求人サイトのUX改善を進めるため、自ら提案して3つの取り組みを開始

    ユーザーにとって使いやすいプロダクトを作るためには、改善施策の全ての議論をユーザー起点で行う必要があると考えました。さらに具体的に言うと、サイト改善に関わるメンバー全員が、架空のユーザー像を元に議論し合うのではなく、実際にユーザーに触れて得た一次情報を元に議論できる状態になる必要があると考えました。

    ユーザーの一次情報から議論を行えるチームになるため、以下の3点を提案し、チームで実行しました。

    1. デプスインタビューを実施し、看護師転職において求人サイトがどのように使われているのかを調査する

    まず看護師転職の課題について解像度を上げるためにデプスインタビューを行いました。

    デプスインタビューとは、特定テーマについて対象者と1対1でインタビューを行う定性調査手法です。対象者の表面的な行動・思考だけではなく、行動・思考の背景にある事情や、対象者ですら言語化できていない課題を明らかにすることができます。

    デプスインタビューでは、調査したいテーマに合わせた対象者の選定が調査の成果を左右します。私たちは調査結果を求人サイトの改善施策に落とし込みたかったため、「転職検討中であるが、転職エージェントサービスの利用経験がない人」を対象とし、5名に対し調査を実施しました。

    転職エージェントサービスの利用経験がある人は、求人サイトの使いやすさよりも、過去の利用体験を検討材料としてサービスを判断する可能性があります。そのため、転職エージェントサービスの利用経験がない人にインタビューを実施することで、転職を通して得たいものに対し、どのように求人サイトを利用するのか・転職サービスに何を期待するのかを明らかにしようと試みました。

    2. オープンタスクでユーザビリティテストを行い、現状のサイトを再評価する

    デプスインタビューを通して得られた示唆は大きく2点でした。1点目は転職を通して得たいものやその理由、2点目は求人サイトを「単純な求人検索ツールとして使用するのではなく、その求人サイトを運営する転職エージェントサービスを比較するために使用している」という実態です。

    その示唆に対し、オープンタスクでユーザビリティテストを行い現状のサイトを再評価しました。単純にタスクをお願いして実行してもらうのではなく、「このページではどのような情報が得られましたか?」「次はどのように操作したいと思いますか」「サイトを離れた後は何をしたいと思いますか?」というオープンタスクでサイトの評価を行いました。

    これにより「なぜページ内のコンテンツが読み飛ばされているのか」「なぜ特定のボタンが頻繁にクリックされるのか」など、なぜユーザーはこのように操作するのかというところまで定性データを収集することができました。

    3. 改善施策の起点となる仮説を、ユーザーを起点にして言語化する

    デプスインタビューやユーザビリティテストでユーザーに対する解像度が高まり、チーム内で改善施策の議論が活発になりました。

    その議論を”ユーザー起点”という観点で質のよいものにするため、「タスク管理ツールに改善施策案を起票する際、ユーザーを起点に仮説を言語化する」というルールを作り、徹底しました。

    具体例を挙げると「求人応募ページの応募ボタン文言が〇〇であることにより、ユーザーは違和感を覚えて求人応募を完了することができないのではないか」という起票内容です。

    このルールを履行することで、改善施策においてユーザーを軸に仮説を立てることが仕組み化され、常にユーザーの一次情報を材料とした議論を行うことをメンバー全員に定着させることができました。


    施策リリース後にぶつかった「ABテストで悪化傾向が出た時に仮説が途切れてしまう」という問題

    前述の3点の取り組みにより、サイト改善における議論の質が高まりました。しかし、いくら議論を重ねても、すべての施策で改善傾向が出るとは限りません。

    実際に、チームメンバー全員で考え抜いた施策をリリースした際、CVRの悪化傾向が出てしまったことがありました。

    サイト全体のコンバージョンへのインパクトが大きい求人詳細ページのUIを大幅に変える施策を打ち出した時のことです。プロトタイピングでのユーザビリティテストを何度も実施しながら施策内容のブラッシュアップを重ねていたものの、実際に施策をリリースしてABテストを実施したところ、CVRが大きく悪化する結果が出てしまいました。

    インタビューやユーザビリティテストを重ねながらメンバー全員で考え抜いて打ち出した施策だったため、各数値指標の悪化を目の当たりにした時には、次の打ち手を見失う状況に陥りました。


    問題を解決するために、リリース直後から定量・定性両方のデータで施策を振り返る

    この状況に陥った根本的な原因は、定性データから打ち出された施策効果を検証する仕組みが整っていなかったことでした。

    Aパターン・BパターンそれぞれのCVRの結果や各ページの遷移率などの定量的な施策効果の検証に関しては以前から仕組み化されていましたが、「施策内容によってユーザーの操作はどのように変化したか」「なぜ変化したか」といった定性的な施策効果を検証する仕組みはなく、1から構築する必要がありました。

    私はこの状況を改善するため、リリースした施策を定量・定性両方のデータで検証することを提案し、チームメンバーで実行に移しました。UI上、施策の要点となるすべての箇所にクリックログを実装してクリック率やその後のコンバージョンを計測しながら、ユーザビリティテストも並行して実施し、操作の背景にあるユーザーの心理を探るという検証方法です。

    これにより施策の仮説が途切れることがなくなり、1つの仮説から後続施策・新しい仮説が生まれる状態を作ることができました。

    前述した求人詳細ページのUIを大幅に変える施策も、定量・定性両方のデータで検証することで、求人応募ページへ遷移するボタンが致命的な悪化要因となっていたことを発見することができました。その後の追加施策でボタン部分を改修し、98%以上の有意差でマイクロコンバージョンの完了率を29%改善することができ、CVRを向上させることができました。


    結果、どのような変化があったか

    これらの取り組みを継続した結果、ユーザーを起点とした施策立案・施策のリリース・定量と定性の両方を駆使した効果検証・追加のブラッシュアップ施策の実行まで、UX改善のPDCAサイクルを回すことができるようになりました。

    その変化は数値成果に表れ、1年間で最重要指標であるCVRを13%改善することができました。

    また、UX改善のPDCAサイクルは途切れず、施策の確度も向上しています。施策立案においてはこれまでに実施していなかった新しい定性調査手法やヒューリスティック評価を取り入れ、施策実行においては開発メンバーが着手優先度を柔軟に調整することでABテストの実行数が増加しています。

    サイト改善に関わるメンバー全員が、前年度以上の成果を出そうとモチベーション高く取り組んでいます。


    終わりに

    昨年度一年間の取り組みは、自分自身の今後にも大きな変化を与えたと思っています。

    開発職として入社したものの、「ユーザーが迷いながら操作している」という現場に遭遇したことで、それまで実務で取り組んだことがなかった「UX」という分野に取り組むきっかけができました。

    また開発業務の枠を超えて、リサーチ・施策立案・施策の実装とリリース・効果検証の全てのプロセスで提案と実行を行う中で、レバレジーズが顧客体験の改善という観点で優れているところ・未熟なところの両方が見えてきました。

    上記の経験を通し「エンジニアでありながらマーケティング分野にも職域を広げたい」という思いが「レバレジーズの顧客体験の改善を自分が牽引できるようになりたい」という思いに変わりました。それを実現するために業務内容も少しずつ変わり、昨年度はマーケターが持っていた「UX改善によるCVRの向上」というミッションが、今年度からは自分のミッションとして与えられました。

    レバレジーズには社員一人ひとりが事業や担当業務の課題を見つけ、新しい取り組みを提案する・責任をもって実行するという行動を評価する文化があります。そこには年次はもちろん、職種の枠も関係なく、その文化があったからこそ自分の目標に近づくための働き方ができていると感じています。

    レバレジーズには事業やプロダクトの成長に、開発の職域を超えて取り組んでいるエンジニアが多く在籍しています。開発職として技術を磨きつつ、事業やプロダクトの成長に幅広く関わっていきたいという方は、カジュアル面談などでレバレジーズのエンジニアと話してみませんか?きっと面白い発見があると思います。

    ご興味がある方は是非こちらからエントリーをお願いします!


    leverages.jp