18.5K Views
April 08, 23
スライド概要
もっと開発を早くできそう、もっと品質担保のためにできることがありそう、でもやるべきことが多すぎてどこがボトルネックになっているのかの調査も難しい。いつか余裕ができた際には、この「もやもや」を脱してより解像度が高い状態を目指し、改善を進めていきたいものの、時間がたてば立つほど、この「もやもや」は増えていってしまう。
HireRoo でも、少数のエンジニアでフロントエンドとサーバサイドの両方を担当して開発を行っており、開発速度の遅さや、品質担保の維持に漠然とした課題感、つまり「もやもや」を抱えていたことがありました。
本登壇では、HireRoo が抱えていた「もやもや」を技術的な負債として解像度を上げ、それらの負債に対して具体的にどのような対応を行ったか、アーキテクチャをどう変えて負債を生みにくくしたか、それらの対応にどの程度のコストがかかったのか、実際に経験した内容をお話します。
鋼の意思で実施した、 技術的負債解消のための リアーキテクチャ @Himenon 1
イントロダクション 自己紹介 姫野 滉盛 @Himenon SNS ● ● GitHub: Himenon Twitter: himenoglyph エンジニアリング領域 ● Webフロントエンド(メイン) ● 開発支援ツールを作るのが趣味 前職 ● 株式会社ドワンゴ 代表的なOSS ● Himenon/openapi-typescript-code-generator © HireRoo, Inc. 2
Agenda 1 フロントエンドの開発体制と課題 開発体制 & 開発者のスキル傾向 2 課題解決に向けたリアーキテクチャ 何をリアーキテクチャするか? 従来の不具合の発生とトラブルシューティング 課題の精査 責務分離の戦略 現在の不具合の発生とトラブルシューティング 3 持続可能なアーキテクチャへ コードテンプレートを作成 UIの初期化のテスト 依存関係のテスト 4 開発者のスキルアップ支援 UIコンポーネントの名前空間のテスト
イントロダクション 最近のフロントエンドの開発事情① 初期 リリース後 ● ● ● 技術選定 要件定義 実装 壊れやすい ● ● ライブラリのバージョンアップで壊れる 実装とスタイルが密結合なのでちょっとの変更があ ちこちに波及する 長期的に保守する ● ● 機能が減ることはなかなかない 開発当時はベストプラクティスでも時が経てばベス トではなくなる 全体の実装量と開発者の人数の乖離していく 考えることが多い ● © HireRoo, Inc. ● ● 追加実装 修正 4
イントロダクション 最近のフロントエンドの開発事情② 開発を続ける限り戦いは続く 実装の老朽化(いわゆる技術的負債)vs 開発可能な時間と実装量 技術的負債と戦う理由 ● ● 機能の追加・変更の難易度を上げる 不具合発生時のトラブルシューティングに時間がかかる どうやって技術的負債を解消して行くか ● 問題の解像度を上げ、分解し、問題を局所的になるようにしていく → これについてHirerooでの事例を紹介します © HireRoo, Inc. 5
01 フロントエンドの開発体制と課題
フロントエンドの開発体制と課題 ハイヤールーの開発体制 & 開発者のスキル傾向 機能ごとに一貫して開発・保守を行う ● インフラ・バックエンド・フロントエンドのような明確な区切りはない全部やる 開発者のスキル傾向 ● インフラ・バックエンドのほうがフロントエンドよりも得意 フロントエンドの技術スタック ● React + TypeScript + 色々 © HireRoo, Inc. 7
フロントエンドの開発体制と課題 ソフトウェア開発で必ず発生すること バグ修正 © HireRoo, Inc. 機能変更 機能追加 8
フロントエンドの開発体制と課題 従来の不具合の発生とトラブルシューティング Backendは疎結合なので 不具合が機能単位・MS 単位で分解できる Backend 機能A =開発者A 機能B =開発者B 機能C =開発者C 機能D =開発者D 不具合 不具合 不具合 不具合 不具合 Frontend 不具合 不具合 密結合な状態なので特定の機能の不 具合が別の機能の不具合に影響お及 ぼす(実際はそうでなくてもユー ザーにそう見えるケースも) © HireRoo, Inc. 不具合が発生した場合、報告のあった機能から 調査= 担当者が機能開発者に。 → コードを読めば読むほど実は別の機能が原 因なこともしばしば。 9
フロントエンドの開発体制と課題 現状のフロントエンドの開発体制・アーキテクチャ下での問題点 ● 不具合の根本原因に当たりをつけるのが難しい ○ いろんな機能のコードを紐解きながらトラブルシューティングをする必要がある ● 機能 ○ ● 追加・修正が針に糸を通すような作業 他の機能を壊さずに追加するために針の穴に糸を通すような作業が続く 変更の影響範囲を特定しにくい ○ 機能ごとのコンテキストが巨大なファイルの中に複数入り込んでいるため、正しい変更か自信が持てない サービスが成長すればするほどコードの変更・追加の難易度が高くなる ここに終止符を・・・! © HireRoo, Inc. 10
02 課題解決に向けたリアーキテクチャ
課題解決に向けたリアーキテクチャ 何をリアーキテクチャするか? 大前提として・・・ ● ● 課題がわかっていてもその解き方がわからないとリアーキテクチャ することができない 今解決したい課題の本質がわかっていないと解決したことにならない。 現状のフロントエンド開発はどのようなものなのか振り返る “近年のWeb 開発は、虫食いのテンプレートエンジンにデータをはめ込む方式から、デザインシステムに カタログされたコンポーネント群に、 API から取得したステートを流し込み、それらを「いつ、どこ で、どう」レンダリングするかという課題への最適解を、各位が模索するフェーズとなっている。” 抜粋: 次世代 CSS 仕様が与えるコンポーネント時代の Web への影響 https://blog.jxck.io/entries/2023-01-07/new-css-capabilities-for-component.html ある程度までは道筋がありそうだが、途中から自分たちで探すようになっている...! © HireRoo, Inc. 12
課題解決に向けたリアーキテクチャ ライブラリやフレームワークはいい感じに課題を解決してくれるか? 結論:NO ● 課題 Framework フレームワーク(Next.js、NestJS等)、開発ツール (Storybook等)、テストツール(Cypress、jest)を 導入したからといってすべてが解決するわけではな い 各ツールが解決するスコープには限りがある。 いま自分たちが解決したい課題の解像度を上げ、どのツー ルを使い、どう扱えば解決できるか評価する必要がある。 Library B Library A もう守りきれないよ・・・ © HireRoo, Inc. 13
課題解決に向けたリアーキテクチャ 課題の精査 - 現在の課題の分解能を上げる① - ● 考えられる問題の要因 ○ 責務分離の知識が乏しく、コンテキストの境界が実装上で不明瞭 ● 従来の抑制する方法 ○ レビュー ● 抑止力が機能していない理由 ○ 名前空間が効果的に分離されていないためレビューでそれを指摘できない ○ 機能ごとの境界が見えていないのでレビューで指摘できず、責務の境界線を安易に侵犯する © HireRoo, Inc. 14
課題解決に向けたリアーキテクチャ 課題の精査 - 現在の課題の分解能を上げる② - ● 考えられる問題の要因 ○ フロントエンドの開発はTry & Errorで書いて動いたコードが「動いてしまう」ので、「動いているから大 丈夫」というコードが生まれやすい → 実行時エラーなどで初めて分かるケースも。 ● 従来の抑制する方法 ○ レビュー ● 抑止力が機能していない理由 ○ テストを効果的に利用できていない ○ 設計方針が人によってバラバラ © HireRoo, Inc. 15
課題解決に向けたリアーキテクチャ 課題の精査 - 課題として見えること - 課題 実現方法 責務分離をしやすい状態・責務分離 の恩恵を享受しやすい状態にする ● ● 誰が実装しても同じ構造になるようにコードをある程度自動生成する モノレポ構成に変更し、packageを1つの境界とする 責務分離を行う ● ● ● BFF層を形成する(追加要求) Presentation層 / Container層に分離する 巨大なStoreを機能単位で動くように解体する 責務分離を維持する ● ● ● 初期化テストを行う 依存関係の方向をテストする 利用可能な名前空間を制限する React作者 Dan先生による Presentational and Container Components の記事 https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 © HireRoo, Inc. 16
Hirerooのアーキテクチャを見直す 17
課題解決に向けたリアーキテクチャ Hirerooのリアーキテクチャ前の境界線 Backend MicroService A MicroService B Page 1 Frontend 機能AのComponent MicroService C Page 2 機能BのComponent MicroService D Page 3 機能CとDのComponent 共通化されたUIパーツ © HireRoo, Inc. 18
課題解決に向けたリアーキテクチャ Hirerooのリアーキテクチャ前の境界線 Backend MicroService A トラブルシューティングのとき MicroService B MicroService C MicroService D 特定のページで不具合が発生した場合、そ れに関連するcomponentをたどっていく Page 1 Frontend 機能AのComponent 場合によってはロジックを持っているもの もある。これを変更するとあちこちに影響 が波及する。 © HireRoo, Inc. Page 2(不具合) 機能BのComponent Page 3 機能CとDのComponent CとDの機能が実装されたコンポーネントを 変更すると、Page 3に影響を及ぼす。デグ レの可能性。 共通化されたUIパーツ 19
課題解決に向けたリアーキテクチャ Hirerooのリアーキテクチャ前の境界線 実際はより複雑 React.useEffect関連の不具合 ● ● ● Stateを更新すると期待していないところが変化する すぐにデグレが発生する useMemoを使うべきところをuseEffectを利用している 同じUIだけどロジックが内包されているため、再利用できない ● Storeを使いたいために内部でif分を持っている(依存性を注入すべきところをしていない) 不具合が発生した箇所はTry & Errorを繰り返して修正されている ● Try & Errorの修正の上にTry & Errorの修正がある(n回) © HireRoo, Inc. 20
Hirerooのリアーキテクチャの世界 21
課題解決に向けたリアーキテクチャ Hirerooのリアーキテクチャ後の境界線 Backend MicroService A Page MicroService B Page 1 MicroService C MicroService D Page 2 Page 3 Container層 Frontend Presentation層 Widget 機能AのWidget 機能BのWidget Widget Widget A Widget B Usecase AとBの機能の共通ドメインで利用するUIコンポーネント Primitive © HireRoo, Inc. 機能CのWidget 機能DのWidget Widget C どのドメインにも属さないUIコンポーネント 22
課題解決に向けたリアーキテクチャ 新しいアーキテクチャの責務分離はどのように行われているか? ● ディレクトリ構成 - モノレポを作成する ● Presentation層(Presentational Component) ● Container層(Container Component) © HireRoo, Inc. 23
課題解決に向けたリアーキテクチャ 責務分離の戦略 - モノレポ構成を作成する ● ● pnpm workspaceで実現 import * as Module from “@hireroo/{package name}”; という形式でパッケージ 間の参照が可能。 Container層とPresentation層を次のようなパッケージに収めた ● ● ● @hireroo/backend – @hireroo/app – @hireroo/ui – BFF層 Container層 UI層 @hireroo/appのdependenciesの例 © HireRoo, Inc. ディレクトリ構成 24
課題解決に向けたリアーキテクチャ 責務分離の戦略 - Presentation層(Presentational Component)- ● ● ● UIの責務だけに集中したコンポーネント Atomic Designの構造をベースに Layout/Page/Widget/Usecase/Primitiveという構造を作成 PageとWidgetのみContainer層で利用可能 Layout Page Usecase Primitive Widget @hireroo/uiのディレクトリ構成 © HireRoo, Inc. WidgetはLayoutを持たない。 Container化した後、PageのReact.NodeのPropsに 直接渡す 25
課題解決に向けたリアーキテクチャ Hirerooのリアーキテクチャ後の境界線 - Presentation層 - Backend MicroService A コンポーネント名にドメイン名が入る可能性 があるが、内部の実装はUIの責務に閉じる。 Page MicroService B Page 1 MicroService C MicroService D Page 2 Page 3 Container層 Container層のStoreの更新、APIの通信などは 一切しない。 Frontend Presentation層 Widget 機能AのWidget 機能BのWidget Widget Widget A Widget B Usecase AとBの機能の共通ドメインで利用するUIコンポーネント Primitive © HireRoo, Inc. 機能CのWidget 機能DのWidget Widget C どのドメインにも属さないUIコンポーネント 26
課題解決に向けたリアーキテクチャ 責務分離の戦略 - Container層(Container Component) - 1/2 FetchContinaer ● Container Component データの取得を行う Container ● FetchContainer Container Presentation層との境界 useGenerateProps useGenerateProps ● Presentatino層のInterfaceに沿ってStateをマッピング ● Event Handler中にデータの更新・ビジネスロジックを実装する @hireroo/appのディレクトリ構造 © HireRoo, Inc. 27
課題解決に向けたリアーキテクチャ 責務分離の戦略 - Container層(Container Component) - 2/2 ErrorBoundaryはContainer Componentの単位で設置 ● ロギングのスコープがContainer Component単位で分類される Container Componentは1つのアプリケーションとして完結した単位 ● ● pageのContainerはルーティングに配置される widgetのContainerはpageのContainerに配置される 同じUIかつ振る舞いが異なるContainer Componentも作成できる ● 他Container ComponentのuseGeneratePropsからWidgetが呼び出される Candidate・Employeeといった異なるドメインでContainerが実装される ○ ● ErrorBoundaryはContainer側で差し込む ※ 必要に応じて共有できるコンポーネントも作成可能(shared) WidgetのContainer Componentはpageを跨いで利用できるため、ロジックの再 実装を防げる © HireRoo, Inc. 28
課題解決に向けたリアーキテクチャ Hirerooのリアーキテクチャ後の境界線 - Container層 - Backend MicroService A Page Pageは通信のライフサイクルとStoreを独立し て持つ。Layoutとしての機能を持つことがで き、そこにWidgetを配置することが可能。 MicroService B Page 1 MicroService C MicroService D Page 2 Page 3 Container層 Frontend Widget 機能AのWidget 機能BのWidget Widget Widget A Widget B Presentation層のComponentに対してpropsを 提供する層。通信のライフサイクル、Storeの 更新などの責務に集中する Presentation層 Usecase Primitive © HireRoo, Inc. 機能CのWidget 機能DのWidget Widget C Widgetは通信のライフサイクルとStoreを独立 して持ち、各ページに配置するだけで機能す る単位。 AとBの機能の共通ドメインで利用するUIコンポーネント どのドメインにも属さないUIコンポーネント 29
Hirerooのリアーキテクチャ後の トラブルシューティング 30
課題解決に向けたリアーキテクチャ Hirerooのリアーキテクチャ後の境界線 - トラブルシューティングのとき - Backend MicroService A Page MicroService B Page 1 MicroService C MicroService D Page 2 Page 3 Container層 Widget Frontend 機能AのWidget 機能BのWidget 不具合 Widget A Widget B Widget ビジネスロジックに不具合がある。機能Bの担当者が修正 ErrorBoundaryによって原因箇所がすぐにわかるように Presentation層 Usecase Primitive © HireRoo, Inc. 機能CのWidget 機能DのWidget Widget C AとBの機能の共通ドメインで利用するUIコンポーネント どのドメインにも属さないUIコンポーネント 31
課題解決に向けたリアーキテクチャ Hirerooのリアーキテクチャ後の境界線 - トラブルシューティングのとき - Backend MicroService A Page MicroService B Page 1 MicroService C MicroService D Page 2 Page 3 Container層 Widget 機能BのWidget Widget Widget A Widget B Usecase AとBの機能の共通ドメインで利用するUIコンポーネント UIに表示崩れがある 不具合レポートはPage1/Page2からやってくる。共通 のドメイン単位でコンポーネントが作成されているた め、機能A/Bのどちらかの実装者が修正可能。 Frontend Presentation層 機能CのWidget 機能AのWidget Primitive © HireRoo, Inc. 機能DのWidget Widget C どのドメインにも属さないUIコンポーネント 32
課題解決に向けたリアーキテクチャ Hirerooのリアーキテクチャ後の境界線 - トラブルシューティングのとき 実装面 ● ● ● ● Presentation層とContainer層を構築することでフロントエンドの実装を2つの大きな責務に分離した Presentation層が再利用可能な状態になったことで異なるコンテキストで同じUIを利用できた Container Componentはそれ単体で機能する一つの小さなアプリケーションであるため、ロジックもまるごと 再利用可能なコンポーネントを実装できるようになった。 修正したい場所に対して名前がついているため、実装場所と実装意図がレビュー時に明確にわかり、認知負荷 を低減させる トラブルシューティング面 ● ● Presentation層 / Container層のどちらの問題か区別がつくようになった Container Componentは機能ごと単位で分離され、エラーログもこの単位で送信されるため問題発生時の根本 原因の特定時間が短くなる © HireRoo, Inc. 33
03 持続可能なアーキテクチャへ
持続可能なアーキテクチャへ アーキテクチャを維持を困難にする課題と対策 「長期的に保守する」とは.... ● ● ● 機能が減ることはなかなかない 開発当時はベストプラクティスでも時が経てばベストではなくなる 全体の実装量と開発者の人数の乖離が大きくなる どうやって老朽化に抗うか ● ● 実装の規格化 手動で作業するとアーキテクチャが崩壊しそうな場所は自動化する フロントエンドの実装の老朽化防止のための施策(ガードーレール) ● ● コードテンプレートを作成 テスト ○ 依存関係 ○ 名前空間 ○ UIの初期化 © HireRoo, Inc. 35
持続可能なアーキテクチャへ コードテンプレートを作成 Presentation層 ● ● ● [ComponentName].tsx [ComponentName].stories.tsx [ComponentName].spec.tsx pnpm run ui-generator -o src/widget/HelloWorld -n HelloWorld Container層 ● ● ● FetchContainer.tsx (Optional) Container.tsx useGenerateProps.tsx pnpm run container-generator -o src/widget/shared/HelloWorld -n HelloWorld -ns widget commanderパッケージを利用してCLI化。内部実装はCLIの引数で与え られた情報を元にString Literalでテンプレートコードを生成する単純 な実装。 © HireRoo, Inc. テンプレートコードを作成するための実装 36
持続可能なアーキテクチャへ コードテンプレートを作成 [ComponentName].spec.tsx ● ● Presentation層のコードテンプレート中にあるテスト ファイル 中身はUIコンポーネントのスナップショットを作成 するだけのテスト 実はかなり厳しい設定が入っている ● Snapshot作成時にconsole.warn、console.errorが検知 されたらテストを失敗させる。 ○ keyの設定漏れの検知 ○ ValidateDomNesting Errorの検知 → 例外なくすべてのUIコンポーネントに入っている © HireRoo, Inc. jest.spyOnでconsole.warn/error検出時にErrorをthrowする 38
持続可能なアーキテクチャへ UIの初期化のテスト - [ComponentName].spec.tsx - もう一つの狙い jest --collectCoverageによってカバレッジスコアが循環的 複雑度と類似した指標を表すようになる。 スナップショットテスト用に与えらてたサンプルデータに よってカバレッジスコアの満点にならない場合、何らかの 分岐がコンポーネント内に含まれていることがわかる。 次のようにCoverage Scoreを利用できる。 ● ● 点数の低いコンポーネントほど挙動が不安定である リファクタリング・テストの優先度が高いコンポー ネントとなる Coverageスコア 一覧。 e2eテストでしかカバーできないようなコンポーネント スコアが低い © HireRoo, Inc. 39
持続可能なアーキテクチャへ 名前空間のテスト @hireroo/ui内の書く粒度(page/widget/usecase/primitive)で利用 可能な名前を制限するテスト WidgetやPageにはDomain Keywordが1語以上含まれていなければテ ストが失敗するようになっている。 Domain Keyword Algorithm Answers Form AnswersFormだけでは境界が抽象的すぎる AnswersFormだけの場合、Domain(Quiz/Challenge/SystemDesign)を内包することを許容してしまう Widgetのコンポーネント名は利用シーンが わかるような名前になっている © HireRoo, Inc. 40
持続可能なアーキテクチャへ 依存関係のテスト 2つ利用 ● ● package.jsonのexportsでexport可能なモジュールを制限 ○ → viteがexportsを解釈するため、違反した場合ビルド時が失敗する dependency-cruiserで依存関係をテスト テスト内容 ● ● 基本的なテスト ○ 循環参照の検出 ○ どこからも参照されていない孤立したファイルの検出 カスタムテスト ○ package間の依存 ○ Presentation層 ■ page, widget, usecaseの参照方向を制限 ○ Container層 ■ 各ディレクトリから参照可能なディレクトリを制限 ● 例:CandidateからEmployeeを参照できないように © HireRoo, Inc. 41
持続可能なアーキテクチャへ まとめ ● ● ● 各レイヤーで必要なコードを自動生成する Presentation層(UI)の側のテスト ○ Snapshotテストによって、初期化がerror/warnなく実施できていることを厳密に確認 ■ カバレッジスコアが循環的複雑と同等の指標を表し、リファクタリングの必要性などを示す材料となる ○ コンポーネント名の制限を導入 ■ page/widget/usecase/primitiveなどで利用可能な単語を制限している 依存関係の制限 ○ pacakge.jsonのexportsフィールドを利用して各パッケージから公開するAPIを制限 ○ Dependency-cruiserにより循環参照などを検知している ■ カスタムテストを追加することでアーキテクチャの依存関係をテストで表現している © HireRoo, Inc. 42
04 開発者のスキルアップ支援
開発者のスキルアップ支援 持続可能なアーキテクチャへの続き 問題の要因 フロントエンドの開発はTry & Errorで書いて動いたコードが「動いてしまう」ので、「動いているから大丈夫」 というコードが生まれやすい →リアーキテクチャしてもしなくてもこの問題は発生する 堅牢にシステムを維持するためには開発者のスキルアップを促す必要がある ● ● ● 新しいアーキテクチャに対する理解 デザインパターンやDDDに基づく実装パターン 第一原理がフロントエンドのシステムの中でどのように使われているのか、基礎と応用のマッピング © HireRoo, Inc. 44
開発者のスキルアップ支援 Lunch And Learn やってること ● ● ● ● ● Lunch and Learnという時間を12:00 - 13:00の間で 確保している 話して以外はご飯食べながら聞く会 アーキテクチャのルールや、テクニカルな内容 を紹介していく Google Meetで行っており、録画されている 録画を後から見返すこともでき、新しく入って きた人たちも見ることができる状態にしている → 全員のレベルアップを促す会を開催している © HireRoo, Inc. 45
Take Away 全体のまとめ ● リアーキテクチャを行う前に課題に対する分解能を上げた ● モノレポを導入し、Presentation層とContainerそうに分離した ● アーキテクチャが持続可能な状態にするための施策を実施 ○ テンプレートコードを自動生成した ○ UI ○ ■ 初期化が厳しいSnapshotテスト ■ コンポーネント名を制限 依存関係のテストを実施 ■ package.jsonのexportsを利用 ■ dependency-cruiserでpackage間やディレクトリ間で依存可能な方向を制御 © HireRoo, Inc. 46
ご清聴ありがとうございました 47