Upgrade to Pro — share decks privately, control downloads, hide ads and more …

大規模プロダクトにおける フロントエンドモダナイズの取り組み紹介

大規模プロダクトにおける フロントエンドモダナイズの取り組み紹介

2025/2/19に開催したRecruit Tech Conference 2025の可児の資料です

Recruit

March 03, 2025
Tweet

More Decks by Recruit

Other Decks in Technology

Transcript

  1. 可児 潤也 読書、2歳の我が子との公園遊び 経歴 / Career 2018年にリクルートにキャリア採用入社。 横断開発組織でフロントエンドエンジニアとして 多領域のリプレイスや初期開発を多数経験。 現在は主務を美容領域に変えて、『サロンボード』のフ

    ロントエンドチームのリーダーをやりつつ、新規大型案 件の顧客カルテプロジェクトの開発PLも行なっている。 趣味 / Hobbies プロダクトディベロップメント室 販促領域エンジニアリング2ユニット ビューティー領域エンジニアリング部 ビューティープロダクト開発1グループ
  2. サロンボードの内部品質 HTML/CSS/JS は、 - 型 なし - Linter なし -

    自動テストなし - クラスに命名規則なし 影響範囲の調査や確認のために以下を実施 - 重厚な設計書レビュー - 膨大な作業チェックリスト - 多重な動作確認テスト 内部品質が良いとは言えない状態 外部品質を 担保するために どうしてるの? 開発生産性が 低い状態 内部品質が 低い状態 内部品質が低いと影響範囲調査に時間がかかり開発生産性が下がる 開発生産性が低いと内部品質を高めるための時間が取りづらい 開発生産性が低いと、 案件のリリース頻度も下がりがち😭
  3. 妥協しない技術選定 No. 技術 選定理由 1 Next.js (Pages Router) ゼロコンフィグでの開発が採りたいメリット。 社内実績もあったため

    Next.js を採用。 ちなみに当時はまだ AppRouter はリリース前だったため Pages Router の選択肢しかなかった。 2 GraphQL (Relay / Yoga) 複雑でかつ重量感のあるデータを扱う UI コンポーネントを作るた めに、柔軟なデータフェッチができる GraphQL を採用。バック エンド側の REST API を BFF の GraphQL でアグリゲートする方 式を選定。 3 TypeScript 型付きでのデファクトスタンダード 4 CSS Modules CSS in JS よりもピュアな CSS の方が安定性が高いと判断。 Tailwind は習熟度的に学習コストが高く採用は見送り。 5 Jest/ESLint/Storybook 等 保守性と開発効率を高めるため、開発環境を整備。 ※ 2022 時点の技術選定であるため、現在はもう少し異なる技術スタックを選ぶ可能性があります。
  4. GraphQL 成熟度モデル GraphQL 成熟度モデル (GraphQL maturity model) - Meta社の Jordan

    Eldredge 氏が X(旧Twitter)で紹介 - GraphQLの作者視点で、どの程度使いこなせているかを測る 13の指標がある サロンボードでの達成状況 - 13指標のうち 11指標 を達成 - defer, stream, subscription は未実装 - 達成している指標も完璧ではないが意識して取り組んでいる - GraphQL 活用の際に役立つ考え方が多いので一読をおすすめ A GraphQL Maturity Model - Jordan Eldredge https://jordaneldredge.com/notes/e27872d4-e70c-4d25-bb04-ad74bd025b40/ 1. クエリの作成やデータの追加・ 削除ができる 2. 型やフィールドがドキュメント 化されている 3. 型システムの導入 4. スキーマをデフォルトで null 許 容する 5. Node の仕様を採用している 6. 正規化された形式でデータを保 存している 7. Connections 仕様を実装してい る 8. フラグメントコロケーションし ている 9. 単一のクエリにまとめている 10. @defer / @stream をサポート している 11. サブスクリプションを構築して いる 12. エディタのランゲージサーバー を活用している 13. エディタ上でコードの定義ジャ ンプができる 簡易的な訳 ⭕ ⭕ ⭕ ⭕ ⭕ ⭕ ⭕ ⭕ ⭕ ⭕ ⭕ ❌ ❌
  5. GraphQL の基本概念 type User { id: Int! name: String!} type

    Query { user(id: Int!): User! } スキーマの定義 - 扱うデータの型を定義する クエリの実装 - 必要なデータを指定して取得する 例: query { user(id: 1) { name } } 例: リゾルバの実装 - クエリで指定されたフィールドに対してデータを返す user: () => return { name: "Hoge" } 例: 特徴 - 柔軟なデータフェッチが実現でき、クライアントが必 要なデータだけ取得できる (Overfetch/Underfetch の解消) - スキーマによって仕様が明確になるとともに、 TypeScript の型も自動生成可能 - フラグメントコロケーションにより、 UI コンポーネン トの実装と同じ場所に必要データを定義できる Page Component A Component B コンポーネントが必要としているデータが、 コンポーネント内で定義されているので可読性が高い! id が欲しい! name が欲しい! ビルド時に自動的に id と name を取得するクエリになる
  6. GraphQL の基本概念 type User { id: Int! name: String!} type

    Query { user(id: Int!): User! } スキーマの定義 - 扱うデータの型を定義する クエリの実装 - 必要なデータを指定して取得する 例: query { user(id: 1) { name } } 例: リゾルバの実装 - クエリで指定されたフィールドに対してデータを返す user: () => return { name: "Hoge" } 例: 特徴 - 柔軟なデータフェッチが実現でき、クライアントが必 要なデータだけ取得できる (Overfetch/Underfetch の解消) - スキーマによって仕様が明確になるとともに、 TypeScript の型も自動生成可能 - フラグメントコロケーションにより、 UI コンポーネン トの実装と同じ場所に必要データを定義できる Page Component A Component B コンポーネントが必要としているデータが、 コンポーネント内で定義されているので可読性が高い! id が欲しい! name が欲しい! ビルド時に自動的に id と name を取得するクエリになる
  7. GraphQL リゾルバ - REST API は各エンドポイント (ex. /users/:id) が固定のレスポンスを返す -

    GraphQL は1つのエンドポイントからクライアントが必要なデータだけを取得できる - GraphQL リゾルバは複数のリゾルバが連携してオペレーションを解決している - クエリは最上位のリゾルバから処理される - スキーマで定義された各フィールドのリゾルバへと処理が引き継がれる - デフォルトのフィールドリゾルバを適用すれば、処理を明示的に記載しなくてもよい query { user(id: 1) { name posts { title } } } GraphQL クエリ クライアント側 サーバー側 クエリに対する 最上位リゾルバ user(id: 1) name posts User Post User を解決する リゾルバ Post を解決する リゾルバ title デフォルトのフィールドリゾ ルバを使って、name, posts の値をここで作って返してし まっても良い デフォルトのフィールドリゾル バを使って、posts の値をここ で作って返してしまっても良い それぞれ型の値は、 フィールドリゾルバに作成を引き継ぐ
  8. GraphQL リゾルバの書き方は一つに限らない GraphQL リゾルバの書き方は自由度が高い const resolvers = { Query: {

    user: () => { // User type を返すクエリのリゾルバ return { // `name` が User type と同じなので // `User` 型に name のリゾルバはなくても OK name: "Hoge", } }, }, User: {}, // フィールドリゾルバは省略できる } const resolvers = { Query: { user: () => return { // User type を返すクエリのリゾルバ // User type の値は、 // User type のリゾルバに任せるため何も返さない。 }, }, User: { // User type のリゾルバ実装 name: () => { return "Hoge" }, }, } デフォルトのフィールドリゾルバで解決されている デフォルトのフィールドリゾルバは使わず自分で定義する 書き手:記述量が少なくて済む 読み手:記述が暗黙的になり読みづらい 書き手:記述量が多くて面倒 読み手:記述が明示的になり読みやすい 例:どちらの書き方でも OK
  9. 読み手にとって良いコードの方がメンテナンスしやすい 大規模システムにおいては、読み手側に寄り添ったメンテナブルなコードにすることを意識していくべき 一律 GraphQL リゾルバ-の記載方針としては、デフォルトのフィールドリゾルバは使わない方針に const resolvers = { Query:

    { user: () => { // User type を返すクエリのリゾルバ return { // `name` が User type と同じなので // `User` 型に name のリゾルバはなくても OK name: "Hoge", } }, }, User: {}, // フィールドリゾルバは省略できる } const resolvers = { Query: { user: () => return { // User type を返すクエリのリゾルバ // User type の値は、 // User type のリゾルバに任せるため何も返さない。 }, }, User: { // User type のリゾルバ実装 name: () => { return "Hoge" }, }, } デフォルトのフィールドリゾルバで解決されている デフォルトのフィールドリゾルバは使わず自分で定義する 書き手:記述量が少なくて済む 読み手:記述が暗黙的になり読みづらい 書き手:記述量が多くて面倒 読み手:記述が明示的になり読みやすい 例:どちらの書き方でも OK
  10. GraphQL の記述自由度をあえて減らし型の制約を厳しくする - デフォルトのフィールドリゾルバに頼らせない仕組み作り - GraphQL Code Generator で生成された型定義に対して フィールドリゾルバを全て

    `Required` に型変換にする(`RequiredFieldResolvers`) - 厳密な型チェックを実施するようにすることで、 - 一貫性のある書き方に統一できる - さらにリゾルバに必要なフィールドが漏れていたら型エラーにできる const userTypeResolversObject: UserResolvers<ContextType> = { userId: ({ _response }, args, ctx, info) =>_response.userId, name: ({ _response }, args, ctx, info) => _response.name, ... } const userTypeResolversObject: RequiredFieldResolvers< UserResolvers<ContextType> > = { userId: ({ _response }, args, ctx, info) =>_response.userId, name: ({ _response }, args, ctx, info) => _response.name, ... } リゾルバが漏れていても 型で怒られない リゾルバが漏れていたら型エラーになる! ※実際には、より細かい設計や型のルールがあるが省略
  11. とにかくテストと Lint を拡充させる ロジック ユニットテスト (Jest) UI VRT (reg-suit) 単体

    UI ロジッ ク コンポーネントテスト (Jest) 複数 UI ロジッ ク E2E テスト (Playwright) テストの拡充 ほぼ全てのコードに対して Lint (TS, tsx, CSS, GraphQL Schema, Dockerfile etc.) 既存のルールにないものはカスタムルール化 Lint の拡充 例: - URLパスを直接ハードコーディングできない ようにする - Pages コンポーネントに対するストーリーに は横向きのストーリーを必須にする etc. 大規模ならではの長期に渡る運用保守を見込んでいるために、誰が書いてもなるべく品質を落とさない
  12. モダナイズがもたらす恩恵 - モダナイズによる内部品質の向上で、開発生産性を高めることができる - 変更や修正の影響範囲が明確になったり、デバッグ・レビューが効率化されたり - 開発スピードの向上が恩恵をもたらす場面は、機能改修時が多い サロン ボードの 画面

    機能改修する画面 機能改修しない画面 例:予約詳細画面は予約管理画面における根幹で、さまざまな機能追加時に修正が必要な画面 例:お問い合わせ画面はこれまで改修の必要がなく、今後も基本的に改修が入る予定はない モダナイズされると嬉しい! モダナイズしても嬉しみが少ない
  13. 対象画面の戦略的絞り込み - 逆に高頻度な改善が行われる予定がない画面は、モダナイズの優先度を下げる - モダナイズされないにしても、最低限の EOSL 対応や大掛かりではない形で生産性改善を実施 これからも 高頻度に改善を行っていく画面 一定の価値を提供し終えており

    運用保守フェーズに入っている画面 サロンボード全体 低頻度だが改善を加えることはある画面 モダン リプレイス クラシックな FW に対する 開発生産性改善 EOSL 対応のみ モダン 構築 23% 27% 50% 長期にわたるモダナイズまたはリプレイスを高い効果 が得られるような道のりで進めることができる!
  14. 長期目線で考えるモダナイズ 過去 いま 未来 いまはレガシーと言われてしまう技術スタックも、 もともと約20年前当初はモダンだった 一度モダナイズすれば永続的に モダナイズしなくてもよい? このタイミングで実施するモダナイズは、 あくまでスナップショット的に現時点の

    最新にアップデートしているに過ぎない ❌ ⭕ モダンな技術スタックも、 いずれはモダンではなくなる 我々に必要なのは、 モダナイズされたシステムだけでなく 継続的にモダナイズしていく開発体制も
  15. 開発体制を作り育てる取り組み - 長期目線で見たときには今後も継続的に技術スタックをアップデートしていける開発体制を 構築することが技術スタックのアップデート以上に大事になる - サロンボードでも、過去に一部画面への React 導入にチャレンジした過去がある - しかしその後継続的に

    React 化は行われず、一部画面だけ古い React が残っており、 むしろ技術負債になってしまっている - これはフロントエンド開発体制の継続的サポートが難しかったため - じゃあ今回はどのようにモダナイズを進めているか? 継続的なモダナイズを行う開発体制 モダナイズのための 組織設計 スキル成長を加速する 開発プロセス &
  16. モダナイズのための組織設計 大規模プロダクトにおけるリアーキ・組織設計プロセス エンジニアは事業成長をどう担うか? │RECRUIT TECH CONFERENCE 2024 https://www.youtube.com/watch?v=BaCPYytzeBg&list=PL4yY3G_ooyLXjTjIEVJrpMjqkrqISwciN&index=5 - RECRUIT

    TECH CONFERENCE 2024 にて発表された内容 - プロダクトの位置付けから再定義することでモダン化するための開発体制を新規に設計 - 去年からのアップデートは、SP版専任のチームは現在PC版でもモダナイズを実施できてお り、SP版専任チームからモダンアーキ専任チームとして活躍の幅を広げている💪
  17. スキル成長を加速させるスクラム開発 実はスクラム開発は、スキル成長しやすいフレームワークとしても活用できる デイリー スクラム PRの対面モブレビューを実施 短いサイクルでの 技術面でのフィードバック スクラムイベント 実施したこと 得られたこと

    スプリント レビュー その週の成果を開発環境でデモ。 後から振り返られるように スクショアルバムを用意 エンジニア以外からの 成果物に対するフィードバック レトロ スペクティブ KPT に加えて Thanks の用意 モチベーション向上のための 感謝のフィードバック リリース リリースを機能ごとに分割。 さらに限定店舗にβ版提供 実際のユーザーによる 使用感フィードバックの早期化 周 期 長 短
  18. 大規模プロダクトのフロントエンドモダナイズ サロンボードをフロントエンドモダナイズするために実施していること 1. 品質を低下させない技術スタックモダナイズの取り組み 3. 長期に渡る開発を支える体制を作り育てる取り組み 2. 開発生産性向上のための対象画面の戦略的絞り込み システム 戦略

    体制 妥協しない技術選定を行いシステマチックに品質を低下させない仕組みを導入 … 改修頻度の高い画面から部分的にモダナイズを実施し開発生産性の旨みを享受 … 長期に渡る開発を支えるためにスクラム開発を通じてチーム・メンバーを育成 …
  19. サロンボードをモダン化したことで得られたこと Lint やテストを追加し GitHub Actions での CI/CD 構築によりチェックを自動化 🎉 これまで実現できていなかった

    自動回帰テストを可能に! 開発生産性も上がりリリース頻度も増え、 エンジニア1人あたりの 取り込まれる案件数は 約 2 倍に 🥳 品質 リリース 頻度 開発 生産性 1システムの機能の横展開がコード共通化によ り最短で実装できるようになり、 直近の案件では工数を約 1/4 に削減 🎊 Before After フロントエンドコードに対して Lint や自動テストは存在せず 手動による確認となっており、 回帰テストはできていなかった 低頻度なリリースだった システムのシームレスな横展開が不十 分だったため、機能追加時は4システ ムそれぞれに同様の工数がかかった モダナイズの結果、保守性は高めつつ開発生産性が上がっていることを確認できている! これからもまだまだモダン化は継続💪
  20. モダン化して苦労したことの紹介記事 開発メンバーが記事にしている内容も多数あるのでご参照ください! - 「SP版サロンボード」へGraphQL導入と考察 - https://techblog.recruit.co.jp/article-1146/ - Next.js x Relay

    な GraphQL 環境で Render-as-you-fetch の良さを最大限生かしつつ SSR にも対応したいあなたへ - https://qiita.com/p_irisawa/items/7d945ebe74b18a2cc5ee - Next.jsのバンドル問題を解決するServiceLocatorパターン - https://qiita.com/hkano2022/items/dd08551adf2d14a0a2e3 - 初めての Relay Connection によるページネーション実装 - https://qiita.com/tmmhri/items/aeae9d591d21a29d837f - App Router移行時に0.01%の確率でCSR遷移が404エラーになる - https://oisham.hatenablog.com/entry/2024/04/04/105444
  21. まとめ - 『サロンボード』は多数の機能・画面を抱える大規模プロダクトでありながら高い外部品質を維 持して運用されていますが、内部品質に対しては課題があります。 - それに対してフロントエンドモダナイズを行い内部品質の改善に取り組んでいます。 - 大規模プロダクトへ品質を下げずに長期的なフロントエンドモダナイズを行うために、以下の3 つの取り組みを紹介しました。 -

    1. 品質を低下させない技術スタックモダナイズの取り組み - 2. 開発生産性向上のための対象画面の戦略的絞り込み - 3. 長期に渡る開発を支える体制を作り育てる取り組み - これにより、高品質を保ちながら高頻度の改修リリースを実現し、開発効率向上によるプロダク ト価値を高めることに貢献しています。 - さらに、チャレンジングな技術課題にも積極的に取り組み、今後もプロダクト価値を高める開発 を続けていきます。 - 【宣伝】 明日 Day2 では、『ホットペッパービューティー』 Web のリアーキについての発表があり ます!