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

フロントエンド技術の波を乗り越える!Vue2からReactへの移行とアーキテクチャ設計による堅牢化

 フロントエンド技術の波を乗り越える!Vue2からReactへの移行とアーキテクチャ設計による堅牢化

フロントエンド技術の変化に翻弄されることなく、ソフトウェアのコアを守り抜くことが重要です。今回は、Vue2からVue3への移行検討で課題に直面した事例を紹介します。大規模な書き換えが必要だったため、Reactへの移行とフロントエンドコードのクリーンアーキテクチャによる再設計を行い、コアロジックを守りぬくことができました。フロントエンド技術の変化に左右されないソフトウェア設計の方法を解説します。

クラスメソッド株式会社 リテールアプリ共創部 中野ヨシユキ(@engin_yo)

Yoshiyuki Nakano

July 30, 2024
Tweet

More Decks by Yoshiyuki Nakano

Other Decks in Programming

Transcript

  1. 2 / 70 中野ヨシユキ (@engin_yo) 所属: クラスメソッド 産業支援グループ リテールアプリ共創部 やってること:

    LINE ミニアプリの機能開発、保守、運用 ロール: ソフトウェアエンジニア 趣味: ランニング、自作キーボード 自己紹介
  2. 7 / 70 Vue2は2023年12月末に EOL (End of Life) を迎えていた 参考:

    https://v2.vuejs.org/eol/ 正規ルートだと「Vue2からVue3」 、現トレンドだと「Vue2からReact」にするか、世間では大きく 2つのルートに分かれていた Vue 2 の最新リリースである 2.7.16 は、Vue 2 の最終リリース。2.7 機能の最終的な修正が含まれ ており、さらに、Vue 3 との型の整合性がある 理由1: Vue2のEOL
  3. 9 / 70 Vue2の場合(コンポーネントがthis依存) 理由2: Vue2からVue3の移行のコスト import Vue from "vue/dist/vue.esm";

    new Vue({ el: "#app", data: { message: "This is Vue 2", count: 0, }, methods: { increment() { this.count++; }, decrement() { this.count--; }, }, template: ` <div> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> <button @click="decrement">Decrement</button> </div>
  4. 10 / 70 Vue3の場合(Composition APIによる関数再利用) 理由2: Vue2からVue3の移行のコスト import { createApp,

    ref } from "vue"; import { useCounter } from "./useCounter"; const App = { setup() { const message = ref("This is Vue 3"); const { count, increment, decrement } = useCounter(); return { message, count, increment, decrement, }; }, template: ` <div> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> <button @click="decrement">Decrement</button> </div> `, };
  5. 11 / 70 import { ref } from "vue"; export

    const useCounter = () => { const count = ref(0); const increment = () => { count.value++; }; const decrement = () => { count.value--; }; return { count, increment, decrement, }; };
  6. 14 / 70 システムエントロピーという概念 定義 組織全体で技術スタックが統一されていれば「エントロピーは低い」 バラバラな技術が採用されていれば「エントロピーは高い」 特徴 エントロピー低い状態 システムのエントロピーが低ければ低いほど、システム理解は簡単

    改修のハードルが低く、トイル(手作業)の削減や自動化を推進しやすい エントロピー高い状態 エントロピーが高くなると、システム理解に時間がかかりやすい 自動化や改善のハードルや手間が高まる傾向にある 参考:Satoshi Tajima 「システムのエントロピーをコントロールすることの大切さ」より 理由4: チームの技術スタックをそろえることによる効用
  7. 15 / 70 標準スタック フロントエンド:React、TypeScript バックエンド:Node.js(Express) 、TypeScript インフラ:AWS, Google Cloud,

    AWS CDKやCDKTfなどのIaCサービス 実際に各プロダクト毎の技術スタックをそろえた実際の効果 新しくプロジェクトにはいったオンボーディングのキャッチアップ速度が早い 1つのプロダクトを理解すると、それ以外のプロダクトはそれまでのシステム構成の差異をもと にキャッチアップできる(メンタルモデル形成の後押し) 参考: 「プログラマー脳 ~優れたプログラマーになるための認知科学に基づくアプローチ」 細かいライブラリやビジネスルールは異なるが、各プロダクトの良い部分・悪い部分が明確にわ かる 私たちのチームの例
  8. 17 / 70 ただ、ビックリライトを進めるだけならソフトウェアエンジニア側からみたらにコードを書けるか らうれしい リライトはいい面だけでなくリスクも大きい リグレッションのリスク 見積もりに対する予算超過 ステークホルダーやユーザーにとってのメリットを考える必要もある リライトすることによるステークホルダーの見返りを考える

    例 ビッグリライトする代わりに新機能を追加で入れる(LINEミニアプリに新機能追加) セキュリティ面が補強される最新のアーキテクチャにできる キャッシュ機構を導入しレスポンスが高速化し品質向上 理由6: 今後のビジネス価値への寄与に期待できた
  9. 21 / 70 複数のツールにドキュメントが散らばっていたり、Wikiに大事な情報がのっていなかったり 各種ツール GitHub Wiki Backlog Slack etc…

    できる限りドメイン知識はソースコードをドメインモデルとして構築 ドメインモデルを読めばドメイン知識がわかるようにすることで実際に動いているコードがドキュ メントにもなる 課題2: ドキュメントの散在
  10. 28 / 70 AWS(PRD) AWS(STG) AWS(DEV) mainブランチ(GitHub) featureブランチ(GitHub) Local PC

    AWS(PRD) AWS(STG) AWS(DEV) mainブランチ(GitHub) featureブランチ(GitHub) Local PC DEV・STG環境へデプロイ PRD環境へデプロイ プルリクエストを作成 git push テストコード実行 コードレビュー プルリクエストをマージ プルリクエストをmainにマージ GitHub Actions ワークフローがトリガー DEV環境にデプロイ(cdk deploy) GitHub Actions ワークフローがトリガー STG環境にデプロイ(cdk deploy) デプロイ完了通知 リリースノートを作成 リリースノートをpublish GitHub Actions ワークフローがトリガー PRD環境にデプロイ デプロイ完了通知
  11. 36 / 70 1. フレームワークに依存しない 特定のフレームワークやライブラリに依存しない フレームワークやライブラリをツールとして使用でき、システムをその制約にべったり合わせる ことは不要 つまり、フレームワークは、目的を達成するための手段の1つとしてとらえることができる 2.

    テスタビリティ(テスト容易性)向上 ビジネスルールはUI、データベース、Webサーバーなどの外部要素なしでテスト可能 3. UIに依存しない UIは簡単に変更可能で、ビジネスルールに影響を与えません 例として、Web UI(GUI)をコマンドラインインターフェース(CLI)に置き換えることが可能 クリーンアーテクチャで設計されたシステムの特徴
  12. 37 / 70 4. 特定のデータベースに依存しない OracleやSQL ServerをMongo、BigTable、CouchDBなどのDB が変わってもアプリケーションの機能を維持できる。ビジネスルールはデータベースに依存しませ ん。 5.

    外部API、外部システムに依存しない ビジネスルールは外部の世界について何も知る必要がない https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.htmlより クリーンアーテクチャで設計されたシステムの特徴
  13. 39 / 70 Web フレームワークやデータベースなどのアーキテクチャの外部に位置するツールが古くなった場 合、それらの古くなった要素を最小限の手間で別のツールに置き換えることができる 例: Vue -> React

    React -> Next.js React -> Svelte DynamoDB -> MongoDB 仮にフレームワークやデータベースに結合しすぎたソフトウェアの場合、リライトや大規模なリフ ァクタリングを迫られる 私たちの場合は、今後プロダクトとして成長を維持できると考えたため、このルールに従うことを 決断 ルールに従うことで得られる恩恵
  14. 45 / 70 JSONPlaceholder フェイクデータをオンラインで返してくれるREST APIサーバー サンプルコードのテストやプロトタイプ構築に活用 参考: JSONPlaceholder -

    Free Fake REST API 利用したAPI fetch("https://jsonplaceholder.typicode.com/albums/1/photos") .then((response) => response.json()) .then((json) => console.log(json)); [ { "albumId": 1, "id": 1, "title": "accusamus beatae ad facilis cum similique qui sunt", "url": "https://via.placeholder.com/600/92c952", "thumbnailUrl": "https://via.placeholder.com/150/92c952" }, --- 中略 --- ]
  15. 46 / 70 ディレクトリ構造 . └── src ├── core //

    コアロジック │ ├── domain // ドメイン層 │ │ ├── entities // エンティティ定義 │ │ ├── repository // リポジトリのインターフェース定義 │ │ └── support // API Clientなどのサポート関数の定義 │ ├── infrastructure // インフラ層のインターフェース格納先 │ │ ├── api-client // API Clientの実装詳細 │ │ ├── logger // Loggerの実装詳細 │ │ └── repository // リポジトリの実装詳細 │ ├── usecase // アプリケーションユースケース層 │ └── util // Coreで利用可能なユーティリティ関数 ├── di-container // 依存注入周り │ ├── env-util.ts │ ├── register-container.ts │ └── service-id.ts └── framework // フレームワーク層 ├── cli // CLI │ ├── controllers // Usecase層への橋渡し │ └── main.ts // CLIのエントリポイント └── web // Web ├── presenters // Usecase層への橋渡し ├── EntryPoint.tsx // Webのエントリポイント ├── components // Reactのコンポーネント群
  16. 49 / 70 層 説明 Domain層 ドメインロジックを定義する層。ビジネスルールやエンティティを含む Infrastructure層 外部システムとのやり取りを担当。APIクライアントやデータベースアクセスを含む UseCase層

    ユースケース。ドメイン層とインフラ層をつなぐ Framework層 ユーザーインターフェースを担当。Reactコンポーネントを含む レイヤー毎の定義
  17. 51 / 70 "dependencies": { "@vanilla-extract/css": "1.15.3", "inversify": "6.0.2", "react":

    "18.3.1", "react-dom": "18.3.1", "react-lazy-load-image-component": "1.6.2", "react-router-dom": "6.25.0", "swr": "2.2.5", "zod": "3.23.8" }, "devDependencies": { "@inquirer/prompts": "5.1.2", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@types/react-lazy-load-image-component": "^1.6.4", "@vanilla-extract/vite-plugin": "4.0.13", "@vitejs/plugin-react-swc": "3.7.0", "@vitest/coverage-v8": "2.0.3", "dotenv": "16.4.5", "typescript": "5.5.3", "vite": "5.3.4", "vite-tsconfig-paths": "4.3.2", "vitest": "2.0.3" }, 技術スタック
  18. 52 / 70 Framework層にAtomicDesignのコンポーネントを配置 コンポーネント毎にvanilla-extractでデザインファイルを読み込む運用 TSの型付けができること、CSS Modulesへの移行コストが低そう、デザイナーにいただいたデザイ ンを流用しやすい Framework層にAtomicDesignのコンポーネントを内包 //

    photo-album/src/framework/web/components/templates/AlbumList.css.ts import { style } from "@vanilla-extract/css"; export const albumList = style({ display: "flex", flexWrap: "wrap", gap: "1rem", justifyContent: "center", padding: "1rem", }); --- 以下省略 ---
  19. 53 / 70 UseCaseの戻り値をDTOに履き替えて、ドメイン層の情報がしみださないようにする DTO定義は、以下 DTOでFramework層にCoreの情報が染み出させない // photo-album/src/core/usecase/find-album-use-case-dto.ts import type

    { Album } from "@/core/domain/entities/album"; type AlbumInfo = { id: number; title: string; }; export class FindAlbumUseCaseResponseDto { readonly albums: AlbumInfo[]; constructor({ albums }: { albums: Album[] }) { this.albums = albums.map((album) => ({ id: album.id, title: album.title, })); } }
  20. 54 / 70 DTOでFramework層にCoreの情報が染み出させない // photo-album/src/core/usecase/find-album-use-case-impl.ts export const buildFindAlbumUseCase =

    ({ albumRepository, logger, }: { albumRepository: AlbumRepository; logger: Logger; }): FindAlbumUseCase => { return async () => { logger.debug({ message: "use-case: find-album-use-case-impl" }); const result = await albumRepository.findAll(); --- 中略 --- return { success: true, data: new FindAlbumUseCaseResponseDto({ albums: result.data }), }; }; };
  21. 55 / 70 Repository、UseCase、Domain層に対するユニットテストの実装が可能 自前で作ったダミーオブジェクトを使って、依存性注入させている テストコード導入 // photo-album/src/core/usecase/find-album-use-case-impl.small.test.ts describe("FindAlbumUseCase tests",

    () => { test("should return all albums on successful fetch", async () => { const findAlbumUseCase = buildFindAlbumUseCase({ albumRepository: new AlbumRepositoryDummy({ findAllReturnValue: { success: true, data: [ { userId: 1, id: 1, title: "Album 1" }, { userId: 2, id: 2, title: "Album 2" }, ], }, }), logger: new LoggerDummy(), }); const result = await findAlbumUseCase(); expect(result).toEqual({ success: true, data: { albums: [
  22. 57 / 70 SWRをいれたことによりAPIフェッチで初回は時間がかかるものの、キャッシュを利用できるよう になった SWRとTanstack Queryで実装比較したが、SWRの方がバンドルサイズが軽量なことやオプションが 少なくシンプルな実装になるためSWRを選定 SWRの導入効果 //

    photo-album/src/framework/web/components/templates/AlbumList.tsx export const AlbumList: React.FC<AlbumListProps> = ({ onSelectAlbum }) => { const { container } = useContext(DIContainerContext); const { data: albums, error: albumsError, isLoading: isAlbumsLoading, } = useSWR({ container }, useFetchAlbums, { suspense: true }); if (isAlbumsLoading) return <Loading />; if (albumsError) throw new Error(`loading albums: ${albumsError.message}`); return ( --- 以下省略 ---
  23. 58 / 70 不十分だったドキュメントを整備したことや、テストコードを書いたことで機能開発や保守での修 正を自信を持てるようになった 保守性の向上 // photo-album/src/core/domain/entities/album.ts import {

    z } from "zod"; const AlbumSchema = z.object({ userId: z.number(), id: z.number(), title: z.string(), }); export type AlbumProps = z.infer<typeof AlbumSchema>; export class Album { // --- 中略 ---- /** * アルバムID * * @example 1 */ readonly id: number; // --- 中略 ---
  24. 64 / 70 それは砕けた定義でいうと、 「開発者のコードが呼び出すものがライブラリで、開発者のコードを呼び出すものがフレームワー ク」 というものだ。 — 中略 —

    フレームワークは開発者のコードを呼び出すので、コードはフレームワークと高度な結合を作る。 ライブラリは一般的に、より汎用的なコードのため、結合の度合いは低くなる — 中略 — フレームワークはアプリケーションの基礎部分であるため、チームは積極的に更新を行わなければ ならない。 ライブラリは一般的にフレームワークよりも弱い結合点を形成するため、更新についてチームはよ りカジュアルに行うことができる 進化的アーキテクチャ「6.5.8 ライブラリのアップデートとフレームワークのアップデート」 教訓4: フレームワークは積極的更新を計画しておく
  25. 68 / 70 Vue.js 公式: Vue 3 移行ガイド React 公式:

    Reactリファレンス Chris Birchall 著: レガシーソフトウェア改善ガイド Robert C. Martin 著: Clean Architecture 達人に学ぶソフトウェアの構造と設計 Neal Ford, Rebecca Parsons, Patrick Kua 著: 進化的アーキテクチャ 絶え間ない変化を支える 参考資料