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

決定的なフロントエンドアーキテクチャがいい

2.8k

 決定的なフロントエンドアーキテクチャがいい

Vue Fes Japan 2026 における、プロダクト開発本部 クラウドサインProduct Engineering部 SWE 辻 佳佑 (t0daaay) による登壇資料です。

【Vue Fes Japan 2026】
https://vuefes.jp/2026/

■ 弁護士ドットコム株式会社 CompanyDeck for Engineers
https://speakerdeck.com/bengo4com/2026-companydeck-for-engineers

■ 採用情報
https://hrmos.co/pages/bengo4/jobs

■ テックブログ:弁護士ドットコム CREATORS’ BLOG
https://creators.bengo4.com/

■ X(Twitter):弁護士ドットコム CREATORS
https://x.com/bengo4_creators

Avatar for 弁護士ドットコム

弁護士ドットコム

October 23, 2026

More Decks by 弁護士ドットコム

Transcript

  1. 例えば、コードレビューでの指摘・提案 Aさん 🙋‍♀️ utils に 切り出し ませんか? Bさん 🙋 composable

    化しませんか? Cさん 🙋‍♂️ データ fetch は ページ層で やりたいです Dさん 🙋 watch より computed で 書きたいです Bengo4.com, Inc. 5
  2. 1. 何をどこに記述・配置するか 必須 〜は〜に置く 例: .vue ファイルには必ず対応するテストファイル(.spec)を置く foo/ 移す 禁止

    〜を〜に置いてはいけない 例: types/ に関数や定数を置いてはいけない bar/ × 置換 〜ではなく〜に置く 例: 1 箇所でしか使わない関数は、共通フォルダではなく 使っているファイルの隣に置く baz/ 👉 同じものが常に同じ場所にある状態に整う Bengo4.com, Inc. 19
  3. 2. 依存の向き 必須 依存は A 層 → B 層の向きに限る A層

    リンタールールでは直接表現できないため、禁止・置換で経路を 塞ぐことで実現する 禁止 C 層 → A 層への依存を禁止 例: components/ や utils/ から、pages/ のコードを import × B層 してはいけない a × b 置換 経路 a ではなく経路 b で依存する 例: 日付ライブラリを直接 import せず、自分たちで用意した 関数を通して使う C層 👉 依存が常に一方向・非循環に整う Bengo4.com, Inc. 20
  4. 3. どう書くか 必須 〜には、〜を書き足す 例: composable が返す状態には readonly() を付けて返す 禁止

    〜という書き方をしてはいけない 例: props を直接書き換えてはいけない 置換 A ではなく B に書き換える 例: 値を代入するだけの watch は、watch ではなく computed で書く 👉 同じ処理が常に同じ形で書かれている状態に整う Bengo4.com, Inc. 21
  5. 「ユーティリティ関数 is 何?」を定義 3 つの条件と、それぞれを満たさない例 🙅 Vue・Nuxt の API や

    composable を使わない function getId() { return useRoute().params.id } API 通信をしない function getUser(id: string) { return fetch('/api/users/' + id) } 関数の外で作った変数を、関数の中で使わない let count = 0 function nextId() { return ++count } Bengo4.com, Inc. 28
  6. 3 つの条件をリンタールールにする Vue・Nuxt の API や composable を使わない API 通信をしない

    関数の外で作った変数を、関数の中で使わない 👉 これらの条件が 2 つのリンタールールになる 禁止 上記条件を満たさない関数を utils/ に書くことを禁止 置換 上記条件を満たす関数は、.vue や composables/ に書かず、utils/ へ移す Bengo4.com, Inc. 29
  7. 結果 pages 依存 ユーティリティ関数は 配置されない → composables ユーティリティ関数は 配置されない utils

    依存 → ユーティリティ関数が 一元管理される utils/ にユーティリティ関数が一元管理され、 utils/ への依存の向きが決定的になった 👍 Bengo4.com, Inc. 30
  8. Vue の公式ドキュメントを見ると… 状態を持つロジックをカプセル化して再利用するための関数 と定義されている ✏️ Vue アプリケーションの文脈で「コンポーザブル(composable)」とは、Vue の Composition API

    を活用して状態を持つロジックをカプセル化して再利用するための 関数です。 出典:Vue 公式ドキュメント「コンポーザブル」 ja.vuejs.org/guide/reusability/composables.html#composables Bengo4.com, Inc. 32
  9. 1. 非同期処理のローディング状態がある 🙅 .vue に直書き 🙆 composable に切り出し const isLoading

    = ref(false) async function save() { isLoading.value = true await saveTask() isLoading.value = false } → export function useSaveTask() { const isLoading = ref(false) const save = async () => { isLoading.value = true await saveTask() isLoading.value = false } return { isLoading: readonly(isLoading), save } } isLoading を書き換えられるのが save だけになった 👍 Bengo4.com, Inc. 39
  10. 2. 状態が 3 つ以上で複数箇所で切り替えている 🙅 .vue に直書き 🙆 composable に切り出し

    const status = ref< 'idle' | 'loading' | 'error' >('idle') function start() { status.value = 'loading' } function fail() { status.value = 'error' } → export function useStatus() { const status = ref< 'idle' | 'loading' | 'error' >('idle') const start = () => { status.value = 'loading' } const fail = () => { status.value = 'error' } return { status: readonly(status), start, fail } } status を変えられるのが start と fail だけになった 👍 Bengo4.com, Inc. 40
  11. 3. 片方だけ変えるとバグる ref のペアがある 🙅 .vue に直書き 🙆 composable に切り出し

    const keyword = ref('') const page = ref(1) function setKeyword(value: string) { keyword.value = value page.value = 1 } → export function useSearch() { const keyword = ref('') const page = ref(1) const setKeyword = (value: string) => { keyword.value = value page.value = 1 } return { keyword: readonly(keyword), page: readonly(page), setKeyword, } } keyword の変更が setKeyword だけになり、必ず 1 ページ目に戻るようになった 👍 Bengo4.com, Inc. 41
  12. 4. 外部ライブラリを Vue に繋ぎ込んでいる 🙅 .vue に直書き 「+」の付いていない行を、 そのまま .vue

    に書いていた 🙆 composable に切り出し 「+」の行で包み、 状態は readonly で返す + export function useSocket(url: string) { const socket = shallowRef<ReconnectingWebSocket>() const messages = ref<string[]>([]) onMounted(() => { socket.value = new ReconnectingWebSocket(url) socket.value.addEventListener('message', (e) => { messages.value = [...messages.value, e.data] }) }) onUnmounted(() => { socket.value?.close() }) + return { messages: readonly(messages) } + } socket に触れるのが composable の中だけになり、切断し忘れが起きなくなった 👍 Bengo4.com, Inc. 42
  13. 4 つのパターンをリンタールールにする 非同期処理のローディング状態がある 状態が 3 つ以上で複数箇所で切り替えている 片方だけ変えるとバグる ref のペアがある 外部ライブラリを

    Vue に繋ぎ込んでいる 👉 これらのパターンが 4 つのリンタールールになる 置換 上記パターンに当てはまるロジックは、.vue に直書きせず、composable に切り出す Bengo4.com, Inc. 43
  14. 結果 .vue (pages / components) 4 パターンに当てはまる ロジックは書けない composables 切り出す

    → 4 パターンに当てはまる 状態を持つロジックが 一元管理される 状態を持つロジックの代表 4 パターンは .vue に書けなくなり、 composable に処理がカプセル化されるようになった 👍 Bengo4.com, Inc. 44
  15. 結果 app/ ├── components/ ├── composables/ ├── utils/ └── pages/

    ├── index.vue ├── _components/ ├── _composables/ ├── _utils/ └── tasks/ ├── index.vue ├── _components/ └── … 複数の page から使われているものだけ index.vue でしか使われていないものだけ tasks/index.vue でしか使われていないものだけ コードの利用範囲が、置き場所から読み取れるようになった 👍 Bengo4.com, Inc. 47
  16. その他 pages 関連で取り決めたルール例 他にも以下のようなルールをリンタールール化した 置換 禁止 禁止 page は 1

    ファイルではなく、 1 ディレクトリ 1 ページで切る データフェッチは ページ層でのみ行う 別 page の _ ディレクトリを import しない pages/tasks/ _components/ API × pages/tasks.vue × ◦ pages/tasks/index.vue index.vue _components/ × pages/other/ _components/ Bengo4.com, Inc. 48
  17. 結果 app/components/ └── table/ ├── BaseTable.vue ├── BaseTableContent.vue ├── BaseTablePagination.vue

    ├── _composables/ └── _utils/ ディレクトリ名 = 公開コンポーネント名 外から使えるのはこれだけ 内部実装。この中でしか使われていない コンポーネントの置き場所・ディレクトリ名・公開範囲が、機械的に一意に決まるようになった 👍 Bengo4.com, Inc. 51
  18. 状態を外から書き換えさせない 必須 composable が返す ref には readonly() を付けて返す const count

    = ref(0) const increment = () => { count.value++ } return { count: readonly(count), increment } count を書き換えられるのが increment だけになり、 外からは読むことしかできなくなった 👍 Bengo4.com, Inc. 54
  19. 計算できる値を状態として持たない 置換 値を代入するだけの watch は、watch ではなく computed で書く 🙅 状態が

    2 つできる const total = ref(0) watch(items, (v) => { total.value = sum(v) }) 🙆 計算で出せる値は computed const total = computed( () => sum(items.value) → ) 宣言的に定義したので、total は常に items の合計と一致するようになった 👍 Bengo4.com, Inc. 55
  20. これで人間がしていた指摘・提案を… Aさん 🙋‍♀️ utils に 切り出し ませんか? Bさん 🙋 composable

    化しませんか? Cさん 🙋‍♂️ データ fetch は ページ層で やりたいです Dさん 🙋 watch より computed で 書きたいです Bengo4.com, Inc. 56
  21. リンターが代わりにするようになった リンター 🤖 utils に 切り出し ませんか? リンター 🤖 composable

    化しませんか? リンター 🎉 🤖 データ fetch は ページ層で やりたいです リンター 🤖 watch より computed で 書きたいです Bengo4.com, Inc. 57
  22. 機械的判定の可否 機械的に判定可 機械的に判定不可 事実の判定 意味の判定 これまで話してきた内容 何が 1 つの機能(feature)か .vue

    の隣に .spec が無い 単位 utils/ で Vue を使っている 粒度 watch の中で代入している 同一性 Bengo4.com, Inc. 61
  23. ユーザーフロー図 ユーザーの行動と UI の遷移をモデル化 開始 タスク一覧を開く 目当ての タスクはある? はい チェックを付ける

    完了と表示される 終了 はい いいえ 絞り込み条件を入力する 見つかった? いいえ +新規作成を押す タイトルを入力する 保存する 終了 Bengo4.com, Inc. 70
  24. デザインモック UI の構造・配置・見た目をモデル化 🔍 キーワード / 状態 / 担当者で絞り込み ✓

    + 新規作成 請求書レビュー 進行中 契約書ドラフト 未着手 監査資料の準備 完了 田 佐 Bengo4.com, Inc. 71
  25. コロケーションで分類できたこと(事実) 機械的に 2 分類されている app/ ├── components/ ├── composables/ ├──

    utils/ └── pages/ ├── index.vue ├── _components/ ├── _composables/ ├── _utils/ └── tasks/ ├── index.vue ├── _components/ └── … 「複数の箇所から使われる feature」か 「ドメインを持たない汎用モジュール」のどちらか 「page で使われる feature」がカプセル化される 「page で使われる feature」がカプセル化される Bengo4.com, Inc. 75
  26. コロケーションで分類できなかったこと(意味) 責務が 2 つある状態。 ここではドメインを持たない汎用モジュールだけ管理したい。 app/ ├── components/ ├── composables/

    ├── utils/ 「複数の箇所から使われる feature」か 「ドメインを持たない汎用モジュール」のどちらか Bengo4.com, Inc. 76
  27. 同じ feature が 2 つのデザインに現れる例 🔍 キーワード / 状態 /

    担当… + 新規作成 進行中 請求書レビュー 田 🔍 キーワード / 状態 / 担当… + 新規作成 最近作成したタスク キックオフ資料の共有 未着手 作成 9/16 請求書レビュー 進行中 田 作成 9/15 契約書ドラフト 未着手 作成 9/14 未着手 契約書ドラフト 完了 ✓ 監査資料の準備 佐 タスク一覧 タスク作成履歴 タスク検索 feature タスク作成 feature Bengo4.com, Inc. 78
  28. Nuxt Layers に分類 Nuxt Layers は Nuxt アプリと同じ構造を外に持つことができる仕組み。 複数の箇所から使われる feature

    を管理するために実際に使用した。 layers/ ├── taskCreate/ │ ├── nuxt.config.ts │ └── app/ │ ├── components/ │ └── composables/ └── taskSearch/ └── … タスク作成 feature タスク検索 feature Bengo4.com, Inc. 79
  29. 結果 app/ ├── components/ ├── composables/ ├── utils/ ドメインを持たない 汎用モジュール

    └── pages/ ├── index.vue ├── _components/ ├── _composables/ ├── _utils/ page で使われる feature layers/ ├── taskCreate/ │ ├── nuxt.config.ts │ └── app/ │ ├── components/ │ └── composables/ └── taskSearch/ └── … 複数箇所から 使われる feature └── tasks/ ├── index.vue ├── _components/ └── … page で使われる feature feature をカプセル化して、影響範囲と依存関係を閉じ込めることができた 👍 Bengo4.com, Inc. 80