[{"rendered_body":"\u003cp data-sourcepos=\"2:1-2:110\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Ftop.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=814b90f11caad399dd018f60d1795dfc\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Ftop.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=814b90f11caad399dd018f60d1795dfc\" alt=\"top.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Ftop.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=5ceefa8b95dc8296ffa0eafe6a91259d 1x\" data-canonical-src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/top.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"4:1-4:8\"\u003e\n\u003cspan id=\"tldr\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#tldr\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eTL;DR\u003c/h2\u003e\n\u003cul data-sourcepos=\"6:1-10:0\"\u003e\n\u003cli data-sourcepos=\"6:1-6:147\"\u003eブラウザだけで使えるガントチャートWebアプリ \u003cstrong\u003e\u003ca href=\"https://moguchart.jp/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eMoguChart (moguchart.jp)\u003c/a\u003e\u003c/strong\u003e を個人開発しました\u003c/li\u003e\n\u003cli data-sourcepos=\"7:1-7:147\"\u003e\n\u003cstrong\u003e3つの表示モード\u003c/strong\u003e（時間単位/日単位/月単位）で、数時間の作業計画から数年の長期プロジェクトまで対応\u003c/li\u003e\n\u003cli data-sourcepos=\"8:1-8:189\"\u003eドラッグ＆ドロップ中心の直感操作、リアルタイム共同編集、PDF/Excel エクスポートなど、実務で「ほしい」と思った機能を詰め込みました\u003c/li\u003e\n\u003cli data-sourcepos=\"9:1-10:0\"\u003eGoogleアカウント不要の\u003cstrong\u003eゲストログイン\u003c/strong\u003eで今すぐ試せます\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"11:1-11:24\"\u003e\n\u003cspan id=\"なぜ作ったのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C%E4%BD%9C%E3%81%A3%E3%81%9F%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜ作ったのか\u003c/h2\u003e\n\u003cp data-sourcepos=\"13:1-13:154\"\u003eコアライブラリである \u003ca href=\"https://github.com/mogura-org/moguchart-core\" rel=\"nofollow noopener\" target=\"_blank\"\u003emoguchart-core\u003c/a\u003e を自作したことが、すべての始まりでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"15:1-15:398\"\u003e当初は「Web Components製のガントチャートライブラリ」としてライブラリ単体での公開を目指していましたが、開発が進むにつれて \u003cstrong\u003e「どれだけ優れたライブラリであっても、実際にプロダクトに組み込まれた姿を見せなければ、その真価は伝わらないのではないか」\u003c/strong\u003e と考えるようになりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"17:1-17:234\"\u003eそこで、ライブラリの「究極のデモ」として、そして自分自身が「本当に実務で使いたい」と思えるプロジェクト管理アプリを目指して開発したのが、この \u003cstrong\u003eMoguChart\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"19:1-19:535\"\u003e世の中には数多くのプロジェクト管理ツールが存在しますが、多機能すぎて学習コストが高かったり、逆にシンプルすぎてガントチャートとしての表現力が足りなかったりすることが多々あります。これまでの業務で培った Firebase などの技術知見をフルに活かし、「ガントチャートだけを徹底的に、かつ直感的に使えるWebアプリ」という、ちょうどいいニッチを埋めるプロダクトを目指しました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"21:1-21:32\"\u003e\n\u003cspan id=\"こだわったuxポイント\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%A0%E3%82%8F%E3%81%A3%E3%81%9Fux%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこだわったUXポイント\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"23:1-23:81\"\u003e\n\u003cspan id=\"1--3つの表示モード--プロジェクトの粒度に合わせる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1--3%E3%81%A4%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%83%A2%E3%83%BC%E3%83%89--%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E7%B2%92%E5%BA%A6%E3%81%AB%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 🕐 3つの表示モード ─ プロジェクトの粒度に合わせる\u003c/h3\u003e\n\u003cp data-sourcepos=\"25:1-25:86\"\u003eMoguChart では、プロジェクト作成時に\u003cstrong\u003e表示の粒度\u003c/strong\u003eを選べます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"27:1-31:77\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"27:1-27:46\"\u003e\n\u003cth data-sourcepos=\"27:2-27:12\"\u003eモード\u003c/th\u003e\n\u003cth data-sourcepos=\"27:14-27:27\"\u003e最小単位\u003c/th\u003e\n\u003cth data-sourcepos=\"27:29-27:45\"\u003e使いどころ\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"29:1-29:90\"\u003e\n\u003ctd data-sourcepos=\"29:2-29:19\"\u003e\u003cstrong\u003e時間単位\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"29:21-29:25\"\u003e分\u003c/td\u003e\n\u003ctd data-sourcepos=\"29:27-29:89\"\u003e1日のイベントスケジュール、短期の作業計画\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"30:1-30:83\"\u003e\n\u003ctd data-sourcepos=\"30:2-30:16\"\u003e\u003cstrong\u003e日単位\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"30:18-30:22\"\u003e日\u003c/td\u003e\n\u003ctd data-sourcepos=\"30:24-30:82\"\u003e一般的なプロジェクト管理（デフォルト）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"31:1-31:77\"\u003e\n\u003ctd data-sourcepos=\"31:2-31:16\"\u003e\u003cstrong\u003e月単位\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"31:18-31:22\"\u003e月\u003c/td\u003e\n\u003ctd data-sourcepos=\"31:24-31:76\"\u003e年間ロードマップ、長期プロジェクト\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"33:1-34:55\"\u003e「日単位だと粗すぎるけど、時間で管理したい作業がある」\u003cbr\u003e\n「3年計画だから月レベルで俯瞰したい」\u003c/p\u003e\n\u003cp data-sourcepos=\"36:1-36:82\"\u003eそんなニーズに、1つのアプリで応えられるようにしました。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"38:1-38:60\"\u003e\n\u003cspan id=\"2-️-ドラッグドロップですべて完結\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%EF%B8%8F-%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E3%81%A7%E3%81%99%E3%81%B9%E3%81%A6%E5%AE%8C%E7%B5%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 🖱️ ドラッグ＆ドロップですべて完結\u003c/h3\u003e\n\u003cp data-sourcepos=\"40:1-40:112\"\u003eタスク管理の操作は、可能な限り\u003cstrong\u003eマウス操作だけで完結する\u003c/strong\u003eようにしています。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"42:1-42:20\"\u003e\n\u003cspan id=\"タスク操作\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%BF%E3%82%B9%E3%82%AF%E6%93%8D%E4%BD%9C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eタスク操作\u003c/h4\u003e\n\u003ctable data-sourcepos=\"44:1-51:90\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"44:1-44:22\"\u003e\n\u003cth data-sourcepos=\"44:2-44:9\"\u003e操作\u003c/th\u003e\n\u003cth data-sourcepos=\"44:11-44:21\"\u003eやり方\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"46:1-46:46\"\u003e\n\u003ctd data-sourcepos=\"46:2-46:21\"\u003eタスクの移動\u003c/td\u003e\n\u003ctd data-sourcepos=\"46:23-46:45\"\u003eバーをドラッグ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"47:1-47:49\"\u003e\n\u003ctd data-sourcepos=\"47:2-47:18\"\u003e期間の変更\u003c/td\u003e\n\u003ctd data-sourcepos=\"47:20-47:48\"\u003eバーの端をドラッグ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"48:1-48:49\"\u003e\n\u003ctd data-sourcepos=\"48:2-48:15\"\u003e行の移動\u003c/td\u003e\n\u003ctd data-sourcepos=\"48:17-48:48\"\u003e行ヘッダーをドラッグ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"49:1-49:85\"\u003e\n\u003ctd data-sourcepos=\"49:2-49:24\"\u003e依存関係の作成\u003c/td\u003e\n\u003ctd data-sourcepos=\"49:26-49:84\"\u003eバー端のハンドルを他のタスクへドラッグ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"50:1-50:94\"\u003e\n\u003ctd data-sourcepos=\"50:2-50:42\"\u003eテンプレートからタスク作成\u003c/td\u003e\n\u003ctd data-sourcepos=\"50:44-50:93\"\u003eサイドバーからチャートへドラッグ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"51:1-51:90\"\u003e\n\u003ctd data-sourcepos=\"51:2-51:33\"\u003e複数タスクの一括移動\u003c/td\u003e\n\u003ctd data-sourcepos=\"51:35-51:89\"\u003eCmd/Ctrl+クリックで複数選択 → ドラッグ\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"53:1-53:154\"\u003e「右クリックメニュー」と「ダブルクリックで編集」の2つさえ覚えれば、ほぼすべての操作にアクセスできます。\u003c/p\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fdrag-and-drop.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=544f4be7f6174ca33bd94973714c6c77\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fdrag-and-drop.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=544f4be7f6174ca33bd94973714c6c77\" width=\"400\" alt=\"drag-and-drop.gif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fdrag-and-drop.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=8002e2ed1a42b7609daa738266431492 1x\" data-canonical-src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/drag-and-drop.gif\" loading=\"lazy\"\u003e\u003c/a\u003e\n\u003ch4 data-sourcepos=\"57:1-57:32\"\u003e\n\u003cspan id=\"右クリックメニュー\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%B3%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e右クリックメニュー\u003c/h4\u003e\n\u003cp data-sourcepos=\"59:1-59:156\"\u003eチャートの空白エリアやタスクバーを右クリックすると、その場所に応じたコンテキストメニューが表示されます。\u003c/p\u003e\n\u003cul data-sourcepos=\"61:1-64:0\"\u003e\n\u003cli data-sourcepos=\"61:1-61:73\"\u003e\n\u003cstrong\u003e空白エリア\u003c/strong\u003e: 新規タスク作成 / ペースト / Undo / Redo\u003c/li\u003e\n\u003cli data-sourcepos=\"62:1-62:65\"\u003e\n\u003cstrong\u003eタスクバー\u003c/strong\u003e: 編集 / コピー / 削除 / コメント\u003c/li\u003e\n\u003cli data-sourcepos=\"63:1-64:0\"\u003e\n\u003cstrong\u003e行ヘッダー\u003c/strong\u003e: 行の追加 / 編集 / 非表示 / 削除\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"65:1-65:69\"\u003e\n\u003cspan id=\"3--タスクバーの見た目を細かくカスタマイズ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3--%E3%82%BF%E3%82%B9%E3%82%AF%E3%83%90%E3%83%BC%E3%81%AE%E8%A6%8B%E3%81%9F%E7%9B%AE%E3%82%92%E7%B4%B0%E3%81%8B%E3%81%8F%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 🎨 タスクバーの見た目を細かくカスタマイズ\u003c/h3\u003e\n\u003cp data-sourcepos=\"67:1-67:121\"\u003e「色分けしたい」は当然として、MoguChart ではさらに踏み込んだ見た目の設定ができます。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"69:1-69:26\"\u003e\n\u003cspan id=\"カラーパレット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%AB%E3%83%A9%E3%83%BC%E3%83%91%E3%83%AC%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eカラーパレット\u003c/h4\u003e\n\u003cp data-sourcepos=\"71:1-71:277\"\u003eプロジェクトごとにカラーパレットを定義し、各パレットに\u003cstrong\u003e名前\u003c/strong\u003eを付けられます。「進行中」「レビュー待ち」「完了」のように意味のある名前を付ければ、チーム内でバーの色の意味が統一されます。\u003c/p\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fcolor-palette.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=e9a8e4d364e7455fc8db7dac4e7682fc\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fcolor-palette.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=e9a8e4d364e7455fc8db7dac4e7682fc\" width=\"400\" alt=\"color-palette.gif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fcolor-palette.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=04f23985751478efe63130fede57b8be 1x\" data-canonical-src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/color-palette.gif\" loading=\"lazy\"\u003e\u003c/a\u003e\n\u003ch4 data-sourcepos=\"75:1-75:31\"\u003e\n\u003cspan id=\"パターン13種類\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B313%E7%A8%AE%E9%A1%9E\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eパターン（13種類）\u003c/h4\u003e\n\u003cp data-sourcepos=\"77:1-77:72\"\u003eタスクバーの背景に重ねるパターンを設定できます。\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"79:1-79:206\"\u003e\n\u003cp data-sourcepos=\"79:3-79:206\"\u003ediagonal-stripe / diagonal-stripe-thin / diagonal-stripe-thick / diagonal-stripe-reverse / vertical-stripe / horizontal-stripe / checkerboard / dots / dots-dense / triangle / circle / grid / diagonal-grid\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"81:1-81:159\"\u003e「仮タスクはストライプ」「保留中はドット」のように、色＋パターンの組み合わせで状態を視覚的に区別できます。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"83:1-83:23\"\u003e\n\u003cspan id=\"枠線スタイル\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9E%A0%E7%B7%9A%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e枠線スタイル\u003c/h4\u003e\n\u003cp data-sourcepos=\"85:1-85:186\"\u003e実線（細/太）、破線（細/太）、点線（細/太）から選べます。パターンと組み合わせることで、印刷時にも区別しやすいバーが作れます。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"87:1-87:17\"\u003e\n\u003cspan id=\"バーの影\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%90%E3%83%BC%E3%81%AE%E5%BD%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eバーの影\u003c/h4\u003e\n\u003cp data-sourcepos=\"89:1-89:217\"\u003eドロップシャドウの強さを4段階（なし / 小 / 中 / 大）で調整できます。影を付けるとバーが浮き上がって見え、情報量の多いチャートでも視認性が上がります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"91:1-91:54\"\u003e\n\u003cspan id=\"4--マイルストーンで節目を可視化\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4--%E3%83%9E%E3%82%A4%E3%83%AB%E3%82%B9%E3%83%88%E3%83%BC%E3%83%B3%E3%81%A7%E7%AF%80%E7%9B%AE%E3%82%92%E5%8F%AF%E8%A6%96%E5%8C%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. 🏁 マイルストーンで節目を可視化\u003c/h3\u003e\n\u003cp data-sourcepos=\"93:1-93:258\"\u003eプロジェクト全体の節目（リリース日、デッドラインなど）を縦線＋バッジで表示します。マウスオーバーでハイライトされるので、「この日までに何を終わらせるか」が一目でわかります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"95:1-95:45\"\u003e\n\u003cspan id=\"5--進捗率のプログレスバー\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5--%E9%80%B2%E6%8D%97%E7%8E%87%E3%81%AE%E3%83%97%E3%83%AD%E3%82%B0%E3%83%AC%E3%82%B9%E3%83%90%E3%83%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. 📊 進捗率のプログレスバー\u003c/h3\u003e\n\u003cp data-sourcepos=\"97:1-97:232\"\u003eタスクごとに 0〜100% の進捗率を設定でき、バー上にプログレスバーとして表示されます。毎日の朝会で「今どこまで進んでる？」の確認がチャートを見るだけで済みます。\u003c/p\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fprogress-bar.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=32ba36273e15d3a3cc4d0d1dad168528\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fprogress-bar.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=32ba36273e15d3a3cc4d0d1dad168528\" width=\"200\" alt=\"progress-bar.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fprogress-bar.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=e8376be782895d0fe0bc52f47fcd3537 1x\" data-canonical-src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/progress-bar.png\" loading=\"lazy\"\u003e\u003c/a\u003e\n\u003ch3 data-sourcepos=\"101:1-101:39\"\u003e\n\u003cspan id=\"6--依存関係の矢印表示\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6--%E4%BE%9D%E5%AD%98%E9%96%A2%E4%BF%82%E3%81%AE%E7%9F%A2%E5%8D%B0%E8%A1%A8%E7%A4%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. 🔗 依存関係の矢印表示\u003c/h3\u003e\n\u003cp data-sourcepos=\"103:1-103:125\"\u003e「タスクBはタスクAが完了してから」という依存関係を、矢印付きの曲線で可視化できます。\u003c/p\u003e\n\u003cul data-sourcepos=\"105:1-107:0\"\u003e\n\u003cli data-sourcepos=\"105:1-105:101\"\u003eバーの端にある接続ハンドルをドラッグして、依存先のタスクにドロップ\u003c/li\u003e\n\u003cli data-sourcepos=\"106:1-107:0\"\u003e右→左方向の依存関係は\u003cstrong\u003eS字カーブ\u003c/strong\u003eで自動的に見やすく描画\u003c/li\u003e\n\u003c/ul\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Frelation.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=6474e5b81512689470baac6b4899594a\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Frelation.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=6474e5b81512689470baac6b4899594a\" width=\"200\" alt=\"relation.gif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Frelation.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=9fafdaed85c547262dbe7eeb26b2f7a8 1x\" data-canonical-src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/relation.gif\" loading=\"lazy\"\u003e\u003c/a\u003e\n\u003ch3 data-sourcepos=\"110:1-110:51\"\u003e\n\u003cspan id=\"7--うっかり操作を防ぐ機能\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#7--%E3%81%86%E3%81%A3%E3%81%8B%E3%82%8A%E6%93%8D%E4%BD%9C%E3%82%92%E9%98%B2%E3%81%90%E6%A9%9F%E8%83%BD\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e7. 🔒 「うっかり操作」を防ぐ機能\u003c/h3\u003e\n\u003ch4 data-sourcepos=\"112:1-112:23\"\u003e\n\u003cspan id=\"タスクロック\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%BF%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%83%E3%82%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eタスクロック\u003c/h4\u003e\n\u003cp data-sourcepos=\"114:1-114:225\"\u003e確定済みのタスクに対して「ロック」をかけると、ドラッグでの移動・リサイズ・削除が禁止されます。「このタスクは動かさないで！」をシステムで担保できます。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"116:1-116:32\"\u003e\n\u003cspan id=\"読み取り専用モード\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%AA%AD%E3%81%BF%E5%8F%96%E3%82%8A%E5%B0%82%E7%94%A8%E3%83%A2%E3%83%BC%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e読み取り専用モード\u003c/h4\u003e\n\u003cp data-sourcepos=\"118:1-118:218\"\u003e表示設定メニューから ON にすると、\u003cstrong\u003eすべての編集操作が無効化\u003c/strong\u003eされます。「報告用に画面を見せたいけど、操作はさせたくない」といったシーンで便利です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"120:1-120:75\"\u003e\n\u003cspan id=\"8--スナップショットであの時の状態に戻れる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#8--%E3%82%B9%E3%83%8A%E3%83%83%E3%83%97%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%81%A7%E3%81%82%E3%81%AE%E6%99%82%E3%81%AE%E7%8A%B6%E6%85%8B%E3%81%AB%E6%88%BB%E3%82%8C%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e8. 📸 スナップショットで「あの時の状態」に戻れる\u003c/h3\u003e\n\u003cp data-sourcepos=\"122:1-122:99\"\u003eスナップショットは、プロジェクトの状態をまるごと保存する機能です。\u003c/p\u003e\n\u003ctable data-sourcepos=\"124:1-130:79\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"124:1-124:19\"\u003e\n\u003cth data-sourcepos=\"124:2-124:9\"\u003e機能\u003c/th\u003e\n\u003cth data-sourcepos=\"124:11-124:18\"\u003e説明\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"126:1-126:79\"\u003e\n\u003ctd data-sourcepos=\"126:2-126:15\"\u003e手動保存\u003c/td\u003e\n\u003ctd data-sourcepos=\"126:17-126:78\"\u003e任意のタイミングでスナップショットを作成\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"127:1-127:91\"\u003e\n\u003ctd data-sourcepos=\"127:2-127:15\"\u003e自動保存\u003c/td\u003e\n\u003ctd data-sourcepos=\"127:17-127:90\"\u003e5分〜24時間の間隔で自動保存（保持期間も設定可能）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"128:1-128:85\"\u003e\n\u003ctd data-sourcepos=\"128:2-128:9\"\u003e復元\u003c/td\u003e\n\u003ctd data-sourcepos=\"128:11-128:84\"\u003e過去のスナップショットの状態にワンクリックで復元\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"129:1-129:67\"\u003e\n\u003ctd data-sourcepos=\"129:2-129:9\"\u003e閲覧\u003c/td\u003e\n\u003ctd data-sourcepos=\"129:11-129:66\"\u003eスナップショットを読み取り専用で閲覧\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"130:1-130:79\"\u003e\n\u003ctd data-sourcepos=\"130:2-130:21\"\u003eダウンロード\u003c/td\u003e\n\u003ctd data-sourcepos=\"130:23-130:78\"\u003eスナップショットをファイルとして保存\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"132:1-132:159\"\u003e「昨日の時点ではどうなってたっけ？」を即座に確認でき、「やっぱり戻したい」にもワンクリックで対応できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"134:1-134:42\"\u003e\n\u003cspan id=\"9--リアルタイム共同編集\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#9--%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E5%85%B1%E5%90%8C%E7%B7%A8%E9%9B%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e9. 👥 リアルタイム共同編集\u003c/h3\u003e\n\u003cp data-sourcepos=\"136:1-136:60\"\u003e同じプロジェクトを複数人で同時に開くと：\u003c/p\u003e\n\u003cul data-sourcepos=\"138:1-141:0\"\u003e\n\u003cli data-sourcepos=\"138:1-138:92\"\u003e\n\u003cstrong\u003eアクティブユーザー表示\u003c/strong\u003e: 誰が今見ているかがアバターでわかる\u003c/li\u003e\n\u003cli data-sourcepos=\"139:1-139:95\"\u003e\n\u003cstrong\u003e変更履歴パネル\u003c/strong\u003e: リアルタイムで他のメンバーの操作が表示される\u003c/li\u003e\n\u003cli data-sourcepos=\"140:1-141:0\"\u003e\n\u003cstrong\u003e未読バッジ\u003c/strong\u003e: 見逃した変更件数が通知される\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"142:1-142:255\"\u003e変更履歴パネルでは、ログ内のタスク名をクリックすると該当タスクにフォーカスし、ダブルクリックで編集ダイアログが開きます。「誰が何を変えたのか」を追跡しながら作業できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"144:1-144:40\"\u003e\n\u003cspan id=\"10--多彩なエクスポート\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#10--%E5%A4%9A%E5%BD%A9%E3%81%AA%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e10. 📤 多彩なエクスポート\u003c/h3\u003e\n\u003ctable data-sourcepos=\"146:1-152:59\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"146:1-146:19\"\u003e\n\u003cth data-sourcepos=\"146:2-146:9\"\u003e形式\u003c/th\u003e\n\u003cth data-sourcepos=\"146:11-146:18\"\u003e用途\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"148:1-148:47\"\u003e\n\u003ctd data-sourcepos=\"148:2-148:10\"\u003e\u003cstrong\u003eCSV\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"148:12-148:46\"\u003e他ツールへのデータ連携\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"149:1-149:74\"\u003e\n\u003ctd data-sourcepos=\"149:2-149:20\"\u003e\n\u003cstrong\u003eExcel\u003c/strong\u003e (.xlsx)\u003c/td\u003e\n\u003ctd data-sourcepos=\"149:22-149:73\"\u003e報告書に添付、Excelユーザーへの共有\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"150:1-150:65\"\u003e\n\u003ctd data-sourcepos=\"150:2-150:10\"\u003e\u003cstrong\u003ePNG\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"150:12-150:64\"\u003eスライドやドキュメントへの貼り付け\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"151:1-151:38\"\u003e\n\u003ctd data-sourcepos=\"151:2-151:10\"\u003e\u003cstrong\u003ePDF\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"151:12-151:37\"\u003e印刷・メール添付\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"152:1-152:59\"\u003e\n\u003ctd data-sourcepos=\"152:2-152:10\"\u003e\u003cstrong\u003eZIP\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"152:12-152:58\"\u003eプロジェクト全体のバックアップ\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"154:1-154:284\"\u003eCSV/Excel エクスポートには、行名・タスク名・開始日・終了日・日数・ラベル・説明が含まれます。ラベルフィルタを適用中なら、\u003cstrong\u003e表示中のデータだけ\u003c/strong\u003eがエクスポートされるので、レポート作成にも使えます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"156:1-156:45\"\u003e\n\u003cspan id=\"地味だけど便利な機能たち\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%9C%B0%E5%91%B3%E3%81%A0%E3%81%91%E3%81%A9%E4%BE%BF%E5%88%A9%E3%81%AA%E6%A9%9F%E8%83%BD%E3%81%9F%E3%81%A1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e「地味だけど便利」な機能たち\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"158:1-158:22\"\u003e\n\u003cspan id=\"期間スライド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%9F%E9%96%93%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e期間スライド\u003c/h3\u003e\n\u003cp data-sourcepos=\"160:1-160:316\"\u003eプロジェクトの開始日が変わったとき、\u003cstrong\u003e全タスク・全マイルストーンの日付をまとめてスライド\u003c/strong\u003eできます。新しい開始日を指定するだけで、プロジェクト期間を維持したまま全体が自動でずれます。進捗率のクリアオプション付き。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"162:1-162:28\"\u003e\n\u003cspan id=\"プロジェクト複製\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E8%A4%87%E8%A3%BD\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eプロジェクト複製\u003c/h3\u003e\n\u003cp data-sourcepos=\"164:1-164:144\"\u003e既存プロジェクトを複製する際、プロジェクト詳細ダイアログの「複製モード」で以下の設定が行えます。\u003c/p\u003e\n\u003col data-sourcepos=\"166:1-169:0\"\u003e\n\u003cli data-sourcepos=\"166:1-166:159\"\u003e\n\u003cstrong\u003eプロジェクト名\u003c/strong\u003e: デフォルトで「（元のプロジェクト名）のコピー」と入力され、任意の名前に変更が可能です。\u003c/li\u003e\n\u003cli data-sourcepos=\"167:1-167:330\"\u003e\n\u003cstrong\u003e日付の連動スライド\u003c/strong\u003e: 開始日を変更すると、元のプロジェクト期間を維持したまま終了日が自動でスライド（調整）されます（終了日入力欄は非活性になります）。これに伴い、全タスクやマイルストーンの日付も自動的にスライドします。\u003c/li\u003e\n\u003cli data-sourcepos=\"168:1-169:0\"\u003e\n\u003cstrong\u003e進捗率のクリア\u003c/strong\u003e: 「進捗率をクリアする」チェックボックス（デフォルトON）が用意されており、複製時にタスクの進捗率をリセットするかどうかを選択できます。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"170:1-170:273\"\u003eカラーパレット、ラベル、マイルストーン、権限設定なども元のプロジェクトからそのまま引き継がれるため、「前期のプロジェクトをベースに今期を作る」といった作業が非常にスムーズに行えます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"172:1-172:31\"\u003e\n\u003cspan id=\"タスクテンプレート\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%BF%E3%82%B9%E3%82%AF%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eタスクテンプレート\u003c/h3\u003e\n\u003cp data-sourcepos=\"174:1-174:258\"\u003eよく使うタスク設定（名前・色・ラベル）をテンプレートとして保存し、サイドバーからドラッグ＆ドロップでチャートに配置できます。「毎週のルーティンタスク」を一瞬で配置できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"176:1-176:66\"\u003e\n\u003cspan id=\"️-ラベルで分類と絞り込みを両立\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%EF%B8%8F-%E3%83%A9%E3%83%99%E3%83%AB%E3%81%A7%E5%88%86%E9%A1%9E%E3%81%A8%E7%B5%9E%E3%82%8A%E8%BE%BC%E3%81%BF%E3%82%92%E4%B8%A1%E7%AB%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e🏷️ ラベルで「分類」と「絞り込み」を両立\u003c/h3\u003e\n\u003cp data-sourcepos=\"178:1-178:302\"\u003eタスクや行が増えてくると、「このタスクはどのカテゴリ？」「フロントエンドの作業だけ見たい」といったニーズが出てきます。MoguChart の\u003cstrong\u003eラベル機能\u003c/strong\u003eは、こうした「分類」と「絞り込み」を一つの仕組みで実現します。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"180:1-180:53\"\u003e\n\u003cspan id=\"ラベルの定義プロジェクト設定\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A9%E3%83%99%E3%83%AB%E3%81%AE%E5%AE%9A%E7%BE%A9%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E8%A8%AD%E5%AE%9A\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eラベルの定義（プロジェクト設定）\u003c/h4\u003e\n\u003cp data-sourcepos=\"182:1-182:179\"\u003eラベルはプロジェクト単位で管理します。プロジェクト詳細ダイアログで、\u003cstrong\u003e名前\u003c/strong\u003eと\u003cstrong\u003e色\u003c/strong\u003eを指定してラベルを自由に追加できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"184:1-184:66\"\u003e例えば、以下のようなラベルセットを作れます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"186:1-192:51\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"186:1-186:31\"\u003e\n\u003cth data-sourcepos=\"186:2-186:15\"\u003eラベル名\u003c/th\u003e\n\u003cth data-sourcepos=\"186:17-186:21\"\u003e色\u003c/th\u003e\n\u003cth data-sourcepos=\"186:23-186:30\"\u003e用途\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"188:1-188:68\"\u003e\n\u003ctd data-sourcepos=\"188:2-188:24\"\u003eフロントエンド\u003c/td\u003e\n\u003ctd data-sourcepos=\"188:26-188:35\"\u003e🟦 青\u003c/td\u003e\n\u003ctd data-sourcepos=\"188:37-188:67\"\u003eUI・画面の実装タスク\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"189:1-189:62\"\u003e\n\u003ctd data-sourcepos=\"189:2-189:21\"\u003eバックエンド\u003c/td\u003e\n\u003ctd data-sourcepos=\"189:23-189:32\"\u003e🟩 緑\u003c/td\u003e\n\u003ctd data-sourcepos=\"189:34-189:61\"\u003eAPI・DB関連のタスク\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"190:1-190:48\"\u003e\n\u003ctd data-sourcepos=\"190:2-190:15\"\u003eデザイン\u003c/td\u003e\n\u003ctd data-sourcepos=\"190:17-190:26\"\u003e🟪 紫\u003c/td\u003e\n\u003ctd data-sourcepos=\"190:28-190:47\"\u003eデザイン作業\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"191:1-191:78\"\u003e\n\u003ctd data-sourcepos=\"191:2-191:21\"\u003eレビュー待ち\u003c/td\u003e\n\u003ctd data-sourcepos=\"191:23-191:41\"\u003e🟧 オレンジ\u003c/td\u003e\n\u003ctd data-sourcepos=\"191:43-191:77\"\u003eレビューが必要なタスク\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"192:1-192:51\"\u003e\n\u003ctd data-sourcepos=\"192:2-192:9\"\u003e緊急\u003c/td\u003e\n\u003ctd data-sourcepos=\"192:11-192:20\"\u003e🟥 赤\u003c/td\u003e\n\u003ctd data-sourcepos=\"192:22-192:50\"\u003e優先度の高いタスク\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 data-sourcepos=\"194:1-194:41\"\u003e\n\u003cspan id=\"タスク行へのラベル付与\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%BF%E3%82%B9%E3%82%AF%E8%A1%8C%E3%81%B8%E3%81%AE%E3%83%A9%E3%83%99%E3%83%AB%E4%BB%98%E4%B8%8E\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eタスク・行へのラベル付与\u003c/h4\u003e\n\u003cp data-sourcepos=\"196:1-196:101\"\u003e定義したラベルは、\u003cstrong\u003eタスク\u003c/strong\u003eと**行（グループ）**の両方に付与できます。\u003c/p\u003e\n\u003cul data-sourcepos=\"198:1-200:0\"\u003e\n\u003cli data-sourcepos=\"198:1-198:107\"\u003e\n\u003cstrong\u003eタスク\u003c/strong\u003e: タスク編集ダイアログで、ドロップダウンから複数のラベルを選択\u003c/li\u003e\n\u003cli data-sourcepos=\"199:1-200:0\"\u003e\n\u003cstrong\u003e行（グループ）\u003c/strong\u003e: 行編集ダイアログで同様にラベルを選択\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"201:1-201:176\"\u003e1つのタスク・行に\u003cstrong\u003e複数のラベルを同時に付与\u003c/strong\u003eできるので、「フロントエンド」かつ「緊急」のように複合的な分類が可能です。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"203:1-203:32\"\u003e\n\u003cspan id=\"バー上のラベル表示\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%90%E3%83%BC%E4%B8%8A%E3%81%AE%E3%83%A9%E3%83%99%E3%83%AB%E8%A1%A8%E7%A4%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eバー上のラベル表示\u003c/h4\u003e\n\u003cp data-sourcepos=\"205:1-205:391\"\u003eタスクバーにラベルが付与されていると、バー上に\u003cstrong\u003eカラーチップ\u003c/strong\u003eとして表示されます。タスク名の横にラベル名が色付きで並ぶため、チャートを見るだけでタスクのカテゴリが一目でわかります。行ヘッダーにもラベルチップが表示されるので、行レベルの分類も視覚的に把握できます。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"207:1-207:36\"\u003e\n\u003cspan id=\"2種類のラベルフィルタ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2%E7%A8%AE%E9%A1%9E%E3%81%AE%E3%83%A9%E3%83%99%E3%83%AB%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2種類のラベルフィルタ\u003c/h4\u003e\n\u003cp data-sourcepos=\"209:1-209:144\"\u003eラベルの真価は\u003cstrong\u003eフィルタリング\u003c/strong\u003eで発揮されます。MoguChart では2種類のラベルフィルタを用意しています。\u003c/p\u003e\n\u003ctable data-sourcepos=\"211:1-214:98\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"211:1-211:40\"\u003e\n\u003cth data-sourcepos=\"211:2-211:15\"\u003eフィルタ\u003c/th\u003e\n\u003cth data-sourcepos=\"211:17-211:24\"\u003e対象\u003c/th\u003e\n\u003cth data-sourcepos=\"211:26-211:39\"\u003e操作場所\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"213:1-213:107\"\u003e\n\u003ctd data-sourcepos=\"213:2-213:37\"\u003e\u003cstrong\u003eタスクラベルフィルタ\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"213:39-213:88\"\u003e特定のラベルを持つタスクだけ表示\u003c/td\u003e\n\u003ctd data-sourcepos=\"213:90-213:106\"\u003eツールバー\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"214:1-214:98\"\u003e\n\u003ctd data-sourcepos=\"214:2-214:31\"\u003e\u003cstrong\u003e行ラベルフィルタ\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"214:33-214:76\"\u003e特定のラベルを持つ行だけ表示\u003c/td\u003e\n\u003ctd data-sourcepos=\"214:78-214:97\"\u003eコーナーセル\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"216:1-216:253\"\u003eどちらのフィルタにも**「ラベルなし」**の選択肢があり、ラベルが未設定のタスク・行を絞り込むことも可能です。「すべて選択」「選択解除」のワンクリック操作にも対応しています。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"218:1-218:35\"\u003e\n\u003cspan id=\"エクスポートとの連携\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88%E3%81%A8%E3%81%AE%E9%80%A3%E6%90%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eエクスポートとの連携\u003c/h4\u003e\n\u003cp data-sourcepos=\"220:1-220:377\"\u003eラベルフィルタを適用した状態で CSV / Excel エクスポートを実行すると、\u003cstrong\u003e表示中のデータだけ\u003c/strong\u003eがエクスポートされます。各タスクに付与されたラベル名もエクスポートデータに含まれるため、「バックエンドのタスクだけ抜き出してレポートに添付」といった使い方ができます。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"222:1-222:44\"\u003e\n\u003cspan id=\"タスクテンプレートとの連携\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%BF%E3%82%B9%E3%82%AF%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%81%A8%E3%81%AE%E9%80%A3%E6%90%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eタスクテンプレートとの連携\u003c/h4\u003e\n\u003cp data-sourcepos=\"224:1-224:273\"\u003eタスクテンプレートにもラベルを含めて保存できます。「毎週のフロントエンド定例」のように、ラベル付きのテンプレートをドラッグ＆ドロップするだけで、分類済みのタスクが一瞬で配置されます。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"226:1-226:47\"\u003e\n\u003cspan id=\"プロジェクト複製時の引き継ぎ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E8%A4%87%E8%A3%BD%E6%99%82%E3%81%AE%E5%BC%95%E3%81%8D%E7%B6%99%E3%81%8E\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eプロジェクト複製時の引き継ぎ\u003c/h4\u003e\n\u003cp data-sourcepos=\"228:1-228:225\"\u003eプロジェクトを複製すると、ラベル定義もそのまま引き継がれます。「前期のプロジェクトをベースに今期を作る」際に、ラベルを一から作り直す必要はありません。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"230:1-230:52\"\u003e\n\u003cspan id=\"フリーワード検索ラベルフィルタ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%95%E3%83%AA%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E6%A4%9C%E7%B4%A2%E3%83%A9%E3%83%99%E3%83%AB%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eフリーワード検索＋ラベルフィルタ\u003c/h3\u003e\n\u003cp data-sourcepos=\"232:1-232:66\"\u003eタスクが増えてきたときの「探す」をサポート。\u003c/p\u003e\n\u003cul data-sourcepos=\"234:1-237:0\"\u003e\n\u003cli data-sourcepos=\"234:1-234:92\"\u003e\n\u003cstrong\u003eフリーワード検索\u003c/strong\u003e: タスク名にマッチするバーがハイライト表示\u003c/li\u003e\n\u003cli data-sourcepos=\"235:1-235:86\"\u003e\n\u003cstrong\u003eタスクラベルフィルタ\u003c/strong\u003e: 特定のラベルを持つタスクだけ表示\u003c/li\u003e\n\u003cli data-sourcepos=\"236:1-237:0\"\u003e\n\u003cstrong\u003e行ラベルフィルタ\u003c/strong\u003e: 特定のラベルを持つ行だけ表示\u003c/li\u003e\n\u003c/ul\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Ffilter.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=be7fef713d318b872656e1ca409668de\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Ffilter.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=be7fef713d318b872656e1ca409668de\" width=\"800\" alt=\"filter.gif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Ffilter.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=4d5efad63c097235bb5d5d245cb41880 1x\" data-canonical-src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/filter.gif\" loading=\"lazy\"\u003e\u003c/a\u003e\n\u003ch3 data-sourcepos=\"240:1-240:22\"\u003e\n\u003cspan id=\"コメント機能\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%E6%A9%9F%E8%83%BD\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eコメント機能\u003c/h3\u003e\n\u003cp data-sourcepos=\"242:1-242:241\"\u003eタスク・行・プロジェクトの3レベルでコメントを残せます。プロジェクトコメントは画面右端のサイドバーで管理でき、幅の調整や開閉状態がプロジェクトごとに保存されます。\u003c/p\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fcomment.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9c701d8ce5d1d414cce6de792b36e8cc\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fcomment.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9c701d8ce5d1d414cce6de792b36e8cc\" width=\"400\" alt=\"comment.gif\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fhiro-murakami%2Fqiita-content%2Fmain%2Fimages%2Fmoguchart-app-ux%2Fcomment.gif?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=dee5d63993ca1515350a8e014b309772 1x\" data-canonical-src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/comment.gif\" loading=\"lazy\"\u003e\u003c/a\u003e\n\u003ch3 data-sourcepos=\"246:1-246:40\"\u003e\n\u003cspan id=\"キーボードショートカット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eキーボードショートカット\u003c/h3\u003e\n\u003cp data-sourcepos=\"248:1-248:105\"\u003eMoguChartでは、マウス操作だけでなくキーボードによる操作も充実しています。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"250:1-250:47\"\u003e\n\u003cspan id=\"チャート操作フォーカス移動\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%81%E3%83%A3%E3%83%BC%E3%83%88%E6%93%8D%E4%BD%9C%E3%83%95%E3%82%A9%E3%83%BC%E3%82%AB%E3%82%B9%E7%A7%BB%E5%8B%95\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eチャート操作・フォーカス移動\u003c/h4\u003e\n\u003ctable data-sourcepos=\"252:1-259:102\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"252:1-252:34\"\u003e\n\u003cth data-sourcepos=\"252:2-252:24\"\u003eショートカット\u003c/th\u003e\n\u003cth data-sourcepos=\"252:26-252:33\"\u003e動作\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"254:1-254:87\"\u003e\n\u003ctd data-sourcepos=\"254:2-254:32\"\u003e\n\u003ccode\u003e↑\u003c/code\u003e / \u003ccode\u003e↓\u003c/code\u003e / \u003ccode\u003e←\u003c/code\u003e / \u003ccode\u003e→\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"254:34-254:86\"\u003eフォーカスを上下左右のタスクに移動\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"255:1-255:81\"\u003e\n\u003ctd data-sourcepos=\"255:2-255:17\"\u003e\n\u003ccode\u003eHome\u003c/code\u003e / \u003ccode\u003eEnd\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"255:19-255:80\"\u003e現在の行の最初 / 最後のタスクにフォーカス\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"256:1-256:129\"\u003e\n\u003ctd data-sourcepos=\"256:2-256:20\"\u003e\n\u003ccode\u003eEnter\u003c/code\u003e / \u003ccode\u003eSpace\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"256:22-256:128\"\u003eフォーカス中のタスクを選択（\u003ccode\u003eCmd\u003c/code\u003e / \u003ccode\u003eCtrl\u003c/code\u003e を押しながらで複数選択のトグル）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"257:1-257:102\"\u003e\n\u003ctd data-sourcepos=\"257:2-257:32\"\u003e\n\u003ccode\u003eShift + ←\u003c/code\u003e / \u003ccode\u003eShift + →\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"257:34-257:101\"\u003e選択中のタスクを左 / 右に移動（スナップ単位）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"258:1-258:59\"\u003e\n\u003ctd data-sourcepos=\"258:2-258:25\"\u003e\n\u003ccode\u003eDelete\u003c/code\u003e / \u003ccode\u003eBackspace\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"258:27-258:58\"\u003e選択中のタスクを削除\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"259:1-259:102\"\u003e\n\u003ctd data-sourcepos=\"259:2-259:8\"\u003e\u003ccode\u003eEsc\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"259:10-259:101\"\u003e選択・フォーカスのクリア / ドラッグやリサイズ操作のキャンセル\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 data-sourcepos=\"261:1-261:38\"\u003e\n\u003cspan id=\"編集コピーペースト\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%B7%A8%E9%9B%86%E3%82%B3%E3%83%94%E3%83%BC%E3%83%9A%E3%83%BC%E3%82%B9%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e編集・コピー＆ペースト\u003c/h4\u003e\n\u003ctable data-sourcepos=\"263:1-269:53\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"263:1-263:34\"\u003e\n\u003cth data-sourcepos=\"263:2-263:24\"\u003eショートカット\u003c/th\u003e\n\u003cth data-sourcepos=\"263:26-263:33\"\u003e動作\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"265:1-265:58\"\u003e\n\u003ctd data-sourcepos=\"265:2-265:21\"\u003e\n\u003ccode\u003eCmd+C\u003c/code\u003e / \u003ccode\u003eCtrl+C\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"265:23-265:57\"\u003e選択中のタスクをコピー\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"266:1-266:64\"\u003e\n\u003ctd data-sourcepos=\"266:2-266:21\"\u003e\n\u003ccode\u003eCmd+V\u003c/code\u003e / \u003ccode\u003eCtrl+V\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"266:23-266:63\"\u003eコピーしたタスクをペースト\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"267:1-267:123\"\u003e\n\u003ctd data-sourcepos=\"267:2-267:32\"\u003e\n\u003ccode\u003eCtrl\u003c/code\u003e / \u003ccode\u003eAlt\u003c/code\u003e + ドラッグ\u003c/td\u003e\n\u003ctd data-sourcepos=\"267:34-267:122\"\u003eタスクをコピー（複製）して配置（ドラッグ時のコピーモード）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"268:1-268:47\"\u003e\n\u003ctd data-sourcepos=\"268:2-268:21\"\u003e\n\u003ccode\u003eCmd+Z\u003c/code\u003e / \u003ccode\u003eCtrl+Z\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"268:23-268:46\"\u003e元に戻す（Undo）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"269:1-269:53\"\u003e\n\u003ctd data-sourcepos=\"269:2-269:27\"\u003e\n\u003ccode\u003eCmd+Shift+Z\u003c/code\u003e / \u003ccode\u003eCtrl+Y\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"269:29-269:52\"\u003eやり直し（Redo）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch4 data-sourcepos=\"271:1-271:29\"\u003e\n\u003cspan id=\"ダイアログ入力\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%80%E3%82%A4%E3%82%A2%E3%83%AD%E3%82%B0%E5%85%A5%E5%8A%9B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eダイアログ・入力\u003c/h4\u003e\n\u003ctable data-sourcepos=\"273:1-277:75\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"273:1-273:34\"\u003e\n\u003cth data-sourcepos=\"273:2-273:24\"\u003eショートカット\u003c/th\u003e\n\u003cth data-sourcepos=\"273:26-273:33\"\u003e動作\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"275:1-275:54\"\u003e\n\u003ctd data-sourcepos=\"275:2-275:29\"\u003e\n\u003ccode\u003eCmd+Enter\u003c/code\u003e / \u003ccode\u003eCtrl+Enter\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"275:31-275:53\"\u003eコメントを送信\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"276:1-276:68\"\u003e\n\u003ctd data-sourcepos=\"276:2-276:10\"\u003e\u003ccode\u003eEnter\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"276:12-276:67\"\u003e行名編集時の確定 / ダイアログでの決定\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"277:1-277:75\"\u003e\n\u003ctd data-sourcepos=\"277:2-277:8\"\u003e\u003ccode\u003eEsc\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"277:10-277:74\"\u003e行名編集時のキャンセル / ダイアログを閉じる\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"279:1-279:59\"\u003e\n\u003cspan id=\"権限管理--チームでの運用を考えた設計\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A8%A9%E9%99%90%E7%AE%A1%E7%90%86--%E3%83%81%E3%83%BC%E3%83%A0%E3%81%A7%E3%81%AE%E9%81%8B%E7%94%A8%E3%82%92%E8%80%83%E3%81%88%E3%81%9F%E8%A8%AD%E8%A8%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e権限管理 ─ チームでの運用を考えた設計\u003c/h2\u003e\n\u003cp data-sourcepos=\"281:1-281:92\"\u003eMoguChart は3段階の権限モデルで、チームでの運用を想定しています。\u003c/p\u003e\n\u003ctable data-sourcepos=\"283:1-291:40\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"283:1-283:49\"\u003e\n\u003cth data-sourcepos=\"283:2-283:9\"\u003e操作\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"283:11-283:24\"\u003eオーナー\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"283:26-283:36\"\u003e編集者\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"283:38-283:48\"\u003e閲覧者\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"285:1-285:40\"\u003e\n\u003ctd data-sourcepos=\"285:2-285:21\"\u003eチャート閲覧\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"285:23-285:27\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"285:29-285:33\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"285:35-285:39\"\u003e✅\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"286:1-286:46\"\u003e\n\u003ctd data-sourcepos=\"286:2-286:27\"\u003eタスク・行の編集\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"286:29-286:33\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"286:35-286:39\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"286:41-286:45\"\u003e❌\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"287:1-287:46\"\u003e\n\u003ctd data-sourcepos=\"287:2-287:27\"\u003eプロジェクト設定\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"287:29-287:33\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"287:35-287:39\"\u003e❌\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"287:41-287:45\"\u003e❌\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"288:1-288:52\"\u003e\n\u003ctd data-sourcepos=\"288:2-288:33\"\u003eスナップショット作成\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"288:35-288:39\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"288:41-288:45\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"288:47-288:51\"\u003e❌\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"289:1-289:40\"\u003e\n\u003ctd data-sourcepos=\"289:2-289:21\"\u003eコメント追加\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"289:23-289:27\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"289:29-289:33\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"289:35-289:39\"\u003e❌\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"290:1-290:46\"\u003e\n\u003ctd data-sourcepos=\"290:2-290:27\"\u003eプロジェクト削除\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"290:29-290:33\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"290:35-290:39\"\u003e❌\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"290:41-290:45\"\u003e❌\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"291:1-291:40\"\u003e\n\u003ctd data-sourcepos=\"291:2-291:21\"\u003eエクスポート\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"291:23-291:27\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"291:29-291:33\"\u003e✅\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"291:35-291:39\"\u003e✅\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"293:1-293:141\"\u003eメールアドレスで権限を付与する方式で、過去に入力したアドレスは自動補完候補として保存されます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"295:1-295:18\"\u003e\n\u003cspan id=\"テーマ対応\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%86%E3%83%BC%E3%83%9E%E5%AF%BE%E5%BF%9C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eテーマ対応\u003c/h2\u003e\n\u003cp data-sourcepos=\"297:1-297:175\"\u003eライト / ダーク / システム連動の3モードに対応。チャートを開いたまま表示設定メニューからワンクリックで切り替えられます。\u003c/p\u003e\n\u003cp data-sourcepos=\"299:1-299:230\"\u003e\u003cstrong\u003eバーの高さも5段階\u003c/strong\u003e（極小 / 小 / 中 / 大 / 特大）で調整できるので、タスク数が多い場合は「極小」で俯瞰、少ない場合は「大」でゆったり表示、と使い分けられます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"301:1-301:45\"\u003e\n\u003cspan id=\"ゲストログインで今すぐ試せる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B2%E3%82%B9%E3%83%88%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%A7%E4%BB%8A%E3%81%99%E3%81%90%E8%A9%A6%E3%81%9B%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eゲストログインで今すぐ試せる\u003c/h2\u003e\n\u003cp data-sourcepos=\"303:1-303:205\"\u003e\u003cstrong\u003e\u003ca href=\"https://moguchart.jp/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eMoguChart\u003c/a\u003e\u003c/strong\u003e では、「アカウント作るほどじゃないけど、ちょっと試したい」という方のために、\u003cstrong\u003eゲストログイン\u003c/strong\u003eを用意しています。\u003c/p\u003e\n\u003cul data-sourcepos=\"305:1-308:0\"\u003e\n\u003cli data-sourcepos=\"305:1-305:29\"\u003eGoogleアカウント不要\u003c/li\u003e\n\u003cli data-sourcepos=\"306:1-306:47\"\u003eワンクリックで即使い始められる\u003c/li\u003e\n\u003cli data-sourcepos=\"307:1-308:0\"\u003e作成したデータは約24時間後に自動削除\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"309:1-309:144\"\u003e気軽に触ってみて、使えそうだと思ったらGoogleアカウントで本登録すればデータは永続的に保存されます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"311:1-311:54\"\u003e\n\u003cspan id=\"開発の裏側ちょっとだけ技術の話\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%96%8B%E7%99%BA%E3%81%AE%E8%A3%8F%E5%81%B4%E3%81%A1%E3%82%87%E3%81%A3%E3%81%A8%E3%81%A0%E3%81%91%E6%8A%80%E8%A1%93%E3%81%AE%E8%A9%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e開発の裏側（ちょっとだけ技術の話）\u003c/h2\u003e\n\u003ctable data-sourcepos=\"313:1-319:41\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"313:1-313:25\"\u003e\n\u003cth data-sourcepos=\"313:2-313:15\"\u003eレイヤー\u003c/th\u003e\n\u003cth data-sourcepos=\"313:17-313:24\"\u003e技術\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"315:1-315:104\"\u003e\n\u003ctd data-sourcepos=\"315:2-315:30\"\u003eガントチャート描画\u003c/td\u003e\n\u003ctd data-sourcepos=\"315:32-315:103\"\u003e\n\u003cstrong\u003e自作 Web Components\u003c/strong\u003e (Lit) → npm パッケージとして公開\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"316:1-316:53\"\u003e\n\u003ctd data-sourcepos=\"316:2-316:24\"\u003eフロントエンド\u003c/td\u003e\n\u003ctd data-sourcepos=\"316:26-316:52\"\u003eVue 3 + Vuetify 4 + Pinia\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"317:1-317:66\"\u003e\n\u003ctd data-sourcepos=\"317:2-317:21\"\u003eバックエンド\u003c/td\u003e\n\u003ctd data-sourcepos=\"317:23-317:65\"\u003eFirebase Cloud Functions + Prisma + MySQL\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"318:1-318:73\"\u003e\n\u003ctd data-sourcepos=\"318:2-318:21\"\u003eリアルタイム\u003c/td\u003e\n\u003ctd data-sourcepos=\"318:23-318:72\"\u003eFirestore (プレゼンス・編集イベント)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"319:1-319:41\"\u003e\n\u003ctd data-sourcepos=\"319:2-319:21\"\u003eホスティング\u003c/td\u003e\n\u003ctd data-sourcepos=\"319:23-319:40\"\u003eFirebase Hosting\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"321:1-321:339\"\u003eガントチャートの描画エンジン（\u003ccode\u003e@mogura/moguchart-core\u003c/code\u003e）はフレームワーク非依存の Web Components として別途開発しています。Vue だけでなく React や Angular でも使えるので、「自分のアプリにガントチャートを組み込みたい」という方にも活用いただけます。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"323:1-327:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"324:1-324:313\"\u003e技術的なアーキテクチャの詳細は、別記事「\u003ca href=\"https://qiita.com/hiroyuki_m/items/d1d2b644890e49b796e7\"\u003e個人開発で本格ガントチャートWebアプリ「MoguChart」を作った話 ─ 自作Web Components × Vue 3 × Firebase のアーキテクチャ全解剖\u003c/a\u003e」で解説しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"326:1-326:299\"\u003eまた、ガントチャート描画ライブラリの詳細については、別記事「\u003ca href=\"https://qiita.com/hiroyuki_m/items/0e4859951a9f652c26c3\"\u003eフレームワークに縛られないガントチャートを作った — Web Components製「moguchart-core」の紹介\u003c/a\u003e」をご覧ください。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"329:1-329:15\"\u003e\n\u003cspan id=\"おわりに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eおわりに\u003c/h2\u003e\n\u003cp data-sourcepos=\"331:1-331:116\"\u003eMoguChart は「\u003cstrong\u003eガントチャートだけをちゃんとやる\u003c/strong\u003e」をコンセプトに開発しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"333:1-333:241\"\u003e余計な機能で煩雑にならず、でも「これが欲しかった」という機能はしっかり揃えている ─ そんな\"ちょうどいい\"ツールを目指して、これからもアップデートを続けていきます。\u003c/p\u003e\n\u003cp data-sourcepos=\"335:1-335:87\"\u003eご意見・ご要望があれば、コメントでお気軽にお寄せください！\u003c/p\u003e\n","body":"\n![top.png](https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/top.png)\n\n## TL;DR\n\n- ブラウザだけで使えるガントチャートWebアプリ **[MoguChart (moguchart.jp)](https://moguchart.jp/)** を個人開発しました\n- **3つの表示モード**（時間単位/日単位/月単位）で、数時間の作業計画から数年の長期プロジェクトまで対応\n- ドラッグ＆ドロップ中心の直感操作、リアルタイム共同編集、PDF/Excel エクスポートなど、実務で「ほしい」と思った機能を詰め込みました\n- Googleアカウント不要の**ゲストログイン**で今すぐ試せます\n\n## なぜ作ったのか\n\nコアライブラリである [moguchart-core](https://github.com/mogura-org/moguchart-core) を自作したことが、すべての始まりでした。\n\n当初は「Web Components製のガントチャートライブラリ」としてライブラリ単体での公開を目指していましたが、開発が進むにつれて **「どれだけ優れたライブラリであっても、実際にプロダクトに組み込まれた姿を見せなければ、その真価は伝わらないのではないか」** と考えるようになりました。\n\nそこで、ライブラリの「究極のデモ」として、そして自分自身が「本当に実務で使いたい」と思えるプロジェクト管理アプリを目指して開発したのが、この **MoguChart** です。\n\n世の中には数多くのプロジェクト管理ツールが存在しますが、多機能すぎて学習コストが高かったり、逆にシンプルすぎてガントチャートとしての表現力が足りなかったりすることが多々あります。これまでの業務で培った Firebase などの技術知見をフルに活かし、「ガントチャートだけを徹底的に、かつ直感的に使えるWebアプリ」という、ちょうどいいニッチを埋めるプロダクトを目指しました。\n\n## こだわったUXポイント\n\n### 1. 🕐 3つの表示モード ─ プロジェクトの粒度に合わせる\n\nMoguChart では、プロジェクト作成時に**表示の粒度**を選べます。\n\n| モード | 最小単位 | 使いどころ |\n|---|---|---|\n| **時間単位** | 分 | 1日のイベントスケジュール、短期の作業計画 |\n| **日単位** | 日 | 一般的なプロジェクト管理（デフォルト） |\n| **月単位** | 月 | 年間ロードマップ、長期プロジェクト |\n\n「日単位だと粗すぎるけど、時間で管理したい作業がある」\n「3年計画だから月レベルで俯瞰したい」\n\nそんなニーズに、1つのアプリで応えられるようにしました。\n\n### 2. 🖱️ ドラッグ＆ドロップですべて完結\n\nタスク管理の操作は、可能な限り**マウス操作だけで完結する**ようにしています。\n\n#### タスク操作\n\n| 操作 | やり方 |\n|---|---|\n| タスクの移動 | バーをドラッグ |\n| 期間の変更 | バーの端をドラッグ |\n| 行の移動 | 行ヘッダーをドラッグ |\n| 依存関係の作成 | バー端のハンドルを他のタスクへドラッグ |\n| テンプレートからタスク作成 | サイドバーからチャートへドラッグ |\n| 複数タスクの一括移動 | Cmd/Ctrl+クリックで複数選択 → ドラッグ |\n\n「右クリックメニュー」と「ダブルクリックで編集」の2つさえ覚えれば、ほぼすべての操作にアクセスできます。\n\n\u003cimg src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/drag-and-drop.gif\" width=\"400\" alt=\"drag-and-drop.gif\"\u003e\n\n#### 右クリックメニュー\n\nチャートの空白エリアやタスクバーを右クリックすると、その場所に応じたコンテキストメニューが表示されます。\n\n- **空白エリア**: 新規タスク作成 / ペースト / Undo / Redo\n- **タスクバー**: 編集 / コピー / 削除 / コメント\n- **行ヘッダー**: 行の追加 / 編集 / 非表示 / 削除\n\n### 3. 🎨 タスクバーの見た目を細かくカスタマイズ\n\n「色分けしたい」は当然として、MoguChart ではさらに踏み込んだ見た目の設定ができます。\n\n#### カラーパレット\n\nプロジェクトごとにカラーパレットを定義し、各パレットに**名前**を付けられます。「進行中」「レビュー待ち」「完了」のように意味のある名前を付ければ、チーム内でバーの色の意味が統一されます。\n\n\u003cimg src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/color-palette.gif\" width=\"400\" alt=\"color-palette.gif\"\u003e\n\n#### パターン（13種類）\n\nタスクバーの背景に重ねるパターンを設定できます。\n\n\u003e diagonal-stripe / diagonal-stripe-thin / diagonal-stripe-thick / diagonal-stripe-reverse / vertical-stripe / horizontal-stripe / checkerboard / dots / dots-dense / triangle / circle / grid / diagonal-grid\n\n「仮タスクはストライプ」「保留中はドット」のように、色＋パターンの組み合わせで状態を視覚的に区別できます。\n\n#### 枠線スタイル\n\n実線（細/太）、破線（細/太）、点線（細/太）から選べます。パターンと組み合わせることで、印刷時にも区別しやすいバーが作れます。\n\n#### バーの影\n\nドロップシャドウの強さを4段階（なし / 小 / 中 / 大）で調整できます。影を付けるとバーが浮き上がって見え、情報量の多いチャートでも視認性が上がります。\n\n### 4. 🏁 マイルストーンで節目を可視化\n\nプロジェクト全体の節目（リリース日、デッドラインなど）を縦線＋バッジで表示します。マウスオーバーでハイライトされるので、「この日までに何を終わらせるか」が一目でわかります。\n\n### 5. 📊 進捗率のプログレスバー\n\nタスクごとに 0〜100% の進捗率を設定でき、バー上にプログレスバーとして表示されます。毎日の朝会で「今どこまで進んでる？」の確認がチャートを見るだけで済みます。\n\n\u003cimg src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/progress-bar.png\" width=\"200\" alt=\"progress-bar.png\"\u003e\n\n### 6. 🔗 依存関係の矢印表示\n\n「タスクBはタスクAが完了してから」という依存関係を、矢印付きの曲線で可視化できます。\n\n- バーの端にある接続ハンドルをドラッグして、依存先のタスクにドロップ\n- 右→左方向の依存関係は**S字カーブ**で自動的に見やすく描画\n\n\u003cimg src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/relation.gif\" width=\"200\" alt=\"relation.gif\"\u003e\n\n### 7. 🔒 「うっかり操作」を防ぐ機能\n\n#### タスクロック\n\n確定済みのタスクに対して「ロック」をかけると、ドラッグでの移動・リサイズ・削除が禁止されます。「このタスクは動かさないで！」をシステムで担保できます。\n\n#### 読み取り専用モード\n\n表示設定メニューから ON にすると、**すべての編集操作が無効化**されます。「報告用に画面を見せたいけど、操作はさせたくない」といったシーンで便利です。\n\n### 8. 📸 スナップショットで「あの時の状態」に戻れる\n\nスナップショットは、プロジェクトの状態をまるごと保存する機能です。\n\n| 機能 | 説明 |\n|---|---|\n| 手動保存 | 任意のタイミングでスナップショットを作成 |\n| 自動保存 | 5分〜24時間の間隔で自動保存（保持期間も設定可能） |\n| 復元 | 過去のスナップショットの状態にワンクリックで復元 |\n| 閲覧 | スナップショットを読み取り専用で閲覧 |\n| ダウンロード | スナップショットをファイルとして保存 |\n\n「昨日の時点ではどうなってたっけ？」を即座に確認でき、「やっぱり戻したい」にもワンクリックで対応できます。\n\n### 9. 👥 リアルタイム共同編集\n\n同じプロジェクトを複数人で同時に開くと：\n\n- **アクティブユーザー表示**: 誰が今見ているかがアバターでわかる\n- **変更履歴パネル**: リアルタイムで他のメンバーの操作が表示される\n- **未読バッジ**: 見逃した変更件数が通知される\n\n変更履歴パネルでは、ログ内のタスク名をクリックすると該当タスクにフォーカスし、ダブルクリックで編集ダイアログが開きます。「誰が何を変えたのか」を追跡しながら作業できます。\n\n### 10. 📤 多彩なエクスポート\n\n| 形式 | 用途 |\n|---|---|\n| **CSV** | 他ツールへのデータ連携 |\n| **Excel** (.xlsx) | 報告書に添付、Excelユーザーへの共有 |\n| **PNG** | スライドやドキュメントへの貼り付け |\n| **PDF** | 印刷・メール添付 |\n| **ZIP** | プロジェクト全体のバックアップ |\n\nCSV/Excel エクスポートには、行名・タスク名・開始日・終了日・日数・ラベル・説明が含まれます。ラベルフィルタを適用中なら、**表示中のデータだけ**がエクスポートされるので、レポート作成にも使えます。\n\n## 「地味だけど便利」な機能たち\n\n### 期間スライド\n\nプロジェクトの開始日が変わったとき、**全タスク・全マイルストーンの日付をまとめてスライド**できます。新しい開始日を指定するだけで、プロジェクト期間を維持したまま全体が自動でずれます。進捗率のクリアオプション付き。\n\n### プロジェクト複製\n\n既存プロジェクトを複製する際、プロジェクト詳細ダイアログの「複製モード」で以下の設定が行えます。\n\n1. **プロジェクト名**: デフォルトで「（元のプロジェクト名）のコピー」と入力され、任意の名前に変更が可能です。\n2. **日付の連動スライド**: 開始日を変更すると、元のプロジェクト期間を維持したまま終了日が自動でスライド（調整）されます（終了日入力欄は非活性になります）。これに伴い、全タスクやマイルストーンの日付も自動的にスライドします。\n3. **進捗率のクリア**: 「進捗率をクリアする」チェックボックス（デフォルトON）が用意されており、複製時にタスクの進捗率をリセットするかどうかを選択できます。\n\nカラーパレット、ラベル、マイルストーン、権限設定なども元のプロジェクトからそのまま引き継がれるため、「前期のプロジェクトをベースに今期を作る」といった作業が非常にスムーズに行えます。\n\n### タスクテンプレート\n\nよく使うタスク設定（名前・色・ラベル）をテンプレートとして保存し、サイドバーからドラッグ＆ドロップでチャートに配置できます。「毎週のルーティンタスク」を一瞬で配置できます。\n\n### 🏷️ ラベルで「分類」と「絞り込み」を両立\n\nタスクや行が増えてくると、「このタスクはどのカテゴリ？」「フロントエンドの作業だけ見たい」といったニーズが出てきます。MoguChart の**ラベル機能**は、こうした「分類」と「絞り込み」を一つの仕組みで実現します。\n\n#### ラベルの定義（プロジェクト設定）\n\nラベルはプロジェクト単位で管理します。プロジェクト詳細ダイアログで、**名前**と**色**を指定してラベルを自由に追加できます。\n\n例えば、以下のようなラベルセットを作れます。\n\n| ラベル名 | 色 | 用途 |\n|---|---|---|\n| フロントエンド | 🟦 青 | UI・画面の実装タスク |\n| バックエンド | 🟩 緑 | API・DB関連のタスク |\n| デザイン | 🟪 紫 | デザイン作業 |\n| レビュー待ち | 🟧 オレンジ | レビューが必要なタスク |\n| 緊急 | 🟥 赤 | 優先度の高いタスク |\n\n#### タスク・行へのラベル付与\n\n定義したラベルは、**タスク**と**行（グループ）**の両方に付与できます。\n\n- **タスク**: タスク編集ダイアログで、ドロップダウンから複数のラベルを選択\n- **行（グループ）**: 行編集ダイアログで同様にラベルを選択\n\n1つのタスク・行に**複数のラベルを同時に付与**できるので、「フロントエンド」かつ「緊急」のように複合的な分類が可能です。\n\n#### バー上のラベル表示\n\nタスクバーにラベルが付与されていると、バー上に**カラーチップ**として表示されます。タスク名の横にラベル名が色付きで並ぶため、チャートを見るだけでタスクのカテゴリが一目でわかります。行ヘッダーにもラベルチップが表示されるので、行レベルの分類も視覚的に把握できます。\n\n#### 2種類のラベルフィルタ\n\nラベルの真価は**フィルタリング**で発揮されます。MoguChart では2種類のラベルフィルタを用意しています。\n\n| フィルタ | 対象 | 操作場所 |\n|---|---|---|\n| **タスクラベルフィルタ** | 特定のラベルを持つタスクだけ表示 | ツールバー |\n| **行ラベルフィルタ** | 特定のラベルを持つ行だけ表示 | コーナーセル |\n\nどちらのフィルタにも**「ラベルなし」**の選択肢があり、ラベルが未設定のタスク・行を絞り込むことも可能です。「すべて選択」「選択解除」のワンクリック操作にも対応しています。\n\n#### エクスポートとの連携\n\nラベルフィルタを適用した状態で CSV / Excel エクスポートを実行すると、**表示中のデータだけ**がエクスポートされます。各タスクに付与されたラベル名もエクスポートデータに含まれるため、「バックエンドのタスクだけ抜き出してレポートに添付」といった使い方ができます。\n\n#### タスクテンプレートとの連携\n\nタスクテンプレートにもラベルを含めて保存できます。「毎週のフロントエンド定例」のように、ラベル付きのテンプレートをドラッグ＆ドロップするだけで、分類済みのタスクが一瞬で配置されます。\n\n#### プロジェクト複製時の引き継ぎ\n\nプロジェクトを複製すると、ラベル定義もそのまま引き継がれます。「前期のプロジェクトをベースに今期を作る」際に、ラベルを一から作り直す必要はありません。\n\n### フリーワード検索＋ラベルフィルタ\n\nタスクが増えてきたときの「探す」をサポート。\n\n- **フリーワード検索**: タスク名にマッチするバーがハイライト表示\n- **タスクラベルフィルタ**: 特定のラベルを持つタスクだけ表示\n- **行ラベルフィルタ**: 特定のラベルを持つ行だけ表示\n\n\u003cimg src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/filter.gif\" width=\"800\" alt=\"filter.gif\"\u003e\n\n### コメント機能\n\nタスク・行・プロジェクトの3レベルでコメントを残せます。プロジェクトコメントは画面右端のサイドバーで管理でき、幅の調整や開閉状態がプロジェクトごとに保存されます。\n\n\u003cimg src=\"https://raw.githubusercontent.com/hiro-murakami/qiita-content/main/images/moguchart-app-ux/comment.gif\" width=\"400\" alt=\"comment.gif\"\u003e\n\n### キーボードショートカット\n\nMoguChartでは、マウス操作だけでなくキーボードによる操作も充実しています。\n\n#### チャート操作・フォーカス移動\n\n| ショートカット | 動作 |\n|---|---|\n| `↑` / `↓` / `←` / `→` | フォーカスを上下左右のタスクに移動 |\n| `Home` / `End` | 現在の行の最初 / 最後のタスクにフォーカス |\n| `Enter` / `Space` | フォーカス中のタスクを選択（`Cmd` / `Ctrl` を押しながらで複数選択のトグル） |\n| `Shift + ←` / `Shift + →` | 選択中のタスクを左 / 右に移動（スナップ単位） |\n| `Delete` / `Backspace` | 選択中のタスクを削除 |\n| `Esc` | 選択・フォーカスのクリア / ドラッグやリサイズ操作のキャンセル |\n\n#### 編集・コピー＆ペースト\n\n| ショートカット | 動作 |\n|---|---|\n| `Cmd+C` / `Ctrl+C` | 選択中のタスクをコピー |\n| `Cmd+V` / `Ctrl+V` | コピーしたタスクをペースト |\n| `Ctrl` / `Alt` + ドラッグ | タスクをコピー（複製）して配置（ドラッグ時のコピーモード） |\n| `Cmd+Z` / `Ctrl+Z` | 元に戻す（Undo） |\n| `Cmd+Shift+Z` / `Ctrl+Y` | やり直し（Redo） |\n\n#### ダイアログ・入力\n\n| ショートカット | 動作 |\n|---|---|\n| `Cmd+Enter` / `Ctrl+Enter` | コメントを送信 |\n| `Enter` | 行名編集時の確定 / ダイアログでの決定 |\n| `Esc` | 行名編集時のキャンセル / ダイアログを閉じる |\n\n## 権限管理 ─ チームでの運用を考えた設計\n\nMoguChart は3段階の権限モデルで、チームでの運用を想定しています。\n\n| 操作 | オーナー | 編集者 | 閲覧者 |\n|---|:---:|:---:|:---:|\n| チャート閲覧 | ✅ | ✅ | ✅ |\n| タスク・行の編集 | ✅ | ✅ | ❌ |\n| プロジェクト設定 | ✅ | ❌ | ❌ |\n| スナップショット作成 | ✅ | ✅ | ❌ |\n| コメント追加 | ✅ | ✅ | ❌ |\n| プロジェクト削除 | ✅ | ❌ | ❌ |\n| エクスポート | ✅ | ✅ | ✅ |\n\nメールアドレスで権限を付与する方式で、過去に入力したアドレスは自動補完候補として保存されます。\n\n## テーマ対応\n\nライト / ダーク / システム連動の3モードに対応。チャートを開いたまま表示設定メニューからワンクリックで切り替えられます。\n\n**バーの高さも5段階**（極小 / 小 / 中 / 大 / 特大）で調整できるので、タスク数が多い場合は「極小」で俯瞰、少ない場合は「大」でゆったり表示、と使い分けられます。\n\n## ゲストログインで今すぐ試せる\n\n**[MoguChart](https://moguchart.jp/)** では、「アカウント作るほどじゃないけど、ちょっと試したい」という方のために、**ゲストログイン**を用意しています。\n\n- Googleアカウント不要\n- ワンクリックで即使い始められる\n- 作成したデータは約24時間後に自動削除\n\n気軽に触ってみて、使えそうだと思ったらGoogleアカウントで本登録すればデータは永続的に保存されます。\n\n## 開発の裏側（ちょっとだけ技術の話）\n\n| レイヤー | 技術 |\n|---|---|\n| ガントチャート描画 | **自作 Web Components** (Lit) → npm パッケージとして公開 |\n| フロントエンド | Vue 3 + Vuetify 4 + Pinia |\n| バックエンド | Firebase Cloud Functions + Prisma + MySQL |\n| リアルタイム | Firestore (プレゼンス・編集イベント) |\n| ホスティング | Firebase Hosting |\n\nガントチャートの描画エンジン（`@mogura/moguchart-core`）はフレームワーク非依存の Web Components として別途開発しています。Vue だけでなく React や Angular でも使えるので、「自分のアプリにガントチャートを組み込みたい」という方にも活用いただけます。\n\n:::note info\n技術的なアーキテクチャの詳細は、別記事「[個人開発で本格ガントチャートWebアプリ「MoguChart」を作った話 ─ 自作Web Components × Vue 3 × Firebase のアーキテクチャ全解剖](https://qiita.com/hiroyuki_m/items/d1d2b644890e49b796e7)」で解説しています。\n\nまた、ガントチャート描画ライブラリの詳細については、別記事「[フレームワークに縛られないガントチャートを作った — Web Components製「moguchart-core」の紹介](https://qiita.com/hiroyuki_m/items/0e4859951a9f652c26c3)」をご覧ください。\n:::\n\n## おわりに\n\nMoguChart は「**ガントチャートだけをちゃんとやる**」をコンセプトに開発しています。\n\n余計な機能で煩雑にならず、でも「これが欲しかった」という機能はしっかり揃えている ─ そんな\"ちょうどいい\"ツールを目指して、これからもアップデートを続けていきます。\n\nご意見・ご要望があれば、コメントでお気軽にお寄せください！\n","coediting":false,"comments_count":0,"created_at":"2026-05-24T14:24:25+09:00","group":null,"id":"bfdaf141de040cb387b9","likes_count":0,"private":false,"reactions_count":0,"stocks_count":1,"tags":[{"name":"UX","versions":[]},{"name":"プロジェクト管理","versions":[]},{"name":"Vue.js","versions":[]},{"name":"個人開発","versions":[]},{"name":"ガントチャート","versions":[]}],"title":"無料で使えるWebガントチャート「MoguChart」を作った ─ 個人開発で追求した\"ちょうどいい\"プロジェクト管理UX","updated_at":"2026-05-30T20:53:41+09:00","url":"https://qiita.com/hiroyuki_m/items/bfdaf141de040cb387b9","user":{"description":"","facebook_id":"","followees_count":1,"followers_count":0,"github_login_name":"hiro-murakami","id":"hiroyuki_m","items_count":3,"linkedin_id":"","location":"","name":"もぐ","organization":"","permanent_id":2275937,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2275937/profile-images/1778984595","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"2:1-2:15\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h2\u003e\n\u003cp data-sourcepos=\"4:1-4:96\"\u003e「既存のガントチャートライブラリ、かゆいところに手が届かない…」\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:208\"\u003eプロジェクト管理でガントチャートを使いたいと思ったとき、既存のツールやライブラリに満足できなかった経験はありませんか？ 私もその一人でした。\u003c/p\u003e\n\u003cp data-sourcepos=\"8:1-8:174\"\u003eそこで、\u003cstrong\u003eガントチャートの描画エンジンから自作する\u003c/strong\u003e という道を選び、Web アプリケーション \u003cstrong\u003e「MoguChart」\u003c/strong\u003e を開発しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"10:1-10:112\"\u003e本記事では、MoguChart の全体アーキテクチャと、個人開発で得た知見をまとめます。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"12:1-18:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"13:1-13:334\"\u003eアプリケーションとしての機能詳細やUXへのこだわりについては、別記事「\u003ca href=\"https://qiita.com/hiroyuki_m/items/bfdaf141de040cb387b9\"\u003e無料で使えるWebガントチャート「MoguChart」を作った ─ 個人開発で追求した\"ちょうどいい\"プロジェクト管理UX\u003c/a\u003e」をご覧ください。\u003c/p\u003e\n\u003cp data-sourcepos=\"15:1-15:337\"\u003eまた、ガントチャート描画ライブラリ「moguchart-core」の機能や導入方法については、別記事「\u003ca href=\"https://qiita.com/hiroyuki_m/items/0e4859951a9f652c26c3\"\u003eフレームワークに縛られないガントチャートを作った — Web Components製「moguchart-core」の紹介\u003c/a\u003e」で紹介しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"17:1-17:186\"\u003eまた、アプリケーションのソースコードは GitHub リポジトリ \u003ca href=\"https://github.com/hiro-murakami/moguchart-app\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehiro-murakami/moguchart-app\u003c/a\u003e で公開しています。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"20:1-20:19\"\u003e\n\u003cspan id=\"moguchart-とは\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#moguchart-%E3%81%A8%E3%81%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eMoguChart とは\u003c/h2\u003e\n\u003cp data-sourcepos=\"22:1-22:114\"\u003eMoguChart は、\u003cstrong\u003eWeb ブラウザ上で動作するガントチャート管理アプリケーション\u003c/strong\u003eです。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"24:1-24:16\"\u003e\n\u003cspan id=\"主な特徴\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%B8%BB%E3%81%AA%E7%89%B9%E5%BE%B4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e主な特徴\u003c/h3\u003e\n\u003ctable data-sourcepos=\"26:1-37:60\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"26:1-26:19\"\u003e\n\u003cth data-sourcepos=\"26:2-26:9\"\u003e機能\u003c/th\u003e\n\u003cth data-sourcepos=\"26:11-26:18\"\u003e説明\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"28:1-28:117\"\u003e\n\u003ctd data-sourcepos=\"28:2-28:38\"\u003e🖱️ ドラッグ＆ドロップ\u003c/td\u003e\n\u003ctd data-sourcepos=\"28:40-28:116\"\u003eタスクの移動・リサイズ・行間移動・複数選択一括操作\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"29:1-29:70\"\u003e\n\u003ctd data-sourcepos=\"29:2-29:30\"\u003e📅 3つの表示モード\u003c/td\u003e\n\u003ctd data-sourcepos=\"29:32-29:69\"\u003e時間単位 / 日単位 / 月単位\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"30:1-30:112\"\u003e\n\u003ctd data-sourcepos=\"30:2-30:32\"\u003e🔗 依存関係の可視化\u003c/td\u003e\n\u003ctd data-sourcepos=\"30:34-30:111\"\u003eタスク間の依存関係を矢印付き曲線（S字カーブ）で表示\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"31:1-31:96\"\u003e\n\u003ctd data-sourcepos=\"31:2-31:35\"\u003e🎨 高度なカスタマイズ\u003c/td\u003e\n\u003ctd data-sourcepos=\"31:37-31:95\"\u003eカラーパレット、パターン、枠線、バー影\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"32:1-32:87\"\u003e\n\u003ctd data-sourcepos=\"32:2-32:44\"\u003e🏁 マイルストーン＆マーカー\u003c/td\u003e\n\u003ctd data-sourcepos=\"32:46-32:86\"\u003eプロジェクトの節目を可視化\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"33:1-33:105\"\u003e\n\u003ctd data-sourcepos=\"33:2-33:38\"\u003e👥 リアルタイム共同編集\u003c/td\u003e\n\u003ctd data-sourcepos=\"33:40-33:104\"\u003e複数ユーザーでの同時編集（プレゼンス表示）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"34:1-34:47\"\u003e\n\u003ctd data-sourcepos=\"34:2-34:26\"\u003e📤 エクスポート\u003c/td\u003e\n\u003ctd data-sourcepos=\"34:28-34:46\"\u003ePDF / CSV / Excel\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"35:1-35:78\"\u003e\n\u003ctd data-sourcepos=\"35:2-35:32\"\u003e📸 スナップショット\u003c/td\u003e\n\u003ctd data-sourcepos=\"35:34-35:77\"\u003eプロジェクト状態の保存・復元\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"36:1-36:75\"\u003e\n\u003ctd data-sourcepos=\"36:2-36:29\"\u003e🌓 テーマ切り替え\u003c/td\u003e\n\u003ctd data-sourcepos=\"36:31-36:74\"\u003eライト / ダーク / システム連動\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"37:1-37:60\"\u003e\n\u003ctd data-sourcepos=\"37:2-37:20\"\u003e🔒 権限管理\u003c/td\u003e\n\u003ctd data-sourcepos=\"37:22-37:59\"\u003eオーナー / 編集者 / 閲覧者\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"39:1-39:33\"\u003e\n\u003cspan id=\"アーキテクチャ全体図\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3%E5%85%A8%E4%BD%93%E5%9B%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eアーキテクチャ全体図\u003c/h2\u003e\n\u003ciframe id=\"qiita-embed-content__50d0f145f085def090dee28368ee59a0\" src=\"https://qiita.com/embed-contents/mermaid#qiita-embed-content__50d0f145f085def090dee28368ee59a0\" style=\"width:100%;\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" data-content='{\"data\":\"flowchart TB\\n subgraph Client [\\\"クライアント\\\"]\\n subgraph Frontend [\\\"Vue 3 + Vuetify 4 (Frontend)\\\"]\\n Pinia[\\\"Pinia (Store)\\\"]\\n Router[\\\"Router\\\"]\\n Custom[\\\"Custom Components\u0026lt;br\u0026gt;(34+ダイアログ)\\\"]\\n end\\n\\n Core[\\\"@mogura/moguchart-core\u0026lt;br\u0026gt;(Lit Web Component)\u0026lt;br\u0026gt;・仮想スクロール\u0026lt;br\u0026gt;・ドラッグ＆ドロップ\u0026lt;br\u0026gt;・カレンダー描画\\\"]\\n\\n Frontend --\u0026gt; Core\\n end\\n\\n subgraph Firebase [\\\"Firebase\\\"]\\n Hosting[\\\"Hosting\u0026lt;br\u0026gt;(SPA配信)\\\"]\\n Functions[\\\"Functions\u0026lt;br\u0026gt;(API Server)\\\"]\\n Firestore[\\\"Firestore\u0026lt;br\u0026gt;(プレゼンス/イベント)\\\"]\\n Auth[\\\"Auth\u0026lt;br\u0026gt;(認証)\\\"]\\n Prisma[\\\"Prisma ORM\\\"]\\n Storage[\\\"Storage\u0026lt;br\u0026gt;(ファイル管理)\\\"]\\n MySQL[(\\\"MySQL\u0026lt;br\u0026gt;(メインDB)\\\")]\\n\\n Functions --\u0026gt; Prisma\\n Prisma --\u0026gt; MySQL\\n end\\n\\n Client --\u0026gt;|\\\"Firebase SDK / Cloud Functions API\\\"| Firebase\",\"key\":\"049fc06c609f0e0a817cb555f896136a\"}'\u003e\n\u003c/iframe\u003e\n\n\u003ch2 data-sourcepos=\"71:1-71:21\"\u003e\n\u003cspan id=\"技術スタック\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%8A%80%E8%A1%93%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e技術スタック\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"73:1-73:25\"\u003e\n\u003cspan id=\"フロントエンド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eフロントエンド\u003c/h3\u003e\n\u003ctable data-sourcepos=\"75:1-82:33\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"75:1-75:19\"\u003e\n\u003cth data-sourcepos=\"75:2-75:9\"\u003e技術\u003c/th\u003e\n\u003cth data-sourcepos=\"75:11-75:18\"\u003e用途\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"77:1-77:68\"\u003e\n\u003ctd data-sourcepos=\"77:2-77:30\"\u003e\n\u003cstrong\u003eVue 3\u003c/strong\u003e (Composition API)\u003c/td\u003e\n\u003ctd data-sourcepos=\"77:32-77:67\"\u003eメイン UI フレームワーク\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"78:1-78:72\"\u003e\n\u003ctd data-sourcepos=\"78:2-78:16\"\u003e\u003cstrong\u003eVuetify 4\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"78:18-78:71\"\u003eMaterial Design コンポーネントライブラリ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"79:1-79:28\"\u003e\n\u003ctd data-sourcepos=\"79:2-79:12\"\u003e\u003cstrong\u003ePinia\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"79:14-79:27\"\u003e状態管理\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"80:1-80:43\"\u003e\n\u003ctd data-sourcepos=\"80:2-80:17\"\u003e\u003cstrong\u003eVue Router\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"80:19-80:42\"\u003eSPA ルーティング\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"81:1-81:33\"\u003e\n\u003ctd data-sourcepos=\"81:2-81:17\"\u003e\u003cstrong\u003eTypeScript\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"81:19-81:32\"\u003e型安全性\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"82:1-82:33\"\u003e\n\u003ctd data-sourcepos=\"82:2-82:11\"\u003e\u003cstrong\u003eVite\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"82:13-82:32\"\u003eビルドツール\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"84:1-84:37\"\u003e\n\u003cspan id=\"コアライブラリ自作\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%82%A2%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E8%87%AA%E4%BD%9C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eコアライブラリ（自作）\u003c/h3\u003e\n\u003ctable data-sourcepos=\"86:1-91:54\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"86:1-86:19\"\u003e\n\u003cth data-sourcepos=\"86:2-86:9\"\u003e技術\u003c/th\u003e\n\u003cth data-sourcepos=\"86:11-86:18\"\u003e用途\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"88:1-88:35\"\u003e\n\u003ctd data-sourcepos=\"88:2-88:10\"\u003e\u003cstrong\u003eLit\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"88:12-88:34\"\u003eWeb Components 基盤\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"89:1-89:28\"\u003e\n\u003ctd data-sourcepos=\"89:2-89:12\"\u003e\u003cstrong\u003edayjs\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"89:14-89:27\"\u003e日付操作\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"90:1-90:60\"\u003e\n\u003ctd data-sourcepos=\"90:2-90:34\"\u003e\n\u003cstrong\u003ehtml2canvas-pro\u003c/strong\u003e + \u003cstrong\u003ejspdf\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"90:36-90:59\"\u003ePDF エクスポート\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"91:1-91:54\"\u003e\n\u003ctd data-sourcepos=\"91:2-91:29\"\u003e\u003cstrong\u003e@holiday-jp/holiday_jp\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"91:31-91:53\"\u003e日本の祝日判定\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"93:1-93:22\"\u003e\n\u003cspan id=\"バックエンド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eバックエンド\u003c/h3\u003e\n\u003ctable data-sourcepos=\"95:1-102:54\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"95:1-95:19\"\u003e\n\u003cth data-sourcepos=\"95:2-95:9\"\u003e技術\u003c/th\u003e\n\u003cth data-sourcepos=\"95:11-95:18\"\u003e用途\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"97:1-97:51\"\u003e\n\u003ctd data-sourcepos=\"97:2-97:31\"\u003e\u003cstrong\u003eFirebase Cloud Functions\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"97:33-97:50\"\u003eAPI サーバー\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"98:1-98:69\"\u003e\n\u003ctd data-sourcepos=\"98:2-98:35\"\u003e\n\u003cstrong\u003ePrisma ORM\u003c/strong\u003e (MariaDB adapter)\u003c/td\u003e\n\u003ctd data-sourcepos=\"98:37-98:68\"\u003eデータベースアクセス\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"99:1-99:43\"\u003e\n\u003ctd data-sourcepos=\"99:2-99:12\"\u003e\u003cstrong\u003eMySQL\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"99:14-99:42\"\u003eメインデータベース\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"100:1-100:54\"\u003e\n\u003ctd data-sourcepos=\"100:2-100:20\"\u003e\u003cstrong\u003eFirebase Auth\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"100:22-100:53\"\u003e認証（Google / ゲスト）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"101:1-101:74\"\u003e\n\u003ctd data-sourcepos=\"101:2-101:16\"\u003e\u003cstrong\u003eFirestore\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"101:18-101:73\"\u003eリアルタイムプレゼンス・編集イベント\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"102:1-102:54\"\u003e\n\u003ctd data-sourcepos=\"102:2-102:23\"\u003e\u003cstrong\u003eFirebase Storage\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"102:25-102:53\"\u003eファイルストレージ\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"104:1-104:16\"\u003e\n\u003cspan id=\"インフラ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eインフラ\u003c/h3\u003e\n\u003ctable data-sourcepos=\"106:1-110:56\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"106:1-106:19\"\u003e\n\u003cth data-sourcepos=\"106:2-106:9\"\u003e技術\u003c/th\u003e\n\u003cth data-sourcepos=\"106:11-106:18\"\u003e用途\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"108:1-108:43\"\u003e\n\u003ctd data-sourcepos=\"108:2-108:23\"\u003e\u003cstrong\u003eFirebase Hosting\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"108:25-108:42\"\u003eSPA 配信・CDN\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"109:1-109:43\"\u003e\n\u003ctd data-sourcepos=\"109:2-109:21\"\u003e\u003cstrong\u003epnpm workspace\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"109:23-109:42\"\u003eモノレポ管理\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"110:1-110:56\"\u003e\n\u003ctd data-sourcepos=\"110:2-110:34\"\u003e\u003cstrong\u003eGoogle Cloud Secret Manager\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"110:36-110:55\"\u003e環境変数管理\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"112:1-112:70\"\u003e\n\u003cspan id=\"なぜ-web-components-でガントチャートを自作したのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C-web-components-%E3%81%A7%E3%82%AC%E3%83%B3%E3%83%88%E3%83%81%E3%83%A3%E3%83%BC%E3%83%88%E3%82%92%E8%87%AA%E4%BD%9C%E3%81%97%E3%81%9F%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜ Web Components でガントチャートを自作したのか\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"114:1-114:34\"\u003e\n\u003cspan id=\"既存ライブラリの課題\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%97%A2%E5%AD%98%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E8%AA%B2%E9%A1%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e既存ライブラリの課題\u003c/h3\u003e\n\u003cp data-sourcepos=\"116:1-116:119\"\u003eガントチャートの OSS ライブラリはいくつかありますが、以下の点で不満がありました：\u003c/p\u003e\n\u003col data-sourcepos=\"118:1-122:0\"\u003e\n\u003cli data-sourcepos=\"118:1-118:112\"\u003e\n\u003cstrong\u003e特定フレームワークに依存\u003c/strong\u003e — React 用、Vue 用とそれぞれ別のライブラリが必要\u003c/li\u003e\n\u003cli data-sourcepos=\"119:1-119:123\"\u003e\n\u003cstrong\u003eカスタマイズ性の限界\u003c/strong\u003e — タスクバーの描画やインタラクションを自由に変えられない\u003c/li\u003e\n\u003cli data-sourcepos=\"120:1-120:78\"\u003e\n\u003cstrong\u003eパフォーマンス\u003c/strong\u003e — 大量タスクでのスクロールが重い\u003c/li\u003e\n\u003cli data-sourcepos=\"121:1-122:0\"\u003e\n\u003cstrong\u003eメンテナンス状況\u003c/strong\u003e — 更新が止まっているものが多い\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 data-sourcepos=\"123:1-123:37\"\u003e\n\u003cspan id=\"web-components-を選んだ理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#web-components-%E3%82%92%E9%81%B8%E3%82%93%E3%81%A0%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eWeb Components を選んだ理由\u003c/h3\u003e\n\u003cp data-sourcepos=\"125:1-125:76\"\u003e\u003cstrong\u003eフレームワーク非依存\u003c/strong\u003eであることが最大の動機です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"127:1-134:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- どのフレームワークでも同じタグで使える --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;gantt-chart\u003c/span\u003e\n  \u003cspan class=\"na\"\u003e.rows=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${rows}\"\u003c/span\u003e\n  \u003cspan class=\"na\"\u003e.option=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${option}\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003etask-update=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${handleTaskUpdate}\"\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/gantt-chart\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"136:1-136:193\"\u003eLit を選択したのは、Web Components の標準仕様に沿いつつ、リアクティブなプロパティ管理やテンプレートリテラルの恩恵を受けられるためです。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"138:1-138:60\"\u003e\n\u003cspan id=\"コアライブラリ-moguramoguchart-core-の設計\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%82%A2%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA-moguramoguchart-core-%E3%81%AE%E8%A8%AD%E8%A8%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eコアライブラリ \u003ccode\u003e@mogura/moguchart-core\u003c/code\u003e の設計\u003c/h3\u003e\n\u003cp data-sourcepos=\"140:1-140:137\"\u003e描画エンジンは別リポジトリ（\u003ccode\u003emoguchart-core\u003c/code\u003e）として切り出し、npm パッケージとして公開しています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"142:1-144:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003enpm \u003cspan class=\"nb\"\u003einstall\u003c/span\u003e @mogura/moguchart-core\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"146:1-146:117\"\u003eアプリ側からは \u003ccode\u003elink:\u003c/code\u003e で参照し、開発中は変更を即座に反映できるようにしています：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"148:1-154:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"dependencies\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"@mogura/moguchart-core\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"link:../../../moguchart-core\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch4 data-sourcepos=\"156:1-156:32\"\u003e\n\u003cspan id=\"コアの主な実装内容\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%82%A2%E3%81%AE%E4%B8%BB%E3%81%AA%E5%AE%9F%E8%A3%85%E5%86%85%E5%AE%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eコアの主な実装内容\u003c/h4\u003e\n\u003cul data-sourcepos=\"158:1-163:0\"\u003e\n\u003cli data-sourcepos=\"158:1-158:118\"\u003e\n\u003cstrong\u003e仮想スクロール\u003c/strong\u003e: 画面に見える範囲だけを描画し、大量の行・タスクでも60FPSを維持\u003c/li\u003e\n\u003cli data-sourcepos=\"159:1-159:107\"\u003e\n\u003cstrong\u003eカレンダー描画\u003c/strong\u003e: 日/週/月/時間単位の切り替え、祝日表示、現在時刻ライン\u003c/li\u003e\n\u003cli data-sourcepos=\"160:1-160:98\"\u003e\n\u003cstrong\u003eドラッグ＆ドロップ\u003c/strong\u003e: タスクの移動・リサイズ・行間移動・複数選択\u003c/li\u003e\n\u003cli data-sourcepos=\"161:1-161:93\"\u003e\n\u003cstrong\u003e依存関係線\u003c/strong\u003e: S字カーブでの矢印描画、右→左方向の自動折り返し\u003c/li\u003e\n\u003cli data-sourcepos=\"162:1-163:0\"\u003e\n\u003cstrong\u003eテーマシステム\u003c/strong\u003e: ライト/ダーク/カスタムテーマの CSS Custom Properties ベース\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"164:1-164:21\"\u003e\n\u003cspan id=\"モノレポ構成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A2%E3%83%8E%E3%83%AC%E3%83%9D%E6%A7%8B%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eモノレポ構成\u003c/h2\u003e\n\u003cp data-sourcepos=\"166:1-166:82\"\u003eアプリ側は pnpm workspace を使ったモノレポ構成にしています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"168:1-191:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003emoguchart-app/\n├── packages/\n│   ├── frontend/          # Vue 3 + Vuetify 4 フロントエンド\n│   │   ├── src/\n│   │   │   ├── components/  # 34+ の Vue コンポーネント\n│   │   │   ├── composables/ # Vue Composable 関数\n│   │   │   ├── modules/     # ユーティリティ・カスタムレンダリング\n│   │   │   ├── stores/      # Pinia ストア\n│   │   │   ├── views/       # ガントチャートビュー\n│   │   │   └── firebase.ts  # Firebase 初期化\n│   │   └── package.json\n│   └── functions/         # Firebase Cloud Functions バックエンド\n│       ├── src/\n│       │   └── index.ts     # API エンドポイント\n│       ├── prisma/\n│       │   ├── schema.prisma # DB スキーマ定義\n│       │   └── seed.ts       # シードデータ\n│       └── package.json\n├── firebase.json          # Firebase 設定\n├── firestore.rules        # Firestore セキュリティルール\n├── storage.rules          # Storage セキュリティルール\n└── pnpm-workspace.yaml    # モノレポ設定\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"193:1-193:169\"\u003eフロントエンドとバックエンドを同一リポジトリで管理することで、\u003cstrong\u003e型定義の共有\u003c/strong\u003eやデプロイの一元化が容易になります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"195:1-195:27\"\u003e\n\u003cspan id=\"データベース設計\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E8%A8%AD%E8%A8%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eデータベース設計\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"197:1-197:58\"\u003e\n\u003cspan id=\"なぜ-firestore-ではなく-mysql-を選んだのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C-firestore-%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F-mysql-%E3%82%92%E9%81%B8%E3%82%93%E3%81%A0%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜ Firestore ではなく MySQL を選んだのか\u003c/h3\u003e\n\u003cp data-sourcepos=\"199:1-199:119\"\u003eFirebase を使っているのに、メインDBは MySQL (Prisma ORM 経由) という構成を採用しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"201:1-201:11\"\u003e\u003cstrong\u003e理由\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"202:1-205:0\"\u003e\n\u003cli data-sourcepos=\"202:1-202:135\"\u003eガントチャートのデータは\u003cstrong\u003eリレーショナルな構造\u003c/strong\u003e（プロジェクト → 行 → タスク → コメント）\u003c/li\u003e\n\u003cli data-sourcepos=\"203:1-203:56\"\u003e複雑なクエリやトランザクションが必要\u003c/li\u003e\n\u003cli data-sourcepos=\"204:1-205:0\"\u003ePrisma の型安全なデータアクセスを利用したい\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"206:1-206:51\"\u003e\u003cstrong\u003eFirestore はリアルタイム機能に特化\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"207:1-209:0\"\u003e\n\u003cli data-sourcepos=\"207:1-207:53\"\u003eプレゼンス情報（誰がオンラインか）\u003c/li\u003e\n\u003cli data-sourcepos=\"208:1-209:0\"\u003e編集イベントのリアルタイム配信\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"prisma\" data-sourcepos=\"210:1-241:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e// データモデルの概要\nmodel Project {\n  id        String     @id @default(uuid())\n  name      String\n  start     DateTime\n  end       DateTime\n  attribute Json       @default(\"{}\")  // 柔軟な拡張属性\n  authority Json       @default(\"{}\")  // 権限情報\n  rows      GanttRow[]\n  comments  Comment[]\n}\n\nmodel GanttRow {\n  id        Int         @id @default(autoincrement())\n  projectId String\n  name      String\n  order     Int         @default(0)\n  tasks     GanttTask[]\n  project   Project     @relation(...)\n}\n\nmodel GanttTask {\n  id        Int       @id @default(autoincrement())\n  rowId     Int\n  name      String\n  start     DateTime\n  end       DateTime\n  attribute Json      @default(\"{}\")  // ラベル、色、パターン等\n  row       GanttRow  @relation(...)\n}\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"243:1-243:48\"\u003e\u003cstrong\u003eポイント: \u003ccode\u003eattribute\u003c/code\u003e カラムの活用\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"245:1-245:304\"\u003eスキーマを頻繁に変更せずに柔軟な属性を追加できるよう、\u003ccode\u003eJson\u003c/code\u003e 型の \u003ccode\u003eattribute\u003c/code\u003e カラムを各テーブルに持たせています。カラーパレット、パターン、枠線スタイル、ラベルなど、UI側で拡張が多い属性をここに格納しています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"247:1-247:27\"\u003e\n\u003cspan id=\"firebase-の使い分け\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#firebase-%E3%81%AE%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eFirebase の使い分け\u003c/h2\u003e\n\u003cp data-sourcepos=\"249:1-249:93\"\u003eMoguChart では Firebase の各サービスを\u003cstrong\u003e適材適所\u003c/strong\u003eで使い分けています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"251:1-257:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eFirebase Auth     → 認証（Google ログイン / ゲストログイン）\nCloud Functions   → API サーバー（Prisma 経由で MySQL にアクセス）\nFirestore         → リアルタイム機能のみ（プレゼンス・編集イベント）\nFirebase Hosting  → SPA の配信\nFirebase Storage  → ファイル管理（バックアップ等）\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"259:1-259:41\"\u003e\n\u003cspan id=\"firestore-セキュリティルール\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#firestore-%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E3%83%AB%E3%83%BC%E3%83%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eFirestore セキュリティルール\u003c/h3\u003e\n\u003cp data-sourcepos=\"261:1-261:87\"\u003eリアルタイム機能部分のルールは必要最小限に設計しています：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"263:1-285:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003erules_version\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e2\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"nx\"\u003eservice\u003c/span\u003e \u003cspan class=\"nx\"\u003ecloud\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003efirestore\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003ematch\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nx\"\u003edatabases\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003edatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"sr\"\u003e/documents \u003c/span\u003e\u003cspan class=\"err\"\u003e{\n\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// プレゼンス: 認証済みユーザーは読み取り可、自分のドキュメントのみ書き込み可\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003ematch\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nx\"\u003eprojects\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003eprojectId\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"sr\"\u003e/presence/\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003eallow\u003c/span\u003e \u003cspan class=\"na\"\u003eread\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eauth\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003eallow\u003c/span\u003e \u003cspan class=\"na\"\u003ewrite\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eauth\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\n        \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eauth\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eemail\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"nx\"\u003euserId\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"nx\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eauth\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003euid\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"nx\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e// 編集イベント: 認証済みなら作成・読み取り可、更新不可（append-only）\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003ematch\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"nx\"\u003eprojects\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003eprojectId\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"sr\"\u003e/editEvents/\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003eeventId\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003eallow\u003c/span\u003e \u003cspan class=\"na\"\u003eread\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eauth\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003eallow\u003c/span\u003e \u003cspan class=\"na\"\u003ecreate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eauth\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003eallow\u003c/span\u003e \u003cspan class=\"na\"\u003edelete\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nx\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eauth\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003eallow\u003c/span\u003e \u003cspan class=\"na\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"287:1-287:33\"\u003e\n\u003cspan id=\"フロントエンドの設計\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E3%81%AE%E8%A8%AD%E8%A8%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eフロントエンドの設計\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"289:1-289:31\"\u003e\n\u003cspan id=\"コンポーネント設計\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E8%A8%AD%E8%A8%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eコンポーネント設計\u003c/h3\u003e\n\u003cp data-sourcepos=\"291:1-291:98\"\u003e34以上のVueコンポーネントを開発しています。主要なものを紹介します：\u003c/p\u003e\n\u003ctable data-sourcepos=\"293:1-305:47\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"293:1-293:49\"\u003e\n\u003cth data-sourcepos=\"293:2-293:15\"\u003eカテゴリ\u003c/th\u003e\n\u003cth data-sourcepos=\"293:17-293:39\"\u003eコンポーネント\u003c/th\u003e\n\u003cth data-sourcepos=\"293:41-293:48\"\u003e説明\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"295:1-295:93\"\u003e\n\u003ctd data-sourcepos=\"295:2-295:25\"\u003e\u003cstrong\u003eメインビュー\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"295:27-295:44\"\u003e\u003ccode\u003eGanttChartView\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"295:46-295:92\"\u003eガントチャート表示・操作の中核\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"296:1-296:108\"\u003e\n\u003ctd data-sourcepos=\"296:2-296:31\"\u003e\u003cstrong\u003eプロジェクト管理\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"296:33-296:53\"\u003e\u003ccode\u003eProjectListDialog\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"296:55-296:107\"\u003eプロジェクト一覧・検索・アーカイブ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"297:1-297:102\"\u003e\n\u003ctd data-sourcepos=\"297:2-297:2\"\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"297:4-297:26\"\u003e\u003ccode\u003eProjectDetailDialog\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"297:28-297:101\"\u003eプロジェクト詳細設定（カラーパレット、ラベル等）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"298:1-298:84\"\u003e\n\u003ctd data-sourcepos=\"298:2-298:2\"\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"298:4-298:29\"\u003e\u003ccode\u003eProjectDuplicateDialog\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"298:31-298:83\"\u003eプロジェクト複製（ステッパー形式）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"299:1-299:81\"\u003e\n\u003ctd data-sourcepos=\"299:2-299:22\"\u003e\u003cstrong\u003eタスク操作\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"299:24-299:41\"\u003e\u003ccode\u003eTaskFormDialog\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"299:43-299:80\"\u003eタスク作成・編集フォーム\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"300:1-300:48\"\u003e\n\u003ctd data-sourcepos=\"300:2-300:2\"\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"300:4-300:23\"\u003e\u003ccode\u003eTaskDetailDialog\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"300:25-300:47\"\u003eタスク詳細表示\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"301:1-301:62\"\u003e\n\u003ctd data-sourcepos=\"301:2-301:2\"\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"301:4-301:25\"\u003e\u003ccode\u003eTaskTemplateDialog\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"301:27-301:61\"\u003eタスクテンプレート管理\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"302:1-302:104\"\u003e\n\u003ctd data-sourcepos=\"302:2-302:31\"\u003e\u003cstrong\u003eコラボレーション\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"302:33-302:55\"\u003e\u003ccode\u003eProjectCommentPanel\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"302:57-302:103\"\u003eプロジェクトコメントサイドバー\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"303:1-303:74\"\u003e\n\u003ctd data-sourcepos=\"303:2-303:2\"\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"303:4-303:31\"\u003e\u003ccode\u003eCollaborationActivityLog\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"303:33-303:73\"\u003e共同編集アクティビティログ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"304:1-304:98\"\u003e\n\u003ctd data-sourcepos=\"304:2-304:19\"\u003e\u003cstrong\u003e表示制御\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"304:21-304:43\"\u003e\u003ccode\u003eDisplaySettingsMenu\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"304:45-304:97\"\u003eテーマ・バー高さ・影・読み取り専用\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"305:1-305:47\"\u003e\n\u003ctd data-sourcepos=\"305:2-305:2\"\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"305:4-305:19\"\u003e\u003ccode\u003eZoomControls\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"305:21-305:46\"\u003eズームレベル調整\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"307:1-307:34\"\u003e\n\u003cspan id=\"カスタムレンダリング\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eカスタムレンダリング\u003c/h3\u003e\n\u003cp data-sourcepos=\"309:1-309:356\"\u003e\u003ccode\u003eganttChartCustomRendering.ts\u003c/code\u003e (約35KB) で、ガントチャートのタスクバー・行ヘッダー・ツールチップなどのカスタム描画ロジックを集中管理しています。Web Components の slot やコールバックを活用して、コアライブラリの描画をアプリ固有の表現にカスタマイズしています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"311:1-311:24\"\u003e\n\u003cspan id=\"状態管理-pinia\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86-pinia\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e状態管理 (Pinia)\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"313:1-319:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// ユーザーストア: 認証状態・テーマ設定・バージョン管理\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003euserStore\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseUserStore\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// プロジェクトストア: 現在開いているプロジェクトの状態\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eprojectStore\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseProjectStore\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"321:1-321:139\"\u003eFirebase Auth のリスナーを Pinia ストアで管理し、認証状態の変化をリアクティブに UI に反映しています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"323:1-323:27\"\u003e\n\u003cspan id=\"開発で工夫した点\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%96%8B%E7%99%BA%E3%81%A7%E5%B7%A5%E5%A4%AB%E3%81%97%E3%81%9F%E7%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e開発で工夫した点\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"325:1-325:40\"\u003e\n\u003cspan id=\"1-バージョン同期の自動化\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E5%90%8C%E6%9C%9F%E3%81%AE%E8%87%AA%E5%8B%95%E5%8C%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. バージョン同期の自動化\u003c/h3\u003e\n\u003cp data-sourcepos=\"327:1-327:102\"\u003eモノレポ内のバージョン番号を一元管理するスクリプトを用意しています：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"329:1-336:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"scripts\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"predev\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"pnpm run sync-version\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"prebuild\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"pnpm run sync-version\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"338:1-338:105\"\u003e\u003ccode\u003edev\u003c/code\u003e や \u003ccode\u003ebuild\u003c/code\u003e の前に自動実行されるため、バージョンのずれが発生しません。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"340:1-340:37\"\u003e\n\u003cspan id=\"2-環境変数の安全な管理\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%AE%E5%AE%89%E5%85%A8%E3%81%AA%E7%AE%A1%E7%90%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 環境変数の安全な管理\u003c/h3\u003e\n\u003cp data-sourcepos=\"342:1-342:151\"\u003eGoogle Cloud Secret Manager からの自動取得スクリプトを用意し、機密情報をリポジトリに含めない設計にしています：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"344:1-350:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"scripts\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"setup:env\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"pnpm -r run setup:env\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"352:1-352:28\"\u003e\n\u003cspan id=\"3-ゲストログイン\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E3%82%B2%E3%82%B9%E3%83%88%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. ゲストログイン\u003c/h3\u003e\n\u003cp data-sourcepos=\"354:1-354:198\"\u003eGoogleアカウントがなくても気軽に試せるよう、\u003cstrong\u003eゲストログイン機能\u003c/strong\u003eを実装しました。ゲストユーザーのデータは約24時間後に自動削除されます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"356:1-356:43\"\u003e\n\u003cspan id=\"4-リリースノートの自動表示\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%83%8E%E3%83%BC%E3%83%88%E3%81%AE%E8%87%AA%E5%8B%95%E8%A1%A8%E7%A4%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. リリースノートの自動表示\u003c/h3\u003e\n\u003cp data-sourcepos=\"358:1-358:159\"\u003eバージョンアップ後の初回ログイン時にリリースノートダイアログを自動表示し、ユーザーに新機能を伝えています：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"360:1-367:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// バージョンアップ時にリリースノートを自動表示\u003c/span\u003e\n\u003cspan class=\"nf\"\u003ewatch\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eversionUpdated\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eupdated\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eupdated\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eshowReleaseNotes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"369:1-369:24\"\u003e\n\u003cspan id=\"開発の振り返り\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%96%8B%E7%99%BA%E3%81%AE%E6%8C%AF%E3%82%8A%E8%BF%94%E3%82%8A\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e開発の振り返り\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"371:1-371:34\"\u003e\n\u003cspan id=\"個人開発で学んだこと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%80%8B%E4%BA%BA%E9%96%8B%E7%99%BA%E3%81%A7%E5%AD%A6%E3%82%93%E3%81%A0%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e個人開発で学んだこと\u003c/h3\u003e\n\u003col data-sourcepos=\"373:1-377:0\"\u003e\n\u003cli data-sourcepos=\"373:1-373:165\"\u003e\n\u003cstrong\u003eコアロジックの切り出しは早めに\u003c/strong\u003e — Web Components として切り出したことで、コアの変更がアプリに波及しにくくなった\u003c/li\u003e\n\u003cli data-sourcepos=\"374:1-374:119\"\u003e\n\u003cstrong\u003eJson カラムの柔軟性\u003c/strong\u003e — UI 側の頻繁な属性追加にマイグレーションなしで対応できた\u003c/li\u003e\n\u003cli data-sourcepos=\"375:1-375:113\"\u003e\n\u003cstrong\u003eFirebase + MySQL のハイブリッド\u003c/strong\u003e — リアルタイム機能とRDBの良いとこ取りができた\u003c/li\u003e\n\u003cli data-sourcepos=\"376:1-377:0\"\u003e\n\u003cstrong\u003eモノレポの恩恵\u003c/strong\u003e — フロント・バックの型定義共有でバグが激減\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 data-sourcepos=\"378:1-378:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"380:1-380:198\"\u003eMoguChart は、\u003cstrong\u003eガントチャートのコアエンジン（Web Components）\u003c/strong\u003e と \u003cstrong\u003eフル機能の Web アプリケーション（Vue 3 + Firebase）\u003c/strong\u003e の2層構造で設計しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"382:1-382:157\"\u003eコアライブラリ \u003ccode\u003e@mogura/moguchart-core\u003c/code\u003e はフレームワーク非依存なので、Vue だけでなく React や Angular でも利用できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"384:1-384:165\"\u003e個人開発でも、適切なアーキテクチャ設計を最初に行うことで、継続的な機能追加がスムーズに進むことを実感しました。\u003c/p\u003e\n\u003chr data-sourcepos=\"386:1-388:0\"\u003e\n\u003cp data-sourcepos=\"389:1-389:96\"\u003eご質問やフィードバックがありましたら、コメントでお気軽にどうぞ！\u003c/p\u003e\n","body":"\n## はじめに\n\n「既存のガントチャートライブラリ、かゆいところに手が届かない…」\n\nプロジェクト管理でガントチャートを使いたいと思ったとき、既存のツールやライブラリに満足できなかった経験はありませんか？ 私もその一人でした。\n\nそこで、**ガントチャートの描画エンジンから自作する** という道を選び、Web アプリケーション **「MoguChart」** を開発しました。\n\n本記事では、MoguChart の全体アーキテクチャと、個人開発で得た知見をまとめます。\n\n:::note info\nアプリケーションとしての機能詳細やUXへのこだわりについては、別記事「[無料で使えるWebガントチャート「MoguChart」を作った ─ 個人開発で追求した\"ちょうどいい\"プロジェクト管理UX](https://qiita.com/hiroyuki_m/items/bfdaf141de040cb387b9)」をご覧ください。\n\nまた、ガントチャート描画ライブラリ「moguchart-core」の機能や導入方法については、別記事「[フレームワークに縛られないガントチャートを作った — Web Components製「moguchart-core」の紹介](https://qiita.com/hiroyuki_m/items/0e4859951a9f652c26c3)」で紹介しています。\n\nまた、アプリケーションのソースコードは GitHub リポジトリ [hiro-murakami/moguchart-app](https://github.com/hiro-murakami/moguchart-app) で公開しています。\n:::\n\n## MoguChart とは\n\nMoguChart は、**Web ブラウザ上で動作するガントチャート管理アプリケーション**です。\n\n### 主な特徴\n\n| 機能 | 説明 |\n|---|---|\n| 🖱️ ドラッグ＆ドロップ | タスクの移動・リサイズ・行間移動・複数選択一括操作 |\n| 📅 3つの表示モード | 時間単位 / 日単位 / 月単位 |\n| 🔗 依存関係の可視化 | タスク間の依存関係を矢印付き曲線（S字カーブ）で表示 |\n| 🎨 高度なカスタマイズ | カラーパレット、パターン、枠線、バー影 |\n| 🏁 マイルストーン＆マーカー | プロジェクトの節目を可視化 |\n| 👥 リアルタイム共同編集 | 複数ユーザーでの同時編集（プレゼンス表示） |\n| 📤 エクスポート | PDF / CSV / Excel |\n| 📸 スナップショット | プロジェクト状態の保存・復元 |\n| 🌓 テーマ切り替え | ライト / ダーク / システム連動 |\n| 🔒 権限管理 | オーナー / 編集者 / 閲覧者 |\n\n## アーキテクチャ全体図\n\n```mermaid\nflowchart TB\n    subgraph Client [\"クライアント\"]\n        subgraph Frontend [\"Vue 3 + Vuetify 4 (Frontend)\"]\n            Pinia[\"Pinia (Store)\"]\n            Router[\"Router\"]\n            Custom[\"Custom Components\u003cbr\u003e(34+ダイアログ)\"]\n        end\n\n        Core[\"@mogura/moguchart-core\u003cbr\u003e(Lit Web Component)\u003cbr\u003e・仮想スクロール\u003cbr\u003e・ドラッグ＆ドロップ\u003cbr\u003e・カレンダー描画\"]\n\n        Frontend --\u003e Core\n    end\n\n    subgraph Firebase [\"Firebase\"]\n        Hosting[\"Hosting\u003cbr\u003e(SPA配信)\"]\n        Functions[\"Functions\u003cbr\u003e(API Server)\"]\n        Firestore[\"Firestore\u003cbr\u003e(プレゼンス/イベント)\"]\n        Auth[\"Auth\u003cbr\u003e(認証)\"]\n        Prisma[\"Prisma ORM\"]\n        Storage[\"Storage\u003cbr\u003e(ファイル管理)\"]\n        MySQL[(\"MySQL\u003cbr\u003e(メインDB)\")]\n\n        Functions --\u003e Prisma\n        Prisma --\u003e MySQL\n    end\n\n    Client --\u003e|\"Firebase SDK / Cloud Functions API\"| Firebase\n```\n\n## 技術スタック\n\n### フロントエンド\n\n| 技術 | 用途 |\n|---|---|\n| **Vue 3** (Composition API) | メイン UI フレームワーク |\n| **Vuetify 4** | Material Design コンポーネントライブラリ |\n| **Pinia** | 状態管理 |\n| **Vue Router** | SPA ルーティング |\n| **TypeScript** | 型安全性 |\n| **Vite** | ビルドツール |\n\n### コアライブラリ（自作）\n\n| 技術 | 用途 |\n|---|---|\n| **Lit** | Web Components 基盤 |\n| **dayjs** | 日付操作 |\n| **html2canvas-pro** + **jspdf** | PDF エクスポート |\n| **@holiday-jp/holiday_jp** | 日本の祝日判定 |\n\n### バックエンド\n\n| 技術 | 用途 |\n|---|---|\n| **Firebase Cloud Functions** | API サーバー |\n| **Prisma ORM** (MariaDB adapter) | データベースアクセス |\n| **MySQL** | メインデータベース |\n| **Firebase Auth** | 認証（Google / ゲスト） |\n| **Firestore** | リアルタイムプレゼンス・編集イベント |\n| **Firebase Storage** | ファイルストレージ |\n\n### インフラ\n\n| 技術 | 用途 |\n|---|---|\n| **Firebase Hosting** | SPA 配信・CDN |\n| **pnpm workspace** | モノレポ管理 |\n| **Google Cloud Secret Manager** | 環境変数管理 |\n\n## なぜ Web Components でガントチャートを自作したのか\n\n### 既存ライブラリの課題\n\nガントチャートの OSS ライブラリはいくつかありますが、以下の点で不満がありました：\n\n1. **特定フレームワークに依存** — React 用、Vue 用とそれぞれ別のライブラリが必要\n2. **カスタマイズ性の限界** — タスクバーの描画やインタラクションを自由に変えられない\n3. **パフォーマンス** — 大量タスクでのスクロールが重い\n4. **メンテナンス状況** — 更新が止まっているものが多い\n\n### Web Components を選んだ理由\n\n**フレームワーク非依存**であることが最大の動機です。\n\n```html\n\u003c!-- どのフレームワークでも同じタグで使える --\u003e\n\u003cgantt-chart\n  .rows=\"${rows}\"\n  .option=\"${option}\"\n  @task-update=\"${handleTaskUpdate}\"\n\u003e\u003c/gantt-chart\u003e\n```\n\nLit を選択したのは、Web Components の標準仕様に沿いつつ、リアクティブなプロパティ管理やテンプレートリテラルの恩恵を受けられるためです。\n\n### コアライブラリ `@mogura/moguchart-core` の設計\n\n描画エンジンは別リポジトリ（`moguchart-core`）として切り出し、npm パッケージとして公開しています。\n\n```bash\nnpm install @mogura/moguchart-core\n```\n\nアプリ側からは `link:` で参照し、開発中は変更を即座に反映できるようにしています：\n\n```json\n{\n  \"dependencies\": {\n    \"@mogura/moguchart-core\": \"link:../../../moguchart-core\"\n  }\n}\n```\n\n#### コアの主な実装内容\n\n- **仮想スクロール**: 画面に見える範囲だけを描画し、大量の行・タスクでも60FPSを維持\n- **カレンダー描画**: 日/週/月/時間単位の切り替え、祝日表示、現在時刻ライン\n- **ドラッグ＆ドロップ**: タスクの移動・リサイズ・行間移動・複数選択\n- **依存関係線**: S字カーブでの矢印描画、右→左方向の自動折り返し\n- **テーマシステム**: ライト/ダーク/カスタムテーマの CSS Custom Properties ベース\n\n## モノレポ構成\n\nアプリ側は pnpm workspace を使ったモノレポ構成にしています。\n\n```\nmoguchart-app/\n├── packages/\n│   ├── frontend/          # Vue 3 + Vuetify 4 フロントエンド\n│   │   ├── src/\n│   │   │   ├── components/  # 34+ の Vue コンポーネント\n│   │   │   ├── composables/ # Vue Composable 関数\n│   │   │   ├── modules/     # ユーティリティ・カスタムレンダリング\n│   │   │   ├── stores/      # Pinia ストア\n│   │   │   ├── views/       # ガントチャートビュー\n│   │   │   └── firebase.ts  # Firebase 初期化\n│   │   └── package.json\n│   └── functions/         # Firebase Cloud Functions バックエンド\n│       ├── src/\n│       │   └── index.ts     # API エンドポイント\n│       ├── prisma/\n│       │   ├── schema.prisma # DB スキーマ定義\n│       │   └── seed.ts       # シードデータ\n│       └── package.json\n├── firebase.json          # Firebase 設定\n├── firestore.rules        # Firestore セキュリティルール\n├── storage.rules          # Storage セキュリティルール\n└── pnpm-workspace.yaml    # モノレポ設定\n```\n\nフロントエンドとバックエンドを同一リポジトリで管理することで、**型定義の共有**やデプロイの一元化が容易になります。\n\n## データベース設計\n\n### なぜ Firestore ではなく MySQL を選んだのか\n\nFirebase を使っているのに、メインDBは MySQL (Prisma ORM 経由) という構成を採用しています。\n\n**理由**:\n- ガントチャートのデータは**リレーショナルな構造**（プロジェクト → 行 → タスク → コメント）\n- 複雑なクエリやトランザクションが必要\n- Prisma の型安全なデータアクセスを利用したい\n\n**Firestore はリアルタイム機能に特化**:\n- プレゼンス情報（誰がオンラインか）\n- 編集イベントのリアルタイム配信\n\n```prisma\n// データモデルの概要\nmodel Project {\n  id        String     @id @default(uuid())\n  name      String\n  start     DateTime\n  end       DateTime\n  attribute Json       @default(\"{}\")  // 柔軟な拡張属性\n  authority Json       @default(\"{}\")  // 権限情報\n  rows      GanttRow[]\n  comments  Comment[]\n}\n\nmodel GanttRow {\n  id        Int         @id @default(autoincrement())\n  projectId String\n  name      String\n  order     Int         @default(0)\n  tasks     GanttTask[]\n  project   Project     @relation(...)\n}\n\nmodel GanttTask {\n  id        Int       @id @default(autoincrement())\n  rowId     Int\n  name      String\n  start     DateTime\n  end       DateTime\n  attribute Json      @default(\"{}\")  // ラベル、色、パターン等\n  row       GanttRow  @relation(...)\n}\n```\n\n**ポイント: `attribute` カラムの活用**\n\nスキーマを頻繁に変更せずに柔軟な属性を追加できるよう、`Json` 型の `attribute` カラムを各テーブルに持たせています。カラーパレット、パターン、枠線スタイル、ラベルなど、UI側で拡張が多い属性をここに格納しています。\n\n## Firebase の使い分け\n\nMoguChart では Firebase の各サービスを**適材適所**で使い分けています。\n\n```\nFirebase Auth     → 認証（Google ログイン / ゲストログイン）\nCloud Functions   → API サーバー（Prisma 経由で MySQL にアクセス）\nFirestore         → リアルタイム機能のみ（プレゼンス・編集イベント）\nFirebase Hosting  → SPA の配信\nFirebase Storage  → ファイル管理（バックアップ等）\n```\n\n### Firestore セキュリティルール\n\nリアルタイム機能部分のルールは必要最小限に設計しています：\n\n```javascript\nrules_version = '2';\n\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    // プレゼンス: 認証済みユーザーは読み取り可、自分のドキュメントのみ書き込み可\n    match /projects/{projectId}/presence/{userId} {\n      allow read: if request.auth != null;\n      allow write: if request.auth != null\n        \u0026\u0026 (request.auth.token.email == userId\n            || request.auth.uid == userId);\n    }\n\n    // 編集イベント: 認証済みなら作成・読み取り可、更新不可（append-only）\n    match /projects/{projectId}/editEvents/{eventId} {\n      allow read: if request.auth != null;\n      allow create: if request.auth != null;\n      allow delete: if request.auth != null;\n      allow update: if false;\n    }\n  }\n}\n```\n\n## フロントエンドの設計\n\n### コンポーネント設計\n\n34以上のVueコンポーネントを開発しています。主要なものを紹介します：\n\n| カテゴリ | コンポーネント | 説明 |\n|---|---|---|\n| **メインビュー** | `GanttChartView` | ガントチャート表示・操作の中核 |\n| **プロジェクト管理** | `ProjectListDialog` | プロジェクト一覧・検索・アーカイブ |\n| | `ProjectDetailDialog` | プロジェクト詳細設定（カラーパレット、ラベル等） |\n| | `ProjectDuplicateDialog` | プロジェクト複製（ステッパー形式） |\n| **タスク操作** | `TaskFormDialog` | タスク作成・編集フォーム |\n| | `TaskDetailDialog` | タスク詳細表示 |\n| | `TaskTemplateDialog` | タスクテンプレート管理 |\n| **コラボレーション** | `ProjectCommentPanel` | プロジェクトコメントサイドバー |\n| | `CollaborationActivityLog` | 共同編集アクティビティログ |\n| **表示制御** | `DisplaySettingsMenu` | テーマ・バー高さ・影・読み取り専用 |\n| | `ZoomControls` | ズームレベル調整 |\n\n### カスタムレンダリング\n\n`ganttChartCustomRendering.ts` (約35KB) で、ガントチャートのタスクバー・行ヘッダー・ツールチップなどのカスタム描画ロジックを集中管理しています。Web Components の slot やコールバックを活用して、コアライブラリの描画をアプリ固有の表現にカスタマイズしています。\n\n### 状態管理 (Pinia)\n\n```typescript\n// ユーザーストア: 認証状態・テーマ設定・バージョン管理\nconst userStore = useUserStore()\n\n// プロジェクトストア: 現在開いているプロジェクトの状態\nconst projectStore = useProjectStore()\n```\n\nFirebase Auth のリスナーを Pinia ストアで管理し、認証状態の変化をリアクティブに UI に反映しています。\n\n## 開発で工夫した点\n\n### 1. バージョン同期の自動化\n\nモノレポ内のバージョン番号を一元管理するスクリプトを用意しています：\n\n```json\n{\n  \"scripts\": {\n    \"predev\": \"pnpm run sync-version\",\n    \"prebuild\": \"pnpm run sync-version\"\n  }\n}\n```\n\n`dev` や `build` の前に自動実行されるため、バージョンのずれが発生しません。\n\n### 2. 環境変数の安全な管理\n\nGoogle Cloud Secret Manager からの自動取得スクリプトを用意し、機密情報をリポジトリに含めない設計にしています：\n\n```json\n{\n  \"scripts\": {\n    \"setup:env\": \"pnpm -r run setup:env\"\n  }\n}\n```\n\n### 3. ゲストログイン\n\nGoogleアカウントがなくても気軽に試せるよう、**ゲストログイン機能**を実装しました。ゲストユーザーのデータは約24時間後に自動削除されます。\n\n### 4. リリースノートの自動表示\n\nバージョンアップ後の初回ログイン時にリリースノートダイアログを自動表示し、ユーザーに新機能を伝えています：\n\n```typescript\n// バージョンアップ時にリリースノートを自動表示\nwatch(versionUpdated, (updated) =\u003e {\n  if (updated) {\n    showReleaseNotes.value = true\n  }\n})\n```\n\n## 開発の振り返り\n\n### 個人開発で学んだこと\n\n1. **コアロジックの切り出しは早めに** — Web Components として切り出したことで、コアの変更がアプリに波及しにくくなった\n2. **Json カラムの柔軟性** — UI 側の頻繁な属性追加にマイグレーションなしで対応できた\n3. **Firebase + MySQL のハイブリッド** — リアルタイム機能とRDBの良いとこ取りができた\n4. **モノレポの恩恵** — フロント・バックの型定義共有でバグが激減\n\n## まとめ\n\nMoguChart は、**ガントチャートのコアエンジン（Web Components）** と **フル機能の Web アプリケーション（Vue 3 + Firebase）** の2層構造で設計しています。\n\nコアライブラリ `@mogura/moguchart-core` はフレームワーク非依存なので、Vue だけでなく React や Angular でも利用できます。\n\n個人開発でも、適切なアーキテクチャ設計を最初に行うことで、継続的な機能追加がスムーズに進むことを実感しました。\n\n---\n\n\nご質問やフィードバックがありましたら、コメントでお気軽にどうぞ！\n","coediting":false,"comments_count":0,"created_at":"2026-05-24T06:52:55+09:00","group":null,"id":"d1d2b644890e49b796e7","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vue.js","versions":[]},{"name":"WebComponents","versions":[]},{"name":"Firebase","versions":[]},{"name":"個人開発","versions":[]},{"name":"ガントチャート","versions":[]}],"title":"個人開発で本格ガントチャートWebアプリ「MoguChart」を作った話 ─ 自作Web Components × Vue 3 × Firebase のアーキテクチャ全解剖","updated_at":"2026-05-30T20:53:41+09:00","url":"https://qiita.com/hiroyuki_m/items/d1d2b644890e49b796e7","user":{"description":"","facebook_id":"","followees_count":1,"followers_count":0,"github_login_name":"hiro-murakami","id":"hiroyuki_m","items_count":3,"linkedin_id":"","location":"","name":"もぐ","organization":"","permanent_id":2275937,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2275937/profile-images/1778984595","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:134\"\u003e\n\u003cspan id=\"同じ業務-web-ui-を-vanilla-html--vue--react--thymeleaf-で実装して比較した--4スタックの違いと選定指針\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%90%8C%E3%81%98%E6%A5%AD%E5%8B%99-web-ui-%E3%82%92-vanilla-html--vue--react--thymeleaf-%E3%81%A7%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%A6%E6%AF%94%E8%BC%83%E3%81%97%E3%81%9F--4%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AE%E9%81%95%E3%81%84%E3%81%A8%E9%81%B8%E5%AE%9A%E6%8C%87%E9%87%9D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e同じ業務 Web UI を Vanilla HTML / Vue / React / Thymeleaf で実装して比較した — 4スタックの違いと選定指針\u003c/h1\u003e\n\u003ch2 data-sourcepos=\"3:1-3:33\"\u003e\n\u003cspan id=\"この記事でわかること\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8B%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこの記事でわかること\u003c/h2\u003e\n\u003cul data-sourcepos=\"5:1-9:0\"\u003e\n\u003cli data-sourcepos=\"5:1-5:180\"\u003e\n\u003cstrong\u003e同一仕様の業務 Web UI\u003c/strong\u003e を Vanilla HTML / Vue 3 / React / Thymeleaf の4スタックで実装した時、コード上で何がどう違うのか（コード例付き）\u003c/li\u003e\n\u003cli data-sourcepos=\"6:1-6:92\"\u003eプロジェクト構成・ビルドの違い（package.json / Vite / pom.xml / 素HTML）\u003c/li\u003e\n\u003cli data-sourcepos=\"7:1-7:116\"\u003eAPI クライアント・状態管理・フォーム処理・エラー表示・デプロイの \u003cstrong\u003e観点別比較\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"8:1-9:0\"\u003e「どんな場面でどのスタックを選ぶか」の判断軸\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"10:1-10:15\"\u003e\n\u003cspan id=\"対象読者\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AF%BE%E8%B1%A1%E8%AA%AD%E8%80%85\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e対象読者\u003c/h2\u003e\n\u003cul data-sourcepos=\"12:1-15:0\"\u003e\n\u003cli data-sourcepos=\"12:1-12:110\"\u003eフレームワーク選定で \u003cstrong\u003eVanilla / Vue / React / Thymeleaf\u003c/strong\u003e のどれを使うか迷っている方\u003c/li\u003e\n\u003cli data-sourcepos=\"13:1-13:133\"\u003eそれぞれを単体では触ったことがあっても、\u003cstrong\u003e同じ仕様で並べたとき何が違うのか\u003c/strong\u003e を知りたい方\u003c/li\u003e\n\u003cli data-sourcepos=\"14:1-15:0\"\u003e業務系 Web UI（フォーム + API呼び出し + 結果表示）を作る前提でスタックを比較したい方\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"16:1-16:15\"\u003e\n\u003cspan id=\"動作環境\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8B%95%E4%BD%9C%E7%92%B0%E5%A2%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e動作環境\u003c/h2\u003e\n\u003ctable data-sourcepos=\"18:1-23:57\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"18:1-18:28\"\u003e\n\u003cth data-sourcepos=\"18:2-18:9\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"18:11-18:27\"\u003eバージョン\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"20:1-20:70\"\u003e\n\u003ctd data-sourcepos=\"20:2-20:15\"\u003eVanilla HTML\u003c/td\u003e\n\u003ctd data-sourcepos=\"20:17-20:69\"\u003eビルド不要（モジュールスクリプト）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"21:1-21:58\"\u003e\n\u003ctd data-sourcepos=\"21:2-21:6\"\u003eVue\u003c/td\u003e\n\u003ctd data-sourcepos=\"21:8-21:57\"\u003e3.x + Vite 5.x + TypeScript 5.x + Vue Router 4.x\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"22:1-22:63\"\u003e\n\u003ctd data-sourcepos=\"22:2-22:8\"\u003eReact\u003c/td\u003e\n\u003ctd data-sourcepos=\"22:10-22:62\"\u003e18.x + Vite 5.x + TypeScript 5.x + React Router 6.x\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"23:1-23:57\"\u003e\n\u003ctd data-sourcepos=\"23:2-23:12\"\u003eThymeleaf\u003c/td\u003e\n\u003ctd data-sourcepos=\"23:14-23:56\"\u003eSpring Boot 3.x + Java 21 + Thymeleaf 3.x\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr data-sourcepos=\"25:1-26:0\"\u003e\n\u003ch2 data-sourcepos=\"27:1-27:18\"\u003e\n\u003cspan id=\"1-はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. はじめに\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"29:1-29:79\"\u003e\n\u003cspan id=\"正直に言うと最初は比較記事を書く予定ではなかった\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%AD%A3%E7%9B%B4%E3%81%AB%E8%A8%80%E3%81%86%E3%81%A8%E6%9C%80%E5%88%9D%E3%81%AF%E6%AF%94%E8%BC%83%E8%A8%98%E4%BA%8B%E3%82%92%E6%9B%B8%E3%81%8F%E4%BA%88%E5%AE%9A%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e正直に言うと、最初は比較記事を書く予定ではなかった\u003c/h3\u003e\n\u003cp data-sourcepos=\"31:1-31:298\"\u003e\u003ccode\u003ebanklink-service\u003c/code\u003e のバックエンド開発中に「API が動いているか確認できる画面がほしい」と思い、その場で一番早く作れる Vanilla HTML で適当に組みました。\u003cstrong\u003e「とりあえず動けばいい」\u003c/strong\u003e を絵に描いたような書き捨て UI です。\u003c/p\u003e\n\u003cp data-sourcepos=\"33:1-33:261\"\u003e開発を進めるにあたっては「いずれちゃんと Vue か React に置き換えて、本格的な開発テンプレートにしよう」と考えていました。フロントエンドの本実装はそこから始めるつもりだったのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"35:1-35:82\"\u003e\u003cstrong\u003eところが、手を動かそうとした瞬間にふと気づきました。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"37:1-37:345\"\u003eこの程度の処理量（フォーム + API 呼び出し + 結果表示）なら、\u003cstrong\u003eThymeleaf も含めて4スタック全部で実装してみてもそんなに時間はかからない\u003c/strong\u003e。しかも全部同じ機能で揃えれば、\u003cstrong\u003e各技術の違いを並べて比較するのに丁度いい題材になるんじゃないか？\u003c/strong\u003e と。\u003c/p\u003e\n\u003cp data-sourcepos=\"39:1-39:221\"\u003e開発作業の進捗としては、これは完全に \u003cstrong\u003e脱線\u003c/strong\u003e です。「テンプレートを1つ作る」予定が、いつの間にか「4スタックで並列実装して比較する」に化けていました。\u003c/p\u003e\n\u003cp data-sourcepos=\"41:1-41:295\"\u003eですが、書き上げてみると自分自身が技術選定の判断軸を整理できたうえ、同じ場面で迷っている人の判断材料にもなりそうだったので、記事として残すことにしました。\u003cstrong\u003e本記事はその「楽しい脱線の副産物」です。\u003c/strong\u003e\u003c/p\u003e\n\u003chr data-sourcepos=\"43:1-44:0\"\u003e\n\u003ch3 data-sourcepos=\"45:1-45:16\"\u003e\n\u003cspan id=\"で本題\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A7%E6%9C%AC%E9%A1%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eで、本題\u003c/h3\u003e\n\u003cp data-sourcepos=\"47:1-47:330\"\u003e業務系の Web UI を作るとき、\u003cstrong\u003e「Vanilla HTML / Vue / React / Thymeleaf のどれで作るのが妥当か」\u003c/strong\u003e は最初に通る判断です。それぞれ単体ではよく語られますが、「\u003cstrong\u003e同じ仕様で4つ並べたら何が違うのか\u003c/strong\u003e」を実コードで横並びにした記事は意外と少ない。\u003c/p\u003e\n\u003cp data-sourcepos=\"49:1-49:245\"\u003eそこで、銀行系 API ラッパーサービス \u003ccode\u003ebanklink-service\u003c/code\u003e（個人練習用プロジェクト）で、まったく同じ仕様の業務 UI を \u003cstrong\u003e4スタックで並行実装\u003c/strong\u003e しました。本記事はその比較の記録です。\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"51:1-52:246\"\u003e\n\u003cp data-sourcepos=\"51:3-52:246\"\u003e\u003cstrong\u003eこの記事のスタンス\u003c/strong\u003e\u003cbr\u003e\n「正解の1つ」を提示する記事ではありません。\u003cstrong\u003e同じ要件を4スタックで書いたコードを見比べて、自分のプロジェクトでどれが妥当かを判断する材料を提供する\u003c/strong\u003e ことが目的です。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"54:1-55:0\"\u003e\n\u003ch2 data-sourcepos=\"56:1-56:64\"\u003e\n\u003cspan id=\"2-共通仕様4スタックで完全に揃えた前提\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E5%85%B1%E9%80%9A%E4%BB%95%E6%A7%984%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%A7%E5%AE%8C%E5%85%A8%E3%81%AB%E6%8F%83%E3%81%88%E3%81%9F%E5%89%8D%E6%8F%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 共通仕様（4スタックで完全に揃えた前提）\u003c/h2\u003e\n\u003cp data-sourcepos=\"58:1-58:55\"\u003e4実装はすべて以下の仕様を満たします。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"60:1-60:10\"\u003e\n\u003cspan id=\"機能\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A9%9F%E8%83%BD\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e機能\u003c/h3\u003e\n\u003cul data-sourcepos=\"61:1-66:0\"\u003e\n\u003cli data-sourcepos=\"61:1-61:72\"\u003e6ページ: ホーム / \u003cstrong\u003e口座\u003c/strong\u003e / ローン / 外貨 / 投資 / KYC\u003c/li\u003e\n\u003cli data-sourcepos=\"62:1-62:166\"\u003e各ページに複数の API 操作セクション（例: 口座ページは「一覧取得・残高取得・入金・出金・取引履歴」の5セクション）\u003c/li\u003e\n\u003cli data-sourcepos=\"63:1-63:83\"\u003e画面上部の \u003cstrong\u003eBearer Token 入力欄\u003c/strong\u003e から API 認証トークンを設定\u003c/li\u003e\n\u003cli data-sourcepos=\"64:1-64:59\"\u003eナビゲーションで各ページを行き来できる\u003c/li\u003e\n\u003cli data-sourcepos=\"65:1-66:0\"\u003eボタンを押すと API を叩いて結果を画面に整形表示\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"67:1-67:31\"\u003e\n\u003cspan id=\"接続先バックエンド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%8E%A5%E7%B6%9A%E5%85%88%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e接続先バックエンド\u003c/h3\u003e\n\u003cul data-sourcepos=\"68:1-70:0\"\u003e\n\u003cli data-sourcepos=\"68:1-68:67\"\u003e同じ Spring Boot API (\u003ccode\u003e/api/v1/accounts\u003c/code\u003e, \u003ccode\u003e/api/v1/loans\u003c/code\u003e, ...)\u003c/li\u003e\n\u003cli data-sourcepos=\"69:1-70:0\"\u003eレスポンスは JSON、エラーは HTTP ステータス + body の構造で統一\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"71:1-71:46\"\u003e\n\u003cspan id=\"揃えていない点差別化箇所\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%8F%83%E3%81%88%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84%E7%82%B9%E5%B7%AE%E5%88%A5%E5%8C%96%E7%AE%87%E6%89%80\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e揃えていない点（差別化箇所）\u003c/h3\u003e\n\u003cul data-sourcepos=\"72:1-74:0\"\u003e\n\u003cli data-sourcepos=\"72:1-72:72\"\u003e\n\u003cstrong\u003eCSS の見た目\u003c/strong\u003e（外部UI / 内部UI で意図的に変える）\u003c/li\u003e\n\u003cli data-sourcepos=\"73:1-74:0\"\u003e内部実装（フレームワーク特性に従う）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"75:1-75:110\"\u003eつまり「\u003cstrong\u003e画面と機能は同じ、中身だけ4通り\u003c/strong\u003e」という比較ベースを作りました。\u003c/p\u003e\n\u003cp data-sourcepos=\"77:1-77:208\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F02_accounts.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=2792f696f7ac845bd5d7f03c3b9e6ad4\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F02_accounts.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=2792f696f7ac845bd5d7f03c3b9e6ad4\" alt=\"本記事で深掘りする「口座」画面 — Vanilla HTML 外部UI 版。4スタックすべてが同じ機能を持つ\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F02_accounts.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=56e5e78ff3b78f61d31edbce919bf438 1x\" data-canonical-src=\"https://mint041223techblog.netlify.app/images/banklink-service/02_accounts.webp\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"79:1-79:259\"\u003e口座画面（上記）には「口座一覧取得・残高取得・入金・出金・取引履歴」の5セクションがあります。\u003cstrong\u003eこの同じ画面・同じ機能を、4スタックで別々に実装した\u003c/strong\u003e のが本記事の比較対象です。\u003c/p\u003e\n\u003chr data-sourcepos=\"81:1-82:0\"\u003e\n\u003ch2 data-sourcepos=\"83:1-83:34\"\u003e\n\u003cspan id=\"3-4スタックの構成概要\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-4%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AE%E6%A7%8B%E6%88%90%E6%A6%82%E8%A6%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 4スタックの構成概要\u003c/h2\u003e\n\u003cp data-sourcepos=\"85:1-85:60\"\u003eそれぞれの「最小構成」をまず一望します。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"87:1-87:16\"\u003e\n\u003cspan id=\"vanilla-html\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vanilla-html\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVanilla HTML\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"89:1-99:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ebanklink-web-vanilla-html/\n├─ index.html         ← トップページ\n├─ accounts.html      ← 口座ページ\n├─ loans.html         ← ローンページ\n├─ ... (他4ページ)\n├─ common-external.js ← トークン管理 + バインド共通\n└─ shared/\n    ├─ api/client.js  ← fetch ラッパー\n    └─ common.js      ← bindAction / renderResponse\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"101:1-101:159\"\u003e\u003cstrong\u003eビルドツールなし\u003c/strong\u003e。\u003ccode\u003e.html\u003c/code\u003e を直接ブラウザで開ける（または nginx で配信）。\u003ccode\u003e\u0026lt;script type=\"module\"\u0026gt;\u003c/code\u003e で JS をインポート。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"103:1-103:7\"\u003e\n\u003cspan id=\"vue\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"105:1-118:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ebanklink-web-vue/\n├─ vite.config.ts\n├─ index.html         ← SPA エントリ\n└─ src/\n    ├─ main.ts         ← createApp + mount\n    ├─ App.vue         ← レイアウト + RouterView\n    ├─ router/index.ts ← Vue Router 設定\n    ├─ api/client.ts   ← fetch ラッパー (TypeScript)\n    └─ views/\n        ├─ HomeView.vue\n        ├─ AccountsView.vue   ← 口座ページ\n        ├─ ... (他4ページ)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"120:1-120:78\"\u003e\u003ccode\u003enpm run build\u003c/code\u003e で \u003ccode\u003edist/\u003c/code\u003e に静的ファイル生成 → nginx で配信。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"122:1-122:9\"\u003e\n\u003cspan id=\"react\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#react\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eReact\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"124:1-132:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ebanklink-web-react/\n├─ vite.config.ts\n├─ index.html\n└─ src/\n    ├─ main.tsx           ← createRoot + render\n    ├─ App.tsx            ← 全6ページを1ファイルに集約（小規模なため）\n    └─ ... (shared/api/client.ts)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"134:1-134:116\"\u003eVue と類似だが、\u003ccode\u003eApp.tsx\u003c/code\u003e に全ページを書く構成にした（コンポーネント数を最小化）。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"136:1-136:13\"\u003e\n\u003cspan id=\"thymeleaf\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#thymeleaf\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eThymeleaf\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"138:1-159:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ebanklink-web-thymeleaf/\n├─ pom.xml\n└─ banklink-external-web-thymeleaf/\n    └─ src/main/\n        ├─ java/com/y104autumn/banklink/external/\n        │   ├─ BanklinkExternalApplication.java\n        │   ├─ controller/\n        │   │   ├─ ExternalTopPageController.java\n        │   │   ├─ AccountsController.java\n        │   │   └─ ... (他4ページ)\n        │   ├─ service/\n        │   │   ├─ AccountsService.java   ← RestClient で API 呼び出し\n        │   │   └─ ...\n        │   └─ form/\n        │       ├─ AccountsForm.java      ← @ModelAttribute 用\n        │       └─ ...\n        └─ resources/templates/\n            ├─ index.html\n            ├─ accounts.html              ← th:field 付き\n            └─ ...\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"161:1-161:86\"\u003e\u003ccode\u003emvn package\u003c/code\u003e で実行可能 jar 生成 → \u003ccode\u003ejava -jar\u003c/code\u003e または Docker で起動。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"163:1-163:34\"\u003e\n\u003cspan id=\"構成ファイル数の比較\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A7%8B%E6%88%90%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E6%95%B0%E3%81%AE%E6%AF%94%E8%BC%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e構成ファイル数の比較\u003c/h3\u003e\n\u003ctable data-sourcepos=\"165:1-170:80\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"165:1-165:73\"\u003e\n\u003cth data-sourcepos=\"165:2-165:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"165:4-165:29\"\u003eプロジェクト全体\u003c/th\u003e\n\u003cth data-sourcepos=\"165:31-165:72\"\u003e1ページ実装に必要なファイル\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"167:1-167:67\"\u003e\n\u003ctd data-sourcepos=\"167:2-167:15\"\u003eVanilla HTML\u003c/td\u003e\n\u003ctd data-sourcepos=\"167:17-167:37\"\u003e約 10 ファイル\u003c/td\u003e\n\u003ctd data-sourcepos=\"167:39-167:66\"\u003e1 (\u003ccode\u003eaccounts.html\u003c/code\u003e のみ)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"168:1-168:54\"\u003e\n\u003ctd data-sourcepos=\"168:2-168:6\"\u003eVue\u003c/td\u003e\n\u003ctd data-sourcepos=\"168:8-168:28\"\u003e約 15 ファイル\u003c/td\u003e\n\u003ctd data-sourcepos=\"168:30-168:53\"\u003e1 (\u003ccode\u003eAccountsView.vue\u003c/code\u003e)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"169:1-169:59\"\u003e\n\u003ctd data-sourcepos=\"169:2-169:8\"\u003eReact\u003c/td\u003e\n\u003ctd data-sourcepos=\"169:10-169:29\"\u003e約 5 ファイル\u003c/td\u003e\n\u003ctd data-sourcepos=\"169:31-169:58\"\u003e1 (\u003ccode\u003eApp.tsx\u003c/code\u003e 内の関数)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"170:1-170:80\"\u003e\n\u003ctd data-sourcepos=\"170:2-170:12\"\u003eThymeleaf\u003c/td\u003e\n\u003ctd data-sourcepos=\"170:14-170:34\"\u003e約 25 ファイル\u003c/td\u003e\n\u003ctd data-sourcepos=\"170:36-170:79\"\u003e3 (Controller + Service + Form + template)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"172:1-172:162\"\u003eThymeleaf は \u003cstrong\u003eMVC を3層に分けるための定型コード\u003c/strong\u003e が多いです。ファイル数は最多ですが、各ファイルの役割は明確です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"174:1-174:65\"\u003e\n\u003cspan id=\"実装した6ページ参考スクリーンショット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%9F6%E3%83%9A%E3%83%BC%E3%82%B8%E5%8F%82%E8%80%83%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実装した6ページ（参考スクリーンショット）\u003c/h3\u003e\n\u003cp data-sourcepos=\"176:1-176:151\"\u003e口座画面以外の5ページも全4スタックで同じ機能を持っています。参考までに Vanilla HTML 版の画面を列挙します。\u003c/p\u003e\n\u003cp data-sourcepos=\"178:1-178:181\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F01_top.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=38b6a83b03eeb15f1cc0bc552f7fd689\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F01_top.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=38b6a83b03eeb15f1cc0bc552f7fd689\" alt=\"トップ画面（外部UI）— トークン入力 + ナビゲーション + 各ページへの導線\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F01_top.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=a0354933567502c1ea7681df973b24cc 1x\" data-canonical-src=\"https://mint041223techblog.netlify.app/images/banklink-service/01_top.webp\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"180:1-180:188\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F03_loans.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9cf5ff4c933c2953c1b444fe1249917f\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F03_loans.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9cf5ff4c933c2953c1b444fe1249917f\" alt=\"ローン画面 — 申込・残高照会・支払いシミュレーション等のAPI操作セクション\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F03_loans.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=0cfdea3961bb7fca1185567d29f2c801 1x\" data-canonical-src=\"https://mint041223techblog.netlify.app/images/banklink-service/03_loans.webp\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"182:1-182:141\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F04_forex.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b46529efd1f99c5e58e6680c419c583e\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F04_forex.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b46529efd1f99c5e58e6680c419c583e\" alt=\"外貨画面 — レート取得・両替実行 等の操作\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F04_forex.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=336da9c764a1f5f977dbcd3be2d42016 1x\" data-canonical-src=\"https://mint041223techblog.netlify.app/images/banklink-service/04_forex.webp\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"184:1-184:153\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F05_investments.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=217e6900da77175eb2cb1e03f56db12a\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F05_investments.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=217e6900da77175eb2cb1e03f56db12a\" alt=\"投資画面 — ポートフォリオ・売買注文 等の操作\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F05_investments.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=0c355472685d7e84f345335daaa18c98 1x\" data-canonical-src=\"https://mint041223techblog.netlify.app/images/banklink-service/05_investments.webp\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"186:1-186:139\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F07_kyc.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=aaf12459f34b0c9290582a33425210df\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F07_kyc.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=aaf12459f34b0c9290582a33425210df\" alt=\"KYC画面 — 本人確認状況・書類提出 等の操作\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F07_kyc.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=3da68cc26aad3b08884719bdcf1516d6 1x\" data-canonical-src=\"https://mint041223techblog.netlify.app/images/banklink-service/07_kyc.webp\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003ch3 data-sourcepos=\"188:1-188:47\"\u003e\n\u003cspan id=\"外部uiと内部uiの見た目の差別化\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%A4%96%E9%83%A8ui%E3%81%A8%E5%86%85%E9%83%A8ui%E3%81%AE%E8%A6%8B%E3%81%9F%E7%9B%AE%E3%81%AE%E5%B7%AE%E5%88%A5%E5%8C%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e外部UIと内部UIの見た目の差別化\u003c/h3\u003e\n\u003cp data-sourcepos=\"190:1-190:185\"\u003e「\u003cstrong\u003e機能は同じ、見た目は外部UI / 内部UI で意図的に変える\u003c/strong\u003e」という方針を採っており、内部UI（業務端末寄り）はこのような外観です。\u003c/p\u003e\n\u003cp data-sourcepos=\"192:1-192:195\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F06_internal_top.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=bde416d7d06bae9f63e6da27e5ea650e\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F06_internal_top.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=bde416d7d06bae9f63e6da27e5ea650e\" alt=\"内部UI トップ画面（Vanilla HTML 版）— 業務端末寄りのデザインに意図的に差別化\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmint041223techblog.netlify.app%2Fimages%2Fbanklink-service%2F06_internal_top.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=8906d2c6198446c567d1186459f9fc36 1x\" data-canonical-src=\"https://mint041223techblog.netlify.app/images/banklink-service/06_internal_top.webp\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"194:1-194:227\"\u003e本記事の比較は \u003cstrong\u003e外部UI\u003c/strong\u003e を題材にしていますが、各スタックとも external/internal の2セットを実装しており、CSS だけ入れ替えれば両方に対応できる構造になっています。\u003c/p\u003e\n\u003chr data-sourcepos=\"196:1-197:0\"\u003e\n\u003ch2 data-sourcepos=\"198:1-198:51\"\u003e\n\u003cspan id=\"4-プロジェクト構成ビルドの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%A7%8B%E6%88%90%E3%83%93%E3%83%AB%E3%83%89%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. プロジェクト構成・ビルドの違い\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"200:1-200:42\"\u003e\n\u003cspan id=\"vanilla-html--ビルド設定なし\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vanilla-html--%E3%83%93%E3%83%AB%E3%83%89%E8%A8%AD%E5%AE%9A%E3%81%AA%E3%81%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVanilla HTML — ビルド設定なし\u003c/h3\u003e\n\u003cp data-sourcepos=\"202:1-202:131\"\u003e\u003ccode\u003epackage.json\u003c/code\u003e も \u003ccode\u003etsconfig.json\u003c/code\u003e も無し。ブラウザでファイルを直接開くか、nginx で静的配信するだけ。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"204:1-210:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# 開発: 直接ブラウザで開く\u003c/span\u003e\nopen accounts.html\n\n\u003cspan class=\"c\"\u003e# 本番: nginx の document root に置く\u003c/span\u003e\nnginx \u003cspan class=\"nt\"\u003e-c\u003c/span\u003e nginx-external.conf\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"212:1-212:84\"\u003e依存パッケージなし、ビルドプロセスなし、\u003ccode\u003enode_modules\u003c/code\u003e なし。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"214:1-214:29\"\u003e\n\u003cspan id=\"vue--vite--typescript\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue--vite--typescript\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue — Vite + TypeScript\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"216:1-235:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"err\"\u003e//\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003epackage.json\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e(主要部分)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"scripts\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"dev\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"vite\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"build\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"vite build\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"preview\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"vite preview\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"dependencies\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"vue\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^3.4.0\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"vue-router\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^4.3.0\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"devDependencies\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"@vitejs/plugin-vue\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^5.0.0\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"typescript\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^5.5.0\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"vite\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^5.3.0\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"vue-tsc\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^2.0.0\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"237:1-241:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003enpm \u003cspan class=\"nb\"\u003einstall\u003c/span\u003e      \u003cspan class=\"c\"\u003e# node_modules を作る\u003c/span\u003e\nnpm run dev      \u003cspan class=\"c\"\u003e# http://localhost:5173 でホットリロード開発\u003c/span\u003e\nnpm run build    \u003cspan class=\"c\"\u003e# dist/ に静的ファイル生成\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"243:1-243:52\"\u003e\n\u003cspan id=\"react--vite--typescript-vue-と同じ-vite\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#react--vite--typescript-vue-%E3%81%A8%E5%90%8C%E3%81%98-vite\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eReact — Vite + TypeScript (Vue と同じ Vite)\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"245:1-262:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"scripts\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"dev\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"vite\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"build\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"tsc \u0026amp;\u0026amp; vite build\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"dependencies\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"react\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^18.3.0\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"react-dom\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^18.3.0\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"react-router-dom\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^6.24.0\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"devDependencies\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"@vitejs/plugin-react\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^4.3.0\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"typescript\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^5.5.0\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"vite\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"^5.3.0\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"264:1-264:97\"\u003eVue とほぼ同じ操作感。違いは \u003ccode\u003e@vitejs/plugin-vue\u003c/code\u003e vs \u003ccode\u003e@vitejs/plugin-react\u003c/code\u003e だけ。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"266:1-266:37\"\u003e\n\u003cspan id=\"thymeleaf--maven--spring-boot\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#thymeleaf--maven--spring-boot\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eThymeleaf — Maven + Spring Boot\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"xml\" data-sourcepos=\"268:1-286:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- pom.xml (主要部分) --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;parent\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.boot\u003cspan class=\"nt\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-boot-starter-parent\u003cspan class=\"nt\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e3.4.5\u003cspan class=\"nt\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/parent\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;dependencies\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"nt\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.boot\u003cspan class=\"nt\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"nt\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-boot-starter-web\u003cspan class=\"nt\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"nt\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.boot\u003cspan class=\"nt\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"nt\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-boot-starter-thymeleaf\u003cspan class=\"nt\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/dependencies\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"288:1-291:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003emvn clean package      \u003cspan class=\"c\"\u003e# target/*.jar 生成\u003c/span\u003e\njava \u003cspan class=\"nt\"\u003e-jar\u003c/span\u003e target/banklink-external-web-thymeleaf.jar  \u003cspan class=\"c\"\u003e# 起動\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"293:1-293:160\"\u003eJVM が必要。\u003ccode\u003etarget/\u003c/code\u003e に生成される jar には \u003cstrong\u003eTomcat も埋め込まれている\u003c/strong\u003eので、追加でアプリケーションサーバーは不要。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"295:1-295:34\"\u003e\n\u003cspan id=\"ビルド時間の体感比較\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%93%E3%83%AB%E3%83%89%E6%99%82%E9%96%93%E3%81%AE%E4%BD%93%E6%84%9F%E6%AF%94%E8%BC%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eビルド時間の体感比較\u003c/h3\u003e\n\u003ctable data-sourcepos=\"297:1-302:100\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"297:1-297:72\"\u003e\n\u003cth data-sourcepos=\"297:2-297:15\"\u003eスタック\u003c/th\u003e\n\u003cth data-sourcepos=\"297:17-297:38\"\u003e初回 \u003ccode\u003einstall\u003c/code\u003e 等\u003c/th\u003e\n\u003cth data-sourcepos=\"297:40-297:56\"\u003e本番ビルド\u003c/th\u003e\n\u003cth data-sourcepos=\"297:58-297:71\"\u003e起動時間\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"299:1-299:39\"\u003e\n\u003ctd data-sourcepos=\"299:2-299:15\"\u003eVanilla HTML\u003c/td\u003e\n\u003ctd data-sourcepos=\"299:17-299:22\"\u003e0秒\u003c/td\u003e\n\u003ctd data-sourcepos=\"299:24-299:29\"\u003e0秒\u003c/td\u003e\n\u003ctd data-sourcepos=\"299:31-299:38\"\u003e即時\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"300:1-300:66\"\u003e\n\u003ctd data-sourcepos=\"300:2-300:6\"\u003eVue\u003c/td\u003e\n\u003ctd data-sourcepos=\"300:8-300:35\"\u003e30〜60秒 (\u003ccode\u003enpm install\u003c/code\u003e)\u003c/td\u003e\n\u003ctd data-sourcepos=\"300:37-300:47\"\u003e5〜15秒\u003c/td\u003e\n\u003ctd data-sourcepos=\"300:49-300:65\"\u003enginx 起動分\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"301:1-301:52\"\u003e\n\u003ctd data-sourcepos=\"301:2-301:8\"\u003eReact\u003c/td\u003e\n\u003ctd data-sourcepos=\"301:10-301:21\"\u003e30〜60秒\u003c/td\u003e\n\u003ctd data-sourcepos=\"301:23-301:33\"\u003e5〜15秒\u003c/td\u003e\n\u003ctd data-sourcepos=\"301:35-301:51\"\u003enginx 起動分\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"302:1-302:100\"\u003e\n\u003ctd data-sourcepos=\"302:2-302:12\"\u003eThymeleaf\u003c/td\u003e\n\u003ctd data-sourcepos=\"302:14-302:43\"\u003e30〜120秒 (Maven 依存DL)\u003c/td\u003e\n\u003ctd data-sourcepos=\"302:45-302:72\"\u003e20〜40秒 (\u003ccode\u003emvn package\u003c/code\u003e)\u003c/td\u003e\n\u003ctd data-sourcepos=\"302:74-302:99\"\u003eJVM起動分 (5〜15秒)\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr data-sourcepos=\"304:1-305:0\"\u003e\n\u003ch2 data-sourcepos=\"306:1-306:64\"\u003e\n\u003cspan id=\"5-同じ画面口座ページを4スタックで実装\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E5%90%8C%E3%81%98%E7%94%BB%E9%9D%A2%E5%8F%A3%E5%BA%A7%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%924%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%A7%E5%AE%9F%E8%A3%85\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. 同じ画面（口座ページ）を4スタックで実装\u003c/h2\u003e\n\u003cp data-sourcepos=\"308:1-308:202\"\u003eここがこの記事の中心です。\u003cstrong\u003e全く同じ「口座一覧取得・残高取得・入金・出金・取引履歴」の5セクションを、4通りに書いたコード\u003c/strong\u003e を順に見ます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"310:1-310:20\"\u003e\n\u003cspan id=\"vanilla-html-版\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vanilla-html-%E7%89%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVanilla HTML 版\u003c/h3\u003e\n\u003cp data-sourcepos=\"312:1-312:99\"\u003eHTML と JS が同じ \u003ccode\u003eaccounts.html\u003c/code\u003e に同居（\u003ccode\u003e\u0026lt;script type=\"module\"\u0026gt;\u003c/code\u003e でモジュール）。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"314:1-351:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- accounts.html (口座一覧と入金部分の抜粋) --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;section\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"section-card\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;h2\u0026gt;\u003c/span\u003e口座一覧取得\u003cspan class=\"nt\"\u003e\u0026lt;/h2\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"accounts-list-btn\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003eGET /api/v1/accounts\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"accounts-list-response\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/section\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;section\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"section-card\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;h2\u0026gt;\u003c/span\u003e入金\u003cspan class=\"nt\"\u003e\u0026lt;/h2\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eaccountId\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"deposit-account-id\"\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"ACC-0001\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eamount\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"deposit-amount\"\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"number\"\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"10000\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eIdempotency-Key\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"deposit-key\"\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"dep-key-001\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"accounts-deposit-btn\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003ePOST /api/v1/accounts/{id}/deposit\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"accounts-deposit-response\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/section\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;script \u003c/span\u003e\u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"module\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003ebindAction\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003enumberValue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e./common-external.js\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n  \u003cspan class=\"c1\"\u003e// ボタン id とレスポンス表示先 id をマッピング\u003c/span\u003e\n  \u003cspan class=\"nf\"\u003ebindAction\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eaccounts-list-btn\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eaccounts-list-response\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e({\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eGET\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/v1/accounts\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}));\u003c/span\u003e\n\n  \u003cspan class=\"nf\"\u003ebindAction\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eaccounts-deposit-btn\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eaccounts-deposit-response\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e({\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003ePOST\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e`/api/v1/accounts/\u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nf\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edeposit-account-id\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)}\u003c/span\u003e\u003cspan class=\"s2\"\u003e/deposit`\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eidempotencyKey\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nf\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edeposit-key\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nf\"\u003enumberValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edeposit-amount\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ecurrency\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nf\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edeposit-currency\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ereference\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nf\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edeposit-reference\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}));\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/script\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"353:1-353:11\"\u003e\u003cstrong\u003e特徴\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"354:1-357:0\"\u003e\n\u003cli data-sourcepos=\"354:1-354:118\"\u003eHTML 要素を \u003ccode\u003eid\u003c/code\u003e で識別 → JS から \u003ccode\u003edocument.getElementById\u003c/code\u003e で参照（\u003ccode\u003ebindAction\u003c/code\u003e 関数内で行う）\u003c/li\u003e\n\u003cli data-sourcepos=\"355:1-355:143\"\u003e入力値は \u003ccode\u003evalue(\"input-id\")\u003c/code\u003e ヘルパで都度取得（リアクティブではなく「クリック時点の値」を読みに行く）\u003c/li\u003e\n\u003cli data-sourcepos=\"356:1-357:0\"\u003e結果は \u003ccode\u003einnerHTML\u003c/code\u003e で文字列として描画\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"358:1-358:11\"\u003e\n\u003cspan id=\"vue-版\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue-%E7%89%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue 版\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"360:1-410:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- AccountsView.vue (script setup + template) --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e \u003cspan class=\"na\"\u003elang=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"ts\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003einject\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"nx\"\u003etype\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eRef\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003erequestApi\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e../api/client\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etoken\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003einject\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eRef\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003estring\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003etoken\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003efmt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eunknown\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 口座一覧\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003elistRes\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003estring\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003elistStatus\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003enumber\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003egetAccounts\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003er\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003erequestApi\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"na\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eGET\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/v1/accounts\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003elistStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003elistRes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003efmt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 入金\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edepId\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eACC-0001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"nx\"\u003edepAmount\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003edepKey\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edep-key-001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"nx\"\u003edepCurrency\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eJPY\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"nx\"\u003edepRef\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eTEST-DEP-001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edepRes\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edepStatus\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003enumber\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003edeposit\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003er\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003erequestApi\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003ePOST\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e`/api/v1/accounts/\u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003edepId\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e/deposit`\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eidempotencyKey\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003edepKey\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003edepAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003ecurrency\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003edepCurrency\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003ereference\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003edepRef\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003edepStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nx\"\u003edepRes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003efmt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;section\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"section-card\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;h2\u0026gt;\u003c/span\u003e口座一覧取得\u003cspan class=\"nt\"\u003e\u0026lt;/h2\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eclick=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"getAccounts\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003eGET /api/v1/accounts\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;pre\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"listStatus !== null\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003eHTTP \u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003elistStatus\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\\n\u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003elistRes\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/pre\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/section\u0026gt;\u003c/span\u003e\n\n  \u003cspan class=\"nt\"\u003e\u0026lt;section\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"section-card\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;h2\u0026gt;\u003c/span\u003e入金\u003cspan class=\"nt\"\u003e\u0026lt;/h2\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eaccountId\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"depId\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eamount\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model.number=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"depAmount\"\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"number\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eIdempotency-Key\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"depKey\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eclick=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"deposit\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003ePOST /api/v1/accounts/{id}/deposit\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;pre\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"depStatus !== null\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003eHTTP \u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003edepStatus\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\\n\u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003edepRes\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/pre\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/section\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"412:1-412:11\"\u003e\u003cstrong\u003e特徴\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"413:1-416:0\"\u003e\n\u003cli data-sourcepos=\"413:1-413:96\"\u003e\n\u003ccode\u003eref()\u003c/code\u003e で\u003cstrong\u003eリアクティブ変数\u003c/strong\u003eを宣言、\u003ccode\u003ev-model\u003c/code\u003e で双方向バインディング\u003c/li\u003e\n\u003cli data-sourcepos=\"414:1-414:72\"\u003e\n\u003ccode\u003einject\u0026lt;Ref\u0026lt;string\u0026gt;\u0026gt;(\"token\")\u003c/code\u003e で親から共有トークンを取得\u003c/li\u003e\n\u003cli data-sourcepos=\"415:1-416:0\"\u003e\n\u003ccode\u003ev-if\u003c/code\u003e でレスポンス表示を条件分岐、\u003ccode\u003e{{ }}\u003c/code\u003e で変数を埋め込み\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"417:1-417:13\"\u003e\n\u003cspan id=\"react-版\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#react-%E7%89%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eReact 版\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"tsx\" data-sourcepos=\"419:1-476:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// App.tsx の AccountsPage 関数（抜粋）\u003c/span\u003e\n\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eAccountsPage\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"nx\"\u003etoken\u003c/span\u003e \u003cspan class=\"p\"\u003e}:\u003c/span\u003e \u003cspan class=\"nx\"\u003ePageProps\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003elistResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetListResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003euseState\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eApiResult\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetDepositResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003euseState\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eApiResult\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetDepositAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eACC-0001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetDepositAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e10000\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositKey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetDepositKey\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edep-key-001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositCurrency\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetDepositCurrency\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eJPY\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositReference\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetDepositReference\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eTEST-DEP-001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003ereturn \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e \u003cspan class=\"na\"\u003eclassName\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"page-grid\"\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n      \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eSection\u003c/span\u003e \u003cspan class=\"na\"\u003etitle\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"口座一覧取得\"\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ebutton\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eonClick\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"k\"\u003easync \u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e\n            \u003cspan class=\"nf\"\u003esetListResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003erequestApi\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"na\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eGET\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e/api/v1/accounts\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003etoken\u003c/span\u003e \u003cspan class=\"p\"\u003e}))\u003c/span\u003e\n          \u003cspan class=\"si\"\u003e}\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n          GET /api/v1/accounts\n        \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ebutton\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eResponsePanel\u003c/span\u003e \u003cspan class=\"na\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003elistResponse\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n      \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nc\"\u003eSection\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\n      \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eSection\u003c/span\u003e \u003cspan class=\"na\"\u003etitle\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"入金\"\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003elabel\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003eaccountId\n          \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003einput\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAccountId\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"na\"\u003eonChange\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nf\"\u003esetDepositAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003elabel\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003elabel\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003eamount\n          \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003einput\u003c/span\u003e \u003cspan class=\"na\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"number\"\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAmount\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"na\"\u003eonChange\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nf\"\u003esetDepositAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003elabel\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003elabel\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003eIdempotency-Key\n          \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003einput\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositKey\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"na\"\u003eonChange\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nf\"\u003esetDepositKey\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003elabel\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ebutton\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eonClick\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"k\"\u003easync \u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e\n            \u003cspan class=\"nf\"\u003esetDepositResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003erequestApi\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n              \u003cspan class=\"na\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003ePOST\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n              \u003cspan class=\"na\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e`/api/v1/accounts/\u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e/deposit`\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n              \u003cspan class=\"nx\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n              \u003cspan class=\"na\"\u003eidempotencyKey\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003edepositKey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n              \u003cspan class=\"na\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n                \u003cspan class=\"na\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nc\"\u003eNumber\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n                \u003cspan class=\"na\"\u003ecurrency\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003edepositCurrency\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n                \u003cspan class=\"na\"\u003ereference\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003edepositReference\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n              \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n            \u003cspan class=\"p\"\u003e}))\u003c/span\u003e\n          \u003cspan class=\"si\"\u003e}\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n          POST /api/v1/accounts/\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e{id}\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e/deposit\n        \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ebutton\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eResponsePanel\u003c/span\u003e \u003cspan class=\"na\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositResponse\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n      \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nc\"\u003eSection\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"478:1-478:11\"\u003e\u003cstrong\u003e特徴\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"479:1-482:0\"\u003e\n\u003cli data-sourcepos=\"479:1-479:134\"\u003e各入力フィールド・各レスポンスごとに \u003ccode\u003euseState\u003c/code\u003e で状態を宣言（\u003cstrong\u003eVue の \u003ccode\u003eref\u003c/code\u003e より宣言量が多い\u003c/strong\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"480:1-480:146\"\u003e\n\u003ccode\u003eonChange={e =\u0026gt; setX(e.target.value)}\u003c/code\u003e のイベントハンドラを毎回書く必要（Vue の \u003ccode\u003ev-model\u003c/code\u003e のような糖衣構文がない）\u003c/li\u003e\n\u003cli data-sourcepos=\"481:1-482:0\"\u003e\n\u003ccode\u003eJSX\u003c/code\u003e 内に直接ボタンの async ハンドラを書ける\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"483:1-483:17\"\u003e\n\u003cspan id=\"thymeleaf-版\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#thymeleaf-%E7%89%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eThymeleaf 版\u003c/h3\u003e\n\u003cp data-sourcepos=\"485:1-485:102\"\u003e3層構造: テンプレート + コントローラ + サービス + フォームオブジェクト。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"487:1-511:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- accounts.html (口座一覧と入金部分の抜粋) --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;section\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"section-card\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;h2\u0026gt;\u003c/span\u003e口座一覧取得（サーバーサイドForm）\u003cspan class=\"nt\"\u003e\u0026lt;/h2\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;form\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"accounts-form\"\u003c/span\u003e \u003cspan class=\"na\"\u003eth:action=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"@{/api/v1/accounts}\"\u003c/span\u003e \u003cspan class=\"na\"\u003eth:object=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${accountsForm}\"\u003c/span\u003e \u003cspan class=\"na\"\u003emethod=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"post\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"hidden\"\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{authorization}\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"submit\"\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"action-button\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003ePOST /api/v1/accounts\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/form\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eth:if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${accountsForm != null and accountsForm.apiResponse != null}\"\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"response-box\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;p\u0026gt;\u0026lt;strong\u0026gt;\u003c/span\u003eStatus:\u003cspan class=\"nt\"\u003e\u0026lt;/strong\u0026gt;\u003c/span\u003e \u003cspan class=\"nt\"\u003e\u0026lt;span\u003c/span\u003e \u003cspan class=\"na\"\u003eth:text=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${accountsForm.apiResponse.statusCode}\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e0\u003cspan class=\"nt\"\u003e\u0026lt;/span\u0026gt;\u0026lt;/p\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;pre\u003c/span\u003e \u003cspan class=\"na\"\u003eth:text=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${accountsForm.apiResponse.body}\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/pre\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/section\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;section\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"section-card\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;h2\u0026gt;\u003c/span\u003e入金\u003cspan class=\"nt\"\u003e\u0026lt;/h2\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;form\u003c/span\u003e \u003cspan class=\"na\"\u003eth:action=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"@{/api/v1/accounts/deposit}\"\u003c/span\u003e \u003cspan class=\"na\"\u003eth:object=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${accountsForm}\"\u003c/span\u003e \u003cspan class=\"na\"\u003emethod=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"post\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"hidden\"\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{authorization}\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eaccountId\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{depositAccountId}\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eamount\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{depositAmount}\"\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"number\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eIdempotency-Key\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{depositIdempotencyKey}\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"submit\"\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"action-button\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003ePOST /api/v1/accounts/deposit\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/form\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/section\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"java\" data-sourcepos=\"513:1-546:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// AccountsController.java\u003c/span\u003e\n\u003cspan class=\"nd\"\u003e@Controller\u003c/span\u003e\n\u003cspan class=\"nd\"\u003e@RequiredArgsConstructor\u003c/span\u003e\n\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountsController\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003efinal\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountsService\u003c/span\u003e \u003cspan class=\"n\"\u003eaccountsService\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n\n    \u003cspan class=\"nd\"\u003e@PostMapping\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\"/api/v1/accounts\"\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"nf\"\u003elistAccounts\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\n            \u003cspan class=\"nd\"\u003e@ModelAttribute\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\"accountsForm\"\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountsForm\u003c/span\u003e \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"nc\"\u003eModel\u003c/span\u003e \u003cspan class=\"n\"\u003emodel\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eapplyToken\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetAuthorization\u003c/span\u003e\u003cspan class=\"o\"\u003e());\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetApiResponse\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccountsService\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003elistAccounts\u003c/span\u003e\u003cspan class=\"o\"\u003e());\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emodel\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eaddAttribute\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\"accountsForm\"\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e);\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\"accounts\"\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// accounts.html を再レンダリング\u003c/span\u003e\n    \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\n    \u003cspan class=\"nd\"\u003e@PostMapping\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\"/api/v1/accounts/deposit\"\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"nf\"\u003edeposit\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\n            \u003cspan class=\"nd\"\u003e@ModelAttribute\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\"accountsForm\"\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountsForm\u003c/span\u003e \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"nc\"\u003eModel\u003c/span\u003e \u003cspan class=\"n\"\u003emodel\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eapplyToken\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetAuthorization\u003c/span\u003e\u003cspan class=\"o\"\u003e());\u003c/span\u003e\n        \u003cspan class=\"nc\"\u003eMap\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eString\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nc\"\u003eObject\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ebody\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eMap\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eof\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\n            \u003cspan class=\"s\"\u003e\"amount\"\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetDepositAmount\u003c/span\u003e\u003cspan class=\"o\"\u003e(),\u003c/span\u003e\n            \u003cspan class=\"s\"\u003e\"currency\"\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetDepositCurrency\u003c/span\u003e\u003cspan class=\"o\"\u003e(),\u003c/span\u003e\n            \u003cspan class=\"s\"\u003e\"reference\"\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetDepositReference\u003c/span\u003e\u003cspan class=\"o\"\u003e()\u003c/span\u003e\n        \u003cspan class=\"o\"\u003e);\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetApiResponse\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccountsService\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003edeposit\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\n            \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetDepositAccountId\u003c/span\u003e\u003cspan class=\"o\"\u003e(),\u003c/span\u003e \u003cspan class=\"n\"\u003ebody\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetDepositIdempotencyKey\u003c/span\u003e\u003cspan class=\"o\"\u003e()));\u003c/span\u003e\n        \u003cspan class=\"n\"\u003emodel\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eaddAttribute\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\"accountsForm\"\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"o\"\u003e);\u003c/span\u003e\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\"accounts\"\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"java\" data-sourcepos=\"548:1-561:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// AccountsForm.java (抜粋)\u003c/span\u003e\n\u003cspan class=\"nd\"\u003e@Data\u003c/span\u003e\n\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountsForm\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"n\"\u003eauthorization\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"n\"\u003edepositAccountId\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eInteger\u003c/span\u003e \u003cspan class=\"n\"\u003edepositAmount\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"n\"\u003edepositCurrency\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"n\"\u003edepositReference\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"n\"\u003edepositIdempotencyKey\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eExternalApiResponse\u003c/span\u003e \u003cspan class=\"n\"\u003eapiResponse\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e// ... (他フィールド)\u003c/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"563:1-563:11\"\u003e\u003cstrong\u003e特徴\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"564:1-567:0\"\u003e\n\u003cli data-sourcepos=\"564:1-564:107\"\u003e各操作で \u003cstrong\u003eHTTP POST → サーバー処理 → ページ再描画\u003c/strong\u003e という従来の Web フロー\u003c/li\u003e\n\u003cli data-sourcepos=\"565:1-565:143\"\u003e\n\u003ccode\u003eth:field=\"*{depositAmount}\"\u003c/code\u003e で Form Object のフィールドと input を双方向バインド（Spring が自動で値を受け渡し）\u003c/li\u003e\n\u003cli data-sourcepos=\"566:1-567:0\"\u003e結果表示も\u003cstrong\u003eサーバーサイドで HTML 化\u003c/strong\u003eしてレンダリング → クライアント側 JS は最小\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"568:1-568:56\"\u003e\n\u003cspan id=\"コード行数の比較口座ページ-全体\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%83%BC%E3%83%89%E8%A1%8C%E6%95%B0%E3%81%AE%E6%AF%94%E8%BC%83%E5%8F%A3%E5%BA%A7%E3%83%9A%E3%83%BC%E3%82%B8-%E5%85%A8%E4%BD%93\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eコード行数の比較（口座ページ 全体）\u003c/h3\u003e\n\u003ctable data-sourcepos=\"570:1-575:69\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"570:1-570:45\"\u003e\n\u003cth data-sourcepos=\"570:2-570:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"570:4-570:20\"\u003eコード行数\u003c/th\u003e\n\u003cth data-sourcepos=\"570:22-570:44\"\u003e言語 / ファイル\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"572:1-572:61\"\u003e\n\u003ctd data-sourcepos=\"572:2-572:15\"\u003eVanilla HTML\u003c/td\u003e\n\u003ctd data-sourcepos=\"572:17-572:29\"\u003e約 130 行\u003c/td\u003e\n\u003ctd data-sourcepos=\"572:31-572:60\"\u003eHTML + JS（1ファイル）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"573:1-573:64\"\u003e\n\u003ctd data-sourcepos=\"573:2-573:6\"\u003eVue\u003c/td\u003e\n\u003ctd data-sourcepos=\"573:8-573:20\"\u003e約 130 行\u003c/td\u003e\n\u003ctd data-sourcepos=\"573:22-573:63\"\u003eTypeScript + Template（1ファイル）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"574:1-574:53\"\u003e\n\u003ctd data-sourcepos=\"574:2-574:8\"\u003eReact\u003c/td\u003e\n\u003ctd data-sourcepos=\"574:10-574:22\"\u003e約 350 行\u003c/td\u003e\n\u003ctd data-sourcepos=\"574:24-574:52\"\u003eTypeScript JSX（1関数）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"575:1-575:69\"\u003e\n\u003ctd data-sourcepos=\"575:2-575:12\"\u003eThymeleaf\u003c/td\u003e\n\u003ctd data-sourcepos=\"575:14-575:26\"\u003e約 250 行\u003c/td\u003e\n\u003ctd data-sourcepos=\"575:28-575:68\"\u003eHTML + Java（5ファイルに分散）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote data-sourcepos=\"577:1-577:278\"\u003e\n\u003cp data-sourcepos=\"577:3-577:278\"\u003eReact が長い理由: 各入力フィールドの \u003ccode\u003euseState\u003c/code\u003e 宣言と \u003ccode\u003eonChange\u003c/code\u003e ハンドラ、各ボタンの async コールバックを \u003cstrong\u003e個別に書く必要\u003c/strong\u003e があるため。Vue の \u003ccode\u003ev-model\u003c/code\u003e のような糖衣構文がない分、コード量が増える傾向。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"579:1-580:0\"\u003e\n\u003ch2 data-sourcepos=\"581:1-581:37\"\u003e\n\u003cspan id=\"6-api-クライアントの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6-api-%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. API クライアントの違い\u003c/h2\u003e\n\u003cp data-sourcepos=\"583:1-583:140\"\u003eすべて同じバックエンド API（\u003ccode\u003e/api/v1/accounts\u003c/code\u003e 等）を叩きますが、クライアント実装は微妙に異なります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"585:1-585:57\"\u003e\n\u003cspan id=\"vanilla-html--vue--react--javascript-の-fetch\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vanilla-html--vue--react--javascript-%E3%81%AE-fetch\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVanilla HTML / Vue / React — JavaScript の \u003ccode\u003efetch\u003c/code\u003e\n\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"587:1-622:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// Vue 版 client.ts\u003c/span\u003e\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kr\"\u003einterface\u003c/span\u003e \u003cspan class=\"nx\"\u003eApiResult\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eunknown\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003erequestApi\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eidempotencyKey\u003c/span\u003e\u003cspan class=\"p\"\u003e?:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e?:\u003c/span\u003e \u003cspan class=\"nx\"\u003eunknown\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}):\u003c/span\u003e \u003cspan class=\"nb\"\u003ePromise\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eApiResult\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"na\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eRecord\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eContent-Type\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eapplication/json\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eAuthorization\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e`Bearer \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e`\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eidempotencyKey\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eIdempotency-Key\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eidempotencyKey\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003etry\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eres\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003efetch\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"na\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e \u003cspan class=\"o\"\u003e!==\u003c/span\u003e \u003cspan class=\"kc\"\u003eundefined\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003eundefined\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etext\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003eres\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003etext\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eresponseBody\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003etext\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eparse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eres\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eresponseBody\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003ecatch \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"624:1-624:131\"\u003eVanilla / Vue / React で \u003cstrong\u003eほぼ同一\u003c/strong\u003e。違いは型注釈の有無くらい（Vanilla は \u003ccode\u003e.js\u003c/code\u003e、Vue / React は \u003ccode\u003e.ts\u003c/code\u003e）。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"626:1-626:46\"\u003e\n\u003cspan id=\"thymeleaf--spring-boot-の-restclient\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#thymeleaf--spring-boot-%E3%81%AE-restclient\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eThymeleaf — Spring Boot の \u003ccode\u003eRestClient\u003c/code\u003e\n\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"java\" data-sourcepos=\"628:1-651:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// AccountsService.java\u003c/span\u003e\n\u003cspan class=\"nd\"\u003e@Service\u003c/span\u003e\n\u003cspan class=\"nd\"\u003e@RequiredArgsConstructor\u003c/span\u003e\n\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountsService\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003efinal\u003c/span\u003e \u003cspan class=\"nc\"\u003eRestClient\u003c/span\u003e \u003cspan class=\"n\"\u003erestClient\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n\n    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"nc\"\u003eExternalApiResponse\u003c/span\u003e \u003cspan class=\"nf\"\u003elistAccounts\u003c/span\u003e\u003cspan class=\"o\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"nc\"\u003eResponseEntity\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nc\"\u003eString\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erestClient\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eget\u003c/span\u003e\u003cspan class=\"o\"\u003e()\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003euri\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\"/api/v1/accounts\"\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eheader\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\"Authorization\"\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\"Bearer \"\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eretrieve\u003c/span\u003e\u003cspan class=\"o\"\u003e()\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003etoEntity\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nc\"\u003eString\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eclass\u003c/span\u003e\u003cspan class=\"o\"\u003e);\u003c/span\u003e\n\n        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nf\"\u003eExternalApiResponse\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\n            \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetStatusCode\u003c/span\u003e\u003cspan class=\"o\"\u003e().\u003c/span\u003e\u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"o\"\u003e(),\u003c/span\u003e\n            \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e\n            \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetBody\u003c/span\u003e\u003cspan class=\"o\"\u003e()\u003c/span\u003e\n        \u003cspan class=\"o\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"653:1-653:129\"\u003eJava の \u003ccode\u003eRestClient\u003c/code\u003e（Spring 6.1+）でビルダー記法。\u003ccode\u003efetch\u003c/code\u003e と比べて型が厳密で IDE 補完が効きやすい。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"655:1-655:25\"\u003e\n\u003cspan id=\"共通点と相違点\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%85%B1%E9%80%9A%E7%82%B9%E3%81%A8%E7%9B%B8%E9%81%95%E7%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e共通点と相違点\u003c/h3\u003e\n\u003ctable data-sourcepos=\"657:1-663:80\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"657:1-657:42\"\u003e\n\u003cth data-sourcepos=\"657:2-657:9\"\u003e観点\u003c/th\u003e\n\u003cth data-sourcepos=\"657:11-657:29\"\u003eVanilla/Vue/React\u003c/th\u003e\n\u003cth data-sourcepos=\"657:31-657:41\"\u003eThymeleaf\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"659:1-659:43\"\u003e\n\u003ctd data-sourcepos=\"659:2-659:9\"\u003e言語\u003c/td\u003e\n\u003ctd data-sourcepos=\"659:11-659:35\"\u003eJavaScript / TypeScript\u003c/td\u003e\n\u003ctd data-sourcepos=\"659:37-659:42\"\u003eJava\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"660:1-660:83\"\u003e\n\u003ctd data-sourcepos=\"660:2-660:26\"\u003eHTTP クライアント\u003c/td\u003e\n\u003ctd data-sourcepos=\"660:28-660:48\"\u003e\n\u003ccode\u003efetch\u003c/code\u003e（標準）\u003c/td\u003e\n\u003ctd data-sourcepos=\"660:50-660:82\"\u003e\n\u003ccode\u003eRestClient\u003c/code\u003e（Spring 標準）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"661:1-661:84\"\u003e\n\u003ctd data-sourcepos=\"661:2-661:18\"\u003eエラー処理\u003c/td\u003e\n\u003ctd data-sourcepos=\"661:20-661:44\"\u003etry/catch + status code\u003c/td\u003e\n\u003ctd data-sourcepos=\"661:46-661:83\"\u003etry/catch + HttpClientErrorException\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"662:1-662:80\"\u003e\n\u003ctd data-sourcepos=\"662:2-662:15\"\u003e型安全性\u003c/td\u003e\n\u003ctd data-sourcepos=\"662:17-662:41\"\u003eTypeScript で型注釈\u003c/td\u003e\n\u003ctd data-sourcepos=\"662:43-662:79\"\u003eJava で型安全 (デフォルト)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"663:1-663:80\"\u003e\n\u003ctd data-sourcepos=\"663:2-663:33\"\u003eネットワーク発生場所\u003c/td\u003e\n\u003ctd data-sourcepos=\"663:35-663:56\"\u003eブラウザ → API\u003c/td\u003e\n\u003ctd data-sourcepos=\"663:58-663:79\"\u003eサーバー → API\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"665:1-665:108\"\u003e最後の「ネットワーク発生場所」が \u003cstrong\u003eアーキテクチャ上一番重要な違い\u003c/strong\u003e です。\u003c/p\u003e\n\u003cul data-sourcepos=\"666:1-668:0\"\u003e\n\u003cli data-sourcepos=\"666:1-666:146\"\u003eSPA系（Vanilla/Vue/React）はブラウザから API を直接叩く → CORS 設定が必要 / API 認証情報がクライアントに渡る\u003c/li\u003e\n\u003cli data-sourcepos=\"667:1-668:0\"\u003eThymeleaf はサーバーが API を叩く → CORS 不要 / 認証情報がサーバー内に閉じる\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"669:1-670:0\"\u003e\n\u003ch2 data-sourcepos=\"671:1-671:60\"\u003e\n\u003cspan id=\"7-状態管理データバインディングの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#7-%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86%E3%83%87%E3%83%BC%E3%82%BF%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e7. 状態管理・データバインディングの違い\u003c/h2\u003e\n\u003cp data-sourcepos=\"673:1-673:141\"\u003e「画面上の入力値・取得したレスポンス・トークン」をどう保持するかが各スタックの個性が出る部分。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"675:1-675:40\"\u003e\n\u003cspan id=\"vanilla-html--dom-が状態の源\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vanilla-html--dom-%E3%81%8C%E7%8A%B6%E6%85%8B%E3%81%AE%E6%BA%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVanilla HTML — DOM が状態の源\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"677:1-686:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// 入力値: いつでも DOM から取り出す\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eaccountId\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetElementById\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003ebalance-account-id\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// レスポンス表示: innerHTML で文字列を直接書き込む\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetElementById\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003ebalance-response\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nx\"\u003einnerHTML\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eformatResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// トークン: localStorage に保存\u003c/span\u003e\n\u003cspan class=\"nx\"\u003elocalStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esetItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003ebanklink_token\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"688:1-688:164\"\u003e\u003cstrong\u003e「状態」は概念がない\u003c/strong\u003e。常に DOM か localStorage の値を読みに行く。シンプルだがアプリが大きくなると同期がしんどい。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"690:1-690:45\"\u003e\n\u003cspan id=\"vue--ref-でリアクティブ宣言\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue--ref-%E3%81%A7%E3%83%AA%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E5%AE%A3%E8%A8%80\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue — \u003ccode\u003eref\u003c/code\u003e でリアクティブ宣言\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"692:1-698:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ebalanceId\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eACC-0001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// 文字列の値を持つリアクティブ変数\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ebalanceRes\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e           \u003cspan class=\"c1\"\u003e// テンプレートで {{ balanceRes }} と書くと自動更新\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// テンプレート側で \u0026lt;input v-model=\"balanceId\" /\u0026gt; と書けば\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// ユーザー入力が balanceId.value に自動反映される(双方向バインディング)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"700:1-700:151\"\u003e\u003cstrong\u003e「リアクティブ変数を宣言 → テンプレートが自動追従」\u003c/strong\u003e のモデル。書き手が状態同期を意識しなくていい。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"702:1-702:52\"\u003e\n\u003cspan id=\"react--usestate-で状態フックを宣言\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#react--usestate-%E3%81%A7%E7%8A%B6%E6%85%8B%E3%83%95%E3%83%83%E3%82%AF%E3%82%92%E5%AE%A3%E8%A8%80\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eReact — \u003ccode\u003euseState\u003c/code\u003e で状態フックを宣言\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"tsx\" data-sourcepos=\"704:1-710:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003ebalanceId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetBalanceId\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eACC-0001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003ebalanceRes\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetBalanceRes\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003euseState\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eApiResult\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// テンプレート側: 値とハンドラを別々に渡す\u003c/span\u003e\n\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003einput\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003ebalanceId\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"na\"\u003eonChange\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nf\"\u003esetBalanceId\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"712:1-712:271\"\u003e\u003cstrong\u003e「変数と setter のペアを宣言 → setter 経由で更新 → 再レンダリング」\u003c/strong\u003e のモデル。Vue の \u003ccode\u003ev-model\u003c/code\u003e のような糖衣構文がないので、入力フィールドごとに \u003ccode\u003eonChange\u003c/code\u003e を書く必要があり\u003cstrong\u003eコード量が増える\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"714:1-714:66\"\u003e\n\u003cspan id=\"thymeleaf--form-object-でサーバー側に状態を集約\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#thymeleaf--form-object-%E3%81%A7%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E5%81%B4%E3%81%AB%E7%8A%B6%E6%85%8B%E3%82%92%E9%9B%86%E7%B4%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eThymeleaf — Form Object でサーバー側に状態を集約\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"java\" data-sourcepos=\"716:1-724:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nd\"\u003e@Data\u003c/span\u003e\n\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountsForm\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"n\"\u003eauthorization\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eString\u003c/span\u003e \u003cspan class=\"n\"\u003ebalanceAccountId\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"nc\"\u003eExternalApiResponse\u003c/span\u003e \u003cspan class=\"n\"\u003eapiResponse\u003c/span\u003e\u003cspan class=\"o\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e// ... 他フィールド\u003c/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"726:1-728:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{balanceAccountId}\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"730:1-730:277\"\u003e\u003cstrong\u003eサーバー側の Form Object が「状態の正本」\u003c/strong\u003e。POST のたびに input の値が Form Object に詰められ、Controller が処理 → 結果を Form Object に詰めて再描画。「クライアント側に状態を持たない」古典的な Web モデル。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"732:1-732:34\"\u003e\n\u003cspan id=\"場面ごとの向き不向き\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%A0%B4%E9%9D%A2%E3%81%94%E3%81%A8%E3%81%AE%E5%90%91%E3%81%8D%E4%B8%8D%E5%90%91%E3%81%8D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e場面ごとの向き不向き\u003c/h3\u003e\n\u003ctable data-sourcepos=\"734:1-739:86\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"734:1-734:43\"\u003e\n\u003cth data-sourcepos=\"734:2-734:21\"\u003eアプリの性質\u003c/th\u003e\n\u003cth data-sourcepos=\"734:23-734:42\"\u003e向くスタック\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"736:1-736:66\"\u003e\n\u003ctd data-sourcepos=\"736:2-736:46\"\u003e小規模・1画面・状態がほぼない\u003c/td\u003e\n\u003ctd data-sourcepos=\"736:48-736:65\"\u003e\u003cstrong\u003eVanilla HTML\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"737:1-737:63\"\u003e\n\u003ctd data-sourcepos=\"737:2-737:52\"\u003eリアクティブ UI 多用、フォーム多い\u003c/td\u003e\n\u003ctd data-sourcepos=\"737:54-737:62\"\u003e\u003cstrong\u003eVue\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"738:1-738:82\"\u003e\n\u003ctd data-sourcepos=\"738:2-738:69\"\u003eコンポーネント再利用が多い、エコシステム重視\u003c/td\u003e\n\u003ctd data-sourcepos=\"738:71-738:81\"\u003e\u003cstrong\u003eReact\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"739:1-739:86\"\u003e\n\u003ctd data-sourcepos=\"739:2-739:69\"\u003e状態をサーバー側に持ちたい（業務システム的）\u003c/td\u003e\n\u003ctd data-sourcepos=\"739:71-739:85\"\u003e\u003cstrong\u003eThymeleaf\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr data-sourcepos=\"741:1-742:0\"\u003e\n\u003ch2 data-sourcepos=\"743:1-743:33\"\u003e\n\u003cspan id=\"8-フォーム処理の違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#8-%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E5%87%A6%E7%90%86%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e8. フォーム処理の違い\u003c/h2\u003e\n\u003cp data-sourcepos=\"745:1-745:121\"\u003e入金フォーム（accountId / amount / currency / reference / Idempotency-Key の5フィールド）の実装方法。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"747:1-747:16\"\u003e\n\u003cspan id=\"vanilla-html-1\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vanilla-html-1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVanilla HTML\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"749:1-759:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// 各 input に id を付ける\u003c/span\u003e\n\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003einput\u003c/span\u003e \u003cspan class=\"nx\"\u003eid\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edeposit-account-id\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eACC-0001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// クリック時にまとめて値を読み取る\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ebody\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eaccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetElementById\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edeposit-account-id\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nc\"\u003eNumber\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetElementById\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003edeposit-amount\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"761:1-761:147\"\u003eヘルパ関数 \u003ccode\u003evalue()\u003c/code\u003e を書けばこの繰り返しは減らせるが、\u003cstrong\u003e「フォーム」という概念は HTML/JS 側にしかない\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"763:1-763:38\"\u003e\n\u003cspan id=\"vue--v-model-で5行で済む\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue--v-model-%E3%81%A75%E8%A1%8C%E3%81%A7%E6%B8%88%E3%82%80\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue — \u003ccode\u003ev-model\u003c/code\u003e で5行で済む\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"765:1-771:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eaccountId\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"depId\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eamount\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model.number=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"depAmount\"\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"number\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003ecurrency\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"depCurrency\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003ereference\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"depRef\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eIdempotency-Key\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"depKey\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"773:1-773:81\"\u003e\u003ccode\u003ev-model.number\u003c/code\u003e で数値型に自動変換。\u003cstrong\u003e書く量が最も少ない\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"775:1-775:58\"\u003e\n\u003cspan id=\"react--フィールドごとに-usestate--onchange\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#react--%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB%E3%83%89%E3%81%94%E3%81%A8%E3%81%AB-usestate--onchange\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eReact — フィールドごとに useState + onChange\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"tsx\" data-sourcepos=\"777:1-786:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetDepositAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eACC-0001\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetDepositAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e10000\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositCurrency\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esetDepositCurrency\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eJPY\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// ... 各フィールド分\u003c/span\u003e\n\n\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003einput\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAccountId\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"na\"\u003eonChange\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nf\"\u003esetDepositAccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003einput\u003c/span\u003e \u003cspan class=\"na\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"number\"\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003edepositAmount\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"na\"\u003eonChange\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nf\"\u003esetDepositAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u0026gt;\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"788:1-788:176\"\u003e5フィールド = \u003ccode\u003euseState\u003c/code\u003e 5回 + onChange 5回。\u003ccode\u003ereact-hook-form\u003c/code\u003e のような外部ライブラリで省略できるが、本記事では「素の React」での比較。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"790:1-790:60\"\u003e\n\u003cspan id=\"thymeleaf--thfield-で-form-object-と自動結合\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#thymeleaf--thfield-%E3%81%A7-form-object-%E3%81%A8%E8%87%AA%E5%8B%95%E7%B5%90%E5%90%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eThymeleaf — \u003ccode\u003eth:field\u003c/code\u003e で Form Object と自動結合\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"792:1-800:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;form\u003c/span\u003e \u003cspan class=\"na\"\u003eth:action=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"@{/api/v1/accounts/deposit}\"\u003c/span\u003e \u003cspan class=\"na\"\u003eth:object=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${accountsForm}\"\u003c/span\u003e \u003cspan class=\"na\"\u003emethod=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"post\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"hidden\"\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{authorization}\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eaccountId\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{depositAccountId}\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eamount\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{depositAmount}\"\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"number\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;label\u0026gt;\u003c/span\u003eIdempotency-Key\u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003eth:field=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"*{depositIdempotencyKey}\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u0026lt;/label\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"submit\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e送信\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/form\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"802:1-802:189\"\u003e\u003ccode\u003eth:field\u003c/code\u003e 1つで「name属性・id属性・value属性」を自動設定し、サーバー側の Form Object とフィールドが結びつく。Vue の \u003ccode\u003ev-model\u003c/code\u003e に近い書き心地。\u003c/p\u003e\n\u003chr data-sourcepos=\"804:1-805:0\"\u003e\n\u003ch2 data-sourcepos=\"806:1-806:51\"\u003e\n\u003cspan id=\"9-エラーローディング表示の違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#9-%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%AD%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E8%A1%A8%E7%A4%BA%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e9. エラー・ローディング表示の違い\u003c/h2\u003e\n\u003cp data-sourcepos=\"808:1-808:37\"\u003eAPI 失敗時の表示パターン。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"810:1-810:16\"\u003e\n\u003cspan id=\"vanilla-html-2\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vanilla-html-2\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVanilla HTML\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"812:1-823:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003erenderResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etargetId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003etarget\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetElementById\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etargetId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ebadgeClass\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estatus\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"mi\"\u003e200\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estatus\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e300\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eok\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eerr\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003einnerHTML\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e`\n    \u0026lt;div class=\"response-panel\"\u0026gt;\n      \u0026lt;span class=\"badge \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003ebadgeClass\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u0026gt;HTTP \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026lt;/span\u0026gt;\n      \u0026lt;pre\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nf\"\u003eescapeHtml\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e))}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026lt;/pre\u0026gt;\n    \u0026lt;/div\u0026gt;\n  `\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"825:1-825:115\"\u003e\u003cstrong\u003e手動でエスケープが必要\u003c/strong\u003e。\u003ccode\u003eescapeHtml\u003c/code\u003e 関数を自前で用意するか、\u003ccode\u003etextContent\u003c/code\u003e を使う。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"827:1-827:68\"\u003e\n\u003cspan id=\"vue--react--自動エスケープ--条件レンダリング\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue--react--%E8%87%AA%E5%8B%95%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97--%E6%9D%A1%E4%BB%B6%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue / React — 自動エスケープ + 条件レンダリング\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"829:1-835:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- Vue --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"status !== null\"\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"response-panel\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;span\u003c/span\u003e \u003cspan class=\"na\"\u003e:class=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"status \u0026lt; 400 ? 'badge-ok' : 'badge-err'\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003eHTTP {{ status }}\u003cspan class=\"nt\"\u003e\u0026lt;/span\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;pre\u0026gt;\u003c/span\u003e{{ response }}\u003cspan class=\"nt\"\u003e\u0026lt;/pre\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"tsx\" data-sourcepos=\"837:1-845:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// React\u003c/span\u003e\n\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e \u003cspan class=\"na\"\u003eclassName\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"response-panel\"\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003espan\u003c/span\u003e \u003cspan class=\"na\"\u003eclassName\u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estatus\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e400\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003ebadge-ok\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003ebadge-err\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003eHTTP \u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estatus\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003espan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003epre\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003epre\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ediv\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"847:1-847:121\"\u003e\u003ccode\u003e{{ }}\u003c/code\u003e や \u003ccode\u003e{}\u003c/code\u003e で値を埋め込むと\u003cstrong\u003e自動エスケープされる\u003c/strong\u003e。XSS 対策を意識する必要がない。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"849:1-849:52\"\u003e\n\u003cspan id=\"thymeleaf--thtext-で自動エスケープ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#thymeleaf--thtext-%E3%81%A7%E8%87%AA%E5%8B%95%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eThymeleaf — \u003ccode\u003eth:text\u003c/code\u003e で自動エスケープ\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"851:1-856:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eth:if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${accountsForm.apiResponse != null}\"\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"response-box\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u0026gt;\u003c/span\u003eStatus: \u003cspan class=\"nt\"\u003e\u0026lt;span\u003c/span\u003e \u003cspan class=\"na\"\u003eth:text=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${accountsForm.apiResponse.statusCode}\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;/p\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;pre\u003c/span\u003e \u003cspan class=\"na\"\u003eth:text=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"${accountsForm.apiResponse.body}\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/pre\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"858:1-858:97\"\u003e\u003ccode\u003eth:text\u003c/code\u003e も自動エスケープ。\u003ccode\u003eth:utext\u003c/code\u003e を使うと unescape（XSS リスクあり）。\u003c/p\u003e\n\u003chr data-sourcepos=\"860:1-861:0\"\u003e\n\u003ch2 data-sourcepos=\"862:1-862:43\"\u003e\n\u003cspan id=\"10-起動デプロイ構成の違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#10-%E8%B5%B7%E5%8B%95%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E6%A7%8B%E6%88%90%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e10. 起動・デプロイ構成の違い\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"864:1-864:42\"\u003e\n\u003cspan id=\"vanilla-html--nginx-で静的配信\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vanilla-html--nginx-%E3%81%A7%E9%9D%99%E7%9A%84%E9%85%8D%E4%BF%A1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVanilla HTML — nginx で静的配信\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"nginx\" data-sourcepos=\"866:1-877:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# nginx-external.conf\u003c/span\u003e\n\u003cspan class=\"k\"\u003eserver\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kn\"\u003elisten\u003c/span\u003e \u003cspan class=\"mi\"\u003e8080\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"kn\"\u003eroot\u003c/span\u003e \u003cspan class=\"n\"\u003e/app/banklink-external-web-vanilla-html\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"kn\"\u003eindex\u003c/span\u003e \u003cspan class=\"s\"\u003eindex.html\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n  \u003cspan class=\"kn\"\u003elocation\u003c/span\u003e \u003cspan class=\"n\"\u003e/api/\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kn\"\u003eproxy_pass\u003c/span\u003e \u003cspan class=\"s\"\u003ehttp://banklink-api:8080\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e# API へリバプロ\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"dockerfile\" data-sourcepos=\"879:1-883:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"s\"\u003e nginx:alpine\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e banklink-web-vanilla-html /app/\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e nginx-external.conf /etc/nginx/conf.d/default.conf\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"885:1-885:51\"\u003e最小構成。HTML/JS/CSS をそのまま配信。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"887:1-887:51\"\u003e\n\u003cspan id=\"vue--react--vite-ビルド--nginx-配信\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue--react--vite-%E3%83%93%E3%83%AB%E3%83%89--nginx-%E9%85%8D%E4%BF%A1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue / React — Vite ビルド → nginx 配信\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"dockerfile\" data-sourcepos=\"889:1-902:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# build stage\u003c/span\u003e\n\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003enode:20-alpine\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eAS\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003ebuilder\u003c/span\u003e\n\u003cspan class=\"k\"\u003eWORKDIR\u003c/span\u003e\u003cspan class=\"s\"\u003e /app\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e package*.json ./\u003c/span\u003e\n\u003cspan class=\"k\"\u003eRUN \u003c/span\u003enpm ci\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e . .\u003c/span\u003e\n\u003cspan class=\"k\"\u003eRUN \u003c/span\u003enpm run build   \u003cspan class=\"c\"\u003e# dist/ を生成\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# serve stage\u003c/span\u003e\n\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"s\"\u003e nginx:alpine\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e --from=builder /app/dist /usr/share/nginx/html\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e nginx-external.conf /etc/nginx/conf.d/default.conf\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"904:1-904:111\"\u003eビルド成果物を nginx に配置。Vanilla との違いはビルドステージが追加されるだけ。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"906:1-906:46\"\u003e\n\u003cspan id=\"thymeleaf--spring-boot-実行可能-jar\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#thymeleaf--spring-boot-%E5%AE%9F%E8%A1%8C%E5%8F%AF%E8%83%BD-jar\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eThymeleaf — Spring Boot 実行可能 jar\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"dockerfile\" data-sourcepos=\"908:1-918:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003eeclipse-temurin:21-jdk-alpine\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eAS\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003ebuilder\u003c/span\u003e\n\u003cspan class=\"k\"\u003eWORKDIR\u003c/span\u003e\u003cspan class=\"s\"\u003e /app\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e pom.xml .\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e src ./src\u003c/span\u003e\n\u003cspan class=\"k\"\u003eRUN \u003c/span\u003emvn clean package \u003cspan class=\"nt\"\u003e-DskipTests\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"s\"\u003e eclipse-temurin:21-jre-alpine\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e --from=builder /app/target/*.jar app.jar\u003c/span\u003e\n\u003cspan class=\"k\"\u003eENTRYPOINT\u003c/span\u003e\u003cspan class=\"s\"\u003e [\"java\", \"-jar\", \"/app.jar\"]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"920:1-920:79\"\u003eJVM 必須。コンテナサイズも数十 MB ＋ JVM 分（200 MB前後）。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"922:1-922:31\"\u003e\n\u003cspan id=\"イメージサイズ比較\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%82%B5%E3%82%A4%E3%82%BA%E6%AF%94%E8%BC%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eイメージサイズ比較\u003c/h3\u003e\n\u003ctable data-sourcepos=\"924:1-929:57\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"924:1-924:54\"\u003e\n\u003cth data-sourcepos=\"924:2-924:2\"\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"924:4-924:38\"\u003eコンテナイメージサイズ\u003c/th\u003e\n\u003cth data-sourcepos=\"924:40-924:53\"\u003e起動時間\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"926:1-926:44\"\u003e\n\u003ctd data-sourcepos=\"926:2-926:23\"\u003eVanilla HTML (nginx)\u003c/td\u003e\n\u003ctd data-sourcepos=\"926:25-926:34\"\u003e〜25 MB\u003c/td\u003e\n\u003ctd data-sourcepos=\"926:36-926:43\"\u003e\u0026lt; 1秒\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"927:1-927:42\"\u003e\n\u003ctd data-sourcepos=\"927:2-927:21\"\u003eVue (nginx 配信)\u003c/td\u003e\n\u003ctd data-sourcepos=\"927:23-927:32\"\u003e〜30 MB\u003c/td\u003e\n\u003ctd data-sourcepos=\"927:34-927:41\"\u003e\u0026lt; 1秒\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"928:1-928:44\"\u003e\n\u003ctd data-sourcepos=\"928:2-928:23\"\u003eReact (nginx 配信)\u003c/td\u003e\n\u003ctd data-sourcepos=\"928:25-928:34\"\u003e〜30 MB\u003c/td\u003e\n\u003ctd data-sourcepos=\"928:36-928:43\"\u003e\u0026lt; 1秒\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"929:1-929:57\"\u003e\n\u003ctd data-sourcepos=\"929:2-929:32\"\u003eThymeleaf (Spring Boot + JVM)\u003c/td\u003e\n\u003ctd data-sourcepos=\"929:34-929:44\"\u003e〜200 MB\u003c/td\u003e\n\u003ctd data-sourcepos=\"929:46-929:56\"\u003e5〜15秒\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"931:1-931:230\"\u003e軽量化を優先するなら nginx 配信系（前者3つ）が有利。Spring Boot は重い分、サーバーサイドロジック・API プロキシ・認証統合などを \u003cstrong\u003e同一プロセスで扱える\u003c/strong\u003e強みがある。\u003c/p\u003e\n\u003chr data-sourcepos=\"933:1-934:0\"\u003e\n\u003ch2 data-sourcepos=\"935:1-935:25\"\u003e\n\u003cspan id=\"11-横並び比較表\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#11-%E6%A8%AA%E4%B8%A6%E3%81%B3%E6%AF%94%E8%BC%83%E8%A1%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e11. 横並び比較表\u003c/h2\u003e\n\u003ctable data-sourcepos=\"937:1-953:84\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"937:1-937:53\"\u003e\n\u003cth data-sourcepos=\"937:2-937:9\"\u003e観点\u003c/th\u003e\n\u003cth data-sourcepos=\"937:11-937:24\"\u003eVanilla HTML\u003c/th\u003e\n\u003cth data-sourcepos=\"937:26-937:32\"\u003eVue 3\u003c/th\u003e\n\u003cth data-sourcepos=\"937:34-937:40\"\u003eReact\u003c/th\u003e\n\u003cth data-sourcepos=\"937:42-937:52\"\u003eThymeleaf\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"939:1-939:42\"\u003e\n\u003ctd data-sourcepos=\"939:2-939:13\"\u003e\u003cstrong\u003e言語\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"939:15-939:18\"\u003eJS\u003c/td\u003e\n\u003ctd data-sourcepos=\"939:20-939:23\"\u003eTS\u003c/td\u003e\n\u003ctd data-sourcepos=\"939:25-939:34\"\u003eTS (TSX)\u003c/td\u003e\n\u003ctd data-sourcepos=\"939:36-939:41\"\u003eJava\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"940:1-940:48\"\u003e\n\u003ctd data-sourcepos=\"940:2-940:16\"\u003e\u003cstrong\u003eビルド\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"940:18-940:25\"\u003e不要\u003c/td\u003e\n\u003ctd data-sourcepos=\"940:27-940:32\"\u003eVite\u003c/td\u003e\n\u003ctd data-sourcepos=\"940:34-940:39\"\u003eVite\u003c/td\u003e\n\u003ctd data-sourcepos=\"940:41-940:47\"\u003eMaven\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"941:1-941:79\"\u003e\n\u003ctd data-sourcepos=\"941:2-941:22\"\u003e\u003cstrong\u003e学習コスト\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"941:24-941:28\"\u003e低\u003c/td\u003e\n\u003ctd data-sourcepos=\"941:30-941:34\"\u003e中\u003c/td\u003e\n\u003ctd data-sourcepos=\"941:36-941:46\"\u003e中〜高\u003c/td\u003e\n\u003ctd data-sourcepos=\"941:48-941:78\"\u003e中（Java 既習なら低）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"942:1-942:101\"\u003e\n\u003ctd data-sourcepos=\"942:2-942:43\"\u003e\u003cstrong\u003eコード行数（口座ページ）\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"942:45-942:55\"\u003e約130行\u003c/td\u003e\n\u003ctd data-sourcepos=\"942:57-942:67\"\u003e約130行\u003c/td\u003e\n\u003ctd data-sourcepos=\"942:69-942:79\"\u003e約350行\u003c/td\u003e\n\u003ctd data-sourcepos=\"942:81-942:100\"\u003e約250行 (分散)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"943:1-943:94\"\u003e\n\u003ctd data-sourcepos=\"943:2-943:28\"\u003e\u003cstrong\u003e状態管理モデル\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"943:30-943:34\"\u003eDOM\u003c/td\u003e\n\u003ctd data-sourcepos=\"943:36-943:59\"\u003eリアクティブ ref\u003c/td\u003e\n\u003ctd data-sourcepos=\"943:61-943:70\"\u003euseState\u003c/td\u003e\n\u003ctd data-sourcepos=\"943:72-943:93\"\u003eForm Object (server)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"944:1-944:78\"\u003e\n\u003ctd data-sourcepos=\"944:2-944:25\"\u003e\u003cstrong\u003eフォーム結合\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"944:27-944:34\"\u003e手動\u003c/td\u003e\n\u003ctd data-sourcepos=\"944:36-944:46\"\u003e\u003ccode\u003ev-model\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"944:48-944:64\"\u003eonChange 個別\u003c/td\u003e\n\u003ctd data-sourcepos=\"944:66-944:77\"\u003e\u003ccode\u003eth:field\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"945:1-945:68\"\u003e\n\u003ctd data-sourcepos=\"945:2-945:31\"\u003e\u003cstrong\u003eXSS自動エスケープ\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"945:33-945:40\"\u003e手動\u003c/td\u003e\n\u003ctd data-sourcepos=\"945:42-945:49\"\u003e自動\u003c/td\u003e\n\u003ctd data-sourcepos=\"945:51-945:58\"\u003e自動\u003c/td\u003e\n\u003ctd data-sourcepos=\"945:60-945:67\"\u003e自動\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"946:1-946:89\"\u003e\n\u003ctd data-sourcepos=\"946:2-946:28\"\u003e\u003cstrong\u003eAPI呼び出し場所\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"946:30-946:43\"\u003eブラウザ\u003c/td\u003e\n\u003ctd data-sourcepos=\"946:45-946:58\"\u003eブラウザ\u003c/td\u003e\n\u003ctd data-sourcepos=\"946:60-946:73\"\u003eブラウザ\u003c/td\u003e\n\u003ctd data-sourcepos=\"946:75-946:88\"\u003eサーバー\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"947:1-947:103\"\u003e\n\u003ctd data-sourcepos=\"947:2-947:34\"\u003e\u003cstrong\u003e認証情報の持ち場所\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"947:36-947:49\"\u003elocalStorage\u003c/td\u003e\n\u003ctd data-sourcepos=\"947:51-947:64\"\u003elocalStorage\u003c/td\u003e\n\u003ctd data-sourcepos=\"947:66-947:79\"\u003elocalStorage\u003c/td\u003e\n\u003ctd data-sourcepos=\"947:81-947:102\"\u003eサーバー Session\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"948:1-948:58\"\u003e\n\u003ctd data-sourcepos=\"948:2-948:18\"\u003e\u003cstrong\u003eCORS 必要\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"948:20-948:27\"\u003eはい\u003c/td\u003e\n\u003ctd data-sourcepos=\"948:29-948:36\"\u003eはい\u003c/td\u003e\n\u003ctd data-sourcepos=\"948:38-948:45\"\u003eはい\u003c/td\u003e\n\u003ctd data-sourcepos=\"948:47-948:57\"\u003eいいえ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"949:1-949:68\"\u003e\n\u003ctd data-sourcepos=\"949:2-949:31\"\u003e\u003cstrong\u003e依存パッケージ数\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"949:33-949:35\"\u003e0\u003c/td\u003e\n\u003ctd data-sourcepos=\"949:37-949:43\"\u003e〜10\u003c/td\u003e\n\u003ctd data-sourcepos=\"949:45-949:51\"\u003e〜10\u003c/td\u003e\n\u003ctd data-sourcepos=\"949:53-949:67\"\u003e〜20 (Maven)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"950:1-950:77\"\u003e\n\u003ctd data-sourcepos=\"950:2-950:31\"\u003e\u003cstrong\u003eコンテナイメージ\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"950:33-950:42\"\u003e〜25 MB\u003c/td\u003e\n\u003ctd data-sourcepos=\"950:44-950:53\"\u003e〜30 MB\u003c/td\u003e\n\u003ctd data-sourcepos=\"950:55-950:64\"\u003e〜30 MB\u003c/td\u003e\n\u003ctd data-sourcepos=\"950:66-950:76\"\u003e〜200 MB\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"951:1-951:65\"\u003e\n\u003ctd data-sourcepos=\"951:2-951:19\"\u003e\u003cstrong\u003e起動時間\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"951:21-951:28\"\u003e即時\u003c/td\u003e\n\u003ctd data-sourcepos=\"951:30-951:37\"\u003e即時\u003c/td\u003e\n\u003ctd data-sourcepos=\"951:39-951:46\"\u003e即時\u003c/td\u003e\n\u003ctd data-sourcepos=\"951:48-951:64\"\u003e5〜15秒 (JVM)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"952:1-952:73\"\u003e\n\u003ctd data-sourcepos=\"952:2-952:15\"\u003e\u003cstrong\u003e動的UI\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"952:17-952:24\"\u003e弱い\u003c/td\u003e\n\u003ctd data-sourcepos=\"952:26-952:33\"\u003e強い\u003c/td\u003e\n\u003ctd data-sourcepos=\"952:35-952:42\"\u003e強い\u003c/td\u003e\n\u003ctd data-sourcepos=\"952:44-952:72\"\u003e弱い (リロード前提)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"953:1-953:84\"\u003e\n\u003ctd data-sourcepos=\"953:2-953:25\"\u003e\u003cstrong\u003eエコシステム\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"953:27-953:34\"\u003eなし\u003c/td\u003e\n\u003ctd data-sourcepos=\"953:36-953:46\"\u003e中規模\u003c/td\u003e\n\u003ctd data-sourcepos=\"953:48-953:55\"\u003e巨大\u003c/td\u003e\n\u003ctd data-sourcepos=\"953:57-953:83\"\u003eSpring エコシステム\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr data-sourcepos=\"955:1-956:0\"\u003e\n\u003ch2 data-sourcepos=\"957:1-957:67\"\u003e\n\u003cspan id=\"12-どう選ぶか--4スタックの強み弱みと判断軸\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#12-%E3%81%A9%E3%81%86%E9%81%B8%E3%81%B6%E3%81%8B--4%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AE%E5%BC%B7%E3%81%BF%E5%BC%B1%E3%81%BF%E3%81%A8%E5%88%A4%E6%96%AD%E8%BB%B8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e12. どう選ぶか — 4スタックの強み弱みと判断軸\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"959:1-959:16\"\u003e\n\u003cspan id=\"vanilla-html-3\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vanilla-html-3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVanilla HTML\u003c/h3\u003e\n\u003cp data-sourcepos=\"961:1-961:11\"\u003e\u003cstrong\u003e強み\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"962:1-965:0\"\u003e\n\u003cli data-sourcepos=\"962:1-962:82\"\u003eゼロ依存・ゼロビルド・ゼロ学習コスト（HTML/JS 基礎のみ）\u003c/li\u003e\n\u003cli data-sourcepos=\"963:1-963:56\"\u003e配信コスト最小・コンテナイメージ最小\u003c/li\u003e\n\u003cli data-sourcepos=\"964:1-965:0\"\u003e「30分で動くデモを作る」用途に最強\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"966:1-966:11\"\u003e\u003cstrong\u003e弱み\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"967:1-970:0\"\u003e\n\u003cli data-sourcepos=\"967:1-967:90\"\u003eアプリが大きくなると状態管理が破綻する（DOM 直接操作の限界）\u003c/li\u003e\n\u003cli data-sourcepos=\"968:1-968:31\"\u003eTypeScript 型安全性なし\u003c/li\u003e\n\u003cli data-sourcepos=\"969:1-970:0\"\u003eリアクティブ UI が苦手\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"971:1-971:17\"\u003e\u003cstrong\u003e選ぶ場面\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"972:1-975:0\"\u003e\n\u003cli data-sourcepos=\"972:1-972:87\"\u003eAPI 動作確認ツール、社内検証用フォーム、簡易ダッシュボード\u003c/li\u003e\n\u003cli data-sourcepos=\"973:1-973:65\"\u003e「フレームワーク要らない、画面だけほしい」\u003c/li\u003e\n\u003cli data-sourcepos=\"974:1-975:0\"\u003e後で SPA に置き換える前提のプロトタイプ\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"976:1-976:7\"\u003e\n\u003cspan id=\"vue-1\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue-1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVue\u003c/h3\u003e\n\u003cp data-sourcepos=\"978:1-978:11\"\u003e\u003cstrong\u003e強み\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"979:1-982:0\"\u003e\n\u003cli data-sourcepos=\"979:1-979:77\"\u003e\n\u003ccode\u003ev-model\u003c/code\u003e などの糖衣構文で\u003cstrong\u003eコード量が React より少ない\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"980:1-980:77\"\u003eテンプレート構文が HTML に近く、初心者にも読みやすい\u003c/li\u003e\n\u003cli data-sourcepos=\"981:1-982:0\"\u003e単一ファイルコンポーネント (.vue) でロジック/テンプレート/スタイルが1ファイルに収まる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"983:1-983:11\"\u003e\u003cstrong\u003e弱み\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"984:1-986:0\"\u003e\n\u003cli data-sourcepos=\"984:1-984:50\"\u003eReact に比べてエコシステムが小さい\u003c/li\u003e\n\u003cli data-sourcepos=\"985:1-986:0\"\u003e採用案件・採用人材の数で React に劣る\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"987:1-987:17\"\u003e\u003cstrong\u003e選ぶ場面\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"988:1-991:0\"\u003e\n\u003cli data-sourcepos=\"988:1-988:53\"\u003e業務系 SPA でフォーム・入力 UI が多い\u003c/li\u003e\n\u003cli data-sourcepos=\"989:1-989:67\"\u003e「フレームワーク選定で迷ったら Vue から試す」\u003c/li\u003e\n\u003cli data-sourcepos=\"990:1-991:0\"\u003eWeb エンジニア人材市場が日本国内中心の案件\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"992:1-992:9\"\u003e\n\u003cspan id=\"react-1\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#react-1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eReact\u003c/h3\u003e\n\u003cp data-sourcepos=\"994:1-994:11\"\u003e\u003cstrong\u003e強み\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"995:1-998:0\"\u003e\n\u003cli data-sourcepos=\"995:1-995:95\"\u003e巨大なエコシステム（UI ライブラリ・状態管理・テスト・モバイル）\u003c/li\u003e\n\u003cli data-sourcepos=\"996:1-996:58\"\u003eTypeScript との相性が良い（型定義が充実）\u003c/li\u003e\n\u003cli data-sourcepos=\"997:1-998:0\"\u003e採用市場で最も求人が多い\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"999:1-999:11\"\u003e\u003cstrong\u003e弱み\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"1000:1-1003:0\"\u003e\n\u003cli data-sourcepos=\"1000:1-1000:64\"\u003e同じ機能を書くのに Vue より行数が増える傾向\u003c/li\u003e\n\u003cli data-sourcepos=\"1001:1-1001:85\"\u003e\n\u003ccode\u003euseState\u003c/code\u003e, \u003ccode\u003euseEffect\u003c/code\u003e, \u003ccode\u003euseMemo\u003c/code\u003e, \u003ccode\u003euseCallback\u003c/code\u003e の使い分けが学習コスト\u003c/li\u003e\n\u003cli data-sourcepos=\"1002:1-1003:0\"\u003e関数コンポーネント再レンダリング理解が必須\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1004:1-1004:17\"\u003e\u003cstrong\u003e選ぶ場面\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"1005:1-1008:0\"\u003e\n\u003cli data-sourcepos=\"1005:1-1005:68\"\u003e大規模 SPA、Next.js / React Native との接続を見据える\u003c/li\u003e\n\u003cli data-sourcepos=\"1006:1-1006:44\"\u003eエンジニア採用を重視する組織\u003c/li\u003e\n\u003cli data-sourcepos=\"1007:1-1008:0\"\u003eUI ライブラリ(MUI / Mantine / shadcn など)を流用したい\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"1009:1-1009:13\"\u003e\n\u003cspan id=\"thymeleaf-1\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#thymeleaf-1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eThymeleaf\u003c/h3\u003e\n\u003cp data-sourcepos=\"1011:1-1011:11\"\u003e\u003cstrong\u003e強み\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"1012:1-1016:0\"\u003e\n\u003cli data-sourcepos=\"1012:1-1012:85\"\u003e\n\u003cstrong\u003eクライアント側 JS を最小に抑えられる\u003c/strong\u003e（業務システム的）\u003c/li\u003e\n\u003cli data-sourcepos=\"1013:1-1013:124\"\u003e認証情報・API トークンが\u003cstrong\u003eサーバー内に閉じる\u003c/strong\u003e（セキュリティ要件が厳しい場面で有利）\u003c/li\u003e\n\u003cli data-sourcepos=\"1014:1-1014:68\"\u003eSpring Security / Spring Boot のエコシステムをフル活用\u003c/li\u003e\n\u003cli data-sourcepos=\"1015:1-1016:0\"\u003eJava エンジニアが既存スキルで Web UI を作れる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1017:1-1017:11\"\u003e\u003cstrong\u003e弱み\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"1018:1-1021:0\"\u003e\n\u003cli data-sourcepos=\"1018:1-1018:105\"\u003eページ遷移ごとにサーバーラウンドトリップが必要（SPA に比べてもっさり）\u003c/li\u003e\n\u003cli data-sourcepos=\"1019:1-1019:42\"\u003eリアクティブな UI に向かない\u003c/li\u003e\n\u003cli data-sourcepos=\"1020:1-1021:0\"\u003eJVM のコンテナイメージが重い\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1022:1-1022:17\"\u003e\u003cstrong\u003e選ぶ場面\u003c/strong\u003e:\u003c/p\u003e\n\u003cul data-sourcepos=\"1023:1-1027:0\"\u003e\n\u003cli data-sourcepos=\"1023:1-1023:57\"\u003e\u003cstrong\u003e業務システム・管理画面（社内向け）\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"1024:1-1024:88\"\u003eセキュリティ要件で API キーをクライアントに露出させたくない\u003c/li\u003e\n\u003cli data-sourcepos=\"1025:1-1025:73\"\u003eJava エンジニアが多い組織、Spring Boot を既に採用済み\u003c/li\u003e\n\u003cli data-sourcepos=\"1026:1-1027:0\"\u003eリッチな UI より「フォーム送信して結果表示」程度で十分\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"1028:1-1028:19\"\u003e\n\u003cspan id=\"判断フロー\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%88%A4%E6%96%AD%E3%83%95%E3%83%AD%E3%83%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e判断フロー\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"1030:1-1038:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eQ1. UI のリアクティブ性は必要か?\n   ├─ No (フォーム送信して結果表示で十分)\n   │   ├─ サーバー側集約したい → Thymeleaf\n   │   └─ 軽量に作りたい → Vanilla HTML\n   └─ Yes (リアクティブ UI が必要)\n       ├─ エコシステムや採用重視 → React\n       └─ コード量を抑えたい / 学習コスト軽 → Vue\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"1040:1-1041:0\"\u003e\n\u003ch2 data-sourcepos=\"1042:1-1042:25\"\u003e\n\u003cspan id=\"13-まとめ学び\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#13-%E3%81%BE%E3%81%A8%E3%82%81%E5%AD%A6%E3%81%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e13. まとめ・学び\u003c/h2\u003e\n\u003cul data-sourcepos=\"1044:1-1049:0\"\u003e\n\u003cli data-sourcepos=\"1044:1-1044:186\"\u003e4スタックは「\u003cstrong\u003e正解 vs 不正解\u003c/strong\u003e」ではなく、\u003cstrong\u003eプロジェクト要件によって有利不利が変わる\u003c/strong\u003e。同じ仕様を並べると相違が浮かび上がる。\u003c/li\u003e\n\u003cli data-sourcepos=\"1045:1-1045:163\"\u003e\n\u003cstrong\u003eコード行数だけ見ると Vanilla / Vue が少なく、React が多い\u003c/strong\u003e。ただし React は採用市場とエコシステムで補って余りある。\u003c/li\u003e\n\u003cli data-sourcepos=\"1046:1-1046:141\"\u003e\n\u003cstrong\u003eThymeleaf は時代遅れではない\u003c/strong\u003e — 業務システムのセキュリティ・運用要件には今でも合うことが多い。\u003c/li\u003e\n\u003cli data-sourcepos=\"1047:1-1047:127\"\u003e\n\u003cstrong\u003e「フレームワークの選定は要件から逆算する」\u003c/strong\u003e が結論。流行で選ぶと数年後に苦労する。\u003c/li\u003e\n\u003cli data-sourcepos=\"1048:1-1049:0\"\u003e4実装を並行運用してみて分かったのは、\u003cstrong\u003eビルドツール（Vite vs Maven vs なし）の違いがプロジェクト立ち上げ初日の体験を大きく分ける\u003c/strong\u003eということ。プロトタイプ段階では Vanilla / Vite 系が圧倒的に速い。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1050:1-1050:76\"\u003e選定の際にこの比較が判断材料の1つになれば幸いです。\u003c/p\u003e\n\u003chr data-sourcepos=\"1052:1-1053:0\"\u003e\n\u003cp data-sourcepos=\"1054:1-1054:291\"\u003e🔗 \u003cstrong\u003e個人ブログに同様の記事と関連記事も書いています\u003c/strong\u003e: \u003ca href=\"https://mint041223techblog.netlify.app/blog/banklink-web-stacks-comparison/\" rel=\"nofollow noopener\" target=\"_blank\"\u003e同じ業務 Web UI を Vanilla HTML / Vue / React / Thymeleaf で実装して比較した — 4スタックの違いと選定指針\u003c/a\u003e\u003c/p\u003e\n","body":"# 同じ業務 Web UI を Vanilla HTML / Vue / React / Thymeleaf で実装して比較した — 4スタックの違いと選定指針\n\n## この記事でわかること\n\n- **同一仕様の業務 Web UI** を Vanilla HTML / Vue 3 / React / Thymeleaf の4スタックで実装した時、コード上で何がどう違うのか（コード例付き）\n- プロジェクト構成・ビルドの違い（package.json / Vite / pom.xml / 素HTML）\n- API クライアント・状態管理・フォーム処理・エラー表示・デプロイの **観点別比較**\n- 「どんな場面でどのスタックを選ぶか」の判断軸\n\n## 対象読者\n\n- フレームワーク選定で **Vanilla / Vue / React / Thymeleaf** のどれを使うか迷っている方\n- それぞれを単体では触ったことがあっても、**同じ仕様で並べたとき何が違うのか** を知りたい方\n- 業務系 Web UI（フォーム + API呼び出し + 結果表示）を作る前提でスタックを比較したい方\n\n## 動作環境\n\n| 項目 | バージョン |\n|---|---|\n| Vanilla HTML | ビルド不要（モジュールスクリプト） |\n| Vue | 3.x + Vite 5.x + TypeScript 5.x + Vue Router 4.x |\n| React | 18.x + Vite 5.x + TypeScript 5.x + React Router 6.x |\n| Thymeleaf | Spring Boot 3.x + Java 21 + Thymeleaf 3.x |\n\n---\n\n## 1. はじめに\n\n### 正直に言うと、最初は比較記事を書く予定ではなかった\n\n`banklink-service` のバックエンド開発中に「API が動いているか確認できる画面がほしい」と思い、その場で一番早く作れる Vanilla HTML で適当に組みました。**「とりあえず動けばいい」** を絵に描いたような書き捨て UI です。\n\n開発を進めるにあたっては「いずれちゃんと Vue か React に置き換えて、本格的な開発テンプレートにしよう」と考えていました。フロントエンドの本実装はそこから始めるつもりだったのです。\n\n**ところが、手を動かそうとした瞬間にふと気づきました。**\n\nこの程度の処理量（フォーム + API 呼び出し + 結果表示）なら、**Thymeleaf も含めて4スタック全部で実装してみてもそんなに時間はかからない**。しかも全部同じ機能で揃えれば、**各技術の違いを並べて比較するのに丁度いい題材になるんじゃないか？** と。\n\n開発作業の進捗としては、これは完全に **脱線** です。「テンプレートを1つ作る」予定が、いつの間にか「4スタックで並列実装して比較する」に化けていました。\n\nですが、書き上げてみると自分自身が技術選定の判断軸を整理できたうえ、同じ場面で迷っている人の判断材料にもなりそうだったので、記事として残すことにしました。**本記事はその「楽しい脱線の副産物」です。**\n\n---\n\n### で、本題\n\n業務系の Web UI を作るとき、**「Vanilla HTML / Vue / React / Thymeleaf のどれで作るのが妥当か」** は最初に通る判断です。それぞれ単体ではよく語られますが、「**同じ仕様で4つ並べたら何が違うのか**」を実コードで横並びにした記事は意外と少ない。\n\nそこで、銀行系 API ラッパーサービス `banklink-service`（個人練習用プロジェクト）で、まったく同じ仕様の業務 UI を **4スタックで並行実装** しました。本記事はその比較の記録です。\n\n\u003e **この記事のスタンス**\n\u003e 「正解の1つ」を提示する記事ではありません。**同じ要件を4スタックで書いたコードを見比べて、自分のプロジェクトでどれが妥当かを判断する材料を提供する** ことが目的です。\n\n---\n\n## 2. 共通仕様（4スタックで完全に揃えた前提）\n\n4実装はすべて以下の仕様を満たします。\n\n### 機能\n- 6ページ: ホーム / **口座** / ローン / 外貨 / 投資 / KYC\n- 各ページに複数の API 操作セクション（例: 口座ページは「一覧取得・残高取得・入金・出金・取引履歴」の5セクション）\n- 画面上部の **Bearer Token 入力欄** から API 認証トークンを設定\n- ナビゲーションで各ページを行き来できる\n- ボタンを押すと API を叩いて結果を画面に整形表示\n\n### 接続先バックエンド\n- 同じ Spring Boot API (`/api/v1/accounts`, `/api/v1/loans`, ...)\n- レスポンスは JSON、エラーは HTTP ステータス + body の構造で統一\n\n### 揃えていない点（差別化箇所）\n- **CSS の見た目**（外部UI / 内部UI で意図的に変える）\n- 内部実装（フレームワーク特性に従う）\n\nつまり「**画面と機能は同じ、中身だけ4通り**」という比較ベースを作りました。\n\n![本記事で深掘りする「口座」画面 — Vanilla HTML 外部UI 版。4スタックすべてが同じ機能を持つ](https://mint041223techblog.netlify.app/images/banklink-service/02_accounts.webp)\n\n口座画面（上記）には「口座一覧取得・残高取得・入金・出金・取引履歴」の5セクションがあります。**この同じ画面・同じ機能を、4スタックで別々に実装した** のが本記事の比較対象です。\n\n---\n\n## 3. 4スタックの構成概要\n\nそれぞれの「最小構成」をまず一望します。\n\n### Vanilla HTML\n\n```\nbanklink-web-vanilla-html/\n├─ index.html         ← トップページ\n├─ accounts.html      ← 口座ページ\n├─ loans.html         ← ローンページ\n├─ ... (他4ページ)\n├─ common-external.js ← トークン管理 + バインド共通\n└─ shared/\n    ├─ api/client.js  ← fetch ラッパー\n    └─ common.js      ← bindAction / renderResponse\n```\n\n**ビルドツールなし**。`.html` を直接ブラウザで開ける（または nginx で配信）。`\u003cscript type=\"module\"\u003e` で JS をインポート。\n\n### Vue\n\n```\nbanklink-web-vue/\n├─ vite.config.ts\n├─ index.html         ← SPA エントリ\n└─ src/\n    ├─ main.ts         ← createApp + mount\n    ├─ App.vue         ← レイアウト + RouterView\n    ├─ router/index.ts ← Vue Router 設定\n    ├─ api/client.ts   ← fetch ラッパー (TypeScript)\n    └─ views/\n        ├─ HomeView.vue\n        ├─ AccountsView.vue   ← 口座ページ\n        ├─ ... (他4ページ)\n```\n\n`npm run build` で `dist/` に静的ファイル生成 → nginx で配信。\n\n### React\n\n```\nbanklink-web-react/\n├─ vite.config.ts\n├─ index.html\n└─ src/\n    ├─ main.tsx           ← createRoot + render\n    ├─ App.tsx            ← 全6ページを1ファイルに集約（小規模なため）\n    └─ ... (shared/api/client.ts)\n```\n\nVue と類似だが、`App.tsx` に全ページを書く構成にした（コンポーネント数を最小化）。\n\n### Thymeleaf\n\n```\nbanklink-web-thymeleaf/\n├─ pom.xml\n└─ banklink-external-web-thymeleaf/\n    └─ src/main/\n        ├─ java/com/y104autumn/banklink/external/\n        │   ├─ BanklinkExternalApplication.java\n        │   ├─ controller/\n        │   │   ├─ ExternalTopPageController.java\n        │   │   ├─ AccountsController.java\n        │   │   └─ ... (他4ページ)\n        │   ├─ service/\n        │   │   ├─ AccountsService.java   ← RestClient で API 呼び出し\n        │   │   └─ ...\n        │   └─ form/\n        │       ├─ AccountsForm.java      ← @ModelAttribute 用\n        │       └─ ...\n        └─ resources/templates/\n            ├─ index.html\n            ├─ accounts.html              ← th:field 付き\n            └─ ...\n```\n\n`mvn package` で実行可能 jar 生成 → `java -jar` または Docker で起動。\n\n### 構成ファイル数の比較\n\n| | プロジェクト全体 | 1ページ実装に必要なファイル |\n|---|---|---|\n| Vanilla HTML | 約 10 ファイル | 1 (`accounts.html` のみ) |\n| Vue | 約 15 ファイル | 1 (`AccountsView.vue`) |\n| React | 約 5 ファイル | 1 (`App.tsx` 内の関数) |\n| Thymeleaf | 約 25 ファイル | 3 (Controller + Service + Form + template) |\n\nThymeleaf は **MVC を3層に分けるための定型コード** が多いです。ファイル数は最多ですが、各ファイルの役割は明確です。\n\n### 実装した6ページ（参考スクリーンショット）\n\n口座画面以外の5ページも全4スタックで同じ機能を持っています。参考までに Vanilla HTML 版の画面を列挙します。\n\n![トップ画面（外部UI）— トークン入力 + ナビゲーション + 各ページへの導線](https://mint041223techblog.netlify.app/images/banklink-service/01_top.webp)\n\n![ローン画面 — 申込・残高照会・支払いシミュレーション等のAPI操作セクション](https://mint041223techblog.netlify.app/images/banklink-service/03_loans.webp)\n\n![外貨画面 — レート取得・両替実行 等の操作](https://mint041223techblog.netlify.app/images/banklink-service/04_forex.webp)\n\n![投資画面 — ポートフォリオ・売買注文 等の操作](https://mint041223techblog.netlify.app/images/banklink-service/05_investments.webp)\n\n![KYC画面 — 本人確認状況・書類提出 等の操作](https://mint041223techblog.netlify.app/images/banklink-service/07_kyc.webp)\n\n### 外部UIと内部UIの見た目の差別化\n\n「**機能は同じ、見た目は外部UI / 内部UI で意図的に変える**」という方針を採っており、内部UI（業務端末寄り）はこのような外観です。\n\n![内部UI トップ画面（Vanilla HTML 版）— 業務端末寄りのデザインに意図的に差別化](https://mint041223techblog.netlify.app/images/banklink-service/06_internal_top.webp)\n\n本記事の比較は **外部UI** を題材にしていますが、各スタックとも external/internal の2セットを実装しており、CSS だけ入れ替えれば両方に対応できる構造になっています。\n\n---\n\n## 4. プロジェクト構成・ビルドの違い\n\n### Vanilla HTML — ビルド設定なし\n\n`package.json` も `tsconfig.json` も無し。ブラウザでファイルを直接開くか、nginx で静的配信するだけ。\n\n```bash\n# 開発: 直接ブラウザで開く\nopen accounts.html\n\n# 本番: nginx の document root に置く\nnginx -c nginx-external.conf\n```\n\n依存パッケージなし、ビルドプロセスなし、`node_modules` なし。\n\n### Vue — Vite + TypeScript\n\n```json\n// package.json (主要部分)\n{\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"vue\": \"^3.4.0\",\n    \"vue-router\": \"^4.3.0\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-vue\": \"^5.0.0\",\n    \"typescript\": \"^5.5.0\",\n    \"vite\": \"^5.3.0\",\n    \"vue-tsc\": \"^2.0.0\"\n  }\n}\n```\n\n```bash\nnpm install      # node_modules を作る\nnpm run dev      # http://localhost:5173 でホットリロード開発\nnpm run build    # dist/ に静的ファイル生成\n```\n\n### React — Vite + TypeScript (Vue と同じ Vite)\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc \u0026\u0026 vite build\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.3.0\",\n    \"react-dom\": \"^18.3.0\",\n    \"react-router-dom\": \"^6.24.0\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-react\": \"^4.3.0\",\n    \"typescript\": \"^5.5.0\",\n    \"vite\": \"^5.3.0\"\n  }\n}\n```\n\nVue とほぼ同じ操作感。違いは `@vitejs/plugin-vue` vs `@vitejs/plugin-react` だけ。\n\n### Thymeleaf — Maven + Spring Boot\n\n```xml\n\u003c!-- pom.xml (主要部分) --\u003e\n\u003cparent\u003e\n    \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n    \u003cartifactId\u003espring-boot-starter-parent\u003c/artifactId\u003e\n    \u003cversion\u003e3.4.5\u003c/version\u003e\n\u003c/parent\u003e\n\n\u003cdependencies\u003e\n    \u003cdependency\u003e\n        \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n        \u003cartifactId\u003espring-boot-starter-web\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n        \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n        \u003cartifactId\u003espring-boot-starter-thymeleaf\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n\u003c/dependencies\u003e\n```\n\n```bash\nmvn clean package      # target/*.jar 生成\njava -jar target/banklink-external-web-thymeleaf.jar  # 起動\n```\n\nJVM が必要。`target/` に生成される jar には **Tomcat も埋め込まれている**ので、追加でアプリケーションサーバーは不要。\n\n### ビルド時間の体感比較\n\n| スタック | 初回 `install` 等 | 本番ビルド | 起動時間 |\n|---|---|---|---|\n| Vanilla HTML | 0秒 | 0秒 | 即時 |\n| Vue | 30〜60秒 (`npm install`) | 5〜15秒 | nginx 起動分 |\n| React | 30〜60秒 | 5〜15秒 | nginx 起動分 |\n| Thymeleaf | 30〜120秒 (Maven 依存DL) | 20〜40秒 (`mvn package`) | JVM起動分 (5〜15秒) |\n\n---\n\n## 5. 同じ画面（口座ページ）を4スタックで実装\n\nここがこの記事の中心です。**全く同じ「口座一覧取得・残高取得・入金・出金・取引履歴」の5セクションを、4通りに書いたコード** を順に見ます。\n\n### Vanilla HTML 版\n\nHTML と JS が同じ `accounts.html` に同居（`\u003cscript type=\"module\"\u003e` でモジュール）。\n\n```html\n\u003c!-- accounts.html (口座一覧と入金部分の抜粋) --\u003e\n\u003csection class=\"section-card\"\u003e\n  \u003ch2\u003e口座一覧取得\u003c/h2\u003e\n  \u003cbutton id=\"accounts-list-btn\"\u003eGET /api/v1/accounts\u003c/button\u003e\n  \u003cdiv id=\"accounts-list-response\"\u003e\u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection class=\"section-card\"\u003e\n  \u003ch2\u003e入金\u003c/h2\u003e\n  \u003clabel\u003eaccountId\u003cinput id=\"deposit-account-id\" value=\"ACC-0001\" /\u003e\u003c/label\u003e\n  \u003clabel\u003eamount\u003cinput id=\"deposit-amount\" type=\"number\" value=\"10000\" /\u003e\u003c/label\u003e\n  \u003clabel\u003eIdempotency-Key\u003cinput id=\"deposit-key\" value=\"dep-key-001\" /\u003e\u003c/label\u003e\n  \u003cbutton id=\"accounts-deposit-btn\"\u003ePOST /api/v1/accounts/{id}/deposit\u003c/button\u003e\n  \u003cdiv id=\"accounts-deposit-response\"\u003e\u003c/div\u003e\n\u003c/section\u003e\n\n\u003cscript type=\"module\"\u003e\n  import { bindAction, numberValue, value } from \"./common-external.js\";\n\n  // ボタン id とレスポンス表示先 id をマッピング\n  bindAction(\"accounts-list-btn\", \"accounts-list-response\", () =\u003e ({\n    method: \"GET\",\n    path: \"/api/v1/accounts\",\n  }));\n\n  bindAction(\"accounts-deposit-btn\", \"accounts-deposit-response\", () =\u003e ({\n    method: \"POST\",\n    path: `/api/v1/accounts/${value(\"deposit-account-id\")}/deposit`,\n    idempotencyKey: value(\"deposit-key\"),\n    body: {\n      amount: numberValue(\"deposit-amount\"),\n      currency: value(\"deposit-currency\"),\n      reference: value(\"deposit-reference\"),\n    },\n  }));\n\u003c/script\u003e\n```\n\n**特徴**:\n- HTML 要素を `id` で識別 → JS から `document.getElementById` で参照（`bindAction` 関数内で行う）\n- 入力値は `value(\"input-id\")` ヘルパで都度取得（リアクティブではなく「クリック時点の値」を読みに行く）\n- 結果は `innerHTML` で文字列として描画\n\n### Vue 版\n\n```vue\n\u003c!-- AccountsView.vue (script setup + template) --\u003e\n\u003cscript setup lang=\"ts\"\u003e\nimport { inject, ref } from \"vue\";\nimport type { Ref } from \"vue\";\nimport { requestApi } from \"../api/client\";\n\nconst token = inject\u003cRef\u003cstring\u003e\u003e(\"token\")!;\nfunction fmt(r: unknown) { return JSON.stringify(r, null, 2); }\n\n// 口座一覧\nconst listRes = ref\u003cstring\u003e(\"\");\nconst listStatus = ref\u003cnumber | null\u003e(null);\nasync function getAccounts() {\n  const r = await requestApi({ method: \"GET\", path: \"/api/v1/accounts\", token: token.value });\n  listStatus.value = r.status; listRes.value = fmt(r.body);\n}\n\n// 入金\nconst depId = ref(\"ACC-0001\"), depAmount = ref(10000),\n      depKey = ref(\"dep-key-001\"), depCurrency = ref(\"JPY\"), depRef = ref(\"TEST-DEP-001\");\nconst depRes = ref(\"\"); const depStatus = ref\u003cnumber | null\u003e(null);\nasync function deposit() {\n  const r = await requestApi({\n    method: \"POST\",\n    path: `/api/v1/accounts/${depId.value}/deposit`,\n    token: token.value,\n    idempotencyKey: depKey.value,\n    body: { amount: depAmount.value, currency: depCurrency.value, reference: depRef.value },\n  });\n  depStatus.value = r.status; depRes.value = fmt(r.body);\n}\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003csection class=\"section-card\"\u003e\n    \u003ch2\u003e口座一覧取得\u003c/h2\u003e\n    \u003cbutton @click=\"getAccounts\"\u003eGET /api/v1/accounts\u003c/button\u003e\n    \u003cpre v-if=\"listStatus !== null\"\u003eHTTP {{ listStatus }}\\n{{ listRes }}\u003c/pre\u003e\n  \u003c/section\u003e\n\n  \u003csection class=\"section-card\"\u003e\n    \u003ch2\u003e入金\u003c/h2\u003e\n    \u003clabel\u003eaccountId\u003cinput v-model=\"depId\" /\u003e\u003c/label\u003e\n    \u003clabel\u003eamount\u003cinput v-model.number=\"depAmount\" type=\"number\" /\u003e\u003c/label\u003e\n    \u003clabel\u003eIdempotency-Key\u003cinput v-model=\"depKey\" /\u003e\u003c/label\u003e\n    \u003cbutton @click=\"deposit\"\u003ePOST /api/v1/accounts/{id}/deposit\u003c/button\u003e\n    \u003cpre v-if=\"depStatus !== null\"\u003eHTTP {{ depStatus }}\\n{{ depRes }}\u003c/pre\u003e\n  \u003c/section\u003e\n\u003c/template\u003e\n```\n\n**特徴**:\n- `ref()` で**リアクティブ変数**を宣言、`v-model` で双方向バインディング\n- `inject\u003cRef\u003cstring\u003e\u003e(\"token\")` で親から共有トークンを取得\n- `v-if` でレスポンス表示を条件分岐、`{{ }}` で変数を埋め込み\n\n### React 版\n\n```tsx\n// App.tsx の AccountsPage 関数（抜粋）\nfunction AccountsPage({ token }: PageProps) {\n  const [listResponse, setListResponse] = useState\u003cApiResult | null\u003e(null);\n  const [depositResponse, setDepositResponse] = useState\u003cApiResult | null\u003e(null);\n\n  const [depositAccountId, setDepositAccountId] = useState(\"ACC-0001\");\n  const [depositAmount, setDepositAmount] = useState(\"10000\");\n  const [depositKey, setDepositKey] = useState(\"dep-key-001\");\n  const [depositCurrency, setDepositCurrency] = useState(\"JPY\");\n  const [depositReference, setDepositReference] = useState(\"TEST-DEP-001\");\n\n  return (\n    \u003cdiv className=\"page-grid\"\u003e\n      \u003cSection title=\"口座一覧取得\"\u003e\n        \u003cbutton\n          onClick={async () =\u003e\n            setListResponse(await requestApi({ method: \"GET\", path: \"/api/v1/accounts\", token }))\n          }\n        \u003e\n          GET /api/v1/accounts\n        \u003c/button\u003e\n        \u003cResponsePanel response={listResponse} /\u003e\n      \u003c/Section\u003e\n\n      \u003cSection title=\"入金\"\u003e\n        \u003clabel\u003eaccountId\n          \u003cinput value={depositAccountId} onChange={e =\u003e setDepositAccountId(e.target.value)} /\u003e\n        \u003c/label\u003e\n        \u003clabel\u003eamount\n          \u003cinput type=\"number\" value={depositAmount} onChange={e =\u003e setDepositAmount(e.target.value)} /\u003e\n        \u003c/label\u003e\n        \u003clabel\u003eIdempotency-Key\n          \u003cinput value={depositKey} onChange={e =\u003e setDepositKey(e.target.value)} /\u003e\n        \u003c/label\u003e\n        \u003cbutton\n          onClick={async () =\u003e\n            setDepositResponse(await requestApi({\n              method: \"POST\",\n              path: `/api/v1/accounts/${depositAccountId}/deposit`,\n              token,\n              idempotencyKey: depositKey,\n              body: {\n                amount: Number(depositAmount),\n                currency: depositCurrency,\n                reference: depositReference,\n              },\n            }))\n          }\n        \u003e\n          POST /api/v1/accounts/{\"{id}\"}/deposit\n        \u003c/button\u003e\n        \u003cResponsePanel response={depositResponse} /\u003e\n      \u003c/Section\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n**特徴**:\n- 各入力フィールド・各レスポンスごとに `useState` で状態を宣言（**Vue の `ref` より宣言量が多い**）\n- `onChange={e =\u003e setX(e.target.value)}` のイベントハンドラを毎回書く必要（Vue の `v-model` のような糖衣構文がない）\n- `JSX` 内に直接ボタンの async ハンドラを書ける\n\n### Thymeleaf 版\n\n3層構造: テンプレート + コントローラ + サービス + フォームオブジェクト。\n\n```html\n\u003c!-- accounts.html (口座一覧と入金部分の抜粋) --\u003e\n\u003csection class=\"section-card\"\u003e\n  \u003ch2\u003e口座一覧取得（サーバーサイドForm）\u003c/h2\u003e\n  \u003cform id=\"accounts-form\" th:action=\"@{/api/v1/accounts}\" th:object=\"${accountsForm}\" method=\"post\"\u003e\n    \u003cinput type=\"hidden\" th:field=\"*{authorization}\" /\u003e\n    \u003cbutton type=\"submit\" class=\"action-button\"\u003ePOST /api/v1/accounts\u003c/button\u003e\n  \u003c/form\u003e\n  \u003cdiv th:if=\"${accountsForm != null and accountsForm.apiResponse != null}\" class=\"response-box\"\u003e\n    \u003cp\u003e\u003cstrong\u003eStatus:\u003c/strong\u003e \u003cspan th:text=\"${accountsForm.apiResponse.statusCode}\"\u003e0\u003c/span\u003e\u003c/p\u003e\n    \u003cpre th:text=\"${accountsForm.apiResponse.body}\"\u003e\u003c/pre\u003e\n  \u003c/div\u003e\n\u003c/section\u003e\n\n\u003csection class=\"section-card\"\u003e\n  \u003ch2\u003e入金\u003c/h2\u003e\n  \u003cform th:action=\"@{/api/v1/accounts/deposit}\" th:object=\"${accountsForm}\" method=\"post\"\u003e\n    \u003cinput type=\"hidden\" th:field=\"*{authorization}\" /\u003e\n    \u003clabel\u003eaccountId\u003cinput th:field=\"*{depositAccountId}\" /\u003e\u003c/label\u003e\n    \u003clabel\u003eamount\u003cinput th:field=\"*{depositAmount}\" type=\"number\" /\u003e\u003c/label\u003e\n    \u003clabel\u003eIdempotency-Key\u003cinput th:field=\"*{depositIdempotencyKey}\" /\u003e\u003c/label\u003e\n    \u003cbutton type=\"submit\" class=\"action-button\"\u003ePOST /api/v1/accounts/deposit\u003c/button\u003e\n  \u003c/form\u003e\n\u003c/section\u003e\n```\n\n```java\n// AccountsController.java\n@Controller\n@RequiredArgsConstructor\npublic class AccountsController {\n    private final AccountsService accountsService;\n\n    @PostMapping(\"/api/v1/accounts\")\n    public String listAccounts(\n            @ModelAttribute(\"accountsForm\") AccountsForm form,\n            Model model) {\n        applyToken(form.getAuthorization());\n        form.setApiResponse(accountsService.listAccounts());\n        model.addAttribute(\"accountsForm\", form);\n        return \"accounts\";  // accounts.html を再レンダリング\n    }\n\n    @PostMapping(\"/api/v1/accounts/deposit\")\n    public String deposit(\n            @ModelAttribute(\"accountsForm\") AccountsForm form,\n            Model model) {\n        applyToken(form.getAuthorization());\n        Map\u003cString, Object\u003e body = Map.of(\n            \"amount\", form.getDepositAmount(),\n            \"currency\", form.getDepositCurrency(),\n            \"reference\", form.getDepositReference()\n        );\n        form.setApiResponse(accountsService.deposit(\n            form.getDepositAccountId(), body, form.getDepositIdempotencyKey()));\n        model.addAttribute(\"accountsForm\", form);\n        return \"accounts\";\n    }\n}\n```\n\n```java\n// AccountsForm.java (抜粋)\n@Data\npublic class AccountsForm {\n    private String authorization;\n    private String depositAccountId;\n    private Integer depositAmount;\n    private String depositCurrency;\n    private String depositReference;\n    private String depositIdempotencyKey;\n    private ExternalApiResponse apiResponse;\n    // ... (他フィールド)\n}\n```\n\n**特徴**:\n- 各操作で **HTTP POST → サーバー処理 → ページ再描画** という従来の Web フロー\n- `th:field=\"*{depositAmount}\"` で Form Object のフィールドと input を双方向バインド（Spring が自動で値を受け渡し）\n- 結果表示も**サーバーサイドで HTML 化**してレンダリング → クライアント側 JS は最小\n\n### コード行数の比較（口座ページ 全体）\n\n| | コード行数 | 言語 / ファイル |\n|---|---|---|\n| Vanilla HTML | 約 130 行 | HTML + JS（1ファイル） |\n| Vue | 約 130 行 | TypeScript + Template（1ファイル） |\n| React | 約 350 行 | TypeScript JSX（1関数） |\n| Thymeleaf | 約 250 行 | HTML + Java（5ファイルに分散） |\n\n\u003e React が長い理由: 各入力フィールドの `useState` 宣言と `onChange` ハンドラ、各ボタンの async コールバックを **個別に書く必要** があるため。Vue の `v-model` のような糖衣構文がない分、コード量が増える傾向。\n\n---\n\n## 6. API クライアントの違い\n\nすべて同じバックエンド API（`/api/v1/accounts` 等）を叩きますが、クライアント実装は微妙に異なります。\n\n### Vanilla HTML / Vue / React — JavaScript の `fetch`\n\n```typescript\n// Vue 版 client.ts\nexport interface ApiResult {\n  status: number;\n  body: unknown;\n}\n\nexport async function requestApi(opts: {\n  method: string;\n  path: string;\n  token: string;\n  idempotencyKey?: string;\n  body?: unknown;\n}): Promise\u003cApiResult\u003e {\n  const headers: Record\u003cstring, string\u003e = {\n    \"Content-Type\": \"application/json\",\n    Authorization: `Bearer ${opts.token}`,\n  };\n  if (opts.idempotencyKey) {\n    headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n  }\n\n  try {\n    const res = await fetch(opts.path, {\n      method: opts.method,\n      headers,\n      body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,\n    });\n    const text = await res.text();\n    const responseBody = text ? JSON.parse(text) : null;\n    return { status: res.status, body: responseBody };\n  } catch (e) {\n    return { status: 0, body: String(e) };\n  }\n}\n```\n\nVanilla / Vue / React で **ほぼ同一**。違いは型注釈の有無くらい（Vanilla は `.js`、Vue / React は `.ts`）。\n\n### Thymeleaf — Spring Boot の `RestClient`\n\n```java\n// AccountsService.java\n@Service\n@RequiredArgsConstructor\npublic class AccountsService {\n    private final RestClient restClient;\n    private String token;\n\n    public ExternalApiResponse listAccounts() {\n        ResponseEntity\u003cString\u003e response = restClient\n            .get()\n            .uri(\"/api/v1/accounts\")\n            .header(\"Authorization\", \"Bearer \" + token)\n            .retrieve()\n            .toEntity(String.class);\n\n        return new ExternalApiResponse(\n            response.getStatusCode().value(),\n            true,\n            response.getBody()\n        );\n    }\n}\n```\n\nJava の `RestClient`（Spring 6.1+）でビルダー記法。`fetch` と比べて型が厳密で IDE 補完が効きやすい。\n\n### 共通点と相違点\n\n| 観点 | Vanilla/Vue/React | Thymeleaf |\n|---|---|---|\n| 言語 | JavaScript / TypeScript | Java |\n| HTTP クライアント | `fetch`（標準） | `RestClient`（Spring 標準） |\n| エラー処理 | try/catch + status code | try/catch + HttpClientErrorException |\n| 型安全性 | TypeScript で型注釈 | Java で型安全 (デフォルト) |\n| ネットワーク発生場所 | ブラウザ → API | サーバー → API |\n\n最後の「ネットワーク発生場所」が **アーキテクチャ上一番重要な違い** です。\n- SPA系（Vanilla/Vue/React）はブラウザから API を直接叩く → CORS 設定が必要 / API 認証情報がクライアントに渡る\n- Thymeleaf はサーバーが API を叩く → CORS 不要 / 認証情報がサーバー内に閉じる\n\n---\n\n## 7. 状態管理・データバインディングの違い\n\n「画面上の入力値・取得したレスポンス・トークン」をどう保持するかが各スタックの個性が出る部分。\n\n### Vanilla HTML — DOM が状態の源\n\n```javascript\n// 入力値: いつでも DOM から取り出す\nconst accountId = document.getElementById(\"balance-account-id\").value;\n\n// レスポンス表示: innerHTML で文字列を直接書き込む\ndocument.getElementById(\"balance-response\").innerHTML = formatResponse(result);\n\n// トークン: localStorage に保存\nlocalStorage.setItem(\"banklink_token\", token);\n```\n\n**「状態」は概念がない**。常に DOM か localStorage の値を読みに行く。シンプルだがアプリが大きくなると同期がしんどい。\n\n### Vue — `ref` でリアクティブ宣言\n\n```typescript\nconst balanceId = ref(\"ACC-0001\");    // 文字列の値を持つリアクティブ変数\nconst balanceRes = ref(\"\");           // テンプレートで {{ balanceRes }} と書くと自動更新\n\n// テンプレート側で \u003cinput v-model=\"balanceId\" /\u003e と書けば\n// ユーザー入力が balanceId.value に自動反映される(双方向バインディング)\n```\n\n**「リアクティブ変数を宣言 → テンプレートが自動追従」** のモデル。書き手が状態同期を意識しなくていい。\n\n### React — `useState` で状態フックを宣言\n\n```tsx\nconst [balanceId, setBalanceId] = useState(\"ACC-0001\");\nconst [balanceRes, setBalanceRes] = useState\u003cApiResult | null\u003e(null);\n\n// テンプレート側: 値とハンドラを別々に渡す\n\u003cinput value={balanceId} onChange={e =\u003e setBalanceId(e.target.value)} /\u003e\n```\n\n**「変数と setter のペアを宣言 → setter 経由で更新 → 再レンダリング」** のモデル。Vue の `v-model` のような糖衣構文がないので、入力フィールドごとに `onChange` を書く必要があり**コード量が増える**。\n\n### Thymeleaf — Form Object でサーバー側に状態を集約\n\n```java\n@Data\npublic class AccountsForm {\n    private String authorization;\n    private String balanceAccountId;\n    private ExternalApiResponse apiResponse;\n    // ... 他フィールド\n}\n```\n\n```html\n\u003cinput th:field=\"*{balanceAccountId}\" /\u003e\n```\n\n**サーバー側の Form Object が「状態の正本」**。POST のたびに input の値が Form Object に詰められ、Controller が処理 → 結果を Form Object に詰めて再描画。「クライアント側に状態を持たない」古典的な Web モデル。\n\n### 場面ごとの向き不向き\n\n| アプリの性質 | 向くスタック |\n|---|---|\n| 小規模・1画面・状態がほぼない | **Vanilla HTML** |\n| リアクティブ UI 多用、フォーム多い | **Vue** |\n| コンポーネント再利用が多い、エコシステム重視 | **React** |\n| 状態をサーバー側に持ちたい（業務システム的） | **Thymeleaf** |\n\n---\n\n## 8. フォーム処理の違い\n\n入金フォーム（accountId / amount / currency / reference / Idempotency-Key の5フィールド）の実装方法。\n\n### Vanilla HTML\n\n```javascript\n// 各 input に id を付ける\n\u003cinput id=\"deposit-account-id\" value=\"ACC-0001\" /\u003e\n\n// クリック時にまとめて値を読み取る\nconst body = {\n  accountId: document.getElementById(\"deposit-account-id\").value,\n  amount: Number(document.getElementById(\"deposit-amount\").value),\n  // ...\n};\n```\n\nヘルパ関数 `value()` を書けばこの繰り返しは減らせるが、**「フォーム」という概念は HTML/JS 側にしかない**。\n\n### Vue — `v-model` で5行で済む\n\n```vue\n\u003clabel\u003eaccountId\u003cinput v-model=\"depId\" /\u003e\u003c/label\u003e\n\u003clabel\u003eamount\u003cinput v-model.number=\"depAmount\" type=\"number\" /\u003e\u003c/label\u003e\n\u003clabel\u003ecurrency\u003cinput v-model=\"depCurrency\" /\u003e\u003c/label\u003e\n\u003clabel\u003ereference\u003cinput v-model=\"depRef\" /\u003e\u003c/label\u003e\n\u003clabel\u003eIdempotency-Key\u003cinput v-model=\"depKey\" /\u003e\u003c/label\u003e\n```\n\n`v-model.number` で数値型に自動変換。**書く量が最も少ない**。\n\n### React — フィールドごとに useState + onChange\n\n```tsx\nconst [depositAccountId, setDepositAccountId] = useState(\"ACC-0001\");\nconst [depositAmount, setDepositAmount] = useState(\"10000\");\nconst [depositCurrency, setDepositCurrency] = useState(\"JPY\");\n// ... 各フィールド分\n\n\u003cinput value={depositAccountId} onChange={e =\u003e setDepositAccountId(e.target.value)} /\u003e\n\u003cinput type=\"number\" value={depositAmount} onChange={e =\u003e setDepositAmount(e.target.value)} /\u003e\n// ...\n```\n\n5フィールド = `useState` 5回 + onChange 5回。`react-hook-form` のような外部ライブラリで省略できるが、本記事では「素の React」での比較。\n\n### Thymeleaf — `th:field` で Form Object と自動結合\n\n```html\n\u003cform th:action=\"@{/api/v1/accounts/deposit}\" th:object=\"${accountsForm}\" method=\"post\"\u003e\n  \u003cinput type=\"hidden\" th:field=\"*{authorization}\" /\u003e\n  \u003clabel\u003eaccountId\u003cinput th:field=\"*{depositAccountId}\" /\u003e\u003c/label\u003e\n  \u003clabel\u003eamount\u003cinput th:field=\"*{depositAmount}\" type=\"number\" /\u003e\u003c/label\u003e\n  \u003clabel\u003eIdempotency-Key\u003cinput th:field=\"*{depositIdempotencyKey}\" /\u003e\u003c/label\u003e\n  \u003cbutton type=\"submit\"\u003e送信\u003c/button\u003e\n\u003c/form\u003e\n```\n\n`th:field` 1つで「name属性・id属性・value属性」を自動設定し、サーバー側の Form Object とフィールドが結びつく。Vue の `v-model` に近い書き心地。\n\n---\n\n## 9. エラー・ローディング表示の違い\n\nAPI 失敗時の表示パターン。\n\n### Vanilla HTML\n\n```javascript\nfunction renderResponse(targetId, response) {\n  const target = document.getElementById(targetId);\n  const badgeClass = response.status \u003e= 200 \u0026\u0026 response.status \u003c 300 ? \"ok\" : \"err\";\n  target.innerHTML = `\n    \u003cdiv class=\"response-panel\"\u003e\n      \u003cspan class=\"badge ${badgeClass}\"\u003eHTTP ${response.status}\u003c/span\u003e\n      \u003cpre\u003e${escapeHtml(JSON.stringify(response.body, null, 2))}\u003c/pre\u003e\n    \u003c/div\u003e\n  `;\n}\n```\n\n**手動でエスケープが必要**。`escapeHtml` 関数を自前で用意するか、`textContent` を使う。\n\n### Vue / React — 自動エスケープ + 条件レンダリング\n\n```vue\n\u003c!-- Vue --\u003e\n\u003cdiv v-if=\"status !== null\" class=\"response-panel\"\u003e\n  \u003cspan :class=\"status \u003c 400 ? 'badge-ok' : 'badge-err'\"\u003eHTTP {{ status }}\u003c/span\u003e\n  \u003cpre\u003e{{ response }}\u003c/pre\u003e\n\u003c/div\u003e\n```\n\n```tsx\n// React\n{response \u0026\u0026 (\n  \u003cdiv className=\"response-panel\"\u003e\n    \u003cspan className={response.status \u003c 400 ? \"badge-ok\" : \"badge-err\"}\u003eHTTP {response.status}\u003c/span\u003e\n    \u003cpre\u003e{JSON.stringify(response.body, null, 2)}\u003c/pre\u003e\n  \u003c/div\u003e\n)}\n```\n\n`{{ }}` や `{}` で値を埋め込むと**自動エスケープされる**。XSS 対策を意識する必要がない。\n\n### Thymeleaf — `th:text` で自動エスケープ\n\n```html\n\u003cdiv th:if=\"${accountsForm.apiResponse != null}\" class=\"response-box\"\u003e\n  \u003cp\u003eStatus: \u003cspan th:text=\"${accountsForm.apiResponse.statusCode}\"\u003e\u003c/span\u003e\u003c/p\u003e\n  \u003cpre th:text=\"${accountsForm.apiResponse.body}\"\u003e\u003c/pre\u003e\n\u003c/div\u003e\n```\n\n`th:text` も自動エスケープ。`th:utext` を使うと unescape（XSS リスクあり）。\n\n---\n\n## 10. 起動・デプロイ構成の違い\n\n### Vanilla HTML — nginx で静的配信\n\n```nginx\n# nginx-external.conf\nserver {\n  listen 8080;\n  root /app/banklink-external-web-vanilla-html;\n  index index.html;\n\n  location /api/ {\n    proxy_pass http://banklink-api:8080;  # API へリバプロ\n  }\n}\n```\n\n```dockerfile\nFROM nginx:alpine\nCOPY banklink-web-vanilla-html /app/\nCOPY nginx-external.conf /etc/nginx/conf.d/default.conf\n```\n\n最小構成。HTML/JS/CSS をそのまま配信。\n\n### Vue / React — Vite ビルド → nginx 配信\n\n```dockerfile\n# build stage\nFROM node:20-alpine AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build   # dist/ を生成\n\n# serve stage\nFROM nginx:alpine\nCOPY --from=builder /app/dist /usr/share/nginx/html\nCOPY nginx-external.conf /etc/nginx/conf.d/default.conf\n```\n\nビルド成果物を nginx に配置。Vanilla との違いはビルドステージが追加されるだけ。\n\n### Thymeleaf — Spring Boot 実行可能 jar\n\n```dockerfile\nFROM eclipse-temurin:21-jdk-alpine AS builder\nWORKDIR /app\nCOPY pom.xml .\nCOPY src ./src\nRUN mvn clean package -DskipTests\n\nFROM eclipse-temurin:21-jre-alpine\nCOPY --from=builder /app/target/*.jar app.jar\nENTRYPOINT [\"java\", \"-jar\", \"/app.jar\"]\n```\n\nJVM 必須。コンテナサイズも数十 MB ＋ JVM 分（200 MB前後）。\n\n### イメージサイズ比較\n\n| | コンテナイメージサイズ | 起動時間 |\n|---|---|---|\n| Vanilla HTML (nginx) | 〜25 MB | \u003c 1秒 |\n| Vue (nginx 配信) | 〜30 MB | \u003c 1秒 |\n| React (nginx 配信) | 〜30 MB | \u003c 1秒 |\n| Thymeleaf (Spring Boot + JVM) | 〜200 MB | 5〜15秒 |\n\n軽量化を優先するなら nginx 配信系（前者3つ）が有利。Spring Boot は重い分、サーバーサイドロジック・API プロキシ・認証統合などを **同一プロセスで扱える**強みがある。\n\n---\n\n## 11. 横並び比較表\n\n| 観点 | Vanilla HTML | Vue 3 | React | Thymeleaf |\n|---|---|---|---|---|\n| **言語** | JS | TS | TS (TSX) | Java |\n| **ビルド** | 不要 | Vite | Vite | Maven |\n| **学習コスト** | 低 | 中 | 中〜高 | 中（Java 既習なら低） |\n| **コード行数（口座ページ）** | 約130行 | 約130行 | 約350行 | 約250行 (分散) |\n| **状態管理モデル** | DOM | リアクティブ ref | useState | Form Object (server) |\n| **フォーム結合** | 手動 | `v-model` | onChange 個別 | `th:field` |\n| **XSS自動エスケープ** | 手動 | 自動 | 自動 | 自動 |\n| **API呼び出し場所** | ブラウザ | ブラウザ | ブラウザ | サーバー |\n| **認証情報の持ち場所** | localStorage | localStorage | localStorage | サーバー Session |\n| **CORS 必要** | はい | はい | はい | いいえ |\n| **依存パッケージ数** | 0 | 〜10 | 〜10 | 〜20 (Maven) |\n| **コンテナイメージ** | 〜25 MB | 〜30 MB | 〜30 MB | 〜200 MB |\n| **起動時間** | 即時 | 即時 | 即時 | 5〜15秒 (JVM) |\n| **動的UI** | 弱い | 強い | 強い | 弱い (リロード前提) |\n| **エコシステム** | なし | 中規模 | 巨大 | Spring エコシステム |\n\n---\n\n## 12. どう選ぶか — 4スタックの強み弱みと判断軸\n\n### Vanilla HTML\n\n**強み**:\n- ゼロ依存・ゼロビルド・ゼロ学習コスト（HTML/JS 基礎のみ）\n- 配信コスト最小・コンテナイメージ最小\n- 「30分で動くデモを作る」用途に最強\n\n**弱み**:\n- アプリが大きくなると状態管理が破綻する（DOM 直接操作の限界）\n- TypeScript 型安全性なし\n- リアクティブ UI が苦手\n\n**選ぶ場面**:\n- API 動作確認ツール、社内検証用フォーム、簡易ダッシュボード\n- 「フレームワーク要らない、画面だけほしい」\n- 後で SPA に置き換える前提のプロトタイプ\n\n### Vue\n\n**強み**:\n- `v-model` などの糖衣構文で**コード量が React より少ない**\n- テンプレート構文が HTML に近く、初心者にも読みやすい\n- 単一ファイルコンポーネント (.vue) でロジック/テンプレート/スタイルが1ファイルに収まる\n\n**弱み**:\n- React に比べてエコシステムが小さい\n- 採用案件・採用人材の数で React に劣る\n\n**選ぶ場面**:\n- 業務系 SPA でフォーム・入力 UI が多い\n- 「フレームワーク選定で迷ったら Vue から試す」\n- Web エンジニア人材市場が日本国内中心の案件\n\n### React\n\n**強み**:\n- 巨大なエコシステム（UI ライブラリ・状態管理・テスト・モバイル）\n- TypeScript との相性が良い（型定義が充実）\n- 採用市場で最も求人が多い\n\n**弱み**:\n- 同じ機能を書くのに Vue より行数が増える傾向\n- `useState`, `useEffect`, `useMemo`, `useCallback` の使い分けが学習コスト\n- 関数コンポーネント再レンダリング理解が必須\n\n**選ぶ場面**:\n- 大規模 SPA、Next.js / React Native との接続を見据える\n- エンジニア採用を重視する組織\n- UI ライブラリ(MUI / Mantine / shadcn など)を流用したい\n\n### Thymeleaf\n\n**強み**:\n- **クライアント側 JS を最小に抑えられる**（業務システム的）\n- 認証情報・API トークンが**サーバー内に閉じる**（セキュリティ要件が厳しい場面で有利）\n- Spring Security / Spring Boot のエコシステムをフル活用\n- Java エンジニアが既存スキルで Web UI を作れる\n\n**弱み**:\n- ページ遷移ごとにサーバーラウンドトリップが必要（SPA に比べてもっさり）\n- リアクティブな UI に向かない\n- JVM のコンテナイメージが重い\n\n**選ぶ場面**:\n- **業務システム・管理画面（社内向け）**\n- セキュリティ要件で API キーをクライアントに露出させたくない\n- Java エンジニアが多い組織、Spring Boot を既に採用済み\n- リッチな UI より「フォーム送信して結果表示」程度で十分\n\n### 判断フロー\n\n```\nQ1. UI のリアクティブ性は必要か?\n   ├─ No (フォーム送信して結果表示で十分)\n   │   ├─ サーバー側集約したい → Thymeleaf\n   │   └─ 軽量に作りたい → Vanilla HTML\n   └─ Yes (リアクティブ UI が必要)\n       ├─ エコシステムや採用重視 → React\n       └─ コード量を抑えたい / 学習コスト軽 → Vue\n```\n\n---\n\n## 13. まとめ・学び\n\n- 4スタックは「**正解 vs 不正解**」ではなく、**プロジェクト要件によって有利不利が変わる**。同じ仕様を並べると相違が浮かび上がる。\n- **コード行数だけ見ると Vanilla / Vue が少なく、React が多い**。ただし React は採用市場とエコシステムで補って余りある。\n- **Thymeleaf は時代遅れではない** — 業務システムのセキュリティ・運用要件には今でも合うことが多い。\n- **「フレームワークの選定は要件から逆算する」** が結論。流行で選ぶと数年後に苦労する。\n- 4実装を並行運用してみて分かったのは、**ビルドツール（Vite vs Maven vs なし）の違いがプロジェクト立ち上げ初日の体験を大きく分ける**ということ。プロトタイプ段階では Vanilla / Vite 系が圧倒的に速い。\n\n選定の際にこの比較が判断材料の1つになれば幸いです。\n\n---\n\n🔗 **個人ブログに同様の記事と関連記事も書いています**: [同じ業務 Web UI を Vanilla HTML / Vue / React / Thymeleaf で実装して比較した — 4スタックの違いと選定指針](https://mint041223techblog.netlify.app/blog/banklink-web-stacks-comparison/)\n","coediting":false,"comments_count":1,"created_at":"2026-05-30T03:49:02+09:00","group":null,"id":"6d560e5b87dd54136fe3","likes_count":11,"private":false,"reactions_count":0,"stocks_count":10,"tags":[{"name":"HTML","versions":[]},{"name":"Thymeleaf","versions":[]},{"name":"Vue.js","versions":[]},{"name":"React","versions":[]},{"name":"SpringBoot","versions":[]}],"title":"同じ業務 Web UI を Vanilla HTML / Vue / React / Thymeleaf で実装して比較した — 4スタックの違いと選定指針","updated_at":"2026-05-30T03:49:02+09:00","url":"https://qiita.com/y104autumn/items/6d560e5b87dd54136fe3","user":{"description":"約15年の開発経験を礎に、システムが形になり動くまでの全体像を重視した発信を目標としています。この業界の仕事に携わり始めた頃の情熱を胸に、論理的裏付けを持って皆さんと対話し、質の高いものづくりを追求したいです。知見の発信を通じ、技術で切磋琢磨できる関係を築ければと思います。「いいね」やコメントをいただけると大きな励みになります。","facebook_id":"","followees_count":38,"followers_count":3,"github_login_name":"y104autumn","id":"y104autumn","items_count":25,"linkedin_id":"","location":"東京都","name":"横塚 敏明","organization":"株式会社エンジョイ","permanent_id":4404749,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4404749/profile-images/1776747921","team_only":false,"twitter_screen_name":null,"website_url":"https://mint041223techblog.netlify.app/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:171\"\u003eコーヒーを飲むたびに「これどこ産だっけ？」となるので、産地を地図で調べられるWebサービスを作りました。ベータ版です。\u003c/p\u003e\n\u003cp data-sourcepos=\"3:1-4:46\"\u003e\u003cstrong\u003eBeanAtlas\u003c/strong\u003e: \u003ca href=\"https://beanatlas.net\" class=\"autolink\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://beanatlas.net\u003c/a\u003e\u003cbr\u003e\n\u003cstrong\u003eGitHub\u003c/strong\u003e: \u003ca href=\"https://github.com/ct002/BeanAtlas\" class=\"autolink\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://github.com/ct002/BeanAtlas\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:134\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4435426%2Fb68556e5-ed00-4807-915c-1a113e711ada.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=37dfae387c4b5c581cb8fdd42e430ac6\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4435426%2Fb68556e5-ed00-4807-915c-1a113e711ada.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=37dfae387c4b5c581cb8fdd42e430ac6\" alt=\"beanatlas-preview.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4435426%2Fb68556e5-ed00-4807-915c-1a113e711ada.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=ee5a769f9e541c366db7dd7258123444 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4435426/b68556e5-ed00-4807-915c-1a113e711ada.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003chr data-sourcepos=\"8:1-9:0\"\u003e\n\u003ch2 data-sourcepos=\"10:1-10:21\"\u003e\n\u003cspan id=\"なぜ作ったか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C%E4%BD%9C%E3%81%A3%E3%81%9F%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜ作ったか\u003c/h2\u003e\n\u003col data-sourcepos=\"14:1-17:0\"\u003e\n\u003cli data-sourcepos=\"14:1-14:56\"\u003e\u003cstrong\u003e産地ごとの味の違いが覚えられない\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"15:1-15:96\"\u003e\n\u003cstrong\u003e情報が散在していて調べにくい\u003c/strong\u003e — まとめて確認できる場所がない\u003c/li\u003e\n\u003cli data-sourcepos=\"16:1-17:0\"\u003e地図でみれたら面白そう\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"18:1-18:174\"\u003e「地図で産地をクリックしたら、その国の豆の特徴がわかる」サービスがあれば使いやすいと思い、自分で作ることにしました。\u003c/p\u003e\n\u003chr data-sourcepos=\"20:1-21:0\"\u003e\n\u003ch2 data-sourcepos=\"22:1-22:15\"\u003e\n\u003cspan id=\"機能概要\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A9%9F%E8%83%BD%E6%A6%82%E8%A6%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e機能概要\u003c/h2\u003e\n\u003cul data-sourcepos=\"24:1-33:0\"\u003e\n\u003cli data-sourcepos=\"24:1-24:74\"\u003e\n\u003cstrong\u003e世界地図\u003c/strong\u003e上にコーヒー産地20カ国のマーカーを表示\u003c/li\u003e\n\u003cli data-sourcepos=\"25:1-29:19\"\u003eマーカーをクリックすると\u003cstrong\u003e左パネルがスライドイン\u003c/strong\u003eし、産地情報を表示\n\u003cul data-sourcepos=\"26:3-29:19\"\u003e\n\u003cli data-sourcepos=\"26:3-26:40\"\u003eフレーバーノート（タグ）\u003c/li\u003e\n\u003cli data-sourcepos=\"27:3-27:64\"\u003e酸味・苦味・甘味・コクのレーダーチャート\u003c/li\u003e\n\u003cli data-sourcepos=\"28:3-28:25\"\u003e品種・精製方法\u003c/li\u003e\n\u003cli data-sourcepos=\"29:3-29:19\"\u003e標高・気候\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"30:1-30:87\"\u003e\n\u003cstrong\u003e産地名検索\u003c/strong\u003e（日本語・英語対応）でインクリメンタルサーチ\u003c/li\u003e\n\u003cli data-sourcepos=\"31:1-31:48\"\u003e各産地の\u003cstrong\u003e詳細ページ\u003c/strong\u003e（SEO対応）\u003c/li\u003e\n\u003cli data-sourcepos=\"32:1-33:0\"\u003e\n\u003cstrong\u003e日英切り替え\u003c/strong\u003e対応\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"34:1-35:0\"\u003e\n\u003ch2 data-sourcepos=\"36:1-36:21\"\u003e\n\u003cspan id=\"技術スタック\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%8A%80%E8%A1%93%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e技術スタック\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"38:1-38:25\"\u003e\n\u003cspan id=\"フロントエンド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eフロントエンド\u003c/h3\u003e\n\u003ctable data-sourcepos=\"40:1-48:35\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"40:1-40:19\"\u003e\n\u003cth data-sourcepos=\"40:2-40:9\"\u003e役割\u003c/th\u003e\n\u003cth data-sourcepos=\"40:11-40:18\"\u003e技術\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"42:1-42:50\"\u003e\n\u003ctd data-sourcepos=\"42:2-42:24\"\u003eフレームワーク\u003c/td\u003e\n\u003ctd data-sourcepos=\"42:26-42:49\"\u003eVue3 (Composition API)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"43:1-43:23\"\u003e\n\u003ctd data-sourcepos=\"43:2-43:9\"\u003e言語\u003c/td\u003e\n\u003ctd data-sourcepos=\"43:11-43:22\"\u003eTypeScript\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"44:1-44:20\"\u003e\n\u003ctd data-sourcepos=\"44:2-44:12\"\u003eビルド\u003c/td\u003e\n\u003ctd data-sourcepos=\"44:14-44:19\"\u003eVite\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"45:1-45:37\"\u003e\n\u003ctd data-sourcepos=\"45:2-45:21\"\u003eスタイリング\u003c/td\u003e\n\u003ctd data-sourcepos=\"45:23-45:36\"\u003eTailwind CSS\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"46:1-46:27\"\u003e\n\u003ctd data-sourcepos=\"46:2-46:9\"\u003e地図\u003c/td\u003e\n\u003ctd data-sourcepos=\"46:11-46:26\"\u003eMapLibre GL JS\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"47:1-47:24\"\u003e\n\u003ctd data-sourcepos=\"47:2-47:15\"\u003e状態管理\u003c/td\u003e\n\u003ctd data-sourcepos=\"47:17-47:23\"\u003ePinia\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"48:1-48:35\"\u003e\n\u003ctd data-sourcepos=\"48:2-48:21\"\u003eルーティング\u003c/td\u003e\n\u003ctd data-sourcepos=\"48:23-48:34\"\u003eVue Router\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"50:1-50:22\"\u003e\n\u003cspan id=\"バックエンド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eバックエンド\u003c/h3\u003e\n\u003ctable data-sourcepos=\"52:1-59:31\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"52:1-52:19\"\u003e\n\u003cth data-sourcepos=\"52:2-52:9\"\u003e役割\u003c/th\u003e\n\u003cth data-sourcepos=\"52:11-52:18\"\u003e技術\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"54:1-54:38\"\u003e\n\u003ctd data-sourcepos=\"54:2-54:27\"\u003eAPIフレームワーク\u003c/td\u003e\n\u003ctd data-sourcepos=\"54:29-54:37\"\u003eFastAPI\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"55:1-55:24\"\u003e\n\u003ctd data-sourcepos=\"55:2-55:9\"\u003e言語\u003c/td\u003e\n\u003ctd data-sourcepos=\"55:11-55:23\"\u003ePython 3.12\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"56:1-56:24\"\u003e\n\u003ctd data-sourcepos=\"56:2-56:6\"\u003eORM\u003c/td\u003e\n\u003ctd data-sourcepos=\"56:8-56:23\"\u003eSQLAlchemy 2.0\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"57:1-57:39\"\u003e\n\u003ctd data-sourcepos=\"57:2-57:24\"\u003eバリデーション\u003c/td\u003e\n\u003ctd data-sourcepos=\"57:26-57:38\"\u003ePydantic v2\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"58:1-58:33\"\u003e\n\u003ctd data-sourcepos=\"58:2-58:23\"\u003eDB（ローカル）\u003c/td\u003e\n\u003ctd data-sourcepos=\"58:25-58:32\"\u003eSQLite\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"59:1-59:31\"\u003e\n\u003ctd data-sourcepos=\"59:2-59:17\"\u003eDB（本番）\u003c/td\u003e\n\u003ctd data-sourcepos=\"59:19-59:30\"\u003ePostgreSQL\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"61:1-61:16\"\u003e\n\u003cspan id=\"インフラ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eインフラ\u003c/h3\u003e\n\u003cp data-sourcepos=\"63:1-64:20\"\u003e個人で使用しているVPSにデプロイ。\u003cbr\u003e\nUbuntu24.04です。\u003c/p\u003e\n\u003chr data-sourcepos=\"66:1-67:0\"\u003e\n\u003ch2 data-sourcepos=\"68:1-68:24\"\u003e\n\u003cspan id=\"実装のポイント\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E8%A3%85%E3%81%AE%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実装のポイント\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"70:1-70:58\"\u003e\n\u003cspan id=\"1-maplibre-でカスタムマーカーを実装する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-maplibre-%E3%81%A7%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%9E%E3%83%BC%E3%82%AB%E3%83%BC%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. MapLibre でカスタムマーカーを実装する\u003c/h3\u003e\n\u003cp data-sourcepos=\"72:1-73:174\"\u003eMapLibreのデフォルトマーカーはデザインの自由度が低いため、DOM要素を渡すカスタムマーカーを使いました。\u003cbr\u003e\nヒット領域を広めの \u003ccode\u003ewrapper\u003c/code\u003e で確保しつつ、見た目の \u003ccode\u003edot\u003c/code\u003e を別要素にすることでホバーアニメーションをスムーズにしています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"75:1-102:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ewrapper\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ecreateElement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ediv\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"nx\"\u003ewrapper\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estyle\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecssText\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e`\n  width: 28px; height: 28px;\n  display: flex; align-items: center; justify-content: center;\n  cursor: pointer;\n`\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edot\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ecreateElement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ediv\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"nx\"\u003edot\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estyle\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecssText\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e`\n  width: 12px; height: 12px;\n  background: #4A7C59;\n  border: 2px solid white;\n  border-radius: 50%;\n  transition: background 0.15s, width 0.15s, height 0.15s;\n  pointer-events: none;\n`\u003c/span\u003e\n\n\u003cspan class=\"nx\"\u003ewrapper\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eaddEventListener\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003emouseenter\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003edot\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estyle\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ewidth\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e18px\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003edot\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estyle\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eheight\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e18px\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003edot\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estyle\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebackground\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e#C8813A\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003emarker\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nx\"\u003emaplibregl\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nc\"\u003eMarker\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"na\"\u003eelement\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003ewrapper\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003eanchor\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ecenter\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esetLngLat\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"nx\"\u003eorigin\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elongitude\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eorigin\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elatitude\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eaddTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"104:1-104:52\"\u003e\n\u003cspan id=\"2-ベースマップのラベルを制御する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%83%99%E3%83%BC%E3%82%B9%E3%83%9E%E3%83%83%E3%83%97%E3%81%AE%E3%83%A9%E3%83%99%E3%83%AB%E3%82%92%E5%88%B6%E5%BE%A1%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. ベースマップのラベルを制御する\u003c/h3\u003e\n\u003cp data-sourcepos=\"106:1-107:120\"\u003eOpenFreemapのタイルはデフォルトで多言語のラベルが表示されますが、\u003cbr\u003e\n視認性が悪かったので、ベースマップのラベルを非表示にして独自ラベルを重ねました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"109:1-148:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// OSM の place / poi ラベルを非表示\u003c/span\u003e\n\u003cspan class=\"k\"\u003efor \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003elayer\u003c/span\u003e \u003cspan class=\"k\"\u003eof\u003c/span\u003e \u003cspan class=\"nx\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetStyle\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nx\"\u003elayers\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003elayer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"o\"\u003e!==\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esymbol\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003econtinue\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003esourceLayer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003elayer\u003c/span\u003e \u003cspan class=\"kd\"\u003eas \u003c/span\u003e\u003cspan class=\"kr\"\u003eany\u003c/span\u003e\u003cspan class=\"p\"\u003e)[\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esource-layer\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esourceLayer\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eplace\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"nx\"\u003esourceLayer\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003epoi\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esetLayoutProperty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003elayer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003evisibility\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003enone\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 産地がある国だけ独自ラベルを GeoJSON で追加\u003c/span\u003e\n\u003cspan class=\"nx\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eaddSource\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eorigin-country-labels\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"na\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003egeojson\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eFeatureCollection\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003efeatures\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eorigins\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eo\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e({\u003c/span\u003e\n      \u003cspan class=\"na\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eFeature\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n      \u003cspan class=\"na\"\u003egeometry\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ePoint\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003ecoordinates\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elongitude\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elatitude\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eproperties\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecountry\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e})),\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\n\u003cspan class=\"nx\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eaddLayer\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eorigin-country-label\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esymbol\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003esource\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eorigin-country-labels\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003elayout\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etext-field\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eget\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ename\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etext-size\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003einterpolate\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003elinear\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ezoom\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e14\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etext-anchor\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etop\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etext-offset\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mf\"\u003e1.5\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"na\"\u003epaint\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etext-color\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e#1a3a5c\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etext-halo-color\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ergba(255,255,255,0.85)\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etext-halo-width\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mf\"\u003e1.5\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"150:1-150:67\"\u003e\n\u003cspan id=\"3-フレーバーレーダーチャートをsvgで自作する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E3%83%95%E3%83%AC%E3%83%BC%E3%83%90%E3%83%BC%E3%83%AC%E3%83%BC%E3%83%80%E3%83%BC%E3%83%81%E3%83%A3%E3%83%BC%E3%83%88%E3%82%92svg%E3%81%A7%E8%87%AA%E4%BD%9C%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. フレーバーレーダーチャートをSVGで自作する\u003c/h3\u003e\n\u003cp data-sourcepos=\"152:1-153:105\"\u003eChart.jsなどのライブラリを使わず、Vue3 + SVGだけでレーダーチャートを実装しました。\u003cbr\u003e\n依存を増やさないことが趣味です。デザインも自由にカスタマイズできます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"155:1-173:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ecx\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e80\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ecy\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e80\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eR\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e52\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eangle\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ereturn \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ePI\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"mi\"\u003e4\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ePI\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003epoint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eratio\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003enumber\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ea\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eangle\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003ecx\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003eratio\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003eR\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ecos\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"na\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003ecy\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003eratio\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nx\"\u003eR\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nb\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edataPolygon\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003ecomputed\u003c/span\u003e\u003cspan class=\"p\"\u003e(()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003eacidity\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ebitterness\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003esweetness\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e].\u003c/span\u003e\u003cspan class=\"nf\"\u003emap\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"nx\"\u003ev\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003epoint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ev\u003c/span\u003e \u003cspan class=\"o\"\u003e/\u003c/span\u003e \u003cspan class=\"mi\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s2\"\u003e`\u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e,\u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e`\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}).\u003c/span\u003e\u003cspan class=\"nf\"\u003ejoin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e \u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"175:1-182:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;svg\u003c/span\u003e \u003cspan class=\"na\"\u003eviewBox=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"-30 -28 220 216\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"c\"\u003e\u0026lt;!-- グリッドリング --\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;polygon\u003c/span\u003e \u003cspan class=\"na\"\u003ev-for=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"level in [1,2,3,4,5]\"\u003c/span\u003e \u003cspan class=\"na\"\u003e:points=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"polygonPoints(level / 5)\"\u003c/span\u003e \u003cspan class=\"err\"\u003e...\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"c\"\u003e\u0026lt;!-- データポリゴン --\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;polygon\u003c/span\u003e \u003cspan class=\"na\"\u003e:points=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"dataPolygon\"\u003c/span\u003e \u003cspan class=\"na\"\u003efill=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"#4A7C59\"\u003c/span\u003e \u003cspan class=\"na\"\u003efill-opacity=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"0.22\"\u003c/span\u003e \u003cspan class=\"na\"\u003estroke=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"#4A7C59\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/svg\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"184:1-184:63\"\u003e\n\u003cspan id=\"4-fastapi--sqlalchemy-でシンプルなrest-apiを構築\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-fastapi--sqlalchemy-%E3%81%A7%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AArest-api%E3%82%92%E6%A7%8B%E7%AF%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. FastAPI + SQLAlchemy でシンプルなREST APIを構築\u003c/h3\u003e\n\u003cp data-sourcepos=\"186:1-187:45\"\u003eエンドポイントは産地一覧と産地詳細の2本だけ。\u003cbr\u003e\nMVPなので認証なし・公開APIです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"189:1-203:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# routers/origins.py\n\u003c/span\u003e\u003cspan class=\"n\"\u003erouter\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eAPIRouter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprefix\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e/api/v1/origins\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etags\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eorigins\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\n\u003cspan class=\"nd\"\u003e@router.get\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse_model\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"nb\"\u003elist\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eOriginResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003elist_origins\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edb\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eSession\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eDepends\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eget_db\u003c/span\u003e\u003cspan class=\"p\"\u003e)):\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003edb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003equery\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrigin\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n\u003cspan class=\"nd\"\u003e@router.get\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e/{slug}\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse_model\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eOriginResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_origin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eslug\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003estr\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edb\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eSession\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eDepends\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eget_db\u003c/span\u003e\u003cspan class=\"p\"\u003e)):\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eorigin\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003equery\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrigin\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003efilter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrigin\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eslug\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eslug\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003efirst\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003eorigin\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"nc\"\u003eHTTPException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estatus_code\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e404\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edetail\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eOrigin not found\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eorigin\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"205:1-205:146\"\u003ePydantic v2のおかげで、DBのテキスト列（JSON配列として保存）をレスポンス時に自動でリストに変換できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"207:1-207:70\"\u003e\n\u003cspan id=\"5-スタイル読み込みとデータ取得を並行して待つ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%A8%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%96%E5%BE%97%E3%82%92%E4%B8%A6%E8%A1%8C%E3%81%97%E3%81%A6%E5%BE%85%E3%81%A4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. スタイル読み込みとデータ取得を並行して待つ\u003c/h3\u003e\n\u003cp data-sourcepos=\"209:1-209:155\"\u003eマップ初期化時、MapLibreのスタイル読み込みとAPIデータ取得は独立しているため \u003ccode\u003ePromise.all\u003c/code\u003e で並行実行しています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"211:1-220:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nb\"\u003ePromise\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eall\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003estore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eloadOrigins\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n  \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nb\"\u003ePromise\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003evoid\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eresolve\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nx\"\u003emap\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eon\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eload\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eresolve\u003c/span\u003e\u003cspan class=\"p\"\u003e)),\u003c/span\u003e\n\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 両方完了してからマーカーとラベルを追加\u003c/span\u003e\n\u003cspan class=\"nf\"\u003ecustomizeLabels\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"nf\"\u003eaddMarkers\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"222:1-223:0\"\u003e\n\u003ch2 data-sourcepos=\"224:1-224:21\"\u003e\n\u003cspan id=\"ハマったこと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eハマったこと\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"226:1-226:73\"\u003e\n\u003cspan id=\"マーカーのクリックとマップのクリックが競合する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%9E%E3%83%BC%E3%82%AB%E3%83%BC%E3%81%AE%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%81%A8%E3%83%9E%E3%83%83%E3%83%97%E3%81%AE%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%81%8C%E7%AB%B6%E5%90%88%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eマーカーのクリックとマップのクリックが競合する\u003c/h3\u003e\n\u003cp data-sourcepos=\"228:1-229:100\"\u003eマーカーのクリックイベントがマップまで伝播し、パネルが開いた直後に閉じてしまう問題がありました。\u003cbr\u003e\n\u003ccode\u003ee.stopPropagation()\u003c/code\u003e をマーカーのクリックハンドラに追加して解決しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"231:1-236:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003ewrapper\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eaddEventListener\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eclick\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estopPropagation\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// これがないとマップのクリックイベントも発火する\u003c/span\u003e\n  \u003cspan class=\"nf\"\u003eemit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eselect\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eorigin\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"238:1-238:69\"\u003e\n\u003cspan id=\"maplibreのラベル制御はスタイル読み込み後に行う\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#maplibre%E3%81%AE%E3%83%A9%E3%83%99%E3%83%AB%E5%88%B6%E5%BE%A1%E3%81%AF%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E5%BE%8C%E3%81%AB%E8%A1%8C%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eMapLibreのラベル制御はスタイル読み込み後に行う\u003c/h3\u003e\n\u003cp data-sourcepos=\"240:1-241:125\"\u003e\u003ccode\u003emap.on('load', ...)\u003c/code\u003e の前に \u003ccode\u003emap.getStyle().layers\u003c/code\u003e を参照するとエラーになります。\u003cbr\u003e\nスタイル読み込み完了後にのみラベル操作を実行する必要があります（\u003ccode\u003ePromise.all\u003c/code\u003e で担保）。\u003c/p\u003e\n\u003chr data-sourcepos=\"243:1-244:0\"\u003e\n\u003ch2 data-sourcepos=\"245:1-245:18\"\u003e\n\u003cspan id=\"データ設計\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%87%E3%83%BC%E3%82%BF%E8%A8%AD%E8%A8%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eデータ設計\u003c/h2\u003e\n\u003cp data-sourcepos=\"247:1-247:59\"\u003e主要20カ国のデータを手動で整備しました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"249:1-249:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"251:1-253:172\"\u003eMapLibreはOSSで無料のタイルプロバイダー（OpenFreemap）と組み合わせれば地図系アプリを完全無料で動かせます。\u003cbr\u003e\nVue3のComposition APIとの相性も良く、地図操作のロジックをコンポーネントにきれいに閉じ込めることができました。\u003cbr\u003e\nFastAPIはPydanticによる型安全な設計と自動生成されるOpenAPIドキュメントが開発体験として優れており、小規模なAPIには最適です。\u003c/p\u003e\n\u003chr data-sourcepos=\"255:1-255:3\"\u003e\n\u003cp data-sourcepos=\"256:1-256:117\"\u003eコーヒー好きな方、地図系アプリに興味ある方のフィードバックをお待ちしています！\u003c/p\u003e\n\u003cp data-sourcepos=\"258:1-258:36\"\u003e\u003cstrong\u003eBeanAtlas\u003c/strong\u003e: \u003ca href=\"https://beanatlas.net\" class=\"autolink\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://beanatlas.net\u003c/a\u003e\u003c/p\u003e\n","body":"コーヒーを飲むたびに「これどこ産だっけ？」となるので、産地を地図で調べられるWebサービスを作りました。ベータ版です。\n\n**BeanAtlas**: https://beanatlas.net\n**GitHub**: https://github.com/ct002/BeanAtlas\n\n![beanatlas-preview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4435426/b68556e5-ed00-4807-915c-1a113e711ada.png)\n\n---\n\n## なぜ作ったか\n\n\n\n1. **産地ごとの味の違いが覚えられない** \n2. **情報が散在していて調べにくい** — まとめて確認できる場所がない\n3. 地図でみれたら面白そう\n\n「地図で産地をクリックしたら、その国の豆の特徴がわかる」サービスがあれば使いやすいと思い、自分で作ることにしました。\n\n---\n\n## 機能概要\n\n- **世界地図**上にコーヒー産地20カ国のマーカーを表示\n- マーカーをクリックすると**左パネルがスライドイン**し、産地情報を表示\n  - フレーバーノート（タグ）\n  - 酸味・苦味・甘味・コクのレーダーチャート\n  - 品種・精製方法\n  - 標高・気候\n- **産地名検索**（日本語・英語対応）でインクリメンタルサーチ\n- 各産地の**詳細ページ**（SEO対応）\n- **日英切り替え**対応\n\n---\n\n## 技術スタック\n\n### フロントエンド\n\n| 役割 | 技術 |\n|------|------|\n| フレームワーク | Vue3 (Composition API) |\n| 言語 | TypeScript |\n| ビルド | Vite |\n| スタイリング | Tailwind CSS |\n| 地図 | MapLibre GL JS |\n| 状態管理 | Pinia |\n| ルーティング | Vue Router |\n\n### バックエンド\n\n| 役割 | 技術 |\n|------|------|\n| APIフレームワーク | FastAPI |\n| 言語 | Python 3.12 |\n| ORM | SQLAlchemy 2.0 |\n| バリデーション | Pydantic v2 |\n| DB（ローカル） | SQLite |\n| DB（本番） | PostgreSQL |\n\n### インフラ\n\n個人で使用しているVPSにデプロイ。\nUbuntu24.04です。\n\n---\n\n## 実装のポイント\n\n### 1. MapLibre でカスタムマーカーを実装する\n\nMapLibreのデフォルトマーカーはデザインの自由度が低いため、DOM要素を渡すカスタムマーカーを使いました。\nヒット領域を広めの `wrapper` で確保しつつ、見た目の `dot` を別要素にすることでホバーアニメーションをスムーズにしています。\n\n```typescript\nconst wrapper = document.createElement('div')\nwrapper.style.cssText = `\n  width: 28px; height: 28px;\n  display: flex; align-items: center; justify-content: center;\n  cursor: pointer;\n`\n\nconst dot = document.createElement('div')\ndot.style.cssText = `\n  width: 12px; height: 12px;\n  background: #4A7C59;\n  border: 2px solid white;\n  border-radius: 50%;\n  transition: background 0.15s, width 0.15s, height 0.15s;\n  pointer-events: none;\n`\n\nwrapper.addEventListener('mouseenter', () =\u003e {\n  dot.style.width = '18px'\n  dot.style.height = '18px'\n  dot.style.background = '#C8813A'\n})\n\nconst marker = new maplibregl.Marker({ element: wrapper, anchor: 'center' })\n  .setLngLat([origin.longitude, origin.latitude])\n  .addTo(map)\n```\n\n### 2. ベースマップのラベルを制御する\n\nOpenFreemapのタイルはデフォルトで多言語のラベルが表示されますが、\n視認性が悪かったので、ベースマップのラベルを非表示にして独自ラベルを重ねました。\n\n```typescript\n// OSM の place / poi ラベルを非表示\nfor (const layer of map.getStyle().layers) {\n  if (layer.type !== 'symbol') continue\n  const sourceLayer = (layer as any)['source-layer']\n  if (sourceLayer === 'place' || sourceLayer === 'poi') {\n    map.setLayoutProperty(layer.id, 'visibility', 'none')\n  }\n}\n\n// 産地がある国だけ独自ラベルを GeoJSON で追加\nmap.addSource('origin-country-labels', {\n  type: 'geojson',\n  data: {\n    type: 'FeatureCollection',\n    features: origins.map(o =\u003e ({\n      type: 'Feature',\n      geometry: { type: 'Point', coordinates: [o.longitude, o.latitude] },\n      properties: { name: o.country },\n    })),\n  },\n})\n\nmap.addLayer({\n  id: 'origin-country-label',\n  type: 'symbol',\n  source: 'origin-country-labels',\n  layout: {\n    'text-field': ['get', 'name'],\n    'text-size': ['interpolate', ['linear'], ['zoom'], 1, 10, 5, 14],\n    'text-anchor': 'top',\n    'text-offset': [0, 1.5],\n  },\n  paint: {\n    'text-color': '#1a3a5c',\n    'text-halo-color': 'rgba(255,255,255,0.85)',\n    'text-halo-width': 1.5,\n  },\n})\n```\n\n### 3. フレーバーレーダーチャートをSVGで自作する\n\nChart.jsなどのライブラリを使わず、Vue3 + SVGだけでレーダーチャートを実装しました。\n依存を増やさないことが趣味です。デザインも自由にカスタマイズできます。\n\n```typescript\nconst cx = 80, cy = 80, R = 52\n\nfunction angle(i: number) {\n  return (i * 2 * Math.PI) / 4 - Math.PI / 2\n}\n\nfunction point(i: number, ratio: number) {\n  const a = angle(i)\n  return { x: cx + ratio * R * Math.cos(a), y: cy + ratio * R * Math.sin(a) }\n}\n\nconst dataPolygon = computed(() =\u003e\n  [acidity, bitterness, sweetness, body].map((v, i) =\u003e {\n    const p = point(i, v / 5)\n    return `${p.x},${p.y}`\n  }).join(' ')\n)\n```\n\n```html\n\u003csvg viewBox=\"-30 -28 220 216\"\u003e\n  \u003c!-- グリッドリング --\u003e\n  \u003cpolygon v-for=\"level in [1,2,3,4,5]\" :points=\"polygonPoints(level / 5)\" ... /\u003e\n  \u003c!-- データポリゴン --\u003e\n  \u003cpolygon :points=\"dataPolygon\" fill=\"#4A7C59\" fill-opacity=\"0.22\" stroke=\"#4A7C59\" /\u003e\n\u003c/svg\u003e\n```\n\n### 4. FastAPI + SQLAlchemy でシンプルなREST APIを構築\n\nエンドポイントは産地一覧と産地詳細の2本だけ。\nMVPなので認証なし・公開APIです。\n\n```python\n# routers/origins.py\nrouter = APIRouter(prefix=\"/api/v1/origins\", tags=[\"origins\"])\n\n@router.get(\"\", response_model=list[OriginResponse])\ndef list_origins(db: Session = Depends(get_db)):\n    return db.query(Origin).all()\n\n@router.get(\"/{slug}\", response_model=OriginResponse)\ndef get_origin(slug: str, db: Session = Depends(get_db)):\n    origin = db.query(Origin).filter(Origin.slug == slug).first()\n    if not origin:\n        raise HTTPException(status_code=404, detail=\"Origin not found\")\n    return origin\n```\n\nPydantic v2のおかげで、DBのテキスト列（JSON配列として保存）をレスポンス時に自動でリストに変換できます。\n\n### 5. スタイル読み込みとデータ取得を並行して待つ\n\nマップ初期化時、MapLibreのスタイル読み込みとAPIデータ取得は独立しているため `Promise.all` で並行実行しています。\n\n```typescript\nawait Promise.all([\n  store.loadOrigins(),\n  new Promise\u003cvoid\u003e(resolve =\u003e map!.on('load', resolve)),\n])\n\n// 両方完了してからマーカーとラベルを追加\ncustomizeLabels()\naddMarkers()\n```\n\n---\n\n## ハマったこと\n\n### マーカーのクリックとマップのクリックが競合する\n\nマーカーのクリックイベントがマップまで伝播し、パネルが開いた直後に閉じてしまう問題がありました。\n`e.stopPropagation()` をマーカーのクリックハンドラに追加して解決しました。\n\n```typescript\nwrapper.addEventListener('click', (e) =\u003e {\n  e.stopPropagation()  // これがないとマップのクリックイベントも発火する\n  emit('select', origin)\n})\n```\n\n### MapLibreのラベル制御はスタイル読み込み後に行う\n\n`map.on('load', ...)` の前に `map.getStyle().layers` を参照するとエラーになります。\nスタイル読み込み完了後にのみラベル操作を実行する必要があります（`Promise.all` で担保）。\n\n---\n\n## データ設計\n\n主要20カ国のデータを手動で整備しました。\n\n## まとめ\n\nMapLibreはOSSで無料のタイルプロバイダー（OpenFreemap）と組み合わせれば地図系アプリを完全無料で動かせます。\nVue3のComposition APIとの相性も良く、地図操作のロジックをコンポーネントにきれいに閉じ込めることができました。\nFastAPIはPydanticによる型安全な設計と自動生成されるOpenAPIドキュメントが開発体験として優れており、小規模なAPIには最適です。\n\n---\nコーヒー好きな方、地図系アプリに興味ある方のフィードバックをお待ちしています！\n\n**BeanAtlas**: https://beanatlas.net\n","coediting":false,"comments_count":0,"created_at":"2026-05-29T22:53:55+09:00","group":null,"id":"675860e6c92fa9ed7278","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"PostgreSQL","versions":[]},{"name":"Vue.js","versions":[]},{"name":"個人開発","versions":[]},{"name":"FastAPI","versions":[]},{"name":"MapLibre","versions":[]}],"title":"MapLibre + Vue3 + FastAPIでコーヒー産地マップを作った【BeanAtlas】","updated_at":"2026-05-29T22:53:55+09:00","url":"https://qiita.com/civiltech/items/675860e6c92fa9ed7278","user":{"description":"個人開発しています。製造業で現役SE。\r\n総務・経理を経験後、工程管理の業務改善システムを、\r\nPlayframework(Scala) + Vue.js + MySQL + Ubuntu + Docker でアプリ開発し現在に至る。\r\nPythonでツール作ったり、WordPressサイト運営したりもしています。AIによる商用デザインの画像生成を挑戦中。","facebook_id":"","followees_count":0,"followers_count":0,"github_login_name":"civiltech-system","id":"civiltech","items_count":2,"linkedin_id":"","location":"大阪","name":"t i","organization":"有限会社シビルテック","permanent_id":4435426,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/4435426/a68b979e6329b35ffef28b2566818a71b4507e61/large.png?1779291234","team_only":false,"twitter_screen_name":null,"website_url":"https://civiltech.co.jp"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:23\"\u003e\n\u003cspan id=\"この記事の目的\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E7%9B%AE%E7%9A%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこの記事の目的\u003c/h1\u003e\n\u003cp data-sourcepos=\"3:1-4:94\"\u003eこの記事は、新人研修で使われていた「STEP1〜STEP13の手順に沿って\u003cbr\u003e\nToDoリストを作る教材」を、より分かりやすく解説し直したものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-8:111\"\u003e研修では、手順通りに進めればアプリ自体は完成しますが、\u003cbr\u003e\n「なぜこのコードを書くのか」「何が起きているのか」が分からないまま\u003cbr\u003e\n作業だけが進んでしまうことが多く、理解が追いつかないという声がありました。\u003c/p\u003e\n\u003cp data-sourcepos=\"10:1-12:95\"\u003eそこでこの記事では、各STEPで何をしているのかを丁寧に補足しながら、\u003cbr\u003e\n初心者でも流れを追いやすいように再構成しています。\u003cbr\u003e\n説明は詳しめですが、そのぶん文章量も多めです。ご了承ください。\u003c/p\u003e\n\u003cp data-sourcepos=\"14:1-15:54\"\u003e「とりあえず完成したけど、結局どういう仕組みなのか分からない」\u003cbr\u003e\nという状態を解消することが目的です。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"17:1-17:20\"\u003e\n\u003cspan id=\"使用するもの\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E3%82%82%E3%81%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e使用するもの\u003c/h1\u003e\n\u003cul data-sourcepos=\"18:1-23:0\"\u003e\n\u003cli data-sourcepos=\"18:1-18:10\"\u003eVue.js 2\u003c/li\u003e\n\u003cli data-sourcepos=\"19:1-19:33\"\u003eVisual Studio Code（VS Code）\u003c/li\u003e\n\u003cli data-sourcepos=\"20:1-20:6\"\u003eHTML\u003c/li\u003e\n\u003cli data-sourcepos=\"21:1-21:5\"\u003eCSS\u003c/li\u003e\n\u003cli data-sourcepos=\"22:1-23:0\"\u003eJavaScript\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 data-sourcepos=\"24:1-24:17\"\u003e\n\u003cspan id=\"参考サイト\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E8%80%83%E3%82%B5%E3%82%A4%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参考サイト\u003c/h1\u003e\n\u003cp data-sourcepos=\"25:1-25:36\"\u003e\u003ciframe id=\"qiita-embed-content__b6da2edfe914b8d28d25ca152949f2bf\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__b6da2edfe914b8d28d25ca152949f2bf\" data-content=\"https%3A%2F%2Fcr-vue.mio3io.com%2Ftutorials%2F\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003ch1 data-sourcepos=\"27:1-27:20\"\u003e\n\u003cspan id=\"step0-はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step0-%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP0 はじめに\u003c/h1\u003e\n\u003cp data-sourcepos=\"28:1-29:110\"\u003eここでは、このチュートリアルで使う前提知識について解説します。\u003cbr\u003e\nまずは ToDo アプリとは何か、そして Web ページを構成する基本要素を確認します。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"31:1-31:22\"\u003e\n\u003cspan id=\"todoアプリ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#todo%E3%82%A2%E3%83%97%E3%83%AA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ToDoアプリ】\u003c/h1\u003e\n\u003cp data-sourcepos=\"32:1-32:39\"\u003eやることを管理するアプリ。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"37:1-37:12\"\u003e\n\u003cspan id=\"html\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#html\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【HTML】\u003c/h1\u003e\n\u003cp data-sourcepos=\"38:1-38:51\"\u003eページの骨組み（何を表示するか）。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"40:1-40:11\"\u003e\n\u003cspan id=\"css\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#css\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【CSS】\u003c/h1\u003e\n\u003cp data-sourcepos=\"41:1-41:57\"\u003e見た目のデザイン（色・配置・大きさ）。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"43:1-43:18\"\u003e\n\u003cspan id=\"javascript\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#javascript\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【JavaScript】\u003c/h1\u003e\n\u003cp data-sourcepos=\"44:1-44:63\"\u003e動きをつける（追加・削除・状態変更など）。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"46:1-46:42\"\u003e\n\u003cspan id=\"ブラウザweb-ブラウザ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6web-%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ブラウザ（Web ブラウザ）】\u003c/h1\u003e\n\u003cp data-sourcepos=\"48:1-50:61\"\u003eインターネットのページを見るためのアプリ。\u003cbr\u003e\nスマホやパソコンでニュースを読んだり、動画を見たり、検索したりするときに使う「いつものアプリ」。\u003cbr\u003e\n代表例：Microsoft Edge、Google Chrome、Safari など。\u003c/p\u003e\n\u003cp data-sourcepos=\"52:1-52:77\"\u003eブラウザは Web ページを表示するときに次の処理を行う：\u003c/p\u003e\n\u003col data-sourcepos=\"54:1-57:0\"\u003e\n\u003cli data-sourcepos=\"54:1-54:64\"\u003eHTML を読み取り、ページの「骨組み」を作る\u003c/li\u003e\n\u003cli data-sourcepos=\"55:1-55:96\"\u003eCSS を読み取り、色や文字の大きさ、配置などの「見た目」を整える\u003c/li\u003e\n\u003cli data-sourcepos=\"56:1-57:0\"\u003eJavaScript を読み取り、ボタンを押したときの動きなどを実行する\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"58:1-60:36\"\u003eつまりブラウザは、\u003cbr\u003e\n\u003cstrong\u003e「Web ページを読み取り、人が見て操作できる形にしてくれるアプリ」\u003c/strong\u003e\u003cbr\u003e\nという役割を持っている。\u003c/p\u003e\n\u003cp data-sourcepos=\"62:1-63:60\"\u003e今回の ToDo アプリも、ブラウザが HTML・CSS・JavaScript（Vue.js）を読み込み、\u003cbr\u003e\n画面に表示し、ボタンを押したときに動く。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"65:1-65:15\"\u003e\n\u003cspan id=\"タグ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%BF%E3%82%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【タグ】\u003c/h1\u003e\n\u003cp data-sourcepos=\"67:1-68:61\"\u003eHTML で “ここに何を置くか” を指定するための記号。\u003cbr\u003e\nHTML はタグを使って Web ページの構造を作る。\u003c/p\u003e\n\u003cp data-sourcepos=\"70:1-71:93\"\u003e今回登場するタグの一部を、必要に応じて確認できるよう一覧にまとめました。\u003cbr\u003e\n見なくても読み進められますが、気になる方は開いてみてください。\u003c/p\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e今回登場する HTML のタグ一覧（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cp data-sourcepos=\"75:2-75:24\"\u003e【\u003ccode\u003e\u0026lt;!DOCTYPE html\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"76:1-76:115\"\u003e\n\u003cp data-sourcepos=\"76:2-76:115\"\u003eHTML5 で書かれた文書であることをブラウザに伝える宣言。ページの最初に必ず書く。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"78:2-78:15\"\u003e【\u003ccode\u003e\u0026lt;html\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"79:1-79:83\"\u003e\n\u003cp data-sourcepos=\"79:2-79:83\"\u003eHTML 文書全体を囲むタグ。この中に \u003ccode\u003e\u0026lt;head\u0026gt;\u003c/code\u003e と \u003ccode\u003e\u0026lt;body\u0026gt;\u003c/code\u003e が入る。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"81:2-81:15\"\u003e【\u003ccode\u003e\u0026lt;head\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"82:1-82:92\"\u003e\n\u003cp data-sourcepos=\"82:2-82:92\"\u003eページの設定情報（タイトル、文字コード、CSS など）を書く部分。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"84:2-84:15\"\u003e【\u003ccode\u003e\u0026lt;meta\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"85:1-87:100\"\u003e\n\u003cp data-sourcepos=\"85:2-87:100\"\u003eページのメタ情報（ブラウザに伝える設定）を指定するタグ。\u003cbr\u003e\n今回は文字コードを UTF-8 にする。\u003cbr\u003e\n文字コードが正しく指定されていないと日本語が文字化けするため重要。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"89:1-89:15\"\u003e【\u003ccode\u003e\u0026lt;title\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"90:1-90:64\"\u003e\n\u003cp data-sourcepos=\"90:2-90:64\"\u003eブラウザのタブに表示されるページタイトル。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"92:2-92:15\"\u003e【\u003ccode\u003e\u0026lt;link\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"93:1-93:68\"\u003e\n\u003cp data-sourcepos=\"93:2-93:68\"\u003e外部ファイル（CSS など）を読み込むためのタグ。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"95:2-95:15\"\u003e【\u003ccode\u003e\u0026lt;body\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"96:1-96:58\"\u003e\n\u003cp data-sourcepos=\"96:2-96:58\"\u003e実際に画面に表示される内容を書く部分。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"98:2-98:14\"\u003e【\u003ccode\u003e\u0026lt;div\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"99:1-99:86\"\u003e\n\u003cp data-sourcepos=\"99:2-99:86\"\u003eレイアウトを作るための箱。Vue アプリ全体を囲むために使用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"101:2-101:13\"\u003e【\u003ccode\u003e\u0026lt;h1\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"102:1-102:73\"\u003e\n\u003cp data-sourcepos=\"102:2-102:73\"\u003eページの中で一番大きく、重要な見出しを作るタグ。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"104:2-104:13\"\u003e【\u003ccode\u003e\u0026lt;h2\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"105:1-106:106\"\u003e\n\u003cp data-sourcepos=\"105:2-106:106\"\u003e\u003ccode\u003e\u0026lt;h1\u0026gt;\u003c/code\u003e より少し小さい見出し。\u003cbr\u003e\n見出しタグは h1 → h2 → h3… のように数字が大きくなるほど重要度が下がる。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"108:2-108:12\"\u003e【\u003ccode\u003e\u0026lt;p\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"109:1-109:73\"\u003e\n\u003cp data-sourcepos=\"109:2-109:73\"\u003e段落（文章）を表すタグ。説明文や件数表示に使用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"111:2-111:16\"\u003e【\u003ccode\u003e\u0026lt;label\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"112:1-113:109\"\u003e\n\u003cp data-sourcepos=\"112:2-113:109\"\u003eフォーム部品に説明文をつけるタグ。\u003cbr\u003e\n\u003ccode\u003e\u0026lt;label\u0026gt;\u003c/code\u003e の中に \u003ccode\u003e\u0026lt;input\u0026gt;\u003c/code\u003e がある場合、文字をクリックしてもボタンが選択される。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"115:1-115:15\"\u003e【\u003ccode\u003e\u0026lt;input\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"116:1-117:52\"\u003e\n\u003cp data-sourcepos=\"116:2-117:52\"\u003eユーザーが文字を入力したり、選択したりするためのタグ。\u003cbr\u003e\nテキスト入力やラジオボタンに使用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"119:2-119:15\"\u003e【\u003ccode\u003e\u0026lt;form\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"120:1-121:55\"\u003e\n\u003cp data-sourcepos=\"120:2-121:55\"\u003eユーザーが入力した内容をまとめるための「箱」。\u003cbr\u003e\n入力欄やボタンをひとまとめにできる。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"123:1-123:16\"\u003e【\u003ccode\u003e\u0026lt;button\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"124:1-124:76\"\u003e\n\u003cp data-sourcepos=\"124:2-124:76\"\u003eクリックできるボタン。追加・削除・状態変更に使用。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"126:2-126:17\"\u003e【\u003ccode\u003e\u0026lt;script\u0026gt;\u003c/code\u003e】\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"127:1-129:125\"\u003e\n\u003cp data-sourcepos=\"127:2-129:125\"\u003eJavaScript を読み込むタグ。今回は Vue.js を CDN から読み込んでいる。\u003cbr\u003e\nCDN は「インターネット上に置かれた共有のファイル置き場」のようなもの。\u003cbr\u003e\n有名なライブラリ（Vue.js など）が置かれていて、自分のパソコンに保存しなくても使える。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"134:1-134:35\"\u003e\n\u003cspan id=\"step1-インスタンスの作成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step1-%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%AE%E4%BD%9C%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP1 インスタンスの作成\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP1の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"138:1-142:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"139:1-139:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"141:1-141:84\"\u003eまずは、アプリケーションを紐付ける要素 #app を作成します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"144:1-144:54\"\u003e本文中の言葉について説明していきます\u003c/p\u003e\n\u003ch3 data-sourcepos=\"146:1-146:17\"\u003e\n\u003cspan id=\"要素\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A6%81%E7%B4%A0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【要素】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"147:1-149:64\"\u003e\n\u003cp data-sourcepos=\"147:2-149:64\"\u003eHTMLにある部品のこと\u003ccode\u003e\u0026lt;div\u0026gt;, \u0026lt;p\u0026gt;, \u0026lt;table\u0026gt; \u003c/code\u003eなど、\u003cbr\u003e\nタグで囲まれたものを指します。\u003cbr\u003e\n#はidを表すので\u003ccode\u003e\u0026lt;div id=\"app\"\u0026gt;\u003c/code\u003eの部分が該当します\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"151:1-152:0\"\u003e\n\u003cdiv data-sourcepos=\"153:1-157:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"154:1-156:105\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\nコンストラクタ関数 Vue を使ってルートインスタンスを作成します。\u003cbr\u003e\nアプリケーションで使用したいデータは data オプションへ登録していきます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"160:1-160:33\"\u003e順々に解説していきます\u003c/p\u003e\n\u003ch3 data-sourcepos=\"162:1-162:37\"\u003e\n\u003cspan id=\"コンストラクタ関数\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF%E9%96%A2%E6%95%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【コンストラクタ関数】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"163:1-165:79\"\u003e\n\u003cp data-sourcepos=\"163:2-165:79\"\u003e一言で言うと同じ形のものをたくさん作るための関数\u003cbr\u003e\nVueではnew Vue({...})と書く時Vueの部分がコンストラクタ関数にあたります。\u003cbr\u003e\nまた、el:'#app'の部分が紐づける要素を指定する部分です。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"167:1-168:0\"\u003e\n\u003ch3 data-sourcepos=\"169:1-169:16\"\u003e\n\u003cspan id=\"関数\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%96%A2%E6%95%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【関数】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"172:1-207:35\"\u003e\n\u003cp data-sourcepos=\"172:3-173:76\"\u003eコンストラクタ関数と区別するために、\u003cbr\u003e\n「普通の関数」についても簡単に説明しておきます。\u003c/p\u003e\n\u003cp data-sourcepos=\"175:3-176:68\"\u003e普通の関数は、ある処理をまとめておき、\u003cbr\u003e\n必要なときに呼び出して使うための仕組みです。\u003c/p\u003e\n\u003cp data-sourcepos=\"178:3-178:64\"\u003e例えば（aとbを足し算したものを返す関数）：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"180:3-184:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eadd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003ea\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"185:3-186:94\"\u003eこのように、何かを計算したり、値を返したり、\u003cbr\u003e\n一連の処理をひとまとめにして再利用できるのが普通の関数です。\u003c/p\u003e\n\u003cp data-sourcepos=\"188:3-189:34\"\u003eコンストラクタ関数も見た目は同じ「function」ですが、\u003cbr\u003e\n役割が少し違います。\u003c/p\u003e\n\u003cp data-sourcepos=\"191:3-193:94\"\u003e普通の関数は「処理を実行するためのもの」ですが、\u003cbr\u003e\nコンストラクタ関数は\u003cbr\u003e\n「決まった形を持つ“まとまり”を新しく作るためのもの」です。\u003c/p\u003e\n\u003cp data-sourcepos=\"195:3-195:30\"\u003e例えば Vue の場合、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"197:3-199:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eVue\u003c/span\u003e\u003cspan class=\"p\"\u003e({...})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"201:3-202:65\"\u003eと書くと、Vue というコンストラクタ関数を使って\u003cbr\u003e\n“Vue アプリの本体”を新しく作っています。\u003c/p\u003e\n\u003cp data-sourcepos=\"204:3-207:35\"\u003eこのように、\u003cbr\u003e\n-普通の関数 → 何かの処理を実行するためのもの\u003cbr\u003e\n-コンストラクタ関数 → 決まった形を持つ“まとまり”を作るためのもの\u003cbr\u003e\nという違いがあります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"213:1-214:0\"\u003e\n\u003ch3 data-sourcepos=\"215:1-215:37\"\u003e\n\u003cspan id=\"ルートインスタンス\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AB%E3%83%BC%E3%83%88%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ルートインスタンス】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"216:1-226:95\"\u003e\n\u003cp data-sourcepos=\"218:3-220:85\"\u003eVue が動き始めるスタート時点のことです。\u003cbr\u003e\nnew Vue({ ... }) の中に書いた内容をもとに Vue が処理し、\u003cbr\u003e\nその結果として出来上がるものがルートインスタンスです。\u003c/p\u003e\n\u003cp data-sourcepos=\"222:3-223:100\"\u003e（例えるなら、el:..., data:... の部分が設計図で、\u003cbr\u003e\nVue がその設計図をもとに家（ルートインスタンス）を建てるイメージ）\u003c/p\u003e\n\u003cp data-sourcepos=\"225:2-226:95\"\u003e※「インスタンス」という言葉の詳しい説明は STEP9 で行います。\u003cbr\u003e\nここでは「Vue が作るアプリの本体」くらいのイメージで大丈夫です。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"227:1-228:0\"\u003e\n\u003ch3 data-sourcepos=\"229:1-229:30\"\u003e\n\u003cspan id=\"data-オプション\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#data-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【data オプション】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"230:1-237:112\"\u003e\n\u003cp data-sourcepos=\"230:3-232:91\"\u003eオプションとは、Vue に渡す設定項目のことです。\u003cbr\u003e\ndataオプションとは、簡単に言うと、アプリの中で使いたい「変わる情報」を入れておく場所です。\u003cbr\u003e\n画面に表示する文字や、後で変わる数字などをここに書きます。\u003c/p\u003e\n\u003cp data-sourcepos=\"234:3-235:82\"\u003edata は Vue で決められた特別な名前なので変更できません。\u003cbr\u003e\n（ほかにも el, methods, computed, watch などは変更できません）\u003c/p\u003e\n\u003cp data-sourcepos=\"237:3-237:112\"\u003edata オプションへ登録したデータは、すべてリアクティブデータに変換されます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"239:1-240:0\"\u003e\n\u003ch3 data-sourcepos=\"241:1-241:37\"\u003e\n\u003cspan id=\"リアクティブデータ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AA%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%83%87%E3%83%BC%E3%82%BF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【リアクティブデータ】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"242:1-244:96\"\u003e\n\u003cp data-sourcepos=\"242:2-244:96\"\u003eリアクティブ（reactive）は「反応する」という意味で、\u003cbr\u003e\nデータが変わると自動で画面も変わる仕組みを持ったデータのことです。\u003cbr\u003e\nVue の data の中に書いた値が、このリアクティブデータにあたります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"251:1-251:49\"\u003e\n\u003cspan id=\"step2-ローカルストレージ-api-の使用\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step2-%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8-api-%E3%81%AE%E4%BD%BF%E7%94%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP2 ローカルストレージ API の使用\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP2の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"256:1-262:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"257:1-257:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"259:1-261:51\"\u003eデータはサーバーではなく  「ローカルストレージ」へ保存することにします。\u003cbr\u003e\nストレージ周りの実装は  Vue.js 公式サンプル「TodoMVC の例」\u003cbr\u003e\nのコードを使用させていただきます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"266:1-266:22\"\u003e\n\u003cspan id=\"サーバー\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【サーバー】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"268:1-276:86\"\u003e\n\u003cp data-sourcepos=\"268:3-269:149\"\u003eサーバーとは、インターネット上にある “データを保存したり処理したりするコンピュータ” のことです。\u003cbr\u003e\nユーザーの情報、アプリの設定、画像や文章など、Web サイトを動かすための情報を保存しておく場所です。\u003c/p\u003e\n\u003cp data-sourcepos=\"271:3-271:22\"\u003e特徴として：\u003c/p\u003e\n\u003cul data-sourcepos=\"272:3-275:3\"\u003e\n\u003cli data-sourcepos=\"272:3-272:60\"\u003eインターネットを通してアクセスできる\u003c/li\u003e\n\u003cli data-sourcepos=\"273:3-273:63\"\u003e複数のユーザーが同じデータを共有できる\u003c/li\u003e\n\u003cli data-sourcepos=\"274:3-275:3\"\u003eどの端末からでも同じ情報を見られる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"276:3-276:86\"\u003eたとえるなら \u003cstrong\u003eみんなで使える倉庫\u003c/strong\u003e のようなイメージです。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"278:1-279:0\"\u003e\n\u003ch3 data-sourcepos=\"280:1-280:37\"\u003e\n\u003cspan id=\"ローカルストレージ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ローカルストレージ】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"282:1-298:98\"\u003e\n\u003cp data-sourcepos=\"282:3-284:63\"\u003eストレージとは「データを保存しておく場所」のことです。\u003cbr\u003e\nローカルストレージは、ブラウザ（Chrome や Safari など）の中にある\u003cbr\u003e\n保存場所で、データを端末に保存できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"286:3-286:22\"\u003e特徴として：\u003c/p\u003e\n\u003cul data-sourcepos=\"287:3-291:3\"\u003e\n\u003cli data-sourcepos=\"287:3-287:33\"\u003eインターネット不要\u003c/li\u003e\n\u003cli data-sourcepos=\"288:3-288:45\"\u003eその端末だけにデータが残る\u003c/li\u003e\n\u003cli data-sourcepos=\"289:3-289:44\"\u003e最大 5MB 程度まで保存できる\u003c/li\u003e\n\u003cli data-sourcepos=\"290:3-291:3\"\u003e簡単に使える\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"292:3-292:97\"\u003eたとえるなら \u003cstrong\u003e自分だけが使える机の引き出し\u003c/strong\u003e のようなものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"294:3-294:37\"\u003e今回の ToDo アプリでは、\u003c/p\u003e\n\u003cul data-sourcepos=\"295:3-297:3\"\u003e\n\u003cli data-sourcepos=\"295:3-295:45\"\u003e他の人と共有する必要がない\u003c/li\u003e\n\u003cli data-sourcepos=\"296:3-297:3\"\u003e設定が簡単\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"298:3-298:98\"\u003eという理由からローカルストレージが採用されていると考えられます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"300:1-301:0\"\u003e\n\u003cdiv data-sourcepos=\"302:1-308:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"303:1-303:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"305:1-307:54\"\u003e公式のコードの内容については詳しく説明しませんが、\u003cbr\u003e\nこれは Storage API を使ったデータの取得・保存の処理だけを抜き出したものです。\u003cbr\u003e\n小さなライブラリだと思ってください。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"310:1-311:0\"\u003e\n\u003ch3 data-sourcepos=\"312:1-312:13\"\u003e\n\u003cspan id=\"api\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#api\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【API】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"314:1-322:75\"\u003e\n\u003cp data-sourcepos=\"314:3-315:133\"\u003eAPI（エーピーアイ）とは、アプリやプログラムが “決められた方法で機能を使えるようにする仕組み” のことです。\u003cbr\u003e\n「この方法で呼び出せば、この機能が使えますよ」という \u003cstrong\u003e道具の説明書\u003c/strong\u003e のようなものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"317:3-317:10\"\u003e例：\u003c/p\u003e\n\u003cul data-sourcepos=\"318:3-321:3\"\u003e\n\u003cli data-sourcepos=\"318:3-318:74\"\u003eGoogle Maps の地図をアプリに表示する → Google Maps API\u003c/li\u003e\n\u003cli data-sourcepos=\"319:3-319:79\"\u003eブラウザにデータを保存する → Storage API（今回使用）\u003c/li\u003e\n\u003cli data-sourcepos=\"320:3-321:3\"\u003eカメラを使う → Camera API\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"322:3-322:75\"\u003eAPI は \u003cstrong\u003e機能を安全に・簡単に使うための窓口\u003c/strong\u003e です。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"324:1-325:0\"\u003e\n\u003ch3 data-sourcepos=\"326:1-326:25\"\u003e\n\u003cspan id=\"ライブラリ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ライブラリ】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"328:1-336:67\"\u003e\n\u003cp data-sourcepos=\"328:3-328:141\"\u003eライブラリとは、よく使う処理をまとめて必要な時に呼び出して使える “部品セット” のことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"330:3-330:10\"\u003e例：\u003c/p\u003e\n\u003cul data-sourcepos=\"331:3-335:3\"\u003e\n\u003cli data-sourcepos=\"331:3-331:24\"\u003e保存する処理\u003c/li\u003e\n\u003cli data-sourcepos=\"332:3-332:36\"\u003eデータを読み込む処理\u003c/li\u003e\n\u003cli data-sourcepos=\"333:3-333:27\"\u003e日付を扱う処理\u003c/li\u003e\n\u003cli data-sourcepos=\"334:3-335:3\"\u003e画面を操作する処理\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"336:3-336:67\"\u003e何度も使う処理をまとめた便利な道具箱です。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"340:1-341:0\"\u003e\n\u003cdiv data-sourcepos=\"342:1-346:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"343:1-343:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"345:1-345:108\"\u003e実際にストレージに保存されるデータのフォーマットは、次のような JSON です。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"348:1-349:0\"\u003e\n\u003ch3 data-sourcepos=\"350:1-350:28\"\u003e\n\u003cspan id=\"フォーマット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【フォーマット】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"352:1-359:44\"\u003e\n\u003cp data-sourcepos=\"352:3-352:123\"\u003eフォーマットとは、データをどのような形で保存するかを決めた “書式” のことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"354:3-355:19\"\u003eローカルストレージは \u003cstrong\u003e文字列しか保存できません\u003c/strong\u003e。\u003cbr\u003e\nそのため：\u003c/p\u003e\n\u003cul data-sourcepos=\"356:3-358:3\"\u003e\n\u003cli data-sourcepos=\"356:3-356:83\"\u003e保存するとき → 元のデータを JSON 文字列に変換して保存\u003c/li\u003e\n\u003cli data-sourcepos=\"357:3-358:3\"\u003e読み込むとき → JSON 文字列を元のデータに戻す\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"359:3-359:44\"\u003eという処理が必要になります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"361:1-362:0\"\u003e\n\u003ch3 data-sourcepos=\"363:1-363:14\"\u003e\n\u003cspan id=\"json\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#json\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【JSON】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"365:1-377:63\"\u003e\n\u003cp data-sourcepos=\"365:3-366:109\"\u003eJSON（ジェイソン）とは、データを表現するための “書き方のルール（フォーマット）” のことです。\u003cbr\u003e\n一言で言うと、データを保存したり、やり取りしたりするために使われます。\u003c/p\u003e\n\u003cp data-sourcepos=\"368:3-368:13\"\u003e特徴：\u003c/p\u003e\n\u003cul data-sourcepos=\"369:3-373:3\"\u003e\n\u003cli data-sourcepos=\"369:3-369:33\"\u003e人間にも読みやすい\u003c/li\u003e\n\u003cli data-sourcepos=\"370:3-370:33\"\u003eどの言語でも扱える\u003c/li\u003e\n\u003cli data-sourcepos=\"371:3-371:27\"\u003e軽くてシンプル\u003c/li\u003e\n\u003cli data-sourcepos=\"372:3-373:3\"\u003eローカルストレージと相性が良い\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"374:3-375:68\"\u003eJSON は「名前: 値」の組み合わせを \u003ccode\u003e{ }\u003c/code\u003e で書き、\u003cbr\u003e\n複数のデータを扱うときは \u003ccode\u003e[ ]\u003c/code\u003e でまとめます。\u003c/p\u003e\n\u003cp data-sourcepos=\"377:3-377:63\"\u003e（具体例は STEP3 の ToDo データで説明します）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"386:1-386:26\"\u003e\n\u003cspan id=\"step3-データの構想\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step3-%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E6%A7%8B%E6%83%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP3 データの構想\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP3の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"393:1-408:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"394:1-394:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"396:1-396:54\"\u003eさあ、ここから実際に作るコードです！\u003c/p\u003e\n\u003cp data-sourcepos=\"398:1-398:96\"\u003eどんなデータが必要になりそうかを、ざっくりと考えておきましょう。\u003c/p\u003e\n\u003cul data-sourcepos=\"400:1-406:0\"\u003e\n\u003cli data-sourcepos=\"400:1-403:18\"\u003eToDo のリストデータ\n\u003cul data-sourcepos=\"401:3-403:18\"\u003e\n\u003cli data-sourcepos=\"401:3-401:23\"\u003e要素の固有ID\u003c/li\u003e\n\u003cli data-sourcepos=\"402:3-402:18\"\u003eコメント\u003c/li\u003e\n\u003cli data-sourcepos=\"403:3-403:18\"\u003e今の状態\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"404:1-404:95\"\u003e作業中・完了・すべて などオプションラベルで使用する名称リスト\u003c/li\u003e\n\u003cli data-sourcepos=\"405:1-406:0\"\u003e現在絞り込みしている作業状態\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"407:1-407:102\"\u003eアプリケーションに付けたい機能から考えると、こんなところでしょうか。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"410:1-411:0\"\u003e\n\u003ch3 data-sourcepos=\"412:1-412:36\"\u003e\n\u003cspan id=\"todo-のリストデータ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#todo-%E3%81%AE%E3%83%AA%E3%82%B9%E3%83%88%E3%83%87%E3%83%BC%E3%82%BF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ToDo のリストデータ】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"414:1-427:104\"\u003e\n\u003cp data-sourcepos=\"414:3-414:49\"\u003e下記のような JSON のデータです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"416:3-421:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"id\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"comment\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"買い物に行く\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"state\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"作業中\"\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"id\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"comment\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"Vue の勉強\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"state\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"完了\"\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cul data-sourcepos=\"423:3-427:104\"\u003e\n\u003cli data-sourcepos=\"423:3-424:55\"\u003e\n\u003cstrong\u003e固有ID\u003c/strong\u003e：それぞれの ToDo を区別するための番号で、\u003cbr\u003e\n同じ ID にならないように管理します。\u003c/li\u003e\n\u003cli data-sourcepos=\"425:3-425:78\"\u003e\n\u003cstrong\u003eコメント\u003c/strong\u003e：ToDo の内容（画面に表示するテキスト）\u003c/li\u003e\n\u003cli data-sourcepos=\"426:3-427:104\"\u003e\n\u003cstrong\u003e状態\u003c/strong\u003e：作業中 / 完了 のどちらか\u003cbr\u003e\n画面で「作業中だけ表示」「完了だけ表示」などの絞り込みにも使います。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"433:1-433:32\"\u003e\n\u003cspan id=\"step4-リスト用テーブル\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step4-%E3%83%AA%E3%82%B9%E3%83%88%E7%94%A8%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP4 リスト用テーブル\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP4の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"439:1-443:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"440:1-440:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"442:1-442:95\"\u003eまずは、ToDo リストデータを表示するテーブルの枠組みを作成します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"445:1-446:0\"\u003e\n\u003ch3 data-sourcepos=\"447:1-447:22\"\u003e\n\u003cspan id=\"テーブル\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【テーブル】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"448:1-475:20\"\u003e\n\u003cp data-sourcepos=\"448:2-450:64\"\u003eSTEP3 で考えた ToDo データを、\u003cbr\u003e\n画面に表形式で表示するためにテーブルを使います。\u003cbr\u003e\nここではテーブルに使うタグを紹介します。\u003c/p\u003e\n\u003cp data-sourcepos=\"452:3-452:17\"\u003e\u003cstrong\u003e\u003ccode\u003e\u0026lt;table\u0026gt;\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"454:3-454:90\"\u003e表全体を作るタグ。ToDo リストを表形式で表示するために使用。\u003c/p\u003e\n\u003cp data-sourcepos=\"456:3-456:17\"\u003e\u003cstrong\u003e\u003ccode\u003e\u0026lt;thead\u0026gt;\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"458:3-458:64\"\u003e表のヘッダー部分（列名）をまとめるタグ。\u003c/p\u003e\n\u003cp data-sourcepos=\"460:3-460:17\"\u003e\u003cstrong\u003e\u003ccode\u003e\u0026lt;tbody\u0026gt;\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"462:3-462:64\"\u003e表の本体部分（データ行）をまとめるタグ。\u003c/p\u003e\n\u003cp data-sourcepos=\"464:3-464:14\"\u003e\u003cstrong\u003e\u003ccode\u003e\u0026lt;tr\u0026gt;\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"466:3-466:40\"\u003e表の行（row）を表すタグ。\u003c/p\u003e\n\u003cp data-sourcepos=\"468:3-468:14\"\u003e\u003cstrong\u003e\u003ccode\u003e\u0026lt;th\u0026gt;\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"470:3-470:80\"\u003e表の見出し用のセル（1マス）。太字・中央寄せになる。\u003c/p\u003e\n\u003cp data-sourcepos=\"472:3-472:22\"\u003eまた、表では\u003c/p\u003e\n\u003cul data-sourcepos=\"473:3-475:20\"\u003e\n\u003cli data-sourcepos=\"473:3-473:58\"\u003e\u003cstrong\u003e縦の列のことを「カラム（column）」\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"474:3-475:20\"\u003e\n\u003cstrong\u003e横の行のことを「ロウ（row）」\u003c/strong\u003e\u003cbr\u003e\nと呼びます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"482:1-482:35\"\u003e\n\u003cspan id=\"step5-リストレンダリング\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step5-%E3%83%AA%E3%82%B9%E3%83%88%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP5 リストレンダリング\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP5の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"489:1-496:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"490:1-490:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"492:1-495:60\"\u003eToDo リストデータ用の空の配列を  data オプションへ登録します。\u003cbr\u003e\nこれは、データが何もない時でも  配列として認識されるようにするためと、\u003cbr\u003e\nもともと data オプション直下のデータは  後から追加ができないため\u003cbr\u003e\n初期値で宣言しておく必要があるためです。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"498:1-499:0\"\u003e\n\u003ch3 data-sourcepos=\"500:1-500:37\"\u003e\n\u003cspan id=\"リストレンダリング\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AA%E3%82%B9%E3%83%88%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【リストレンダリング】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"502:1-510:74\"\u003e\n\u003cp data-sourcepos=\"502:3-503:105\"\u003eリストレンダリングとは、\u003cbr\u003e\n配列のデータ（ToDo リストデータ）を画面に繰り返し表示する仕組みです。\u003c/p\u003e\n\u003cp data-sourcepos=\"505:3-507:49\"\u003e（HTML には \u003ccode\u003e\u0026lt;tr\u0026gt;...\u0026lt;/tr\u0026gt;\u003c/code\u003e を 1 つだけ書いておき、\u003cbr\u003e\nVue が ToDo リストデータの件数分 \u003ccode\u003e\u0026lt;tr\u0026gt;...\u0026lt;/tr\u0026gt;\u003c/code\u003e を自動で増やし、\u003cbr\u003e\nその中にデータを入れてくれる）\u003c/p\u003e\n\u003cp data-sourcepos=\"509:3-510:74\"\u003eToDo リストのように複数の項目を一覧で表示する場合に必ず使います。\u003cbr\u003e\nここでは Vue の \u003cstrong\u003ev-for\u003c/strong\u003e（後ほど解説）を使用します。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"512:1-513:0\"\u003e\n\u003ch3 data-sourcepos=\"514:1-514:16\"\u003e\n\u003cspan id=\"配列\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%85%8D%E5%88%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【配列】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"516:1-533:83\"\u003e\n\u003cp data-sourcepos=\"516:3-516:82\"\u003e配列は「番号付きの箱が横に並んでいる」イメージです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"txt\" data-sourcepos=\"518:3-521:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e[ データ1, データ2, データ3, ... ]\n  0        1        2\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"523:3-524:61\"\u003e左から順番に 0番、1番、2番… と番号がつきます（0番から始まるのが一般的）。\u003cbr\u003e\nこの番号を使ってデータを取り出せます。\u003c/p\u003e\n\u003cp data-sourcepos=\"526:3-526:94\"\u003eToDo リストデータは STEP3 で説明した JSON 配列で管理されています。\u003c/p\u003e\n\u003cp data-sourcepos=\"528:3-528:10\"\u003e例：\u003c/p\u003e\n\u003cul data-sourcepos=\"529:3-531:1\"\u003e\n\u003cli data-sourcepos=\"529:3-529:68\"\u003e\u003ccode\u003e{ id: 1, comment: \"買い物に行く\", state: \"作業中\" }\u003c/code\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"530:3-531:1\"\u003e\u003ccode\u003e{ id: 2, comment: \"Vue の勉強\", state: \"完了\" }\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"532:3-533:83\"\u003eToDo リストは 1件だけでなく、2件、3件、10件と増えていきます。\u003cbr\u003e\nこの複数のデータをまとめて扱うために配列を使用します。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"535:1-536:0\"\u003e\n\u003ch3 data-sourcepos=\"537:1-537:31\"\u003e\n\u003cspan id=\"初期値宣言\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%88%9D%E6%9C%9F%E5%80%A4%E5%AE%A3%E8%A8%80\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【初期値】【宣言】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"539:1-555:5\"\u003e\n\u003cp data-sourcepos=\"539:3-539:70\"\u003e初期値とは「最初に入れておく値」のことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"541:3-544:118\"\u003eVue の data に登録したデータは、最初に存在していないと後から追加できない\u003cbr\u003e\n（データを追加しても画面に反映されない）というルールがあります。\u003cbr\u003e\nこれはVue は最初に data の中身を読み取って、リアクティブデータに変換する仕組みのためです。\u003cbr\u003e\nその結果、最初に存在しないデータはリアクティブにならず、画面に反映されません。\u003c/p\u003e\n\u003cp data-sourcepos=\"546:3-547:94\"\u003eそこで ToDo リストを入れるための todos は、\u003cbr\u003e\n最初に空の配列 \u003ccode\u003e[]\u003c/code\u003e を初期値として宣言しておく必要があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"549:3-549:106\"\u003e宣言とは「この名前のデータを使います」とプログラムに教えることです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"551:3-555:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[]\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// ← これが「宣言」\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"557:1-558:0\"\u003e\n\u003cdiv data-sourcepos=\"559:1-566:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"560:1-560:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"562:1-565:48\"\u003eテーブルタグの [1] で\u003cbr\u003e\n配列要素の数だけ繰り返し表示させるには、\u003cbr\u003e\n対象となるタグ（ここでは \u003ccode\u003e\u0026lt;tr\u0026gt;\u003c/code\u003e タグ）に\u003cbr\u003e\nv-for ディレクティブを使用します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"568:1-569:0\"\u003e\n\u003ch3 data-sourcepos=\"570:1-570:37\"\u003e\n\u003cspan id=\"v-for-ディレクティブ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#v-for-%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【v-for ディレクティブ】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"572:1-592:5\"\u003e\n\u003cp data-sourcepos=\"572:3-572:117\"\u003ev-for とは Vue で「繰り返し表示」を行うためのディレクティブ（特別な命令）です。\u003c/p\u003e\n\u003cp data-sourcepos=\"574:3-574:67\"\u003e配列の中に複数のデータが入っているときに：\u003c/p\u003e\n\u003cul data-sourcepos=\"575:3-579:1\"\u003e\n\u003cli data-sourcepos=\"575:3-575:36\"\u003e配列から 1 つ取り出す\u003c/li\u003e\n\u003cli data-sourcepos=\"576:3-576:28\"\u003e\n\u003ccode\u003e\u0026lt;tr\u0026gt;\u003c/code\u003e を 1 つ作る\u003c/li\u003e\n\u003cli data-sourcepos=\"577:3-577:36\"\u003e次のデータを取り出す\u003c/li\u003e\n\u003cli data-sourcepos=\"578:3-579:1\"\u003eまた \u003ccode\u003e\u0026lt;tr\u0026gt;\u003c/code\u003e を作る\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"580:3-580:66\"\u003eという処理を \u003cstrong\u003e自動で繰り返してくれます\u003c/strong\u003e。\u003c/p\u003e\n\u003cp data-sourcepos=\"582:3-582:16\"\u003e書き方：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"584:3-586:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ev-for=\"一時的な名前 in 繰り返したい配列\"\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"588:3-588:16\"\u003e具体例：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"590:3-592:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;tr\u003c/span\u003e \u003cspan class=\"na\"\u003ev-for=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"item in todos\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"594:1-595:0\"\u003e\n\u003cdiv data-sourcepos=\"596:1-602:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"597:1-597:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"599:1-600:93\"\u003eディレクティブの値は JavaScript の式になっており次のように書きます。\u003cbr\u003e\n\u003ccode\u003ev-for=\"各要素の一時的な名前 in 繰り返したい配列やオブジェクト\"\u003c/code\u003e\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"603:1-604:0\"\u003e\n\u003ch3 data-sourcepos=\"605:1-605:28\"\u003e\n\u003cspan id=\"オブジェクト\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【オブジェクト】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"607:1-621:116\"\u003e\n\u003cp data-sourcepos=\"607:3-608:94\"\u003eオブジェクトとは、JavaScript のデータの形のひとつで、\u003cbr\u003e\n「名前（キー）と値（バリュー）のセット」をまとめたものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"610:3-610:10\"\u003e例：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"611:3-613:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nl\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003etitle\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e買い物に行く\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e作業中\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"615:3-615:88\"\u003ev-for は配列だけでなくオブジェクトも繰り返し処理できます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"617:3-619:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ev-for=\"item in items\"\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"621:3-621:116\"\u003eのように書くと、配列の各要素やオブジェクトの各値を順番に取り出して使えます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"623:1-624:0\"\u003e\n\u003cdiv data-sourcepos=\"625:1-632:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"626:1-626:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"628:1-631:122\"\u003ev-for を記述したタグとその内側で\u003cbr\u003e\ntodos データの各要素のプロパティが使用できるようになります。\u003cbr\u003e\n\u003ccode\u003e\u0026lt;tr\u0026gt;\u003c/code\u003e タグの内側に\u003cbr\u003e\n「ID」「コメント」「状態変更ボタン」「削除ボタン」のカラムを追加していきましょう。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"634:1-635:0\"\u003e\n\u003ch3 data-sourcepos=\"636:1-636:25\"\u003e\n\u003cspan id=\"プロパティ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【プロパティ】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"638:1-666:5\"\u003e\n\u003cp data-sourcepos=\"638:3-639:96\"\u003eプロパティとは、STEP2 で説明した JSON と同じ構造で、\u003cbr\u003e\n「名前（プロパティ名）: 値」の組み合わせで情報を持っています。\u003c/p\u003e\n\u003cp data-sourcepos=\"641:3-641:10\"\u003e例：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"642:3-648:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003ecomment\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e買い物に行く\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e作業中\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"650:3-650:72\"\u003eJSON と JavaScript では書き方に少し違いがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"652:3-652:66\"\u003e\u003cstrong\u003eJSON（キーにダブルクオーテーション必須）\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"653:3-658:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"id\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"comment\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"買い物に行く\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"660:3-660:69\"\u003e\u003cstrong\u003eJavaScript（記号やスペースがなければ省略可）\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"661:3-666:5\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003ecomment\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e買い物に行く\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"668:1-669:0\"\u003e\n\u003cdiv data-sourcepos=\"670:1-675:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"671:1-671:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"673:1-674:48\"\u003eこのボタンはまだなにも機能しないモックのため、\u003cbr\u003e\n機能はこれから実装していきます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"677:1-678:0\"\u003e\n\u003ch3 data-sourcepos=\"679:1-679:19\"\u003e\n\u003cspan id=\"モック\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A2%E3%83%83%E3%82%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【モック】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"681:1-687:55\"\u003e\n\u003cp data-sourcepos=\"681:3-681:82\"\u003eモックとは「見た目だけ用意した仮の部品」のことです。\u003c/p\u003e\n\u003cul data-sourcepos=\"683:3-686:1\"\u003e\n\u003cli data-sourcepos=\"683:3-683:42\"\u003eボタンは画面に表示される\u003c/li\u003e\n\u003cli data-sourcepos=\"684:3-684:48\"\u003eクリックもできるように見える\u003c/li\u003e\n\u003cli data-sourcepos=\"685:3-686:1\"\u003eでも中身の処理（動作）はまだ入っていない\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"687:3-687:55\"\u003eという “形だけの状態” を指します。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"695:1-695:38\"\u003e\n\u003cspan id=\"step6-フォーム入力値の取得\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step6-%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E5%85%A5%E5%8A%9B%E5%80%A4%E3%81%AE%E5%8F%96%E5%BE%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP6 フォーム入力値の取得\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP6の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"703:1-708:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"704:1-707:47\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\n新しい ToDo をリストへ追加するための入力フォームを作成します。\u003cbr\u003e\nref 属性を使って参照するための名前をタグに付けておくと、\u003cbr\u003e\nその DOM に直接アクセスできます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"709:1-709:20\"\u003e\n\u003cspan id=\"ref-属性\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#ref-%E5%B1%9E%E6%80%A7\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ref 属性】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"710:1-730:3\"\u003e\n\u003cp data-sourcepos=\"710:3-710:114\"\u003eref（レフ）は、HTML のタグに「あとで呼び出すための名前」を付ける仕組みです。\u003c/p\u003e\n\u003cp data-sourcepos=\"712:3-714:64\"\u003eたとえるなら、\u003cbr\u003e\n学校で先生が「前から3番目の赤いカバンの子」と言うのではなく、\u003cbr\u003e\n「田中さん」と名前で呼ぶようなものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"716:3-717:41\"\u003e例：\u003cbr\u003e\n\u003ccode\u003e\u0026lt;input type=\"text\" ref=\"newComment\"\u0026gt; \u003c/code\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"719:3-720:93\"\u003eこのように ref を付けておくと、\u003cbr\u003e\nVue の中から「newComment」という名前でこの入力欄を呼び出せます。\u003c/p\u003e\n\u003cp data-sourcepos=\"722:3-722:49\"\u003eすると、こんなことができます：\u003c/p\u003e\n\u003cul data-sourcepos=\"723:3-727:3\"\u003e\n\u003cli data-sourcepos=\"723:3-723:57\"\u003e入力欄の中に書かれた文字を取り出す\u003c/li\u003e\n\u003cli data-sourcepos=\"724:3-724:90\"\u003e入力欄にカーソルを合わせる（自動で入力できる状態にする）\u003c/li\u003e\n\u003cli data-sourcepos=\"725:3-725:48\"\u003e特定の場所をスクロールさせる\u003c/li\u003e\n\u003cli data-sourcepos=\"726:3-727:3\"\u003e動画や絵を表示する特別なタグを直接さわる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"728:3-729:75\"\u003e今回は、新しい ToDo を追加するときに\u003cbr\u003e\n入力欄の中の文字を取り出すために ref を使います。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"732:1-733:0\"\u003e\n\u003ch3 data-sourcepos=\"734:1-734:25\"\u003e\n\u003cspan id=\"domドム\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#dom%E3%83%89%E3%83%A0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【DOM（ドム）】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"735:1-762:44\"\u003e\n\u003cp data-sourcepos=\"735:2-736:28\"\u003eDOM とは「ブラウザが HTML を読み取って作る、画面の部品の一覧表」\u003cbr\u003e\nのようなものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"739:3-739:38\"\u003eたとえば、Web ページには\u003c/p\u003e\n\u003cul data-sourcepos=\"740:4-746:3\"\u003e\n\u003cli data-sourcepos=\"740:4-740:16\"\u003e見出し\u003c/li\u003e\n\u003cli data-sourcepos=\"741:4-741:13\"\u003e文字\u003c/li\u003e\n\u003cli data-sourcepos=\"742:4-742:16\"\u003eボタン\u003c/li\u003e\n\u003cli data-sourcepos=\"743:4-743:16\"\u003e入力欄\u003c/li\u003e\n\u003cli data-sourcepos=\"744:4-746:3\"\u003e画像\u003cbr\u003e\nなど、いろいろな部品があります。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"747:3-748:34\"\u003eブラウザは、これらの部品を「木の枝のように、親と子の関係で並べたもの」\u003cbr\u003e\nとして覚えています。\u003c/p\u003e\n\u003cp data-sourcepos=\"750:3-750:75\"\u003eその“画面の部品の並び”のことを DOM と呼びます。\u003c/p\u003e\n\u003cp data-sourcepos=\"752:3-754:79\"\u003eたとえるなら、\u003cbr\u003e\n家の中の家具を「リビング → テーブル → イス」のように\u003cbr\u003e\nどこに何があるか整理してメモしているイメージです。\u003c/p\u003e\n\u003cp data-sourcepos=\"756:3-758:49\"\u003eVue では、この DOM の中にある部品を\u003cbr\u003e\nref で名前を付けて呼び出したり、\u003cbr\u003e\n画面の内容を変えたりできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"760:3-762:44\"\u003e今回は、入力欄（input）が DOM の中のどこにあるかを\u003cbr\u003e\nref を使って名前で呼び出し、\u003cbr\u003e\nその中の文字を取り出します。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"764:1-765:0\"\u003e\n\u003cdiv data-sourcepos=\"766:1-775:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"767:1-774:50\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\nref 属性で名前を付けたタグは、メソッド内から次のように使用できます。\u003cbr\u003e\nthis.$refs.名前\u003cbr\u003e\nテンプレートでは変数名（プロパティ名）だけでデータを使用できましたが、\u003cbr\u003e\nメソッド内でデータやメソッドを使用するときは this を付ける必要があります。\u003cbr\u003e\nたとえば、comment の場合なら次のように使用します。\u003cbr\u003e\nthis.$refs.comment.value\u003cbr\u003e\n実際は、次の STEP7 で使用します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"776:1-776:22\"\u003e\n\u003cspan id=\"メソッド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【メソッド】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"778:1-798:64\"\u003e\n\u003cp data-sourcepos=\"778:3-778:127\"\u003eメソッドとは、「ボタンを押したときに実行したい処理」をまとめて書いておく場所です。\u003c/p\u003e\n\u003cp data-sourcepos=\"780:3-781:120\"\u003eたとえるなら、\u003cbr\u003e\n\u003cstrong\u003e「このボタンを押したら、こう動いてね」という動きの説明書\u003c/strong\u003e のようなものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"783:3-783:10\"\u003e例：\u003c/p\u003e\n\u003cul data-sourcepos=\"784:3-787:3\"\u003e\n\u003cli data-sourcepos=\"784:3-784:36\"\u003e新しい ToDo を追加する\u003c/li\u003e\n\u003cli data-sourcepos=\"785:3-785:26\"\u003eToDo を削除する\u003c/li\u003e\n\u003cli data-sourcepos=\"786:3-787:3\"\u003e状態（作業中／完了）を切り替える\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"788:3-789:46\"\u003eVue では、メソッドの中でデータや ref を使うときは\u003cbr\u003e\n\u003cstrong\u003ethis を付けて呼び出します。\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"791:3-794:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003epush\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003e新しいデータ\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003e$refs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecomment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"796:3-798:64\"\u003eこのように書くことで、\u003cbr\u003e\n「Vue が持っているデータ」や「ref で名前を付けた入力欄」\u003cbr\u003e\nをメソッドの中から使えるようになります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"800:1-801:0\"\u003e\n\u003ch3 data-sourcepos=\"802:1-802:28\"\u003e\n\u003cspan id=\"テンプレート\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【テンプレート】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"804:1-819:36\"\u003e\n\u003cp data-sourcepos=\"804:3-804:106\"\u003eテンプレートとは、「画面にどんな見た目で表示するか」を書く場所です。\u003c/p\u003e\n\u003cp data-sourcepos=\"806:3-807:72\"\u003eたとえるなら、\u003cbr\u003e\n\u003cstrong\u003e家を建てるときの間取り図\u003c/strong\u003e のようなものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"809:3-810:69\"\u003eVue では HTML の中にテンプレートを書き、\u003cbr\u003e\nその中で Vue のデータを使って画面を作ります。\u003c/p\u003e\n\u003cp data-sourcepos=\"812:3-812:10\"\u003e例：\u003c/p\u003e\n\u003cul data-sourcepos=\"813:3-816:3\"\u003e\n\u003cli data-sourcepos=\"813:3-813:61\"\u003e\n\u003ccode\u003e{{ message }}\u003c/code\u003e → message の中身が表示される\u003c/li\u003e\n\u003cli data-sourcepos=\"814:3-814:54\"\u003e\n\u003ccode\u003ev-for\u003c/code\u003e → 配列の数だけ繰り返し表示\u003c/li\u003e\n\u003cli data-sourcepos=\"815:3-816:3\"\u003e\n\u003ccode\u003ev-if\u003c/code\u003e → 条件で表示・非表示を切り替え\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"817:3-819:36\"\u003eテンプレート内では、Vue のデータを\u003cbr\u003e\n\u003cstrong\u003eそのまま名前を書くことで使用できます。\u003c/strong\u003e\u003cbr\u003e\n（例：\u003ccode\u003e{{ todos.length }}\u003c/code\u003e）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"821:1-823:0\"\u003e\n\u003cdiv data-sourcepos=\"824:1-831:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"825:1-825:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"827:1-830:70\"\u003ev-model ディレクティブを使えばデータとフォーム入力を同期することもできますが、\u003cbr\u003e\n今回は入力したデータを画面に表示させないのと\u003cbr\u003e\n常にデータとして持っている必要がないため、\u003cbr\u003e\nこの $refs を使って入力値を取得することにします。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"833:1-833:17\"\u003e\n\u003cspan id=\"v-model\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#v-model\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【v-model】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"834:1-840:78\"\u003e\n\u003cp data-sourcepos=\"834:3-837:64\"\u003ev-model は、入力欄と Vue のデータを \u003cstrong\u003e自動で同期する仕組み\u003c/strong\u003e です。\u003cbr\u003e\n(同期とは「入力欄とデータが常に同じ状態になる」という意味です。)\u003cbr\u003e\n入力欄に書いた文字がそのままデータにも反映されるため、\u003cbr\u003e\n入力欄とデータが常に同じ状態になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"839:3-840:78\"\u003e今回は「入力値を保持する必要がない」ため、\u003cbr\u003e\nv-model ではなく \u003cstrong\u003e$refs で必要なときだけ取得\u003c/strong\u003e します。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"842:1-845:0\"\u003e\n\u003cdiv data-sourcepos=\"846:1-855:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"847:1-847:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"849:1-850:27\"\u003eテーブルの下あたりに追加しておきます。\u003cbr\u003e\nv-on:submit.prevent=\"doAdd\"\u003c/p\u003e\n\u003cp data-sourcepos=\"852:1-854:97\"\u003eこの v-on ディレクティブによって、\u003cbr\u003e\nボタンをクリックしたり入力フォームでエンターを押してフォームのサブミットが行われると、\u003cbr\u003e\nそれをハンドリングして doAdd メソッドが呼び出されるようになります。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"856:1-856:14\"\u003e\n\u003cspan id=\"v-on\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#v-on\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【v-on】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"858:1-880:3\"\u003e\n\u003cp data-sourcepos=\"858:3-859:79\"\u003ev-on は、「〇〇されたら、この動きをしてね」と\u003cbr\u003e\nボタンや入力欄に“合図”をつけるための仕組みです。\u003c/p\u003e\n\u003cp data-sourcepos=\"861:3-864:84\"\u003eたとえるなら、\u003cbr\u003e\n「チャイムが鳴ったら席に着く」\u003cbr\u003e\n「ボールを投げられたらキャッチする」\u003cbr\u003e\nといった “合図と動き” のセットを決めるイメージです。\u003c/p\u003e\n\u003cp data-sourcepos=\"866:3-868:28\"\u003e今回の例では、\u003cbr\u003e\n\u003ccode\u003ev-on:submit.prevent=\"doAdd\"\u003c/code\u003e\u003cbr\u003e\nと書いています。\u003c/p\u003e\n\u003cp data-sourcepos=\"870:3-870:16\"\u003eこれは、\u003c/p\u003e\n\u003cul data-sourcepos=\"871:3-874:3\"\u003e\n\u003cli data-sourcepos=\"871:3-871:82\"\u003eフォームが送信されたら（ボタンを押す or Enter を押す）\u003c/li\u003e\n\u003cli data-sourcepos=\"872:3-872:70\"\u003eページを再読み込みしないようにして（prevent）\u003c/li\u003e\n\u003cli data-sourcepos=\"873:3-874:3\"\u003edoAdd メソッドを実行する\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"875:3-875:37\"\u003eという合図になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"877:3-879:43\"\u003eつまり v-on は、\u003cbr\u003e\n\u003cstrong\u003e「どんな操作が行われたときに、どんなメソッドを動かすか」\u003c/strong\u003e\u003cbr\u003e\nを決めるための仕組みです。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"882:2-883:2\"\u003e\n\u003ch3 data-sourcepos=\"884:1-884:37\"\u003e\n\u003cspan id=\"サブミットsubmit\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B5%E3%83%96%E3%83%9F%E3%83%83%E3%83%88submit\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【サブミット（submit）】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"885:1-901:3\"\u003e\n\u003cp data-sourcepos=\"885:3-885:85\"\u003eサブミットとは、「フォームを送信する」という意味です。\u003c/p\u003e\n\u003cp data-sourcepos=\"887:3-888:70\"\u003e入力フォームには、名前やコメントなどを入力して、\u003cbr\u003e\n最後に「送信」ボタンを押す仕組みがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"890:3-891:54\"\u003eWeb では、この “送信する” という動きを\u003cbr\u003e\n\u003cstrong\u003esubmit（サブミット）\u003c/strong\u003e と呼びます。\u003c/p\u003e\n\u003cp data-sourcepos=\"893:3-893:19\"\u003eたとえば、\u003c/p\u003e\n\u003cul data-sourcepos=\"894:3-896:3\"\u003e\n\u003cli data-sourcepos=\"894:3-894:42\"\u003eボタンをクリックしたとき\u003c/li\u003e\n\u003cli data-sourcepos=\"895:3-896:3\"\u003e入力欄で Enter キーを押したとき\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"897:3-897:97\"\u003eにフォームが送信されると、それが「サブミットされた」状態です。\u003c/p\u003e\n\u003cp data-sourcepos=\"899:3-900:81\"\u003eVue では、このサブミットのタイミングを\u003cbr\u003e\n\u003ccode\u003ev-on:submit\u003c/code\u003e で受信して、好きなメソッドを実行できます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"903:2-905:2\"\u003e\n\u003ch3 data-sourcepos=\"906:1-906:42\"\u003e\n\u003cspan id=\"ハンドリングhandling\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%8F%E3%83%B3%E3%83%89%E3%83%AA%E3%83%B3%E3%82%B0handling\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ハンドリング（handling）】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"907:1-922:53\"\u003e\n\u003cp data-sourcepos=\"907:3-907:124\"\u003eハンドリングとは、「起きた出来事に対して、どう動くかを決めて実行すること」です。\u003c/p\u003e\n\u003cp data-sourcepos=\"909:3-909:25\"\u003eたとえるなら、\u003c/p\u003e\n\u003cul data-sourcepos=\"910:3-912:3\"\u003e\n\u003cli data-sourcepos=\"910:3-910:45\"\u003eチャイムが鳴ったら席に着く\u003c/li\u003e\n\u003cli data-sourcepos=\"911:3-912:3\"\u003eボールが飛んできたらキャッチする\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"913:3-913:75\"\u003eといった “合図に対して行動する” イメージです。\u003c/p\u003e\n\u003cp data-sourcepos=\"915:3-915:141\"\u003eWeb では、クリックされた、送信された、入力された、といった出来事（イベント）が起きたときに、\u003c/p\u003e\n\u003cp data-sourcepos=\"917:3-918:66\"\u003eそれに合わせて処理を実行することを\u003cbr\u003e\n\u003cstrong\u003eイベントをハンドリングする\u003c/strong\u003e と言います。\u003c/p\u003e\n\u003cp data-sourcepos=\"920:3-922:53\"\u003eVue では、v-on を使って\u003cbr\u003e\n\u003cstrong\u003e「このイベントが起きたら、このメソッドを実行する」\u003c/strong\u003e\u003cbr\u003e\nというハンドリングを設定できます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"930:1-930:29\"\u003e\n\u003cspan id=\"step7-リストへの追加\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step7-%E3%83%AA%E3%82%B9%E3%83%88%E3%81%B8%E3%81%AE%E8%BF%BD%E5%8A%A0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP7 リストへの追加\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP7の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"938:1-944:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"939:1-939:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"941:1-943:96\"\u003eつづいて doAdd メソッドを定義しましょう。\u003cbr\u003e\nこのメソッドは、フォームの入力値を取得して新しい ToDo の追加処理をします。\u003cbr\u003e\nルートコンストラクタの methods オプションに、メソッドを登録します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"946:1-946:40\"\u003e\n\u003cspan id=\"ルートコンストラクタ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AB%E3%83%BC%E3%83%88%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ルートコンストラクタ】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"949:1-959:85\"\u003e\n\u003cp data-sourcepos=\"949:3-950:104\"\u003eルートコンストラクタとは、Vue アプリ全体の設定を書いておく場所です。\u003cbr\u003e\nSTEP1 で説明したように、Vue という名前そのものがコンストラクタ関数で、\u003c/p\u003e\n\u003cp data-sourcepos=\"953:3-956:69\"\u003e\u003ccode\u003enew Vue({ ... })\u003c/code\u003e の \u003ccode\u003e{ ... }\u003c/code\u003e に書く設定が\u003cbr\u003e\n「ルートコンストラクタ」にあたり、\u003cbr\u003e\n\u003ccode\u003eel\u003c/code\u003e や \u003ccode\u003edata\u003c/code\u003e、\u003ccode\u003emethods\u003c/code\u003e などアプリ全体の設計図が入っています。\u003cbr\u003e\n（家を建てるための “設計図” のようなもの）\u003c/p\u003e\n\u003cp data-sourcepos=\"958:3-959:85\"\u003eVue はこのルートコンストラクタ（設計図）を読み取り、実際にアプリを作り上げます。\u003cbr\u003e\nその結果できあがるのが \u003cstrong\u003e「ルートインスタンス」\u003c/strong\u003e です。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"961:2-962:1\"\u003e\n\u003ch3 data-sourcepos=\"963:1-963:33\"\u003e\n\u003cspan id=\"methods-オプション\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#methods-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【methods オプション】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"964:1-979:78\"\u003e\n\u003cp data-sourcepos=\"964:3-964:118\"\u003emethods オプションは、Vue の中で使う \u003cstrong\u003e処理（メソッド）\u003c/strong\u003e をまとめて書く場所です。\u003c/p\u003e\n\u003cp data-sourcepos=\"966:3-966:124\"\u003eボタンを押したときなど、ユーザーの操作に応じて実行したい動きをここに定義します。\u003c/p\u003e\n\u003cp data-sourcepos=\"968:3-968:116\"\u003emethods に登録したメソッドは、\u003ccode\u003e@click\u003c/code\u003e や \u003ccode\u003e@submit\u003c/code\u003e などのイベントから呼び出せます。\u003c/p\u003e\n\u003cp data-sourcepos=\"970:3-971:49\"\u003eまた、methods の中で \u003ccode\u003edata\u003c/code\u003e や \u003ccode\u003e$refs\u003c/code\u003e を使うときは\u003cbr\u003e\n\u003cstrong\u003ethis を付けてアクセスします。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"973:3-973:10\"\u003e例：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"974:3-977:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003epush\u003c/span\u003e\u003cspan class=\"p\"\u003e(...)\u003c/span\u003e        \u003cspan class=\"c1\"\u003e// データの追加\u003c/span\u003e\n\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003e$refs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecomment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// 入力欄の値を取得\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"979:3-979:78\"\u003e今回の doAdd メソッドも、この methods の中に定義します。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"981:1-982:0\"\u003e\n\u003cdiv data-sourcepos=\"983:1-988:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"984:1-984:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"986:1-987:96\"\u003eコメントと一緒に1行づつ読んでみてください。\u003cbr\u003e\n通常の配列メソッド push を使うだけで、リストデータへ追加できます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"990:1-990:33\"\u003e\n\u003cspan id=\"配列メソッド-push\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%85%8D%E5%88%97%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89-push\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【配列メソッド push】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"992:1-1031:95\"\u003e\n\u003cp data-sourcepos=\"992:3-993:102\"\u003e\u003cstrong\u003epush とは？\u003c/strong\u003e\u003cbr\u003e\nJavaScript の配列に \u003cstrong\u003e新しい要素を一番うしろに追加する\u003c/strong\u003e メソッドです。\u003c/p\u003e\n\u003cp data-sourcepos=\"995:3-995:10\"\u003e例：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"996:3-1000:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003elist\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003cspan class=\"nx\"\u003elist\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003epush\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e15\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// → [1, 2, 3, 15]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1002:3-1002:67\"\u003eVue の \u003ccode\u003edata\u003c/code\u003e にある \u003ccode\u003etodos\u003c/code\u003e もただの配列なので、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1004:3-1010:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003epush\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003etodoStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003euid\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ecomment\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003ecomment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1012:3-1016:95\"\u003eと書くだけで、新しい ToDo をリストに追加できます。\u003cbr\u003e\ntodoStorage.uid++ とは？\u003cbr\u003e\ntodoStorage.uid は「次に使う ID の番号」を覚えておくための箱（変数）です。\u003cbr\u003e\nuid++ は\u003cbr\u003e\n「今の番号を使ってから、1つ増やしておく」という意味になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1018:2-1018:40\"\u003eたとえば最初の値が 1 なら：\u003c/p\u003e\n\u003cp data-sourcepos=\"1020:2-1020:35\"\u003e新しい ToDo に id: 1 を使う\u003c/p\u003e\n\u003cp data-sourcepos=\"1022:2-1022:42\"\u003eそのあと uid の値が 2 に増える\u003c/p\u003e\n\u003cp data-sourcepos=\"1024:2-1024:34\"\u003eという動きになります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1026:2-1028:34\"\u003eこうすることで、ToDo を追加するたびに\u003cbr\u003e\n1, 2, 3, 4… と重複しない ID を自動で割り振れる\u003cbr\u003e\n仕組みになっています。\u003c/p\u003e\n\u003cp data-sourcepos=\"1030:3-1031:95\"\u003eVue は配列の変化を自動で監視しているため、\u003cbr\u003e\n\u003cstrong\u003epush でデータを追加すると画面のリストも自動的に更新されます。\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"1038:1-1038:47\"\u003e\n\u003cspan id=\"step8-ストレージへの保存の自動化\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step8-%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8%E3%81%B8%E3%81%AE%E4%BF%9D%E5%AD%98%E3%81%AE%E8%87%AA%E5%8B%95%E5%8C%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP8 ストレージへの保存の自動化\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP8の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"1045:1-1060:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1046:1-1046:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"1048:1-1050:63\"\u003eさて、JavaScript 内ではデータは追加されましたが、\u003cbr\u003e\nこれではまだローカルストレージに保存されていません。\u003cbr\u003e\nブラウザをリロードしたら消えてしまいます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1052:1-1053:96\"\u003edoAdd メソッドの最後に todoStorage.save メソッドを使って保存してもよいのですが、\u003cbr\u003e\n追加・削除・作業状態の変更すべて同じ処理をしなければいけません。\u003c/p\u003e\n\u003cp data-sourcepos=\"1055:1-1056:94\"\u003etodos データの内容が変わると、自動的にストレージへ保存してくれたら素敵ですね。\u003cbr\u003e\nこれは watch オプションの「ウォッチャ」機能を使うことで可能です。\u003c/p\u003e\n\u003cp data-sourcepos=\"1058:1-1059:126\"\u003eウォッチャはデータの変化に反応して、あらかじめ登録しておいた処理を自動的に行います。\u003cbr\u003e\nこれで、todos データに何か変化があれば自動的にストレージへ保存されるようになりました。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"1062:1-1063:0\"\u003e\n\u003ch3 data-sourcepos=\"1064:1-1064:58\"\u003e\n\u003cspan id=\"リロードするとデータが消える理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AA%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B%E3%81%A8%E3%83%87%E3%83%BC%E3%82%BF%E3%81%8C%E6%B6%88%E3%81%88%E3%82%8B%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【リロード】するとデータが消える理由\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1066:1-1078:65\"\u003e\n\u003cp data-sourcepos=\"1066:3-1066:115\"\u003eリロードとは、ブラウザで表示しているページを「もう一度読み直す」ことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1068:3-1069:59\"\u003eページを読み直すと、そのページで動いていた JavaScript も\u003cbr\u003e\n\u003cstrong\u003eいったんすべて初期状態に戻ります。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"1071:3-1073:76\"\u003eそのため、画面上で ToDo を追加しても、\u003cbr\u003e\nそれは JavaScript のメモリの中にあるだけで、\u003cbr\u003e\nページをリロードするとデータは消えてしまいます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1075:3-1076:52\"\u003eこれを防ぐには、ページを閉じても残る場所に\u003cbr\u003e\nデータを保存する必要があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1078:3-1078:65\"\u003eその保存場所が \u003cstrong\u003eローカルストレージ\u003c/strong\u003e です。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1080:1-1081:0\"\u003e\n\u003ch3 data-sourcepos=\"1082:1-1082:66\"\u003e\n\u003cspan id=\"todostoragesave-メソッドwatch-オプション\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#todostoragesave-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89watch-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【todoStorage.save メソッド】【watch オプション】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1084:1-1121:91\"\u003e\n\u003cp data-sourcepos=\"1084:3-1085:110\"\u003e\u003cstrong\u003etodoStorage.save とは？\u003c/strong\u003e\u003cbr\u003e\n現在の todos データをブラウザのローカルストレージに保存するメソッドです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1087:3-1089:82\"\u003esave メソッドでは、まず todos（配列）を\u003cbr\u003e\n\u003ccode\u003eJSON.stringify\u003c/code\u003e を使って \u003cstrong\u003e文字列に変換\u003c/strong\u003e します。\u003cbr\u003e\nローカルストレージは文字列しか保存できないためです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1091:3-1091:16\"\u003eそして：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1092:3-1094:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003elocalStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esetItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eSTORAGE_KEY\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1096:3-1097:49\"\u003eとすることで、STORAGE_KEY という名前の箱に\u003cbr\u003e\n現在の ToDo リストを保存します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1099:3-1099:36\"\u003esave メソッドはつまり：\u003c/p\u003e\n\u003cul data-sourcepos=\"1100:3-1102:3\"\u003e\n\u003cli data-sourcepos=\"1100:3-1100:39\"\u003etodos を文字列に変換する\u003c/li\u003e\n\u003cli data-sourcepos=\"1101:3-1102:3\"\u003eローカルストレージに書き込む\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1103:3-1103:58\"\u003eという 2 つの処理をまとめたものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1105:3-1105:88\"\u003esave を呼び出すだけで、その時点の ToDo リストが保存されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1107:2-1109:38\"\u003ewatch（ウォッチ）オプションとは？\u003cbr\u003e\nwatch は「特定のデータに変化があったとき、自動で処理を実行する仕組み」です。\u003cbr\u003e\n今回は todos を監視します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1111:2-1111:16\"\u003eたとえば：\u003c/p\u003e\n\u003cul data-sourcepos=\"1113:3-1116:1\"\u003e\n\u003cli data-sourcepos=\"1113:3-1113:46\"\u003etodos に新しい ToDo が追加された\u003c/li\u003e\n\u003cli data-sourcepos=\"1114:3-1114:27\"\u003eToDo が削除された\u003c/li\u003e\n\u003cli data-sourcepos=\"1115:3-1116:1\"\u003e状態（作業中／完了）が切り替わった\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1117:2-1118:79\"\u003eこうした todos の変化を見張って（ウォッチして）、\u003cbr\u003e\n変化が起きた瞬間に自動で todoStorage.save を呼び出します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1120:2-1121:91\"\u003eつまり watch を使うことで、\u003cbr\u003e\n「todos が変わったら必ず保存する」という動きを自動化できます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1122:1-1123:0\"\u003e\n\u003ch3 data-sourcepos=\"1124:1-1124:62\"\u003e\n\u003cspan id=\"なぜ-doadd-の中で-save-を使わないのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C-doadd-%E3%81%AE%E4%B8%AD%E3%81%A7-save-%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%84%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【なぜ doAdd の中で save を使わないのか？】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1126:1-1146:77\"\u003e\n\u003cp data-sourcepos=\"1126:3-1126:90\"\u003eToDo アプリでは、保存が必要になるタイミングが複数あります：\u003c/p\u003e\n\u003cul data-sourcepos=\"1127:3-1130:3\"\u003e\n\u003cli data-sourcepos=\"1127:3-1127:39\"\u003eやることを追加したとき\u003c/li\u003e\n\u003cli data-sourcepos=\"1128:3-1128:39\"\u003eやることを削除したとき\u003c/li\u003e\n\u003cli data-sourcepos=\"1129:3-1130:3\"\u003e状態（作業中／完了）を切り替えたとき\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1131:3-1132:93\"\u003eこれらすべての処理の中に save を書くと、\u003cbr\u003e\n\u003cstrong\u003e同じコードを何度も書くことになり、管理が大変\u003c/strong\u003e になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1134:3-1134:97\"\u003eまた、どこかで書き忘れると保存されず、バグの原因にもなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1136:3-1138:78\"\u003eそこで Vue の \u003cstrong\u003ewatch（ウォッチャ）\u003c/strong\u003e を使うことで、\u003cbr\u003e\ntodos データに変化があった瞬間に\u003cbr\u003e\n\u003cstrong\u003e自動的に todoStorage.save を呼び出す\u003c/strong\u003e ようにできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1140:3-1140:16\"\u003eつまり：\u003c/p\u003e\n\u003cul data-sourcepos=\"1141:3-1144:3\"\u003e\n\u003cli data-sourcepos=\"1141:3-1141:90\"\u003e「追加・削除・状態変更のたびに save を書く必要がなくなる」\u003c/li\u003e\n\u003cli data-sourcepos=\"1142:3-1142:45\"\u003e「書き忘れやミスを防げる」\u003c/li\u003e\n\u003cli data-sourcepos=\"1143:3-1144:3\"\u003e「コードがすっきりする」\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1145:3-1146:77\"\u003eというメリットがあるため、\u003cbr\u003e\ndoAdd の中で save を直接使わないようにしているのです。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"1153:1-1153:50\"\u003e\n\u003cspan id=\"step9-保存されたリストを取得しよう\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step9-%E4%BF%9D%E5%AD%98%E3%81%95%E3%82%8C%E3%81%9F%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%8F%96%E5%BE%97%E3%81%97%E3%82%88%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP9 保存されたリストを取得しよう\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP9の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"1159:1-1173:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1160:1-1160:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"1162:1-1165:55\"\u003eストレージへの保存ができたので、次はストレージからの取得です。\u003cbr\u003e\nこのアプリケーションの「インスタンス作成時」に、\u003cbr\u003e\nローカルストレージに保存されているデータを「自動的」に取得して、\u003cbr\u003e\nVue.js のデータとして読み込みましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"1167:1-1168:72\"\u003e特定のタイミングに何か処理をはさみたいときは\u003cbr\u003e\n「ライフサイクルフック」のメソッドを使用します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1170:1-1171:102\"\u003eタイミングがいくつか用意されていますが、\u003cbr\u003e\n今回の「インスタンス作成時」には created メソッドを使うとよいでしょう。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003chr data-sourcepos=\"1175:1-1176:0\"\u003e\n\u003ch3 data-sourcepos=\"1177:1-1177:28\"\u003e\n\u003cspan id=\"インスタンス\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【インスタンス】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1179:1-1190:41\"\u003e\n\u003cp data-sourcepos=\"1179:3-1180:71\"\u003eインスタンスとは、STEP1 で作った\u003cbr\u003e\n\u003ccode\u003enew Vue({ ... })\u003c/code\u003e によって作られるもののことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1182:3-1184:70\"\u003e\u003cstrong\u003eVue.js のデータとして読み込む\u003c/strong\u003e とは、\u003cbr\u003e\nローカルストレージに保存されていた ToDo の一覧を\u003cbr\u003e\nVue インスタンスの \u003ccode\u003edata\u003c/code\u003e にセットすることです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1186:3-1186:22\"\u003e具体的には：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1187:3-1189:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003etodoStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003efetch\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1190:3-1190:41\"\u003eの部分で読み込んでいます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1192:1-1193:0\"\u003e\n\u003ch3 data-sourcepos=\"1194:1-1194:66\"\u003e\n\u003cspan id=\"ライフサイクルフックcreated-メソッド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%95%E3%83%83%E3%82%AFcreated-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【ライフサイクルフック】【created メソッド】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1196:1-1215:1\"\u003e\n\u003cp data-sourcepos=\"1196:3-1198:73\"\u003eVue インスタンス（\u003ccode\u003enew Vue(...)\u003c/code\u003e で作られるもの）は、\u003cbr\u003e\n生成されてから画面に表示され、破棄されるまでの間に\u003cbr\u003e\nいくつかの \u003cstrong\u003e決まったタイミング\u003c/strong\u003e を通過します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1200:3-1201:48\"\u003eそのそれぞれのタイミングで処理を差し込める仕組みが\u003cbr\u003e\n\u003cstrong\u003eライフサイクルフック\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"1203:3-1203:10\"\u003e例：\u003c/p\u003e\n\u003cul data-sourcepos=\"1204:3-1208:3\"\u003e\n\u003cli data-sourcepos=\"1204:3-1204:58\"\u003eインスタンスが作られた直後（created）\u003c/li\u003e\n\u003cli data-sourcepos=\"1205:3-1205:53\"\u003e画面に表示される直前（beforeMount）\u003c/li\u003e\n\u003cli data-sourcepos=\"1206:3-1206:49\"\u003e画面に表示された直後（mounted）\u003c/li\u003e\n\u003cli data-sourcepos=\"1207:3-1208:3\"\u003eインスタンスが破棄される直前（beforeDestroy）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1209:3-1209:83\"\u003e今回の「インスタンス作成時」には \u003cstrong\u003ecreated\u003c/strong\u003e を使います。\u003c/p\u003e\n\u003cp data-sourcepos=\"1211:3-1214:82\"\u003ecreated は、Vue がインスタンスを作り、\u003cbr\u003e\n「\u003ccode\u003edata\u003c/code\u003e が使える状態になった直後」に呼ばれるメソッドのため、\u003cbr\u003e\nローカルストレージからデータを読み込むのに最適です。\u003cbr\u003e\nまたcreatedはmethodsの中ではなく、dataと同じ階層に書きます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1218:1-1219:0\"\u003e\n\u003cdiv data-sourcepos=\"1220:1-1229:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1221:1-1223:39\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\nデータの取得には、先に作っておいた todoStorage オブジェクトの\u003cbr\u003e\nfetch メソッドを使用します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1225:1-1225:113\"\u003eライフサイクルメソッドの定義は「methods の中ではない」ことに注意してください。\u003c/p\u003e\n\u003cp data-sourcepos=\"1227:1-1228:63\"\u003eローカルストレージは Ajax と違い同期的に結果を取得できるため、\u003cbr\u003e\n返り値を代入すればいいだけなので簡単です！\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"1232:1-1232:28\"\u003e\n\u003cspan id=\"fetch-メソッド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#fetch-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【fetch メソッド】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1234:1-1243:44\"\u003e\n\u003cp data-sourcepos=\"1234:3-1236:122\"\u003efetch メソッドは、ローカルストレージに保存されている\u003cbr\u003e\nToDo リストの \u003cstrong\u003e文字列データを取り出し\u003c/strong\u003e、\u003cbr\u003e\n\u003ccode\u003eJSON.parse\u003c/code\u003eという 関数 を使って \u003cstrong\u003e文字列をJavaScript の配列に戻して返す\u003c/strong\u003e メソッドです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1238:3-1238:37\"\u003eそのため created の中で：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1239:3-1241:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003etodoStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003efetch\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1242:3-1243:44\"\u003eと書くだけで、保存されていた ToDo リストを\u003cbr\u003e\nVue の \u003ccode\u003edata\u003c/code\u003e にセットできます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1245:1-1246:0\"\u003e\n\u003ch3 data-sourcepos=\"1247:1-1247:53\"\u003e\n\u003cspan id=\"ajaxエイジャックスとの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#ajax%E3%82%A8%E3%82%A4%E3%82%B8%E3%83%A3%E3%83%83%E3%82%AF%E3%82%B9%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【Ajax（エイジャックス）との違い】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1249:1-1275:38\"\u003e\n\u003cp data-sourcepos=\"1249:3-1250:52\"\u003eAjax は、Web ページを \u003cstrong\u003e再読み込みせずに\u003c/strong\u003e サーバーと通信して\u003cbr\u003e\nデータの送受信を行う仕組みです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1252:3-1252:10\"\u003e例：\u003c/p\u003e\n\u003cul data-sourcepos=\"1253:3-1255:3\"\u003e\n\u003cli data-sourcepos=\"1253:3-1253:72\"\u003eボタンを押したらサーバーから最新データを取得\u003c/li\u003e\n\u003cli data-sourcepos=\"1254:3-1255:3\"\u003eページをリロードせずに検索結果を表示\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1256:3-1256:66\"\u003eAjax の特徴は \u003cstrong\u003e非同期処理\u003c/strong\u003e であることです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1258:3-1259:61\"\u003e非同期とは、データが返ってくるまで待たずに\u003cbr\u003e\n次の処理がどんどん進む動きのことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1261:3-1261:43\"\u003eそのため Ajax を使う場合は：\u003c/p\u003e\n\u003cul data-sourcepos=\"1262:3-1265:3\"\u003e\n\u003cli data-sourcepos=\"1262:3-1262:30\"\u003eコールバック関数\u003c/li\u003e\n\u003cli data-sourcepos=\"1263:3-1263:23\"\u003ePromise（then）\u003c/li\u003e\n\u003cli data-sourcepos=\"1264:3-1265:3\"\u003easync / await\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1266:3-1266:100\"\u003eなどを使って「データが返ってきた後の処理」を書く必要があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1268:3-1269:60\"\u003e一方、ローカルストレージは Ajax と違い\u003cbr\u003e\n\u003cstrong\u003e同期的にすぐ結果が返ってくる\u003c/strong\u003e ため、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1271:3-1273:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003etodoStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003efetch\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1275:3-1275:38\"\u003eと書くだけで完了します。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1277:1-1278:0\"\u003e\n\u003ch3 data-sourcepos=\"1279:1-1279:19\"\u003e\n\u003cspan id=\"返り値\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%BF%94%E3%82%8A%E5%80%A4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【返り値】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1281:1-1294:61\"\u003e\n\u003cp data-sourcepos=\"1281:3-1282:102\"\u003e返り値とは、\u003cbr\u003e\n\u003cstrong\u003e「関数やメソッドが実行されたあとに返してくる結果」\u003c/strong\u003e のことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1284:3-1284:10\"\u003e例：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1285:3-1287:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1288:3-1288:79\"\u003eこの場合、計算結果の \u003cstrong\u003e3\u003c/strong\u003e が返り値で、x に入ります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1290:3-1290:31\"\u003e関数でも同じです：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1291:3-1293:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003eresult\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003emyFunction\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1294:3-1294:61\"\u003e\u003ccode\u003emyFunction()\u003c/code\u003e が返した値が result に入ります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"1298:1-1298:42\"\u003e\n\u003cspan id=\"step10-状態の変更と削除の処理\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step10-%E7%8A%B6%E6%85%8B%E3%81%AE%E5%A4%89%E6%9B%B4%E3%81%A8%E5%89%8A%E9%99%A4%E3%81%AE%E5%87%A6%E7%90%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP10 状態の変更と削除の処理\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP10の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"1306:1-1310:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1307:1-1309:82\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\nつづいて「状態の変更」と「削除」機能を実装しましょう。 methods オプションにそれぞれのメソッドを作成します。\u003cbr\u003e\ndoChangeState メソッド（状態変更）item.state の値を反転します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"1313:1-1313:35\"\u003e\n\u003cspan id=\"dochangestateメソッド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#dochangestate%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【doChangeStateメソッド】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1315:1-1348:92\"\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1315:3-1319:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003edoChangeState\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nf\"\u003efunction \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1321:3-1321:29\"\u003e\u003cstrong\u003eコードの解説：\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"1322:3-1326:3\"\u003e\n\u003cli data-sourcepos=\"1322:3-1323:89\"\u003eitem.state が \u003cstrong\u003e1（完了）\u003c/strong\u003e の場合\u003cbr\u003e\n→ \u003ccode\u003e!item.state\u003c/code\u003e は false になるので \u003cstrong\u003e0（作業中）\u003c/strong\u003e が代入される\u003c/li\u003e\n\u003cli data-sourcepos=\"1324:3-1326:3\"\u003eitem.state が \u003cstrong\u003e0（作業中）\u003c/strong\u003e の場合\u003cbr\u003e\n→ \u003ccode\u003e!item.state\u003c/code\u003e は true になるので \u003cstrong\u003e1（完了）\u003c/strong\u003e が代入される\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1327:3-1327:103\"\u003e三項演算子を使って、状態を 0 と 1 で切り替える仕組みになっています。\u003c/p\u003e\n\u003cp data-sourcepos=\"1329:3-1330:63\"\u003e\u003cstrong\u003e意味としては：\u003c/strong\u003e\u003cbr\u003e\n「今の状態が 1 なら 0 に、0 なら 1 にする」\u003c/p\u003e\n\u003cp data-sourcepos=\"1332:3-1333:73\"\u003e\u003cstrong\u003e三項演算子とは？\u003c/strong\u003e\u003cbr\u003e\nJavaScript の「短く書ける if 文」のようなものです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1335:3-1337:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003e条件式\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"nx\"\u003e条件が\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"nx\"\u003eのときの値\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003e条件が\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e \u003cspan class=\"nx\"\u003eのときの値\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1339:3-1339:52\"\u003eif 文で書くと次のようになります：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1340:3-1346:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1348:3-1348:92\"\u003e単純な条件分岐の場合は、三項演算子を使うほうが短く書けます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1350:1-1351:0\"\u003e\n\u003cdiv data-sourcepos=\"1352:1-1358:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1353:1-1353:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"1355:1-1356:96\"\u003edoRemove メソッド（削除）\u003cbr\u003e\nインデックスを取得して配列メソッドの splice を使って削除します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"1360:1-1360:31\"\u003e\n\u003cspan id=\"doremove-メソッド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#doremove-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【doRemove メソッド】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1362:1-1412:35\"\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1362:3-1367:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003edoRemove\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nf\"\u003efunction \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003eindex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eindexOf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esplice\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eindex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1369:3-1370:54\"\u003eこのメソッドは、クリックされた ToDo（＝item）を\u003cbr\u003e\n\u003cstrong\u003etodos 配列から削除する処理\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"1372:3-1372:76\"\u003e削除した後は、画面の一覧からも自動的に消えます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1374:3-1374:34\"\u003eテンプレート側では：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"1375:3-1377:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e@click=\"doRemove(item)\"\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1378:3-1378:107\"\u003eのように呼び出され、削除したい ToDo オブジェクトが item として渡されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1380:2-1380:20\"\u003eコードの流れ:\u003c/p\u003e\n\u003cp data-sourcepos=\"1382:3-1382:56\"\u003e\u003cstrong\u003e1. 配列の中で item が何番目か調べる\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1383:3-1385:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003evar\u003c/span\u003e \u003cspan class=\"nx\"\u003eindex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eindexOf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cul data-sourcepos=\"1386:3-1388:3\"\u003e\n\u003cli data-sourcepos=\"1386:3-1386:55\"\u003e\n\u003ccode\u003ethis.todos\u003c/code\u003e は ToDo の一覧が入った配列\u003c/li\u003e\n\u003cli data-sourcepos=\"1387:3-1388:3\"\u003e\n\u003ccode\u003eindexOf(item)\u003c/code\u003e は、配列の中で item が最初に見つかった位置（index）を返す\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1389:3-1389:14\"\u003e\u003cstrong\u003e例：\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1390:3-1393:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003eA\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eB\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eC\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eindexOf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eB\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// → 結果は1になる（配列は 0 番目から数えるため）\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1398:3-1398:57\"\u003e\u003cstrong\u003e2. splice でその位置の要素を削除する\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1399:3-1401:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etodos\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003esplice\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eindex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cul data-sourcepos=\"1402:3-1404:3\"\u003e\n\u003cli data-sourcepos=\"1402:3-1402:61\"\u003e\n\u003ccode\u003esplice(開始位置, 削除する数)\u003c/code\u003e の形で使う\u003c/li\u003e\n\u003cli data-sourcepos=\"1403:3-1404:3\"\u003eindex の位置から \u003cstrong\u003e1 個だけ削除\u003c/strong\u003e するという意味\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1405:3-1407:81\"\u003eVue の ToDo アプリでは、\u003cbr\u003e\n「削除したい ToDo（item）」はわかっていても、\u003cbr\u003e\n\u003cstrong\u003eその item が配列の何番目にあるかはわからない\u003c/strong\u003e ため、\u003c/p\u003e\n\u003col data-sourcepos=\"1409:3-1411:3\"\u003e\n\u003cli data-sourcepos=\"1409:3-1409:41\"\u003e\n\u003ccode\u003eindexOf(item)\u003c/code\u003e で位置を調べ\u003c/li\u003e\n\u003cli data-sourcepos=\"1410:3-1411:3\"\u003e\n\u003ccode\u003esplice\u003c/code\u003e でその位置を削除する\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"1412:3-1412:35\"\u003eという処理になります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1414:1-1415:0\"\u003e\n\u003cdiv data-sourcepos=\"1416:1-1421:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1417:1-1417:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"1419:1-1419:66\"\u003eどちらも引数として要素の参照を渡しています。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"1424:1-1424:64\"\u003e\n\u003cspan id=\"要素の参照を引数として渡している理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A6%81%E7%B4%A0%E3%81%AE%E5%8F%82%E7%85%A7%E3%82%92%E5%BC%95%E6%95%B0%E3%81%A8%E3%81%97%E3%81%A6%E6%B8%A1%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【要素の参照を引数として渡している理由】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1426:1-1508:41\"\u003e\n\u003cp data-sourcepos=\"1426:3-1427:82\"\u003edoChangeState(item) や doRemove(item) のように、\u003cbr\u003e\nどちらのメソッドも item を引数として受け取っています。\u003c/p\u003e\n\u003cp data-sourcepos=\"1429:3-1430:105\"\u003eここで渡されている item は、単なる値ではなく、\u003cbr\u003e\n\u003cstrong\u003etodos 配列の中に入っている ToDo オブジェクトそのもの（＝参照）\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"1432:3-1433:64\"\u003e\u003cstrong\u003e引数（ひきすう）とは？\u003c/strong\u003e\u003cbr\u003e\n「関数（メソッド）に渡す値のこと」です。\u003c/p\u003e\n\u003cp data-sourcepos=\"1435:3-1435:55\"\u003eテンプレート側で次のように書くと：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"1436:3-1438:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e@click=\"doRemove(item)\"\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1439:3-1439:86\"\u003eitem が引数として doRemove に渡される、という意味になります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"1441:3-1441:30\"\u003e\n\u003cspan id=\"参照を渡すとは\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E7%85%A7%E3%82%92%E6%B8%A1%E3%81%99%E3%81%A8%E3%81%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参照を渡すとは？\u003c/h3\u003e\n\u003cp data-sourcepos=\"1443:3-1443:64\"\u003e（難しい場合は飛ばしても問題ありません）\u003c/p\u003e\n\u003cp data-sourcepos=\"1445:3-1446:112\"\u003eJavaScript のオブジェクトや配列は「箱そのもの」ではなく、\u003cbr\u003e\n\u003cstrong\u003e“箱への矢印（住所）” を変数に入れている\u003c/strong\u003e と考えると理解しやすいです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1448:3-1448:38\"\u003e\u003cstrong\u003e図でイメージすると：\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"1449:3-1451:3\"\u003e\n\u003cli data-sourcepos=\"1449:3-1449:41\"\u003etodos → \u003ccode\u003e[ itemA, itemB, itemC ]\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"1450:3-1451:3\"\u003eitem → itemB を指す矢印\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1452:3-1453:94\"\u003eitem は itemB のコピーではなく、\u003cbr\u003e\n\u003cstrong\u003eitemB そのものを指している（＝参照している）\u003c/strong\u003e ということです。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"1457:3-1457:63\"\u003e\n\u003cspan id=\"item-を変更すると-todos-の中身も変わる理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#item-%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B%E3%81%A8-todos-%E3%81%AE%E4%B8%AD%E8%BA%AB%E3%82%82%E5%A4%89%E3%82%8F%E3%82%8B%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eitem を変更すると todos の中身も変わる理由\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1459:3-1461:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nx\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estate\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1463:3-1463:16\"\u003eこれは：\u003c/p\u003e\n\u003cul data-sourcepos=\"1464:3-1466:3\"\u003e\n\u003cli data-sourcepos=\"1464:3-1464:58\"\u003eitem が指している itemB の state を変える\u003c/li\u003e\n\u003cli data-sourcepos=\"1465:3-1466:3\"\u003etodos の中に入っている itemB も同じもの\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1467:3-1467:77\"\u003eよって \u003cstrong\u003etodos の中身も変わる\u003c/strong\u003e という動きになります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"1471:3-1471:56\"\u003e\n\u003cspan id=\"値渡しプリミティブ型との違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%80%A4%E6%B8%A1%E3%81%97%E3%83%97%E3%83%AA%E3%83%9F%E3%83%86%E3%82%A3%E3%83%96%E5%9E%8B%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\"値渡し\"（プリミティブ型）との違い\u003c/h3\u003e\n\u003cp data-sourcepos=\"1473:3-1474:88\"\u003eJavaScript では、数値や文字列などは \u003cstrong\u003e値渡し\u003c/strong\u003e になります。\u003cbr\u003e\nこれらは \u003cstrong\u003e値そのものが変数に入る\u003c/strong\u003e という特徴があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1476:3-1476:10\"\u003e例：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1477:3-1482:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003ea\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e10\u003c/span\u003e\n\u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ea\u003c/span\u003e\n\u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e20\u003c/span\u003e\n\u003cspan class=\"nx\"\u003econsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 10（変わらない）\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1484:3-1484:78\"\u003eb は a のコピーなので、b を変えても a は変わりません。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"1488:3-1488:59\"\u003e\n\u003cspan id=\"参照型オブジェクト配列の場合\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E7%85%A7%E5%9E%8B%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E9%85%8D%E5%88%97%E3%81%AE%E5%A0%B4%E5%90%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\"参照型\"（オブジェクト・配列）の場合\u003c/h3\u003e\n\u003cp data-sourcepos=\"1490:3-1491:64\"\u003eオブジェクトや配列は \u003cstrong\u003e参照（矢印）\u003c/strong\u003e が渡されるため、\u003cbr\u003e\nコピーではなく \u003cstrong\u003e同じものを共有\u003c/strong\u003e します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1493:3-1493:10\"\u003e例：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1494:3-1499:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003ea\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e10\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ea\u003c/span\u003e\n\u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e20\u003c/span\u003e\n\u003cspan class=\"nx\"\u003econsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 20（変わる）\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1501:3-1501:76\"\u003ea と b は \u003cstrong\u003e同じオブジェクトを指している\u003c/strong\u003e からです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1503:3-1503:31\"\u003e参照を渡すことで：\u003c/p\u003e\n\u003cul data-sourcepos=\"1504:3-1507:3\"\u003e\n\u003cli data-sourcepos=\"1504:3-1504:54\"\u003e正しい ToDo を直接変更・削除できる\u003c/li\u003e\n\u003cli data-sourcepos=\"1505:3-1507:3\"\u003eコピーではなく “同じもの” を共有しているため、\u003cbr\u003e\nitem を変更すると todos の中身も変わる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1508:3-1508:41\"\u003eというメリットがあります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1510:1-1511:0\"\u003e\n\u003cdiv data-sourcepos=\"1512:1-1515:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1513:1-1514:99\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\nまだモックの状態だった、状態変更ボタンのイベントをハンドルします。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"1517:1-1517:117\"\u003eこれまで動いていなかった状態変更ボタンに、実際の処理をつけますという意味です。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"1519:1-1530:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1520:1-1524:117\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\nつづいて削除ボタンもハンドルします。\u003cbr\u003e\n「削除」は注意するべき操作のため、\u003cbr\u003e\nキー修飾子 .ctrl を使って\u003cbr\u003e\n「コントロールキーを押しながらクリック」しなければ呼び出されないようにします。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"1525:1-1529:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u0026lt;button v-on:click.ctrl=\"doRemove(item)\"\u0026gt;\n削除\n\u0026lt;/button\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"1531:1-1534:87\"\u003e削除ボタンも実際の処理を付けます\u003cbr\u003e\n削除する際は注意が必要な操作なので、誤って押してしまわないように、\u003cbr\u003e\nコントロールキー（ctrl）を押しながらクリックしたときだけ\u003cbr\u003e\n削除が実行されるようにします。v-on:click.ctrlでその設定をします\u003c/p\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"1541:1-1541:39\"\u003e\n\u003cspan id=\"step11-選択用フォームの作成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step11-%E9%81%B8%E6%8A%9E%E7%94%A8%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%AE%E4%BD%9C%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP11 選択用フォームの作成\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP11の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"1547:1-1552:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1548:1-1551:116\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\n特定の作業状態のリストのみを表示させる「絞り込み機能」を追加しましょう。\u003cbr\u003e\nスローガンテキストの下にラジオボタンをリストで表示します。\u003cbr\u003e\nToDo リストと同じように動的に作成するため、選択肢の options リストを作成しました。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"1554:1-1554:37\"\u003e\n\u003cspan id=\"スローガンテキスト\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B9%E3%83%AD%E3%83%BC%E3%82%AC%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【スローガンテキスト】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1555:1-1556:88\"\u003e\n\u003cp data-sourcepos=\"1555:2-1556:88\"\u003eスローガンテキストとは、画面上部に表示されている\u003cbr\u003e\n『Hello Vue.js World!!』 のようなアプリのタイトル部分のことです。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1558:1-1559:0\"\u003e\n\u003ch3 data-sourcepos=\"1560:1-1560:25\"\u003e\n\u003cspan id=\"動的に作成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8B%95%E7%9A%84%E3%81%AB%E4%BD%9C%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【動的に作成】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1561:1-1564:62\"\u003e\n\u003cp data-sourcepos=\"1561:2-1564:62\"\u003e動的に作成とは、配列などのデータをもとに、\u003cbr\u003e\n必要な数だけ HTML 要素を自動で生成する仕組みのことです。\u003cbr\u003e\n今回のラジオボタンは、options 配列を v-for でループすることで、\u003cbr\u003e\n3つの選択肢が自動的に画面に表示されます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1566:1-1567:0\"\u003e\n\u003cdiv data-sourcepos=\"1568:1-1575:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1569:1-1574:59\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\noptions リストを  タグで繰り返し描画して、\u003cbr\u003e\n内側の  タグの value 属性には、データ側の label.value データをバインドします。\u003cbr\u003e\nv-model ディレクティブを使って、ラジオボタンの選択値と current データを同期させます。\u003cbr\u003e\nラジオボタンが変更されると、その要素の label.value が\u003cbr\u003e\ncurrent プロパティへ代入される仕組みです。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"1578:1-1578:34\"\u003e\n\u003cspan id=\"実際の処理の流れ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E9%9A%9B%E3%81%AE%E5%87%A6%E7%90%86%E3%81%AE%E6%B5%81%E3%82%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【実際の処理の流れ】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1579:1-1631:40\"\u003e\n\u003cp data-sourcepos=\"1579:3-1581:81\"\u003e※元のコードでは v-for の変数名が label でしたが、\u003cbr\u003e\n以下の説明では読みやすさのために option に変更して解説します。\u003cbr\u003e\n（label.value → option.value と読み替えて理解してください）\u003c/p\u003e\n\u003cp data-sourcepos=\"1583:3-1584:97\"\u003eHTML のこの部分で、ラジオボタンが自動的に生成されています：\u003cbr\u003e\n（元のファイルは \u003ccode\u003e\u0026lt;label v-for=\"label in options\"\u0026gt;\u003c/code\u003e だったが説明用に変更）\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"1586:3-1593:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;label\u003c/span\u003e \u003cspan class=\"na\"\u003ev-for=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"option in options\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"radio\"\u003c/span\u003e\n         \u003cspan class=\"na\"\u003ev-model=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"current\"\u003c/span\u003e\n         \u003cspan class=\"na\"\u003ev-bind:value=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"option.value\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  {{ option.label }}\n\u003cspan class=\"nt\"\u003e\u0026lt;/label\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1595:3-1595:32\"\u003e\u003cstrong\u003e実際の処理の流れ\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"1597:3-1599:100\"\u003e\u003cstrong\u003ev-for=\"option in options\"\u003c/strong\u003e\u003cbr\u003e\noptions 配列をループし、配列の要素数ぶん \u003ccode\u003e\u0026lt;label\u0026gt;\u003c/code\u003e ブロックを自動で生成します。\u003cbr\u003e\n1回のループでは、options 配列の1つのオブジェクトが option に入ります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1601:3-1602:68\"\u003e\u003cstrong\u003e\u003ccode\u003e\u0026lt;input type=\"radio\"\u0026gt;\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003e\nループのたびにラジオボタンが1つ作られます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1604:3-1606:131\"\u003e\u003cstrong\u003ev-model=\"current\"\u003c/strong\u003e\u003cbr\u003e\n選択されたラジオボタンの値と、Vue 側の current データを常に同期させます。\u003cbr\u003e\n初期値として current に -1 が入っているため、最初は「すべて」が選択された状態になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1608:3-1612:70\"\u003e\u003cstrong\u003ev-bind:value=\"option.value\"\u003c/strong\u003e\u003cbr\u003e\noptions 配列の各要素（option オブジェクト）の value を\u003cbr\u003e\n\u003ccode\u003e\u0026lt;input\u0026gt;\u003c/code\u003e の value 属性にバインドします。\u003cbr\u003e\n→ ラジオボタンの値がデータから自動で設定されます。\u003cbr\u003e\n（バインド＝データと HTML を結びつけること。）\u003c/p\u003e\n\u003cp data-sourcepos=\"1614:3-1616:76\"\u003e\u003cstrong\u003e{{ option.label }}\u003c/strong\u003e\u003cbr\u003e\noption オブジェクトの label プロパティの値\u003cbr\u003e\n（\"すべて\" / \"作業中\" / \"完了\"）を画面に表示します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1618:3-1621:46\"\u003e\u003cstrong\u003e1回目のループ：\u003c/strong\u003e\u003cbr\u003e\noption は次のオブジェクト：\u003cbr\u003e\n\u003ccode\u003e{ value: -1, label: \"すべて\" }\u003c/code\u003e\u003cbr\u003e\nlabel プロパティの値は \"すべて\"\u003c/p\u003e\n\u003cp data-sourcepos=\"1623:3-1626:46\"\u003e\u003cstrong\u003e2回目のループ：\u003c/strong\u003e\u003cbr\u003e\noption は次のオブジェクト：\u003cbr\u003e\n\u003ccode\u003e{ value: 0, label: \"作業中\" }\u003c/code\u003e\u003cbr\u003e\nlabel プロパティの値は \"作業中\"\u003c/p\u003e\n\u003cp data-sourcepos=\"1628:3-1631:40\"\u003e\u003cstrong\u003e3回目のループ：\u003c/strong\u003e\u003cbr\u003e\noption は次のオブジェクト：\u003cbr\u003e\n\u003ccode\u003e{ value: 1, label: \"完了\" }\u003c/code\u003e\u003cbr\u003e\nlabel プロパティの値は \"完了\"\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"1638:1-1638:39\"\u003e\n\u003cspan id=\"step12-リストの絞り込み機能\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step12-%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AE%E7%B5%9E%E3%82%8A%E8%BE%BC%E3%81%BF%E6%A9%9F%E8%83%BD\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP12 リストの絞り込み機能\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP12の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"1643:1-1652:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1644:1-1644:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"1646:1-1650:147\"\u003ecurrent データの選択値によって表示させる\u003cbr\u003e\nToDo リストの内容を振り分けるため「算出プロパティ」という機能を使用します。\u003cbr\u003e\n算出プロパティは、データから別の新しいデータを作成する関数型のデータです。\u003cbr\u003e\n定義方法は、computed オプションに加工したデータを返すメソッドを登録します。\u003cbr\u003e\n算出プロパティは、元になったデータに変更があるまで、結果をキャッシュするという性質を持っています。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"1655:1-1655:31\"\u003e\n\u003cspan id=\"算出プロパティ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%AE%97%E5%87%BA%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【算出プロパティ】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1656:1-1690:67\"\u003e\n\u003cp data-sourcepos=\"1656:3-1660:70\"\u003e算出プロパティとは、Vue が提供している\u003cbr\u003e\n「データを元に新しい値を計算して返す仕組み」です。\u003cbr\u003e\n見た目はメソッド（関数）のようですが、\u003cbr\u003e\n実際には「データから派生した別のデータ」を\u003cbr\u003e\n自動的に作り出すための特別なプロパティです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1662:3-1665:49\"\u003e算出プロパティは、computed オプションの中に\u003cbr\u003e\nメソッドとして定義します。\u003cbr\u003e\nそのメソッドの return で返した値が、\u003cbr\u003e\n新しいデータとして扱われます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1667:3-1668:72\"\u003eまた、算出プロパティには\u003cbr\u003e\n\u003cstrong\u003e「キャッシュされる」\u003c/strong\u003e という特徴があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"1670:3-1672:76\"\u003e\u003cstrong\u003e\"キャッシュ\"されるとは？\u003c/strong\u003e\u003cbr\u003e\n一度計算した結果を Vue が覚えておき、\u003cbr\u003e\n元データが変わらない限り、再計算しないことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1674:3-1676:52\"\u003e元になっているデータ（今回でいう current や todos）が変わらない限り、\u003cbr\u003e\n算出プロパティの結果は再計算されず、\u003cbr\u003e\n前回の結果がそのまま使われます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1678:3-1679:46\"\u003eそのため、無駄な処理が減り、\u003cbr\u003e\nアプリが効率よく動作します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1681:3-1685:60\"\u003emethods でも同じような処理はできますが、\u003cbr\u003e\nmethods は「呼ばれるたびに毎回計算される」ため、\u003cbr\u003e\n画面が再描画されるたびに関数が実行され、\u003cbr\u003e\n処理が重くなる可能性があります。\u003cbr\u003e\n（methods はキャッシュされないためです）\u003c/p\u003e\n\u003cp data-sourcepos=\"1687:3-1690:67\"\u003e今回の絞り込み機能では、\u003cbr\u003e\ncurrent の値（-1, 0, 1）に応じて表示する ToDo リストを切り替えるために、\u003cbr\u003e\n算出プロパティを使って\u003cbr\u003e\n\u003cstrong\u003e「表示用の ToDo リスト」\u003c/strong\u003e を作成しています。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr data-sourcepos=\"1692:1-1694:0\"\u003e\n\u003cdiv data-sourcepos=\"1695:1-1706:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1696:1-1704:84\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003cbr\u003e\n定義方法が違うだけで使い方はデータと一緒です。\u003cbr\u003e\n一覧表示テーブルの v-for ディレクティブで使用している todos の部分を\u003cbr\u003e\ncomputedTodos に置き換えましょう。\u003cbr\u003e\nたとえば「◯件見つかりました」という結果の要素数を表示したいとき、\u003cbr\u003e\n単純にその配列の computedTodos.length を見れば欲しい数字が得られます。\u003cbr\u003e\n{{ computedTodos.length }} 件を表示中\u003cbr\u003e\nキャッシュ機能があるおかげで、\u003cbr\u003e\nメソッドと違い何度使用しても処理は 1 度しか行われません。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"1708:1-1708:58\"\u003e\n\u003cspan id=\"todos-を-computedtodos-に置き換える理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#todos-%E3%82%92-computedtodos-%E3%81%AB%E7%BD%AE%E3%81%8D%E6%8F%9B%E3%81%88%E3%82%8B%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【todos を computedTodos に置き換える理由】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1709:1-1724:68\"\u003e\n\u003cp data-sourcepos=\"1709:3-1709:79\"\u003e「絞り込み後のリスト」を画面に表示したいからです。\u003c/p\u003e\n\u003cp data-sourcepos=\"1711:3-1712:75\"\u003etodos … 元の全データ\u003cbr\u003e\ncomputedTodos … current の値に応じて絞り込まれたデータ\u003c/p\u003e\n\u003cp data-sourcepos=\"1714:3-1715:58\"\u003e画面に表示したいのは \u003cstrong\u003e「絞り込まれた結果」\u003c/strong\u003e なので、\u003cbr\u003e\ntodos のままでは目的を達成できません。\u003c/p\u003e\n\u003cp data-sourcepos=\"1717:3-1718:108\"\u003e\u003cstrong\u003ecomputedTodos.length\u003c/strong\u003e\u003cbr\u003e\ncomputedTodos の中にあるオブジェクトの数（＝絞り込み後の件数）を表します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1720:3-1721:108\"\u003etodos は「元データ」であり、ユーザーの選択に応じて内容が変わることはありません。\u003cbr\u003e\n一方、computedTodos は current の値に応じて内容が変わる \u003cstrong\u003e表示用データ\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"1723:3-1724:68\"\u003ecomputedTodos を使うことで、ユーザーがラジオボタンを切り替えた瞬間に\u003cbr\u003e\n表示内容が自動的に更新されるようになります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"1728:1-1728:33\"\u003e\n\u003cspan id=\"step13-文字列の変換処理\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#step13-%E6%96%87%E5%AD%97%E5%88%97%E3%81%AE%E5%A4%89%E6%8F%9B%E5%87%A6%E7%90%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEP13 文字列の変換処理\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP13の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"1733:1-1750:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"1734:1-1734:28\"\u003e\u003cstrong\u003e（本文より引用）\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"1736:1-1748:109\"\u003e最後の仕上げとして「状態変更ボタン」のラベルが数字になっているのを修正しましょう。\u003cbr\u003e\n状態変更ボタンで使っている状態の item.state データは、\u003cbr\u003e\n文字列そのものではなく「キー」になる数字を保存しています。\u003cbr\u003e\n一般的にもカテゴリーなどのデータでは、こういった数字や短い英数字のキーの状態で保存されます。\u003cbr\u003e\nしかし、このままでは作業中なら「0」完了なら「1」と表示され、まったく意味がわかりません。\u003cbr\u003e\n絞り込みのセレクトボックス用の options データをもとに、\u003cbr\u003e\nvalue から label へ変換するための labels 算出プロパティを作成します。\u003cbr\u003e\nMustache で labels オブジェクトを通すように変更します。\u003cbr\u003e\n\u003ccode\u003e\u0026lt;button v-on:click=\"doChangeState(item)\"\u0026gt; {{ labels[item.state] }} \u0026lt;/button\u0026gt;\u003c/code\u003e\u003cbr\u003e\nこれで人が理解できる文字で表示されるようになりました。\u003cbr\u003e\nこのような文字の処理は、フィルタ機能を使っても同じように変換できます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"1752:1-1752:68\"\u003e\n\u003cspan id=\"なぜ-labels算出プロパティが必要なのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C-labels%E7%AE%97%E5%87%BA%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【なぜ labels（算出プロパティ）が必要なのか】\u003c/h3\u003e\n\u003cblockquote data-sourcepos=\"1753:1-1852:82\"\u003e\n\u003cp data-sourcepos=\"1753:3-1756:52\"\u003eToDo の状態（item.state）は、データとして扱いやすいように\u003cbr\u003e\n数字（0 / 1）で保存されています。\u003cbr\u003e\nしかし、この数字をそのまま画面に表示すると「0」「1」と表示され、\u003cbr\u003e\nユーザーには意味が伝わりません。\u003c/p\u003e\n\u003cp data-sourcepos=\"1758:3-1760:63\"\u003eそこで、絞り込み用に使っている options データを利用して、\u003cbr\u003e\nvalue（数字）→ label（文字）に変換するための\u003cbr\u003e\n\u003cstrong\u003elabels（算出プロパティ）\u003c/strong\u003e を作成します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1762:3-1762:29\"\u003e\u003cstrong\u003elabels() のコード\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1763:3-1769:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nf\"\u003elabels\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ereduce\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003efunction \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nb\"\u003eObject\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eassign\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e]:\u003c/span\u003e \u003cspan class=\"nx\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elabel\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"p\"\u003e{})\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1771:3-1771:47\"\u003e\u003cstrong\u003elabels() の処理を一行ずつ説明\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"1773:3-1774:71\"\u003e\u003cstrong\u003e1. \u003ccode\u003elabels() {\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003e\nlabels という算出プロパティの定義を開始します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1776:3-1778:64\"\u003e\u003cstrong\u003e2. \u003ccode\u003ereturn this.options.reduce(function (a, b) {\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003e\noptions 配列を reduce で 1 つずつ処理し、\u003cbr\u003e\n最終的に 1 つのオブジェクトにまとめます。\u003c/p\u003e\n\u003cul data-sourcepos=\"1779:3-1782:3\"\u003e\n\u003cli data-sourcepos=\"1779:3-1779:57\"\u003ea … これまでに作られたオブジェクト\u003c/li\u003e\n\u003cli data-sourcepos=\"1780:3-1782:3\"\u003eb … options の現在の要素\u003cbr\u003e\nreduce の初期値は \u003ccode\u003e{}\u003c/code\u003e（空のオブジェクト）です。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1783:3-1784:83\"\u003e\u003cstrong\u003e3. \u003ccode\u003ereturn Object.assign(a, { [b.value]: b.label })\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003e\nObject.assign を使って、a に新しいプロパティを追加します。\u003c/p\u003e\n\u003cul data-sourcepos=\"1785:3-1788:3\"\u003e\n\u003cli data-sourcepos=\"1785:3-1785:43\"\u003eb.value（数字）をキーにして\u003c/li\u003e\n\u003cli data-sourcepos=\"1786:3-1788:3\"\u003eb.label（文字）を値として\u003cbr\u003e\na に追加する処理です。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1789:3-1791:73\"\u003e\u003cstrong\u003e例：\u003c/strong\u003e\u003cbr\u003e\nb が \u003ccode\u003e{ value: 0, label: \"作業中\" }\u003c/code\u003e の場合、\u003cbr\u003e\n\u003ccode\u003e{ 0: \"作業中\" }\u003c/code\u003e というプロパティが追加されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1793:3-1794:82\"\u003e\u003cstrong\u003e4. \u003ccode\u003e{ [b.value]: b.label }\u003c/code\u003e の意味\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003e[b.value]\u003c/code\u003e は「変数をキーとして使う」ための書き方です。\u003c/p\u003e\n\u003cul data-sourcepos=\"1795:3-1798:3\"\u003e\n\u003cli data-sourcepos=\"1795:3-1795:44\"\u003eb.value が 0 → \u003ccode\u003e{ 0: \"作業中\" }\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"1796:3-1798:3\"\u003eb.value が 1 → \u003ccode\u003e{ 1: \"完了\" }\u003c/code\u003e\u003cbr\u003e\nのように、数字をキーにしたオブジェクトを作れます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1799:3-1801:108\"\u003e\u003cstrong\u003e5. \u003ccode\u003e}, {})\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003e\nreduce の初期値として \u003ccode\u003e{}\u003c/code\u003e を渡しています。\u003cbr\u003e\n空のオブジェクトからスタートし、1 つずつプロパティを追加していきます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1803:3-1804:43\"\u003e\u003cstrong\u003e6. \u003ccode\u003e}\u003c/code\u003e\u003c/strong\u003e\u003cbr\u003e\nlabels() の処理が終了します。\u003c/p\u003e\n\u003cp data-sourcepos=\"1806:3-1807:45\"\u003e\u003cstrong\u003elabels() の最終的な結果\u003c/strong\u003e\u003cbr\u003e\noptions が次のような配列なら：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1808:3-1814:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003elabel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eすべて\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \u003cspan class=\"na\"\u003elabel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e作業中\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \u003cspan class=\"na\"\u003elabel\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e完了\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1816:3-1816:73\"\u003elabels() の返すオブジェクトは次のようになります：\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"1817:3-1823:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eすべて\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n   \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e作業中\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n   \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e完了\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1825:3-1825:85\"\u003eこれにより、数字をキーにして文字を簡単に取り出せます。\u003c/p\u003e\n\u003cul data-sourcepos=\"1827:3-1830:3\"\u003e\n\u003cli data-sourcepos=\"1827:3-1827:31\"\u003elabels[0] → \"作業中\"\u003c/li\u003e\n\u003cli data-sourcepos=\"1828:3-1828:28\"\u003elabels[1] → \"完了\"\u003c/li\u003e\n\u003cli data-sourcepos=\"1829:3-1830:3\"\u003elabels[-1] → \"すべて\"\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"1831:3-1832:94\"\u003e\u003cstrong\u003eMustache（マスタッシュ）で変換結果を表示する\u003c/strong\u003e\u003cbr\u003e\nVue の Mustache（{{ }}）は、データを画面に表示するための構文です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"1834:3-1836:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e{{ labels[item.state] }}\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1838:3-1839:46\"\u003eと書くことで、数字の状態（0 / 1）を labels を通して\u003cbr\u003e\n文字に変換して表示できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"1841:3-1841:14\"\u003e\u003cstrong\u003e例：\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"html\" data-sourcepos=\"1842:3-1846:7\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"na\"\u003ev-on:click=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"doChangeState(item)\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  {{ labels[item.state] }}\n\u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"1848:3-1849:43\"\u003eitem.state が 0 の場合 → \"作業中\"\u003cbr\u003e\nitem.state が 1 の場合 → \"完了\"\u003c/p\u003e\n\u003cp data-sourcepos=\"1851:3-1852:82\"\u003eこのようにして、数字で保存された状態を\u003cbr\u003e\n\u003cstrong\u003e人が読める文字に変換して表示\u003c/strong\u003e できるようになります。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/details\u003e\n\u003ch1 data-sourcepos=\"1858:1-1858:30\"\u003e\n\u003cspan id=\"stepex-記事作成の感想\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#stepex-%E8%A8%98%E4%BA%8B%E4%BD%9C%E6%88%90%E3%81%AE%E6%84%9F%E6%83%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eSTEPEX 記事作成の感想\u003c/h1\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEPEXの本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\u003cdiv data-sourcepos=\"1863:1-1867:3\" class=\"note alert\"\u003e\n\u003cspan class=\"fa fa-fw fa-times-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003ch1 data-sourcepos=\"1865:1-1865:41\"\u003e\n\u003cspan id=\"ながすぎほんとうに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%8C%E3%81%99%E3%81%8E%E3%81%BB%E3%82%93%E3%81%A8%E3%81%86%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003cem\u003e\u003cstrong\u003eながすぎ。ほんとうに。\u003c/strong\u003e\u003c/em\u003e\n\u003c/h1\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"1870:1-1870:75\"\u003e今回用語を調べるにあたり生成AIのCopilotを活用しました\u003c/p\u003e\n\u003cp data-sourcepos=\"1872:1-1874:83\"\u003e最初は他の人よりも少し長めに記事を作成しようとしていたのですが\u003cbr\u003e\n説明の分かりやすさ、正確さを重視した結果\u003cbr\u003e\n製作期間約2週間、約3万字ほどの大作になってしまいました。\u003c/p\u003e\n\u003cp data-sourcepos=\"1876:1-1877:96\"\u003e専門用語がある理由を今回の記事作成で分かったような気がします\u003cbr\u003e\nまた専門用語を誰にでも分かるように説明するのは大変だと感じました\u003c/p\u003e\n\u003cp data-sourcepos=\"1879:1-1879:132\"\u003eこの記事を最後まで見てくれた方、この記事をチェックしてくれた方に最大限の感謝を送ります。\u003c/p\u003e\n\u003c/details\u003e\n","body":"# この記事の目的\n\nこの記事は、新人研修で使われていた「STEP1〜STEP13の手順に沿って  \nToDoリストを作る教材」を、より分かりやすく解説し直したものです。\n\n研修では、手順通りに進めればアプリ自体は完成しますが、  \n「なぜこのコードを書くのか」「何が起きているのか」が分からないまま  \n作業だけが進んでしまうことが多く、理解が追いつかないという声がありました。\n\nそこでこの記事では、各STEPで何をしているのかを丁寧に補足しながら、  \n初心者でも流れを追いやすいように再構成しています。  \n説明は詳しめですが、そのぶん文章量も多めです。ご了承ください。  \n\n「とりあえず完成したけど、結局どういう仕組みなのか分からない」  \nという状態を解消することが目的です。\n\n# 使用するもの\n- Vue.js 2\n- Visual Studio Code（VS Code）\n- HTML\n- CSS\n- JavaScript\n\n# 参考サイト\nhttps://cr-vue.mio3io.com/tutorials/\n\n# STEP0 はじめに\nここでは、このチュートリアルで使う前提知識について解説します。\nまずは ToDo アプリとは何か、そして Web ページを構成する基本要素を確認します。\n\n#  【ToDoアプリ】\nやることを管理するアプリ。\n\n\n\n\n# 【HTML】  \nページの骨組み（何を表示するか）。\n\n# 【CSS】 \n見た目のデザイン（色・配置・大きさ）。\n\n# 【JavaScript】 \n動きをつける（追加・削除・状態変更など）。\n\n# 【ブラウザ（Web ブラウザ）】  \n\nインターネットのページを見るためのアプリ。  \nスマホやパソコンでニュースを読んだり、動画を見たり、検索したりするときに使う「いつものアプリ」。  \n代表例：Microsoft Edge、Google Chrome、Safari など。\n\nブラウザは Web ページを表示するときに次の処理を行う：\n\n1. HTML を読み取り、ページの「骨組み」を作る  \n2. CSS を読み取り、色や文字の大きさ、配置などの「見た目」を整える  \n3. JavaScript を読み取り、ボタンを押したときの動きなどを実行する  \n\nつまりブラウザは、  \n**「Web ページを読み取り、人が見て操作できる形にしてくれるアプリ」**  \nという役割を持っている。\n\n今回の ToDo アプリも、ブラウザが HTML・CSS・JavaScript（Vue.js）を読み込み、  \n画面に表示し、ボタンを押したときに動く。\n\n#  【タグ】\n\nHTML で “ここに何を置くか” を指定するための記号。  \nHTML はタグを使って Web ページの構造を作る。\n\n今回登場するタグの一部を、必要に応じて確認できるよう一覧にまとめました。\n見なくても読み進められますが、気になる方は開いてみてください。\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e今回登場する HTML のタグ一覧（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n 【`\u003c!DOCTYPE html\u003e`】\n\u003eHTML5 で書かれた文書であることをブラウザに伝える宣言。ページの最初に必ず書く。\n\n 【`\u003chtml\u003e`】\n\u003eHTML 文書全体を囲むタグ。この中に `\u003chead\u003e` と `\u003cbody\u003e` が入る。\n\n 【`\u003chead\u003e`】\n\u003eページの設定情報（タイトル、文字コード、CSS など）を書く部分。\n\n 【`\u003cmeta\u003e`】\n\u003eページのメタ情報（ブラウザに伝える設定）を指定するタグ。  \n\u003e今回は文字コードを UTF-8 にする。  \n\u003e文字コードが正しく指定されていないと日本語が文字化けするため重要。\n\n【`\u003ctitle\u003e`】\n\u003eブラウザのタブに表示されるページタイトル。\n\n 【`\u003clink\u003e`】\n\u003e外部ファイル（CSS など）を読み込むためのタグ。\n\n 【`\u003cbody\u003e`】\n\u003e実際に画面に表示される内容を書く部分。\n\n 【`\u003cdiv\u003e`】\n\u003eレイアウトを作るための箱。Vue アプリ全体を囲むために使用。\n\n 【`\u003ch1\u003e`】\n\u003eページの中で一番大きく、重要な見出しを作るタグ。\n\n 【`\u003ch2\u003e`】\n\u003e`\u003ch1\u003e` より少し小さい見出し。  \n\u003e見出しタグは h1 → h2 → h3… のように数字が大きくなるほど重要度が下がる。\n\n 【`\u003cp\u003e`】\n\u003e段落（文章）を表すタグ。説明文や件数表示に使用。\n\n 【`\u003clabel\u003e`】\n\u003eフォーム部品に説明文をつけるタグ。  \n\u003e`\u003clabel\u003e` の中に `\u003cinput\u003e` がある場合、文字をクリックしてもボタンが選択される。\n\n【`\u003cinput\u003e`】\n\u003eユーザーが文字を入力したり、選択したりするためのタグ。  \n\u003eテキスト入力やラジオボタンに使用。\n\n 【`\u003cform\u003e`】\n\u003eユーザーが入力した内容をまとめるための「箱」。  \n\u003e入力欄やボタンをひとまとめにできる。\n\n【`\u003cbutton\u003e`】\n\u003eクリックできるボタン。追加・削除・状態変更に使用。\n\n 【`\u003cscript\u003e`】\n\u003eJavaScript を読み込むタグ。今回は Vue.js を CDN から読み込んでいる。  \n\u003eCDN は「インターネット上に置かれた共有のファイル置き場」のようなもの。  \n\u003e有名なライブラリ（Vue.js など）が置かれていて、自分のパソコンに保存しなくても使える。\n\n\u003c/details\u003e\n\n\n# STEP1 インスタンスの作成\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP1の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n:::note\n**（本文より引用）**\n\nまずは、アプリケーションを紐付ける要素 #app を作成します。\n:::\n\n本文中の言葉について説明していきます\n\n###  【要素】\n\u003eHTMLにある部品のこと`\u003cdiv\u003e, \u003cp\u003e, \u003ctable\u003e `など、\nタグで囲まれたものを指します。\n#はidを表すので`\u003cdiv id=\"app\"\u003e`の部分が該当します\n\n---\n\n:::note\n**（本文より引用）**\nコンストラクタ関数 Vue を使ってルートインスタンスを作成します。\nアプリケーションで使用したいデータは data オプションへ登録していきます。\n:::\n\n\n順々に解説していきます\n\n### 【コンストラクタ関数】\n\u003e一言で言うと同じ形のものをたくさん作るための関数\n\u003eVueではnew Vue({...})と書く時Vueの部分がコンストラクタ関数にあたります。\n\u003eまた、el:'#app'の部分が紐づける要素を指定する部分です。\n\n---\n\n### 【関数】\n\n\n\u003e コンストラクタ関数と区別するために、  \n\u003e 「普通の関数」についても簡単に説明しておきます。  \n\u003e  \n\u003e 普通の関数は、ある処理をまとめておき、  \n\u003e 必要なときに呼び出して使うための仕組みです。\n\u003e\n\u003e 例えば（aとbを足し算したものを返す関数）：\n\u003e \n\u003e ```js\n\u003e function add(a, b) {\n\u003e   return a + b\n\u003e }\n\u003e ```\n\u003e このように、何かを計算したり、値を返したり、  \n\u003e 一連の処理をひとまとめにして再利用できるのが普通の関数です。  \n\u003e  \n\u003e コンストラクタ関数も見た目は同じ「function」ですが、  \n\u003e 役割が少し違います。  \n\u003e  \n\u003e 普通の関数は「処理を実行するためのもの」ですが、  \n\u003e コンストラクタ関数は  \n\u003e 「決まった形を持つ“まとまり”を新しく作るためのもの」です。  \n\u003e  \n\u003e 例えば Vue の場合、  \n\u003e  \n\u003e ```js\n\u003e new Vue({...})\n\u003e ```\n\u003e  \n\u003e と書くと、Vue というコンストラクタ関数を使って  \n\u003e “Vue アプリの本体”を新しく作っています。  \n\u003e  \n\u003e このように、  \n\u003e -普通の関数 → 何かの処理を実行するためのもの  \n\u003e -コンストラクタ関数 → 決まった形を持つ“まとまり”を作るためのもの  \n\u003e という違いがあります。\n\n\n\n\n\n---\n\n### 【ルートインスタンス】\n\u003e\n\u003e\n\u003e Vue が動き始めるスタート時点のことです。  \n\u003e new Vue({ ... }) の中に書いた内容をもとに Vue が処理し、  \n\u003e その結果として出来上がるものがルートインスタンスです。  \n\u003e  \n\u003e （例えるなら、el:..., data:... の部分が設計図で、  \n\u003e  Vue がその設計図をもとに家（ルートインスタンス）を建てるイメージ）\n\u003e\n\u003e※「インスタンス」という言葉の詳しい説明は STEP9 で行います。\n\u003eここでは「Vue が作るアプリの本体」くらいのイメージで大丈夫です。\n---\n\n### 【data オプション】\n\u003e オプションとは、Vue に渡す設定項目のことです。\n\u003e dataオプションとは、簡単に言うと、アプリの中で使いたい「変わる情報」を入れておく場所です。  \n\u003e 画面に表示する文字や、後で変わる数字などをここに書きます。  \n\u003e  \n\u003e data は Vue で決められた特別な名前なので変更できません。  \n\u003e （ほかにも el, methods, computed, watch などは変更できません）  \n\u003e  \n\u003e data オプションへ登録したデータは、すべてリアクティブデータに変換されます。\n\n---\n\n### 【リアクティブデータ】\n\u003eリアクティブ（reactive）は「反応する」という意味で、\n\u003e データが変わると自動で画面も変わる仕組みを持ったデータのことです。  \n\u003e Vue の data の中に書いた値が、このリアクティブデータにあたります。\n\n\n\u003c/details\u003e\n\n\n\n# STEP2 ローカルストレージ API の使用\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP2の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n:::note\n**（本文より引用）**\n\nデータはサーバーではなく  「ローカルストレージ」へ保存することにします。  \nストレージ周りの実装は  Vue.js 公式サンプル「TodoMVC の例」  \nのコードを使用させていただきます。\n:::\n\n\n\n### 【サーバー】\n\n\u003e サーバーとは、インターネット上にある “データを保存したり処理したりするコンピュータ” のことです。  \n\u003e ユーザーの情報、アプリの設定、画像や文章など、Web サイトを動かすための情報を保存しておく場所です。  \n\u003e  \n\u003e 特徴として：  \n\u003e - インターネットを通してアクセスできる  \n\u003e - 複数のユーザーが同じデータを共有できる  \n\u003e - どの端末からでも同じ情報を見られる  \n\u003e  \n\u003e たとえるなら **みんなで使える倉庫** のようなイメージです。\n\n---\n\n### 【ローカルストレージ】\n\n\u003e ストレージとは「データを保存しておく場所」のことです。\n\u003eローカルストレージは、ブラウザ（Chrome や Safari など）の中にある\n\u003e保存場所で、データを端末に保存できます。  \n\u003e  \n\u003e 特徴として：  \n\u003e - インターネット不要  \n\u003e - その端末だけにデータが残る  \n\u003e - 最大 5MB 程度まで保存できる  \n\u003e - 簡単に使える  \n\u003e  \n\u003e たとえるなら **自分だけが使える机の引き出し** のようなものです。  \n\u003e  \n\u003e 今回の ToDo アプリでは、  \n\u003e - 他の人と共有する必要がない  \n\u003e - 設定が簡単  \n\u003e  \n\u003e という理由からローカルストレージが採用されていると考えられます。\n\n---\n\n:::note\n**（本文より引用）**\n\n公式のコードの内容については詳しく説明しませんが、  \nこれは Storage API を使ったデータの取得・保存の処理だけを抜き出したものです。  \n小さなライブラリだと思ってください。\n:::\n\n---\n\n### 【API】\n\n\u003e API（エーピーアイ）とは、アプリやプログラムが “決められた方法で機能を使えるようにする仕組み” のことです。  \n\u003e 「この方法で呼び出せば、この機能が使えますよ」という **道具の説明書** のようなものです。  \n\u003e  \n\u003e 例：  \n\u003e - Google Maps の地図をアプリに表示する → Google Maps API  \n\u003e - ブラウザにデータを保存する → Storage API（今回使用）  \n\u003e - カメラを使う → Camera API  \n\u003e  \n\u003e API は **機能を安全に・簡単に使うための窓口** です。\n\n---\n\n### 【ライブラリ】\n\n\u003e ライブラリとは、よく使う処理をまとめて必要な時に呼び出して使える “部品セット” のことです。  \n\u003e  \n\u003e 例：  \n\u003e - 保存する処理  \n\u003e - データを読み込む処理  \n\u003e - 日付を扱う処理  \n\u003e - 画面を操作する処理  \n\u003e  \n\u003e 何度も使う処理をまとめた便利な道具箱です。  \n\n\n\n---\n\n:::note\n**（本文より引用）**\n\n実際にストレージに保存されるデータのフォーマットは、次のような JSON です。\n:::\n\n---\n\n### 【フォーマット】\n\n\u003e フォーマットとは、データをどのような形で保存するかを決めた “書式” のことです。  \n\u003e  \n\u003e ローカルストレージは **文字列しか保存できません**。  \n\u003e そのため：  \n\u003e - 保存するとき → 元のデータを JSON 文字列に変換して保存  \n\u003e - 読み込むとき → JSON 文字列を元のデータに戻す  \n\u003e  \n\u003e という処理が必要になります。\n\n---\n\n### 【JSON】\n\n\u003e JSON（ジェイソン）とは、データを表現するための “書き方のルール（フォーマット）” のことです。  \n\u003e 一言で言うと、データを保存したり、やり取りしたりするために使われます。  \n\u003e  \n\u003e 特徴：  \n\u003e - 人間にも読みやすい  \n\u003e - どの言語でも扱える  \n\u003e - 軽くてシンプル  \n\u003e - ローカルストレージと相性が良い  \n\u003e  \n\u003e JSON は「名前: 値」の組み合わせを `{ }` で書き、  \n\u003e 複数のデータを扱うときは `[ ]` でまとめます。  \n\u003e  \n\u003e （具体例は STEP3 の ToDo データで説明します）\n\n\n\n\u003c/details\u003e\n\n\n\n\n# STEP3 データの構想\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP3の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n\n:::note\n**（本文より引用）**\n\nさあ、ここから実際に作るコードです！\n\nどんなデータが必要になりそうかを、ざっくりと考えておきましょう。\n\n- ToDo のリストデータ  \n  - 要素の固有ID  \n  - コメント  \n  - 今の状態  \n- 作業中・完了・すべて などオプションラベルで使用する名称リスト  \n- 現在絞り込みしている作業状態  \n\nアプリケーションに付けたい機能から考えると、こんなところでしょうか。\n:::\n\n---\n\n### 【ToDo のリストデータ】\n\n\u003e 下記のような JSON のデータです。  \n\u003e\n\u003e ```json\n\u003e [\n\u003e   { \"id\": 1, \"comment\": \"買い物に行く\", \"state\": \"作業中\" },\n\u003e   { \"id\": 2, \"comment\": \"Vue の勉強\", \"state\": \"完了\" }\n\u003e ]\n\u003e ```\n\u003e\n\u003e - **固有ID**：それぞれの ToDo を区別するための番号で、\n  \u003e同じ ID にならないように管理します。\n\u003e - **コメント**：ToDo の内容（画面に表示するテキスト）  \n\u003e - **状態**：作業中 / 完了 のどちらか\n\u003e 画面で「作業中だけ表示」「完了だけ表示」などの絞り込みにも使います。\n\n\n\u003c/details\u003e\n\n\n# STEP4 リスト用テーブル\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP4の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n\n:::note\n**（本文より引用）**\n\nまずは、ToDo リストデータを表示するテーブルの枠組みを作成します。\n:::\n\n---\n\n### 【テーブル】\n\u003eSTEP3 で考えた ToDo データを、\n\u003e画面に表形式で表示するためにテーブルを使います。\n\u003e ここではテーブルに使うタグを紹介します。  \n\u003e\n\u003e **`\u003ctable\u003e`**  \n\u003e\n\u003e 表全体を作るタグ。ToDo リストを表形式で表示するために使用。  \n\u003e\n\u003e **`\u003cthead\u003e`**  \n\u003e\n\u003e 表のヘッダー部分（列名）をまとめるタグ。  \n\u003e\n\u003e **`\u003ctbody\u003e`**  \n\u003e\n\u003e 表の本体部分（データ行）をまとめるタグ。  \n\u003e\n\u003e **`\u003ctr\u003e`**  \n\u003e\n\u003e 表の行（row）を表すタグ。  \n\u003e\n\u003e **`\u003cth\u003e`**  \n\u003e\n\u003e 表の見出し用のセル（1マス）。太字・中央寄せになる。  \n\u003e\n\u003e また、表では  \n\u003e - **縦の列のことを「カラム（column）」**  \n\u003e - **横の行のことを「ロウ（row）」**  \n\u003e と呼びます。\n\n\n\u003c/details\u003e\n\n\n\n# STEP5 リストレンダリング\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP5の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n\n:::note\n**（本文より引用）**\n\nToDo リストデータ用の空の配列を  data オプションへ登録します。  \nこれは、データが何もない時でも  配列として認識されるようにするためと、  \nもともと data オプション直下のデータは  後から追加ができないため  \n初期値で宣言しておく必要があるためです。\n:::\n\n---\n\n### 【リストレンダリング】\n\n\u003e リストレンダリングとは、  \n\u003e 配列のデータ（ToDo リストデータ）を画面に繰り返し表示する仕組みです。  \n\u003e\n\u003e （HTML には `\u003ctr\u003e...\u003c/tr\u003e` を 1 つだけ書いておき、  \n\u003e Vue が ToDo リストデータの件数分 `\u003ctr\u003e...\u003c/tr\u003e` を自動で増やし、  \n\u003e その中にデータを入れてくれる）  \n\u003e\n\u003e ToDo リストのように複数の項目を一覧で表示する場合に必ず使います。  \n\u003e ここでは Vue の **v-for**（後ほど解説）を使用します。\n\n---\n\n### 【配列】\n\n\u003e 配列は「番号付きの箱が横に並んでいる」イメージです。  \n\u003e\n\u003e ```txt\n\u003e [ データ1, データ2, データ3, ... ]\n\u003e   0        1        2\n\u003e ```\n\u003e\n\u003e 左から順番に 0番、1番、2番… と番号がつきます（0番から始まるのが一般的）。  \n\u003e この番号を使ってデータを取り出せます。  \n\u003e\n\u003e ToDo リストデータは STEP3 で説明した JSON 配列で管理されています。  \n\u003e\n\u003e 例：  \n\u003e - `{ id: 1, comment: \"買い物に行く\", state: \"作業中\" }`  \n\u003e - `{ id: 2, comment: \"Vue の勉強\", state: \"完了\" }`  \n\u003e\n\u003e ToDo リストは 1件だけでなく、2件、3件、10件と増えていきます。  \n\u003e この複数のデータをまとめて扱うために配列を使用します。\n\n---\n\n### 【初期値】【宣言】\n\n\u003e 初期値とは「最初に入れておく値」のことです。  \n\u003e\n\u003e Vue の data に登録したデータは、最初に存在していないと後から追加できない  \n\u003e （データを追加しても画面に反映されない）というルールがあります。  \n\u003eこれはVue は最初に data の中身を読み取って、リアクティブデータに変換する仕組みのためです。\n\u003eその結果、最初に存在しないデータはリアクティブにならず、画面に反映されません。\n\u003e\n\u003e そこで ToDo リストを入れるための todos は、  \n\u003e 最初に空の配列 `[]` を初期値として宣言しておく必要があります。  \n\u003e\n\u003e 宣言とは「この名前のデータを使います」とプログラムに教えることです。  \n\u003e\n\u003e ```js\n\u003e data: {\n\u003e   todos: []   // ← これが「宣言」\n\u003e }\n\u003e ```\n\n---\n\n:::note\n**（本文より引用）**\n\nテーブルタグの [1] で  \n配列要素の数だけ繰り返し表示させるには、  \n対象となるタグ（ここでは `\u003ctr\u003e` タグ）に  \nv-for ディレクティブを使用します。\n:::\n\n---\n\n### 【v-for ディレクティブ】\n\n\u003e v-for とは Vue で「繰り返し表示」を行うためのディレクティブ（特別な命令）です。  \n\u003e\n\u003e 配列の中に複数のデータが入っているときに：  \n\u003e - 配列から 1 つ取り出す  \n\u003e - `\u003ctr\u003e` を 1 つ作る  \n\u003e - 次のデータを取り出す  \n\u003e - また `\u003ctr\u003e` を作る  \n\u003e\n\u003e という処理を **自動で繰り返してくれます**。  \n\u003e\n\u003e 書き方：  \n\u003e\n\u003e ```html\n\u003e v-for=\"一時的な名前 in 繰り返したい配列\"\n\u003e ```\n\u003e\n\u003e 具体例：  \n\u003e\n\u003e ```html\n\u003e \u003ctr v-for=\"item in todos\"\u003e\n\u003e ```\n\n---\n\n:::note\n**（本文より引用）**\n\nディレクティブの値は JavaScript の式になっており次のように書きます。  \n```v-for=\"各要素の一時的な名前 in 繰り返したい配列やオブジェクト\"```\n\n:::\n---\n\n### 【オブジェクト】\n\n\u003e オブジェクトとは、JavaScript のデータの形のひとつで、  \n\u003e 「名前（キー）と値（バリュー）のセット」をまとめたものです。  \n\u003e\n\u003e 例：  \n\u003e ```js\n\u003e { id: 1, title: \"買い物に行く\", state: \"作業中\" }\n\u003e ```\n\u003e\n\u003e v-for は配列だけでなくオブジェクトも繰り返し処理できます。  \n\u003e\n\u003e ```html\n\u003e v-for=\"item in items\"\n\u003e ```\n\u003e\n\u003e のように書くと、配列の各要素やオブジェクトの各値を順番に取り出して使えます。\n\n---\n\n:::note\n**（本文より引用）**\n\nv-for を記述したタグとその内側で  \ntodos データの各要素のプロパティが使用できるようになります。  \n`\u003ctr\u003e` タグの内側に  \n「ID」「コメント」「状態変更ボタン」「削除ボタン」のカラムを追加していきましょう。\n:::\n\n---\n\n### 【プロパティ】\n\n\u003e プロパティとは、STEP2 で説明した JSON と同じ構造で、  \n\u003e 「名前（プロパティ名）: 値」の組み合わせで情報を持っています。  \n\u003e\n\u003e 例：  \n\u003e ```js\n\u003e {\n\u003e   id: 1,\n\u003e   comment: \"買い物に行く\",\n\u003e   state: \"作業中\"\n\u003e }\n\u003e ```\n\u003e\n\u003e JSON と JavaScript では書き方に少し違いがあります。  \n\u003e\n\u003e **JSON（キーにダブルクオーテーション必須）**  \n\u003e ```json\n\u003e {\n\u003e   \"id\": 1,\n\u003e   \"comment\": \"買い物に行く\"\n\u003e }\n\u003e ```\n\u003e\n\u003e **JavaScript（記号やスペースがなければ省略可）**  \n\u003e ```js\n\u003e {\n\u003e   id: 1,\n\u003e   comment: \"買い物に行く\"\n\u003e }\n\u003e ```\n\n---\n\n:::note\n**（本文より引用）**\n\nこのボタンはまだなにも機能しないモックのため、\n機能はこれから実装していきます。\n:::\n\n---\n\n### 【モック】\n\n\u003e モックとは「見た目だけ用意した仮の部品」のことです。  \n\u003e\n\u003e - ボタンは画面に表示される  \n\u003e - クリックもできるように見える  \n\u003e - でも中身の処理（動作）はまだ入っていない  \n\u003e\n\u003e という “形だけの状態” を指します。\n\n\n\n\u003c/details\u003e\n\n\n\n# STEP6 フォーム入力値の取得\n\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP6の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n\n:::note\n**（本文より引用）**\n新しい ToDo をリストへ追加するための入力フォームを作成します。\nref 属性を使って参照するための名前をタグに付けておくと、\nその DOM に直接アクセスできます。\n:::\n### 【ref 属性】\n\u003e ref（レフ）は、HTML のタグに「あとで呼び出すための名前」を付ける仕組みです。  \n\u003e  \n\u003e たとえるなら、  \n\u003e 学校で先生が「前から3番目の赤いカバンの子」と言うのではなく、  \n\u003e 「田中さん」と名前で呼ぶようなものです。  \n\u003e  \n\u003e 例：  \n\u003e `\u003cinput type=\"text\" ref=\"newComment\"\u003e `\n\u003e  \n\u003e このように ref を付けておくと、  \n\u003e Vue の中から「newComment」という名前でこの入力欄を呼び出せます。  \n\u003e  \n\u003e すると、こんなことができます：  \n\u003e - 入力欄の中に書かれた文字を取り出す  \n\u003e - 入力欄にカーソルを合わせる（自動で入力できる状態にする）  \n\u003e - 特定の場所をスクロールさせる  \n\u003e - 動画や絵を表示する特別なタグを直接さわる  \n\u003e  \n\u003e 今回は、新しい ToDo を追加するときに  \n\u003e 入力欄の中の文字を取り出すために ref を使います。  \n\u003e  \n\n---\n\n### 【DOM（ドム）】\n\u003eDOM とは「ブラウザが HTML を読み取って作る、画面の部品の一覧表」\n\u003eのようなものです。\n\u003e   \n\u003e  \n\u003e たとえば、Web ページには  \n\u003e  - 見出し  \n\u003e  - 文字  \n\u003e  - ボタン  \n\u003e  - 入力欄  \n\u003e  - 画像  \n\u003e など、いろいろな部品があります。  \n\u003e  \n\u003e ブラウザは、これらの部品を「木の枝のように、親と子の関係で並べたもの」  \n\u003e として覚えています。  \n\u003e  \n\u003e その“画面の部品の並び”のことを DOM と呼びます。  \n\u003e  \n\u003e たとえるなら、  \n\u003e 家の中の家具を「リビング → テーブル → イス」のように  \n\u003e どこに何があるか整理してメモしているイメージです。  \n\u003e  \n\u003e Vue では、この DOM の中にある部品を  \n\u003e ref で名前を付けて呼び出したり、  \n\u003e 画面の内容を変えたりできます。  \n\u003e  \n\u003e 今回は、入力欄（input）が DOM の中のどこにあるかを  \n\u003e ref を使って名前で呼び出し、  \n\u003e その中の文字を取り出します。\n\n---\n\n:::note\n**（本文より引用）**\n    ref 属性で名前を付けたタグは、メソッド内から次のように使用できます。\n    this.$refs.名前\n    テンプレートでは変数名（プロパティ名）だけでデータを使用できましたが、\n    メソッド内でデータやメソッドを使用するときは this を付ける必要があります。\n    たとえば、comment の場合なら次のように使用します。\n    this.$refs.comment.value\n    実際は、次の STEP7 で使用します。\n:::\n### 【メソッド】\n\n\u003e メソッドとは、「ボタンを押したときに実行したい処理」をまとめて書いておく場所です。  \n\u003e  \n\u003e たとえるなら、  \n\u003e **「このボタンを押したら、こう動いてね」という動きの説明書** のようなものです。  \n\u003e  \n\u003e 例：  \n\u003e - 新しい ToDo を追加する  \n\u003e - ToDo を削除する  \n\u003e - 状態（作業中／完了）を切り替える  \n\u003e  \n\u003e Vue では、メソッドの中でデータや ref を使うときは  \n\u003e **this を付けて呼び出します。**  \n\u003e  \n\u003e ```js\n\u003e this.todos.push(新しいデータ)\n\u003e this.$refs.comment.value\n\u003e ```  \n\u003e  \n\u003e このように書くことで、  \n\u003e 「Vue が持っているデータ」や「ref で名前を付けた入力欄」  \n\u003e をメソッドの中から使えるようになります。  \n\n---\n\n### 【テンプレート】\n\n\u003e テンプレートとは、「画面にどんな見た目で表示するか」を書く場所です。  \n\u003e  \n\u003e たとえるなら、  \n\u003e **家を建てるときの間取り図** のようなものです。  \n\u003e  \n\u003e Vue では HTML の中にテンプレートを書き、  \n\u003e その中で Vue のデータを使って画面を作ります。  \n\u003e  \n\u003e 例：  \n\u003e - `{{ message }}` → message の中身が表示される  \n\u003e - `v-for` → 配列の数だけ繰り返し表示  \n\u003e - `v-if` → 条件で表示・非表示を切り替え  \n\u003e  \n\u003e テンプレート内では、Vue のデータを  \n\u003e **そのまま名前を書くことで使用できます。**  \n\u003e （例：`{{ todos.length }}`）  \n\n---\n\n\n:::note\n**（本文より引用）**\n\nv-model ディレクティブを使えばデータとフォーム入力を同期することもできますが、  \n今回は入力したデータを画面に表示させないのと  \n常にデータとして持っている必要がないため、  \nこの $refs を使って入力値を取得することにします。\n:::\n\n### 【v-model】\n\u003e v-model は、入力欄と Vue のデータを **自動で同期する仕組み** です。  \n\u003e  (同期とは「入力欄とデータが常に同じ状態になる」という意味です。)\n\u003e 入力欄に書いた文字がそのままデータにも反映されるため、  \n\u003e 入力欄とデータが常に同じ状態になります。  \n\u003e  \n\u003e 今回は「入力値を保持する必要がない」ため、  \n\u003e v-model ではなく **$refs で必要なときだけ取得** します。  \n\n---\n\n\n\n:::note\n**（本文より引用）**\n\nテーブルの下あたりに追加しておきます。  \nv-on:submit.prevent=\"doAdd\"\n\nこの v-on ディレクティブによって、  \nボタンをクリックしたり入力フォームでエンターを押してフォームのサブミットが行われると、  \nそれをハンドリングして doAdd メソッドが呼び出されるようになります。\n:::\n### 【v-on】\n\n\u003e v-on は、「〇〇されたら、この動きをしてね」と  \n\u003e ボタンや入力欄に“合図”をつけるための仕組みです。  \n\u003e  \n\u003e たとえるなら、  \n\u003e 「チャイムが鳴ったら席に着く」  \n\u003e 「ボールを投げられたらキャッチする」  \n\u003e といった “合図と動き” のセットを決めるイメージです。  \n\u003e  \n\u003e 今回の例では、  \n\u003e `v-on:submit.prevent=\"doAdd\"`  \n\u003e と書いています。  \n\u003e  \n\u003e これは、  \n\u003e - フォームが送信されたら（ボタンを押す or Enter を押す）  \n\u003e - ページを再読み込みしないようにして（prevent）  \n\u003e - doAdd メソッドを実行する  \n\u003e  \n\u003e という合図になります。  \n\u003e  \n\u003e つまり v-on は、  \n\u003e **「どんな操作が行われたときに、どんなメソッドを動かすか」**  \n\u003e を決めるための仕組みです。  \n\u003e  \n\n ---  \n  \n### 【サブミット（submit）】\n\u003e サブミットとは、「フォームを送信する」という意味です。  \n\u003e  \n\u003e 入力フォームには、名前やコメントなどを入力して、  \n\u003e 最後に「送信」ボタンを押す仕組みがあります。  \n\u003e  \n\u003e Web では、この “送信する” という動きを  \n\u003e **submit（サブミット）** と呼びます。  \n\u003e  \n\u003e たとえば、  \n\u003e - ボタンをクリックしたとき  \n\u003e - 入力欄で Enter キーを押したとき  \n\u003e  \n\u003e にフォームが送信されると、それが「サブミットされた」状態です。  \n\u003e  \n\u003e Vue では、このサブミットのタイミングを  \n\u003e `v-on:submit` で受信して、好きなメソッドを実行できます。  \n\u003e  \n\n ---  \n\n  \n### 【ハンドリング（handling）】  \n\u003e ハンドリングとは、「起きた出来事に対して、どう動くかを決めて実行すること」です。  \n\u003e  \n\u003e たとえるなら、  \n\u003e - チャイムが鳴ったら席に着く  \n\u003e - ボールが飛んできたらキャッチする  \n\u003e  \n\u003e といった “合図に対して行動する” イメージです。  \n\u003e  \n\u003e Web では、クリックされた、送信された、入力された、といった出来事（イベント）が起きたときに、   \n\u003e  \n\u003e それに合わせて処理を実行することを  \n\u003e **イベントをハンドリングする** と言います。  \n\u003e  \n\u003e Vue では、v-on を使って  \n\u003e **「このイベントが起きたら、このメソッドを実行する」**  \n\u003e というハンドリングを設定できます。\n\n\n\u003c/details\u003e\n\n\n\n\n# STEP7 リストへの追加\n\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP7の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n\n:::note\n**（本文より引用）**\n\nつづいて doAdd メソッドを定義しましょう。  \nこのメソッドは、フォームの入力値を取得して新しい ToDo の追加処理をします。  \nルートコンストラクタの methods オプションに、メソッドを登録します。\n:::\n\n### 【ルートコンストラクタ】\n\n\n\u003e ルートコンストラクタとは、Vue アプリ全体の設定を書いておく場所です。\n\u003e STEP1 で説明したように、Vue という名前そのものがコンストラクタ関数で、  \n\u003e\n\u003e\n\u003e `new Vue({ ... })` の `{ ... }` に書く設定が\n\u003e「ルートコンストラクタ」にあたり、  \n\u003e `el` や `data`、`methods` などアプリ全体の設計図が入っています。  \n\u003e （家を建てるための “設計図” のようなもの）  \n\u003e  \n\u003e Vue はこのルートコンストラクタ（設計図）を読み取り、実際にアプリを作り上げます。  \n\u003e その結果できあがるのが **「ルートインスタンス」** です。  \n  \n ---  \n \n### 【methods オプション】 \n\u003e methods オプションは、Vue の中で使う **処理（メソッド）** をまとめて書く場所です。  \n\u003e  \n\u003e ボタンを押したときなど、ユーザーの操作に応じて実行したい動きをここに定義します。  \n\u003e  \n\u003e methods に登録したメソッドは、`@click` や `@submit` などのイベントから呼び出せます。  \n\u003e  \n\u003e また、methods の中で `data` や `$refs` を使うときは  \n\u003e **this を付けてアクセスします。**  \n\u003e  \n\u003e 例：  \n\u003e ```js\n\u003e this.todos.push(...)        // データの追加\n\u003e this.$refs.comment.value    // 入力欄の値を取得\n\u003e ```  \n\u003e  \n\u003e 今回の doAdd メソッドも、この methods の中に定義します。\n\n---\n\n:::note\n**（本文より引用）**\n\nコメントと一緒に1行づつ読んでみてください。  \n通常の配列メソッド push を使うだけで、リストデータへ追加できます。\n:::\n\n### 【配列メソッド push】\n\n\u003e **push とは？**  \n\u003e JavaScript の配列に **新しい要素を一番うしろに追加する** メソッドです。  \n\u003e  \n\u003e 例：  \n\u003e ```js\n\u003e var list = [1, 2, 3]\n\u003e list.push(15)\n\u003e // → [1, 2, 3, 15]\n\u003e ```  \n\u003e  \n\u003e Vue の `data` にある `todos` もただの配列なので、  \n\u003e  \n\u003e ```js\n\u003e this.todos.push({\n\u003e   id: todoStorage.uid++,\n\u003e   comment: comment.value,\n\u003e   state: 0\n\u003e })\n\u003e ```  \n\u003e  \n\u003e と書くだけで、新しい ToDo をリストに追加できます。  \n\u003etodoStorage.uid++ とは？  \n\u003etodoStorage.uid は「次に使う ID の番号」を覚えておくための箱（変数）です。\n\u003euid++ は\n\u003e「今の番号を使ってから、1つ増やしておく」という意味になります。\n\u003e\n\u003eたとえば最初の値が 1 なら：\n\u003e\n\u003e新しい ToDo に id: 1 を使う\n\u003e\n\u003eそのあと uid の値が 2 に増える\n\u003e\n\u003eという動きになります。\n\u003e\n\u003eこうすることで、ToDo を追加するたびに\n\u003e1, 2, 3, 4… と重複しない ID を自動で割り振れる  \n\u003e仕組みになっています。\n\u003e\n\u003e Vue は配列の変化を自動で監視しているため、  \n\u003e **push でデータを追加すると画面のリストも自動的に更新されます。**\n\n\n\n\u003c/details\u003e\n\n\n# STEP8 ストレージへの保存の自動化\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP8の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n\n:::note\n**（本文より引用）**\n\nさて、JavaScript 内ではデータは追加されましたが、  \nこれではまだローカルストレージに保存されていません。  \nブラウザをリロードしたら消えてしまいます。\n\ndoAdd メソッドの最後に todoStorage.save メソッドを使って保存してもよいのですが、  \n追加・削除・作業状態の変更すべて同じ処理をしなければいけません。\n\ntodos データの内容が変わると、自動的にストレージへ保存してくれたら素敵ですね。  \nこれは watch オプションの「ウォッチャ」機能を使うことで可能です。\n\nウォッチャはデータの変化に反応して、あらかじめ登録しておいた処理を自動的に行います。  \nこれで、todos データに何か変化があれば自動的にストレージへ保存されるようになりました。\n:::\n\n---\n\n### 【リロード】するとデータが消える理由\n\n\u003e リロードとは、ブラウザで表示しているページを「もう一度読み直す」ことです。  \n\u003e  \n\u003e ページを読み直すと、そのページで動いていた JavaScript も  \n\u003e **いったんすべて初期状態に戻ります。**  \n\u003e  \n\u003e そのため、画面上で ToDo を追加しても、  \n\u003e それは JavaScript のメモリの中にあるだけで、  \n\u003e ページをリロードするとデータは消えてしまいます。  \n\u003e  \n\u003e これを防ぐには、ページを閉じても残る場所に  \n\u003e データを保存する必要があります。  \n\u003e  \n\u003e その保存場所が **ローカルストレージ** です。\n\n---\n\n### 【todoStorage.save メソッド】【watch オプション】\n\n\u003e **todoStorage.save とは？**  \n\u003e 現在の todos データをブラウザのローカルストレージに保存するメソッドです。  \n\u003e  \n\u003e save メソッドでは、まず todos（配列）を  \n\u003e `JSON.stringify` を使って **文字列に変換** します。  \n\u003e ローカルストレージは文字列しか保存できないためです。  \n\u003e  \n\u003e そして：  \n\u003e ```js\n\u003e localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))\n\u003e ```  \n\u003e  \n\u003e とすることで、STORAGE_KEY という名前の箱に  \n\u003e 現在の ToDo リストを保存します。  \n\u003e  \n\u003e save メソッドはつまり：  \n\u003e - todos を文字列に変換する  \n\u003e - ローカルストレージに書き込む  \n\u003e  \n\u003e という 2 つの処理をまとめたものです。  \n\u003e  \n\u003e save を呼び出すだけで、その時点の ToDo リストが保存されます。\n\u003e\n\u003ewatch（ウォッチ）オプションとは？  \n\u003ewatch は「特定のデータに変化があったとき、自動で処理を実行する仕組み」です。\n\u003e今回は todos を監視します。\n\u003e\n\u003eたとえば：\n\u003e\n\u003e - todos に新しい ToDo が追加された\n\u003e - ToDo が削除された\n\u003e - 状態（作業中／完了）が切り替わった\n\u003e\n\u003eこうした todos の変化を見張って（ウォッチして）、\n\u003e変化が起きた瞬間に自動で todoStorage.save を呼び出します。\n\u003e\n\u003eつまり watch を使うことで、\n\u003e「todos が変わったら必ず保存する」という動きを自動化できます。\n---\n\n### 【なぜ doAdd の中で save を使わないのか？】\n\n\u003e ToDo アプリでは、保存が必要になるタイミングが複数あります：  \n\u003e - やることを追加したとき  \n\u003e - やることを削除したとき  \n\u003e - 状態（作業中／完了）を切り替えたとき  \n\u003e  \n\u003e これらすべての処理の中に save を書くと、  \n\u003e **同じコードを何度も書くことになり、管理が大変** になります。  \n\u003e  \n\u003e また、どこかで書き忘れると保存されず、バグの原因にもなります。  \n\u003e  \n\u003e そこで Vue の **watch（ウォッチャ）** を使うことで、  \n\u003e todos データに変化があった瞬間に  \n\u003e **自動的に todoStorage.save を呼び出す** ようにできます。  \n\u003e  \n\u003e つまり：  \n\u003e - 「追加・削除・状態変更のたびに save を書く必要がなくなる」  \n\u003e - 「書き忘れやミスを防げる」  \n\u003e - 「コードがすっきりする」  \n\u003e  \n\u003e というメリットがあるため、  \n\u003e doAdd の中で save を直接使わないようにしているのです。\n\n\n\u003c/details\u003e\n\n\n\n# STEP9 保存されたリストを取得しよう\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP9の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n:::note\n**（本文より引用）**\n\nストレージへの保存ができたので、次はストレージからの取得です。  \nこのアプリケーションの「インスタンス作成時」に、  \nローカルストレージに保存されているデータを「自動的」に取得して、  \nVue.js のデータとして読み込みましょう。\n\n特定のタイミングに何か処理をはさみたいときは  \n「ライフサイクルフック」のメソッドを使用します。\n\nタイミングがいくつか用意されていますが、  \n今回の「インスタンス作成時」には created メソッドを使うとよいでしょう。\n\n:::\n\n---\n\n### 【インスタンス】\n\n\u003e インスタンスとは、STEP1 で作った  \n\u003e `new Vue({ ... })` によって作られるもののことです。  \n\u003e  \n\u003e **Vue.js のデータとして読み込む** とは、  \n\u003e ローカルストレージに保存されていた ToDo の一覧を  \n\u003e Vue インスタンスの `data` にセットすることです。  \n\u003e  \n\u003e 具体的には：  \n\u003e ```js\n\u003e this.todos = todoStorage.fetch()\n\u003e ```  \n\u003e の部分で読み込んでいます。\n\n---\n\n### 【ライフサイクルフック】【created メソッド】\n\n\u003e Vue インスタンス（`new Vue(...)` で作られるもの）は、  \n\u003e 生成されてから画面に表示され、破棄されるまでの間に  \n\u003e いくつかの **決まったタイミング** を通過します。  \n\u003e  \n\u003e そのそれぞれのタイミングで処理を差し込める仕組みが  \n\u003e **ライフサイクルフック** です。  \n\u003e  \n\u003e 例：  \n\u003e - インスタンスが作られた直後（created）  \n\u003e - 画面に表示される直前（beforeMount）  \n\u003e - 画面に表示された直後（mounted）  \n\u003e - インスタンスが破棄される直前（beforeDestroy）  \n\u003e  \n\u003e 今回の「インスタンス作成時」には **created** を使います。  \n\u003e \n\u003e created は、Vue がインスタンスを作り、  \n\u003e「`data` が使える状態になった直後」に呼ばれるメソッドのため、\n\u003eローカルストレージからデータを読み込むのに最適です。\n\u003eまたcreatedはmethodsの中ではなく、dataと同じ階層に書きます。\n\u003e\n\n\n---\n\n:::note\n**（本文より引用）**\nデータの取得には、先に作っておいた todoStorage オブジェクトの  \nfetch メソッドを使用します。\n\nライフサイクルメソッドの定義は「methods の中ではない」ことに注意してください。\n\nローカルストレージは Ajax と違い同期的に結果を取得できるため、  \n返り値を代入すればいいだけなので簡単です！\n:::\n\n\n### 【fetch メソッド】\n\n\u003e fetch メソッドは、ローカルストレージに保存されている  \n\u003e ToDo リストの **文字列データを取り出し**、  \n\u003e `JSON.parse`という 関数 を使って **文字列をJavaScript の配列に戻して返す** メソッドです。  \n\u003e  \n\u003e そのため created の中で：  \n\u003e ```js\n\u003e this.todos = todoStorage.fetch()\n\u003e ```  \n\u003e と書くだけで、保存されていた ToDo リストを  \n\u003e Vue の `data` にセットできます。\n\n---\n\n### 【Ajax（エイジャックス）との違い】\n\n\u003e Ajax は、Web ページを **再読み込みせずに** サーバーと通信して  \n\u003e データの送受信を行う仕組みです。  \n\u003e  \n\u003e 例：  \n\u003e - ボタンを押したらサーバーから最新データを取得  \n\u003e - ページをリロードせずに検索結果を表示  \n\u003e  \n\u003e Ajax の特徴は **非同期処理** であることです。  \n\u003e  \n\u003e 非同期とは、データが返ってくるまで待たずに  \n\u003e 次の処理がどんどん進む動きのことです。  \n\u003e  \n\u003e そのため Ajax を使う場合は：  \n\u003e - コールバック関数  \n\u003e - Promise（then）  \n\u003e - async / await  \n\u003e  \n\u003e などを使って「データが返ってきた後の処理」を書く必要があります。  \n\u003e  \n\u003e 一方、ローカルストレージは Ajax と違い  \n\u003e **同期的にすぐ結果が返ってくる** ため、  \n\u003e  \n\u003e ```js\n\u003e this.todos = todoStorage.fetch()\n\u003e ```  \n\u003e  \n\u003e と書くだけで完了します。\n\n---\n\n### 【返り値】\n\n\u003e 返り値とは、  \n\u003e **「関数やメソッドが実行されたあとに返してくる結果」** のことです。  \n\u003e  \n\u003e 例：  \n\u003e ```js\n\u003e let x = 1 + 2\n\u003e ```  \n\u003e この場合、計算結果の **3** が返り値で、x に入ります。  \n\u003e  \n\u003e 関数でも同じです：  \n\u003e ```js\n\u003e let result = myFunction()\n\u003e ```  \n\u003e `myFunction()` が返した値が result に入ります。\n\n\u003c/details\u003e\n\n# STEP10 状態の変更と削除の処理\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP10の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n\n\n:::note\n**（本文より引用）**\nつづいて「状態の変更」と「削除」機能を実装しましょう。 methods オプションにそれぞれのメソッドを作成します。\ndoChangeState メソッド（状態変更）item.state の値を反転します。\n:::\n\n\n### 【doChangeStateメソッド】\n\n\u003e ```js\n\u003e doChangeState: function (item) {\n\u003e   item.state = !item.state ? 1 : 0\n\u003e }\n\u003e ```  \n\u003e  \n\u003e **コードの解説：**  \n\u003e - item.state が **1（完了）** の場合  \n\u003e   → `!item.state` は false になるので **0（作業中）** が代入される  \n\u003e - item.state が **0（作業中）** の場合  \n\u003e   → `!item.state` は true になるので **1（完了）** が代入される  \n\u003e  \n\u003e 三項演算子を使って、状態を 0 と 1 で切り替える仕組みになっています。  \n\u003e  \n\u003e **意味としては：**  \n\u003e 「今の状態が 1 なら 0 に、0 なら 1 にする」  \n\u003e  \n\u003e **三項演算子とは？**  \n\u003e JavaScript の「短く書ける if 文」のようなものです。  \n\u003e  \n\u003e ```js\n\u003e 条件式 ? 条件が true のときの値 : 条件が false のときの値\n\u003e ```  \n\u003e  \n\u003e if 文で書くと次のようになります：  \n\u003e ```js\n\u003e if (item.state) {\n\u003e   item.state = 0\n\u003e } else {\n\u003e   item.state = 1\n\u003e }\n\u003e ```  \n\u003e  \n\u003e 単純な条件分岐の場合は、三項演算子を使うほうが短く書けます。\n\n---\n\n:::note\n**（本文より引用）**\n\ndoRemove メソッド（削除）\n    インデックスを取得して配列メソッドの splice を使って削除します。\n\n:::\n\n### 【doRemove メソッド】\n\n\u003e ```js\n\u003e doRemove: function (item) {\n\u003e   var index = this.todos.indexOf(item)\n\u003e   this.todos.splice(index, 1)\n\u003e }\n\u003e ```  \n\u003e  \n\u003e このメソッドは、クリックされた ToDo（＝item）を  \n\u003e **todos 配列から削除する処理** です。  \n\u003e  \n\u003e 削除した後は、画面の一覧からも自動的に消えます。  \n\u003e  \n\u003e テンプレート側では：  \n\u003e ```html\n\u003e @click=\"doRemove(item)\"\n\u003e ```  \n\u003e のように呼び出され、削除したい ToDo オブジェクトが item として渡されます。\n\u003e\n\u003eコードの流れ:\n\u003e\n\u003e **1. 配列の中で item が何番目か調べる**  \n\u003e ```js\n\u003e var index = this.todos.indexOf(item)\n\u003e ```  \n\u003e - `this.todos` は ToDo の一覧が入った配列  \n\u003e - `indexOf(item)` は、配列の中で item が最初に見つかった位置（index）を返す  \n\u003e  \n\u003e **例：**  \n\u003e ```js\n\u003e todos = [A, B, C]\n\u003e todos.indexOf(B)  // → 結果は1になる（配列は 0 番目から数えるため）\n\u003e ```  \n\u003e \n\u003e\n\u003e\n\u003e\n\u003e **2. splice でその位置の要素を削除する**  \n\u003e ```js\n\u003e this.todos.splice(index, 1)\n\u003e ```  \n\u003e - `splice(開始位置, 削除する数)` の形で使う  \n\u003e - index の位置から **1 個だけ削除** するという意味  \n\u003e  \n\u003e Vue の ToDo アプリでは、  \n\u003e 「削除したい ToDo（item）」はわかっていても、  \n\u003e **その item が配列の何番目にあるかはわからない** ため、  \n\u003e  \n\u003e 1. `indexOf(item)` で位置を調べ  \n\u003e 2. `splice` でその位置を削除する  \n\u003e  \n\u003e という処理になります。\n\n---\n\n:::note\n**（本文より引用）**\n\nどちらも引数として要素の参照を渡しています。\n\n:::\n\n\n### 【要素の参照を引数として渡している理由】\n\n\u003e doChangeState(item) や doRemove(item) のように、  \n\u003e どちらのメソッドも item を引数として受け取っています。  \n\u003e  \n\u003e ここで渡されている item は、単なる値ではなく、  \n\u003e **todos 配列の中に入っている ToDo オブジェクトそのもの（＝参照）** です。  \n\u003e  \n\u003e **引数（ひきすう）とは？**  \n\u003e 「関数（メソッド）に渡す値のこと」です。  \n\u003e  \n\u003e テンプレート側で次のように書くと：  \n\u003e ```html\n\u003e @click=\"doRemove(item)\"\n\u003e ```  \n\u003e item が引数として doRemove に渡される、という意味になります。\n\u003e\n\u003e ### 参照を渡すとは？\n\u003e\n\u003e （難しい場合は飛ばしても問題ありません）  \n\u003e  \n\u003e JavaScript のオブジェクトや配列は「箱そのもの」ではなく、  \n\u003e **“箱への矢印（住所）” を変数に入れている** と考えると理解しやすいです。  \n\u003e  \n\u003e **図でイメージすると：**  \n\u003e - todos → `[ itemA, itemB, itemC ]`  \n\u003e - item → itemB を指す矢印  \n\u003e  \n\u003e item は itemB のコピーではなく、  \n\u003e **itemB そのものを指している（＝参照している）** ということです。\n\u003e\n\u003e\n\u003e\n\u003e ### item を変更すると todos の中身も変わる理由\n\u003e\n\u003e ```js\n\u003e item.state = 1\n\u003e ```  \n\u003e  \n\u003e これは：  \n\u003e - item が指している itemB の state を変える  \n\u003e - todos の中に入っている itemB も同じもの  \n\u003e  \n\u003e よって **todos の中身も変わる** という動きになります。\n\u003e\n\u003e\n\u003e\n\u003e ### \"値渡し\"（プリミティブ型）との違い\n\u003e\n\u003e JavaScript では、数値や文字列などは **値渡し** になります。  \n\u003e これらは **値そのものが変数に入る** という特徴があります。  \n\u003e  \n\u003e 例：  \n\u003e ```js\n\u003e let a = 10\n\u003e let b = a\n\u003e b = 20\n\u003e console.log(a) // 10（変わらない）\n\u003e ```  \n\u003e  \n\u003e b は a のコピーなので、b を変えても a は変わりません。\n\u003e\n\u003e\n\u003e\n\u003e ### \"参照型\"（オブジェクト・配列）の場合\n\u003e\n\u003e オブジェクトや配列は **参照（矢印）** が渡されるため、  \n\u003e コピーではなく **同じものを共有** します。  \n\u003e  \n\u003e 例：  \n\u003e ```js\n\u003e let a = { value: 10 }\n\u003e let b = a\n\u003e b.value = 20\n\u003e console.log(a.value) // 20（変わる）\n\u003e ```  \n\u003e  \n\u003e a と b は **同じオブジェクトを指している** からです。\n\u003e\n\u003e 参照を渡すことで：  \n\u003e - 正しい ToDo を直接変更・削除できる  \n\u003e - コピーではなく “同じもの” を共有しているため、  \n\u003e   item を変更すると todos の中身も変わる  \n\u003e  \n\u003e というメリットがあります。\n\n---\n\n:::note\n**（本文より引用）**\nまだモックの状態だった、状態変更ボタンのイベントをハンドルします。\n:::\n\nこれまで動いていなかった状態変更ボタンに、実際の処理をつけますという意味です。\n\n:::note\n**（本文より引用）**\n つづいて削除ボタンもハンドルします。\n「削除」は注意するべき操作のため、\nキー修飾子 .ctrl を使って\n「コントロールキーを押しながらクリック」しなければ呼び出されないようにします。\n``` \n\u003cbutton v-on:click.ctrl=\"doRemove(item)\"\u003e\n削除\n\u003c/button\u003e\n```\n:::\n削除ボタンも実際の処理を付けます\n削除する際は注意が必要な操作なので、誤って押してしまわないように、\nコントロールキー（ctrl）を押しながらクリックしたときだけ\n削除が実行されるようにします。v-on:click.ctrlでその設定をします\n\n\n\n\n\u003c/details\u003e\n\n# STEP11 選択用フォームの作成\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP11の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n:::note\n**（本文より引用）**\n特定の作業状態のリストのみを表示させる「絞り込み機能」を追加しましょう。\nスローガンテキストの下にラジオボタンをリストで表示します。 \nToDo リストと同じように動的に作成するため、選択肢の options リストを作成しました。\n:::\n\n### 【スローガンテキスト】\n\u003eスローガンテキストとは、画面上部に表示されている\n\u003e『Hello Vue.js World!!』 のようなアプリのタイトル部分のことです。\n\n---\n\n### 【動的に作成】\n\u003e動的に作成とは、配列などのデータをもとに、\n\u003e必要な数だけ HTML 要素を自動で生成する仕組みのことです。\n\u003e今回のラジオボタンは、options 配列を v-for でループすることで、\n\u003e3つの選択肢が自動的に画面に表示されます。\n\n---\n\n:::note\n**（本文より引用）**\noptions リストを \u003clabel\u003e タグで繰り返し描画して、\n内側の \u003cinput\u003e タグの value 属性には、データ側の label.value データをバインドします。\nv-model ディレクティブを使って、ラジオボタンの選択値と current データを同期させます。\nラジオボタンが変更されると、その要素の label.value が\ncurrent プロパティへ代入される仕組みです。\n:::\n\n\n### 【実際の処理の流れ】\n\u003e ※元のコードでは v-for の変数名が label でしたが、  \n\u003e 以下の説明では読みやすさのために option に変更して解説します。  \n\u003e （label.value → option.value と読み替えて理解してください）  \n\u003e  \n\u003e HTML のこの部分で、ラジオボタンが自動的に生成されています：  \n\u003e （元のファイルは `\u003clabel v-for=\"label in options\"\u003e` だったが説明用に変更）  \n\u003e  \n\u003e ```html\n\u003e \u003clabel v-for=\"option in options\"\u003e\n\u003e   \u003cinput type=\"radio\"\n\u003e          v-model=\"current\"\n\u003e          v-bind:value=\"option.value\"\u003e\n\u003e   {{ option.label }}\n\u003e \u003c/label\u003e\n\u003e ```  \n\u003e  \n\u003e **実際の処理の流れ**  \n\u003e  \n\u003e **v-for=\"option in options\"**  \n\u003e options 配列をループし、配列の要素数ぶん `\u003clabel\u003e` ブロックを自動で生成します。  \n\u003e 1回のループでは、options 配列の1つのオブジェクトが option に入ります。  \n\u003e  \n\u003e **`\u003cinput type=\"radio\"\u003e`**  \n\u003e ループのたびにラジオボタンが1つ作られます。  \n\u003e  \n\u003e **v-model=\"current\"**  \n\u003e 選択されたラジオボタンの値と、Vue 側の current データを常に同期させます。  \n\u003e 初期値として current に -1 が入っているため、最初は「すべて」が選択された状態になります。  \n\u003e  \n\u003e **v-bind:value=\"option.value\"**  \n\u003e options 配列の各要素（option オブジェクト）の value を  \n\u003e `\u003cinput\u003e` の value 属性にバインドします。  \n\u003e → ラジオボタンの値がデータから自動で設定されます。  \n\u003e （バインド＝データと HTML を結びつけること。）  \n\u003e  \n\u003e **{{ option.label }}**  \n\u003e option オブジェクトの label プロパティの値  \n\u003e （\"すべて\" / \"作業中\" / \"完了\"）を画面に表示します。  \n\u003e  \n\u003e **1回目のループ：**  \n\u003e option は次のオブジェクト：  \n\u003e `{ value: -1, label: \"すべて\" }`  \n\u003e label プロパティの値は \"すべて\"  \n\u003e  \n\u003e **2回目のループ：**  \n\u003e option は次のオブジェクト：  \n\u003e `{ value: 0, label: \"作業中\" }`  \n\u003e label プロパティの値は \"作業中\"  \n\u003e  \n\u003e **3回目のループ：**  \n\u003e option は次のオブジェクト：  \n\u003e `{ value: 1, label: \"完了\" }`  \n\u003elabel プロパティの値は \"完了\"\n\n\n\n\n\u003c/details\u003e\n\n# STEP12 リストの絞り込み機能\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP12の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n:::note\n**（本文より引用）**\n\ncurrent データの選択値によって表示させる\nToDo リストの内容を振り分けるため「算出プロパティ」という機能を使用します。\n算出プロパティは、データから別の新しいデータを作成する関数型のデータです。\n定義方法は、computed オプションに加工したデータを返すメソッドを登録します。\n算出プロパティは、元になったデータに変更があるまで、結果をキャッシュするという性質を持っています。\n\n:::\n\n\n### 【算出プロパティ】\n\u003e 算出プロパティとは、Vue が提供している  \n\u003e 「データを元に新しい値を計算して返す仕組み」です。  \n\u003e 見た目はメソッド（関数）のようですが、  \n\u003e 実際には「データから派生した別のデータ」を  \n\u003e 自動的に作り出すための特別なプロパティです。  \n\u003e  \n\u003e 算出プロパティは、computed オプションの中に  \n\u003e メソッドとして定義します。  \n\u003e そのメソッドの return で返した値が、  \n\u003e 新しいデータとして扱われます。  \n\u003e  \n\u003e また、算出プロパティには  \n\u003e **「キャッシュされる」** という特徴があります。  \n\u003e  \n\u003e **\"キャッシュ\"されるとは？**  \n\u003e 一度計算した結果を Vue が覚えておき、  \n\u003e 元データが変わらない限り、再計算しないことです。  \n\u003e  \n\u003e 元になっているデータ（今回でいう current や todos）が変わらない限り、  \n\u003e 算出プロパティの結果は再計算されず、  \n\u003e 前回の結果がそのまま使われます。  \n\u003e  \n\u003e そのため、無駄な処理が減り、  \n\u003e アプリが効率よく動作します。  \n\u003e  \n\u003e methods でも同じような処理はできますが、  \n\u003e methods は「呼ばれるたびに毎回計算される」ため、  \n\u003e 画面が再描画されるたびに関数が実行され、  \n\u003e 処理が重くなる可能性があります。  \n\u003e （methods はキャッシュされないためです）  \n\u003e  \n\u003e 今回の絞り込み機能では、  \n\u003e current の値（-1, 0, 1）に応じて表示する ToDo リストを切り替えるために、  \n\u003e 算出プロパティを使って  \n\u003e **「表示用の ToDo リスト」** を作成しています。\n\n---\n\n\n:::note\n**（本文より引用）**\n定義方法が違うだけで使い方はデータと一緒です。\n一覧表示テーブルの v-for ディレクティブで使用している todos の部分を\ncomputedTodos に置き換えましょう。\nたとえば「◯件見つかりました」という結果の要素数を表示したいとき、\n単純にその配列の computedTodos.length を見れば欲しい数字が得られます。\n{{ computedTodos.length }} 件を表示中\nキャッシュ機能があるおかげで、\nメソッドと違い何度使用しても処理は 1 度しか行われません。\n\n:::\n\n### 【todos を computedTodos に置き換える理由】\n\u003e 「絞り込み後のリスト」を画面に表示したいからです。  \n\u003e  \n\u003e todos … 元の全データ  \n\u003e computedTodos … current の値に応じて絞り込まれたデータ  \n\u003e  \n\u003e 画面に表示したいのは **「絞り込まれた結果」** なので、  \n\u003e todos のままでは目的を達成できません。  \n\u003e  \n\u003e **computedTodos.length**  \n\u003e computedTodos の中にあるオブジェクトの数（＝絞り込み後の件数）を表します。  \n\u003e  \n\u003e todos は「元データ」であり、ユーザーの選択に応じて内容が変わることはありません。  \n\u003e 一方、computedTodos は current の値に応じて内容が変わる **表示用データ** です。  \n\u003e  \n\u003e computedTodos を使うことで、ユーザーがラジオボタンを切り替えた瞬間に  \n\u003e 表示内容が自動的に更新されるようになります。\n\n\u003c/details\u003e\n\n# STEP13 文字列の変換処理\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEP13の本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n:::note\n**（本文より引用）**\n\n最後の仕上げとして「状態変更ボタン」のラベルが数字になっているのを修正しましょう。\n    状態変更ボタンで使っている状態の item.state データは、\n    文字列そのものではなく「キー」になる数字を保存しています。\n    一般的にもカテゴリーなどのデータでは、こういった数字や短い英数字のキーの状態で保存されます。\n    しかし、このままでは作業中なら「0」完了なら「1」と表示され、まったく意味がわかりません。\n    絞り込みのセレクトボックス用の options データをもとに、\n    value から label へ変換するための labels 算出プロパティを作成します。\n    Mustache で labels オブジェクトを通すように変更します。\n    `\u003cbutton v-on:click=\"doChangeState(item)\"\u003e\n    {{ labels[item.state] }}\n    \u003c/button\u003e`\n    これで人が理解できる文字で表示されるようになりました。\n    このような文字の処理は、フィルタ機能を使っても同じように変換できます。\n\n:::\n\n### 【なぜ labels（算出プロパティ）が必要なのか】\n\u003e ToDo の状態（item.state）は、データとして扱いやすいように  \n\u003e 数字（0 / 1）で保存されています。  \n\u003e しかし、この数字をそのまま画面に表示すると「0」「1」と表示され、  \n\u003e ユーザーには意味が伝わりません。  \n\u003e  \n\u003e そこで、絞り込み用に使っている options データを利用して、  \n\u003e value（数字）→ label（文字）に変換するための  \n\u003e **labels（算出プロパティ）** を作成します。  \n\u003e  \n\u003e **labels() のコード**  \n\u003e ```js\n\u003e labels() {\n\u003e   return this.options.reduce(function (a, b) {\n\u003e     return Object.assign(a, { [b.value]: b.label })\n\u003e   }, {})\n\u003e }\n\u003e ```  \n\u003e  \n\u003e **labels() の処理を一行ずつ説明**  \n\u003e  \n\u003e **1. `labels() {`**  \n\u003e labels という算出プロパティの定義を開始します。  \n\u003e  \n\u003e **2. `return this.options.reduce(function (a, b) {`**  \n\u003e options 配列を reduce で 1 つずつ処理し、  \n\u003e 最終的に 1 つのオブジェクトにまとめます。  \n\u003e - a … これまでに作られたオブジェクト  \n\u003e - b … options の現在の要素  \n\u003e reduce の初期値は `{}`（空のオブジェクト）です。  \n\u003e  \n\u003e **3. `return Object.assign(a, { [b.value]: b.label })`**  \n\u003e Object.assign を使って、a に新しいプロパティを追加します。  \n\u003e - b.value（数字）をキーにして  \n\u003e - b.label（文字）を値として  \n\u003e a に追加する処理です。  \n\u003e  \n\u003e **例：**  \n\u003e b が `{ value: 0, label: \"作業中\" }` の場合、  \n\u003e `{ 0: \"作業中\" }` というプロパティが追加されます。  \n\u003e  \n\u003e **4. `{ [b.value]: b.label }` の意味**  \n\u003e `[b.value]` は「変数をキーとして使う」ための書き方です。  \n\u003e - b.value が 0 → `{ 0: \"作業中\" }`  \n\u003e - b.value が 1 → `{ 1: \"完了\" }`  \n\u003e のように、数字をキーにしたオブジェクトを作れます。  \n\u003e  \n\u003e **5. `}, {})`**  \n\u003e reduce の初期値として `{}` を渡しています。  \n\u003e 空のオブジェクトからスタートし、1 つずつプロパティを追加していきます。  \n\u003e  \n\u003e **6. `}`**  \n\u003e labels() の処理が終了します。  \n\u003e  \n\u003e **labels() の最終的な結果**  \n\u003e options が次のような配列なら：  \n\u003e ```js\n\u003e [\n\u003e   { value: -1, label: \"すべて\" },\n\u003e   { value: 0,  label: \"作業中\" },\n\u003e   { value: 1,  label: \"完了\" }\n\u003e ]\n\u003e ```  \n\u003e  \n\u003e labels() の返すオブジェクトは次のようになります：  \n\u003e ```js\n\u003e {\n\u003e   -1: \"すべて\",\n\u003e    0: \"作業中\",\n\u003e    1: \"完了\"\n\u003e }\n\u003e ```  \n\u003e  \n\u003e これにより、数字をキーにして文字を簡単に取り出せます。  \n\u003e  \n\u003e - labels[0] → \"作業中\"  \n\u003e - labels[1] → \"完了\"  \n\u003e - labels[-1] → \"すべて\"  \n\u003e  \n\u003e **Mustache（マスタッシュ）で変換結果を表示する**  \n\u003e Vue の Mustache（{{ }}）は、データを画面に表示するための構文です。  \n\u003e  \n\u003e ```html\n\u003e {{ labels[item.state] }}\n\u003e ```  \n\u003e  \n\u003e と書くことで、数字の状態（0 / 1）を labels を通して  \n\u003e 文字に変換して表示できます。  \n\u003e  \n\u003e **例：**  \n\u003e ```html\n\u003e \u003cbutton v-on:click=\"doChangeState(item)\"\u003e\n\u003e   {{ labels[item.state] }}\n\u003e \u003c/button\u003e\n\u003e ```  \n\u003e  \n\u003e item.state が 0 の場合 → \"作業中\"  \n\u003e item.state が 1 の場合 → \"完了\"  \n\u003e  \n\u003e このようにして、数字で保存された状態を  \n\u003e **人が読める文字に変換して表示** できるようになります。\n\n\n\u003c/details\u003e\n\n\n# STEPEX 記事作成の感想\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSTEPEXの本文（クリックで開く）\u003c/strong\u003e\u003c/summary\u003e\n\n:::note alert\n\n# ***ながすぎ。ほんとうに。***\n\n:::\n\n\n今回用語を調べるにあたり生成AIのCopilotを活用しました\n\n最初は他の人よりも少し長めに記事を作成しようとしていたのですが\n説明の分かりやすさ、正確さを重視した結果\n製作期間約2週間、約3万字ほどの大作になってしまいました。\n\n専門用語がある理由を今回の記事作成で分かったような気がします\nまた専門用語を誰にでも分かるように説明するのは大変だと感じました\n\nこの記事を最後まで見てくれた方、この記事をチェックしてくれた方に最大限の感謝を送ります。\n\n\n\n\u003c/details\u003e\n","coediting":false,"comments_count":0,"created_at":"2026-05-10T13:37:31+09:00","group":null,"id":"d283e5b20132a1be5af4","likes_count":36,"private":false,"reactions_count":0,"stocks_count":2,"tags":[{"name":"JavaScript","versions":[]},{"name":"初心者向け","versions":[]},{"name":"Vue.js","versions":[]},{"name":"新卒研修","versions":[]}],"title":"ToDoリストアプリの仕組み","updated_at":"2026-05-28T17:15:13+09:00","url":"https://qiita.com/tattttt/items/d283e5b20132a1be5af4","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":1,"github_login_name":null,"id":"tattttt","items_count":1,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4395581,"profile_image_url":"https://secure.gravatar.com/avatar/cb3c47b4cee85e146003d4b9b26712dc","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":"any-plus","slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:15\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-4:159\"\u003e詳細画面（\u003ccode\u003eFilmDetailView.vue\u003c/code\u003e）へ遷移する際に \u003ccode\u003escrollTo(0, 0)\u003c/code\u003e と \u003ccode\u003eblur()\u003c/code\u003e を入れました。\u003cbr\u003e\nまた、\u003ccode\u003eonMounted\u003c/code\u003e だけでなく \u003ccode\u003ewatch\u003c/code\u003e でも呼び出す必要がありました。なぜそうなったのか、実際のコードで解説します。\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:131\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4404749%2Fc1a8bf6c-e1a1-4d5c-af0a-e96ae24c05a6.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=a0c938ef3752079eb24f4d30e42cf5cb\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4404749%2Fc1a8bf6c-e1a1-4d5c-af0a-e96ae24c05a6.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=a0c938ef3752079eb24f4d30e42cf5cb\" alt=\"作品詳細画面\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4404749%2Fc1a8bf6c-e1a1-4d5c-af0a-e96ae24c05a6.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=c7550a87d88ee2b9134e3643e6e0c683 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4404749/c1a8bf6c-e1a1-4d5c-af0a-e96ae24c05a6.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003chr data-sourcepos=\"8:1-9:0\"\u003e\n\u003ch2 data-sourcepos=\"10:1-10:54\"\u003e\n\u003cspan id=\"問題スクロール位置が引き継がれる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%95%8F%E9%A1%8C%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E4%BD%8D%E7%BD%AE%E3%81%8C%E5%BC%95%E3%81%8D%E7%B6%99%E3%81%8C%E3%82%8C%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e問題：スクロール位置が引き継がれる\u003c/h2\u003e\n\u003cp data-sourcepos=\"12:1-13:148\"\u003e\u003ccode\u003eApp.vue\u003c/code\u003e は Vue Router を使わず、\u003ccode\u003ecurrentPage\u003c/code\u003e の切り替えで画面を変えています。\u003cbr\u003e\n\u003ccode\u003ev-if\u003c/code\u003e / \u003ccode\u003ev-else-if\u003c/code\u003e でコンポーネントを差し替えるだけなので、DOM 上は同じページのまま表示が切り替わります。\u003c/p\u003e\n\u003cp data-sourcepos=\"15:1-15:220\"\u003eこのため、\u003cstrong\u003e一覧画面でスクロールして下の方にある作品をクリックすると、詳細画面でもスクロール位置が引き継がれ、画面の途中から表示されてしまいます。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"17:1-17:189\"\u003eまた、一覧のボタンを Tab キーで選択してから詳細画面に遷移した場合、\u003cstrong\u003eフォーカスが残ったまま\u003c/strong\u003e詳細画面が表示されることがあります。\u003c/p\u003e\n\u003chr data-sourcepos=\"19:1-20:0\"\u003e\n\u003ch2 data-sourcepos=\"21:1-21:55\"\u003e\n\u003cspan id=\"解決策onmounted--watch-の両方で呼ぶ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A7%A3%E6%B1%BA%E7%AD%96onmounted--watch-%E3%81%AE%E4%B8%A1%E6%96%B9%E3%81%A7%E5%91%BC%E3%81%B6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e解決策：\u003ccode\u003eonMounted\u003c/code\u003e + \u003ccode\u003ewatch\u003c/code\u003e の両方で呼ぶ\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"23:1-42:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// FilmDetailView.vue（実際のコード）\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eresetDetailViewport\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nb\"\u003ewindow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003escrollTo\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"na\"\u003etop\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003eleft\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003ebehavior\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eauto\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eactiveElement\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eactiveElement\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eactiveElement\u003c/span\u003e \u003cspan class=\"k\"\u003einstanceof\u003c/span\u003e \u003cspan class=\"nx\"\u003eHTMLElement\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eactiveElement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eblur\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003ewatch\u003c/span\u003e\u003cspan class=\"p\"\u003e(()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nx\"\u003efilm\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003efilmId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nf\"\u003eresetDetailViewport\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n  \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003eresolveInitialMedia\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003eonMounted\u003c/span\u003e\u003cspan class=\"p\"\u003e(()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nf\"\u003eresetDetailViewport\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n  \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003eresolveInitialMedia\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"44:1-45:0\"\u003e\n\u003ch2 data-sourcepos=\"46:1-46:36\"\u003e\n\u003cspan id=\"なぜ-watch-が必要なのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C-watch-%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜ \u003ccode\u003ewatch\u003c/code\u003e が必要なのか\u003c/h2\u003e\n\u003cp data-sourcepos=\"48:1-48:67\"\u003e\u003cstrong\u003e\u003ccode\u003eonMounted\u003c/code\u003e だけでは不十分なケースがあります。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"50:1-50:307\"\u003eVue は同じコンポーネントが再利用されることがあります。たとえば、詳細画面を表示している状態で「関連作品」をクリックすると、\u003ccode\u003eFilmDetailView\u003c/code\u003e は新しい作品 props を受け取るだけで、アンマウント→マウントは発生しません。\u003c/p\u003e\n\u003cp data-sourcepos=\"52:1-52:195\"\u003eこの場合、\u003ccode\u003eonMounted\u003c/code\u003e は呼ばれません。\u003ccode\u003ewatch(() =\u0026gt; film.filmId, ...)\u003c/code\u003e を追加することで、\u003cstrong\u003e\u003ccode\u003efilmId\u003c/code\u003e が変わるたびにスクロールリセットが実行\u003c/strong\u003eされます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"54:1-57:108\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"54:1-54:37\"\u003e\n\u003cth data-sourcepos=\"54:2-54:12\"\u003eケース\u003c/th\u003e\n\u003cth data-sourcepos=\"54:14-54:26\"\u003e\u003ccode\u003eonMounted\u003c/code\u003e\u003c/th\u003e\n\u003cth data-sourcepos=\"54:28-54:36\"\u003e\u003ccode\u003ewatch\u003c/code\u003e\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"56:1-56:99\"\u003e\n\u003ctd data-sourcepos=\"56:2-56:33\"\u003e詳細画面へ初めて遷移\u003c/td\u003e\n\u003ctd data-sourcepos=\"56:35-56:52\"\u003e✅ 呼ばれる\u003c/td\u003e\n\u003ctd data-sourcepos=\"56:54-56:98\"\u003e❌ 呼ばれない（filmId変化なし）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"57:1-57:108\"\u003e\n\u003ctd data-sourcepos=\"57:2-57:66\"\u003e詳細→別作品詳細（同コンポーネント再利用）\u003c/td\u003e\n\u003ctd data-sourcepos=\"57:68-57:88\"\u003e❌ 呼ばれない\u003c/td\u003e\n\u003ctd data-sourcepos=\"57:90-57:107\"\u003e✅ 呼ばれる\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"59:1-59:72\"\u003e両方書くことで、どちらのケースもカバーできます。\u003c/p\u003e\n\u003chr data-sourcepos=\"61:1-62:0\"\u003e\n\u003ch2 data-sourcepos=\"63:1-63:37\"\u003e\n\u003cspan id=\"behavior-auto-にした理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#behavior-auto-%E3%81%AB%E3%81%97%E3%81%9F%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003ebehavior: 'auto'\u003c/code\u003e にした理由\u003c/h2\u003e\n\u003cp data-sourcepos=\"65:1-66:145\"\u003e\u003ccode\u003ebehavior: 'smooth'\u003c/code\u003e にするとスクロールがアニメーションするため、詳細画面の表示が始まる前に「スクロールしている」ことがユーザーに見えてしまいます。\u003cbr\u003e\n詳細への遷移は「画面が切り替わった」感覚を出したいため、即座に先頭へ移動する \u003ccode\u003e'auto'\u003c/code\u003e を選びました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"68:1-74:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// behavior: 'smooth' → アニメーション付き（NG）\u003c/span\u003e\n\u003cspan class=\"nb\"\u003ewindow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003escrollTo\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"na\"\u003etop\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003eleft\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003ebehavior\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esmooth\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// behavior: 'auto' → 即座に移動（OK）\u003c/span\u003e\n\u003cspan class=\"nb\"\u003ewindow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003escrollTo\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"na\"\u003etop\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003eleft\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003ebehavior\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eauto\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"76:1-77:0\"\u003e\n\u003ch2 data-sourcepos=\"78:1-78:30\"\u003e\n\u003cspan id=\"blur-が必要な理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#blur-%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003eblur()\u003c/code\u003e が必要な理由\u003c/h2\u003e\n\u003cp data-sourcepos=\"80:1-81:135\"\u003e\u003ccode\u003eblur()\u003c/code\u003e を入れない場合、一覧画面でフォーカスされていた要素のフォーカスリングが詳細画面でも残ります。\u003cbr\u003e\nアクセシビリティ上はフォーカス管理が必要なため、遷移時に明示的にフォーカスを外しています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"83:1-90:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// document.activeElement は現在フォーカスされている要素\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// HTMLElement でなければ blur() メソッドがないので型チェックが必要\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eactiveElement\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eactiveElement\u003c/span\u003e\n\u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eactiveElement\u003c/span\u003e \u003cspan class=\"k\"\u003einstanceof\u003c/span\u003e \u003cspan class=\"nx\"\u003eHTMLElement\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003eactiveElement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eblur\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"92:1-92:168\"\u003e\u003ccode\u003edocument.activeElement\u003c/code\u003e は \u003ccode\u003eElement | null\u003c/code\u003e 型で、\u003ccode\u003eblur()\u003c/code\u003e は \u003ccode\u003eHTMLElement\u003c/code\u003e にしか存在しないため \u003ccode\u003einstanceof HTMLElement\u003c/code\u003e で型を絞っています。\u003c/p\u003e\n\u003chr data-sourcepos=\"94:1-95:0\"\u003e\n\u003ch2 data-sourcepos=\"96:1-96:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"98:1-99:42\"\u003eVue Router を使わない SPA では、ページ遷移に相当する操作を自分で実装する必要があります。\u003cbr\u003e\n今回学んだことをまとめると：\u003c/p\u003e\n\u003cul data-sourcepos=\"101:1-104:74\"\u003e\n\u003cli data-sourcepos=\"101:1-101:80\"\u003e\n\u003cstrong\u003e\u003ccode\u003escrollTo(0, 0)\u003c/code\u003e + \u003ccode\u003eblur()\u003c/code\u003e\u003c/strong\u003e を \u003ccode\u003eonMounted\u003c/code\u003e と \u003ccode\u003ewatch\u003c/code\u003e の両方で呼ぶ\u003c/li\u003e\n\u003cli data-sourcepos=\"102:1-102:95\"\u003e\n\u003cstrong\u003e\u003ccode\u003ebehavior: 'auto'\u003c/code\u003e\u003c/strong\u003e で即座に移動する（smooth はページ遷移感が壊れる）\u003c/li\u003e\n\u003cli data-sourcepos=\"103:1-103:95\"\u003e\n\u003cstrong\u003e\u003ccode\u003einstanceof HTMLElement\u003c/code\u003e\u003c/strong\u003e で blur() を安全に呼ぶ（TypeScript の型チェック）\u003c/li\u003e\n\u003cli data-sourcepos=\"104:1-104:74\"\u003e\u003cstrong\u003e\u003ccode\u003ewatch\u003c/code\u003e は同一コンポーネント再利用時のために必須\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","body":"## はじめに\n\n詳細画面（`FilmDetailView.vue`）へ遷移する際に `scrollTo(0, 0)` と `blur()` を入れました。  \nまた、`onMounted` だけでなく `watch` でも呼び出す必要がありました。なぜそうなったのか、実際のコードで解説します。\n\n![作品詳細画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4404749/c1a8bf6c-e1a1-4d5c-af0a-e96ae24c05a6.png)\n\n---\n\n## 問題：スクロール位置が引き継がれる\n\n`App.vue` は Vue Router を使わず、`currentPage` の切り替えで画面を変えています。  \n`v-if` / `v-else-if` でコンポーネントを差し替えるだけなので、DOM 上は同じページのまま表示が切り替わります。\n\nこのため、**一覧画面でスクロールして下の方にある作品をクリックすると、詳細画面でもスクロール位置が引き継がれ、画面の途中から表示されてしまいます。**\n\nまた、一覧のボタンを Tab キーで選択してから詳細画面に遷移した場合、**フォーカスが残ったまま**詳細画面が表示されることがあります。\n\n---\n\n## 解決策：`onMounted` + `watch` の両方で呼ぶ\n\n```typescript\n// FilmDetailView.vue（実際のコード）\nconst resetDetailViewport = () =\u003e {\n  window.scrollTo({ top: 0, left: 0, behavior: 'auto' })\n  const activeElement = document.activeElement\n  if (activeElement instanceof HTMLElement) {\n    activeElement.blur()\n  }\n}\n\nwatch(() =\u003e film.filmId, () =\u003e {\n  resetDetailViewport()\n  void resolveInitialMedia()\n})\n\nonMounted(() =\u003e {\n  resetDetailViewport()\n  void resolveInitialMedia()\n})\n```\n\n---\n\n## なぜ `watch` が必要なのか\n\n**`onMounted` だけでは不十分なケースがあります。**\n\nVue は同じコンポーネントが再利用されることがあります。たとえば、詳細画面を表示している状態で「関連作品」をクリックすると、`FilmDetailView` は新しい作品 props を受け取るだけで、アンマウント→マウントは発生しません。\n\nこの場合、`onMounted` は呼ばれません。`watch(() =\u003e film.filmId, ...)` を追加することで、**`filmId` が変わるたびにスクロールリセットが実行**されます。\n\n| ケース | `onMounted` | `watch` |\n|---|---|---|\n| 詳細画面へ初めて遷移 | ✅ 呼ばれる | ❌ 呼ばれない（filmId変化なし） |\n| 詳細→別作品詳細（同コンポーネント再利用） | ❌ 呼ばれない | ✅ 呼ばれる |\n\n両方書くことで、どちらのケースもカバーできます。\n\n---\n\n## `behavior: 'auto'` にした理由\n\n`behavior: 'smooth'` にするとスクロールがアニメーションするため、詳細画面の表示が始まる前に「スクロールしている」ことがユーザーに見えてしまいます。  \n詳細への遷移は「画面が切り替わった」感覚を出したいため、即座に先頭へ移動する `'auto'` を選びました。\n\n```typescript\n// behavior: 'smooth' → アニメーション付き（NG）\nwindow.scrollTo({ top: 0, left: 0, behavior: 'smooth' })\n\n// behavior: 'auto' → 即座に移動（OK）\nwindow.scrollTo({ top: 0, left: 0, behavior: 'auto' })\n```\n\n---\n\n## `blur()` が必要な理由\n\n`blur()` を入れない場合、一覧画面でフォーカスされていた要素のフォーカスリングが詳細画面でも残ります。  \nアクセシビリティ上はフォーカス管理が必要なため、遷移時に明示的にフォーカスを外しています。\n\n```typescript\n// document.activeElement は現在フォーカスされている要素\n// HTMLElement でなければ blur() メソッドがないので型チェックが必要\nconst activeElement = document.activeElement\nif (activeElement instanceof HTMLElement) {\n  activeElement.blur()\n}\n```\n\n`document.activeElement` は `Element | null` 型で、`blur()` は `HTMLElement` にしか存在しないため `instanceof HTMLElement` で型を絞っています。\n\n---\n\n## まとめ\n\nVue Router を使わない SPA では、ページ遷移に相当する操作を自分で実装する必要があります。  \n今回学んだことをまとめると：\n\n- **`scrollTo(0, 0)` + `blur()`** を `onMounted` と `watch` の両方で呼ぶ\n- **`behavior: 'auto'`** で即座に移動する（smooth はページ遷移感が壊れる）\n- **`instanceof HTMLElement`** で blur() を安全に呼ぶ（TypeScript の型チェック）\n- **`watch` は同一コンポーネント再利用時のために必須**\n","coediting":false,"comments_count":0,"created_at":"2026-04-28T13:37:46+09:00","group":null,"id":"425b9dc0eb0921a2edfa","likes_count":1,"private":false,"reactions_count":0,"stocks_count":2,"tags":[{"name":"TypeScript","versions":[]},{"name":"Vue.js","versions":[]},{"name":"SpringBoot","versions":[]}],"title":"詳細遷移時に scrollTo(0, 0) と blur() を入れた理由","updated_at":"2026-05-27T00:54:13+09:00","url":"https://qiita.com/y104autumn/items/425b9dc0eb0921a2edfa","user":{"description":"約15年の開発経験を礎に、システムが形になり動くまでの全体像を重視した発信を目標としています。この業界の仕事に携わり始めた頃の情熱を胸に、論理的裏付けを持って皆さんと対話し、質の高いものづくりを追求したいです。知見の発信を通じ、技術で切磋琢磨できる関係を築ければと思います。「いいね」やコメントをいただけると大きな励みになります。","facebook_id":"","followees_count":38,"followers_count":3,"github_login_name":"y104autumn","id":"y104autumn","items_count":25,"linkedin_id":"","location":"東京都","name":"横塚 敏明","organization":"株式会社エンジョイ","permanent_id":4404749,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4404749/profile-images/1776747921","team_only":false,"twitter_screen_name":null,"website_url":"https://mint041223techblog.netlify.app/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:15\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-4:169\"\u003eVue 3 + TypeScript でアプリを作るとき、最初に迷うのが「画面遷移をどう管理するか」です。\u003cbr\u003e\nVue Router を使うのが定番ですが、このアプリでは \u003cstrong\u003e\u003ccode\u003eApp.vue\u003c/code\u003e の \u003ccode\u003ecurrentPage\u003c/code\u003e という \u003ccode\u003eref\u003c/code\u003e で全画面を管理する\u003c/strong\u003e 構成を選びました。\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:120\"\u003eなぜ Vue Router を使わなかったのか、どう実装しているのか、実際のコードで解説します。\u003c/p\u003e\n\u003chr data-sourcepos=\"8:1-9:0\"\u003e\n\u003ch2 data-sourcepos=\"10:1-10:30\"\u003e\n\u003cspan id=\"アプリの画面フロー\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E7%94%BB%E9%9D%A2%E3%83%95%E3%83%AD%E3%83%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eアプリの画面フロー\u003c/h2\u003e\n\u003cp data-sourcepos=\"12:1-12:69\"\u003eこのアプリの画面遷移は大きく 2 つに分かれます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"14:1-19:49\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"14:1-14:28\"\u003e\n\u003cth data-sourcepos=\"14:2-14:12\"\u003eゾーン\u003c/th\u003e\n\u003cth data-sourcepos=\"14:14-14:27\"\u003e主な画面\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"16:1-16:123\"\u003e\n\u003ctd data-sourcepos=\"16:2-16:42\"\u003e公開エリア（ログイン不要）\u003c/td\u003e\n\u003ctd data-sourcepos=\"16:44-16:122\"\u003e作品一覧・CDレンタル・VOD・ランキング・特集・作品詳細\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"17:1-17:163\"\u003e\n\u003ctd data-sourcepos=\"17:2-17:39\"\u003e会員エリア（ログイン後）\u003c/td\u003e\n\u003ctd data-sourcepos=\"17:41-17:162\"\u003eマイページ・レンタル履歴・支払い履歴・おすすめ・あとで借りる・会員情報編集・退会\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"18:1-18:79\"\u003e\n\u003ctd data-sourcepos=\"18:2-18:18\"\u003e認証フロー\u003c/td\u003e\n\u003ctd data-sourcepos=\"18:20-18:78\"\u003eログイン・会員登録（入力→確認→完了）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"19:1-19:49\"\u003e\n\u003ctd data-sourcepos=\"19:2-19:18\"\u003e決済フロー\u003c/td\u003e\n\u003ctd data-sourcepos=\"19:20-19:48\"\u003eカート→決済→完了\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"21:1-21:131\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4404749%2F6692ae96-7601-4035-8a60-0e6773bbcf7f.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=c5882cd52a8a9d16e57b560c5bc18dee\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4404749%2F6692ae96-7601-4035-8a60-0e6773bbcf7f.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=c5882cd52a8a9d16e57b560c5bc18dee\" alt=\"作品一覧画面\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4404749%2F6692ae96-7601-4035-8a60-0e6773bbcf7f.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=7aa4131c6f73cc072e2971ef063a7d0b 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4404749/6692ae96-7601-4035-8a60-0e6773bbcf7f.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003chr data-sourcepos=\"23:1-24:0\"\u003e\n\u003ch2 data-sourcepos=\"25:1-25:45\"\u003e\n\u003cspan id=\"なぜ-vue-router-を使わなかったか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C-vue-router-%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜ Vue Router を使わなかったか\u003c/h2\u003e\n\u003cp data-sourcepos=\"27:1-27:86\"\u003eVue Router を入れると、以下の問題を意識する必要が出てきます。\u003c/p\u003e\n\u003cul data-sourcepos=\"29:1-32:0\"\u003e\n\u003cli data-sourcepos=\"29:1-29:113\"\u003e\n\u003cstrong\u003eルートガード\u003c/strong\u003e — 認証状態によってアクセスできるページを制限する実装が必要\u003c/li\u003e\n\u003cli data-sourcepos=\"30:1-30:119\"\u003e\n\u003cstrong\u003eブラウザの「戻る」ボタン\u003c/strong\u003e — 決済完了後に「戻る」で決済画面に戻れてしまう問題\u003c/li\u003e\n\u003cli data-sourcepos=\"31:1-32:0\"\u003e\n\u003cstrong\u003eURL による直接アクセス\u003c/strong\u003e — \u003ccode\u003e/checkout\u003c/code\u003e を直打ちされると状態が壊れる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"33:1-33:307\"\u003eこのアプリは「会員登録 → 一覧 → 詳細 → カート → 決済 → 完了」という一方向のフローが基本です。URL を公開して直接アクセスさせることを想定しておらず、Vue Router が提供する機能の多くが「余分な複雑さ」になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"35:1-35:126\"\u003e\u003cstrong\u003e\u003ccode\u003ecurrentPage\u003c/code\u003e で管理することで、ルートガードもナビゲーションフックも不要になりました。\u003c/strong\u003e\u003c/p\u003e\n\u003chr data-sourcepos=\"37:1-38:0\"\u003e\n\u003ch2 data-sourcepos=\"39:1-39:29\"\u003e\n\u003cspan id=\"currentpage-の型定義\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#currentpage-%E3%81%AE%E5%9E%8B%E5%AE%9A%E7%BE%A9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003ecurrentPage\u003c/code\u003e の型定義\u003c/h2\u003e\n\u003cp data-sourcepos=\"41:1-42:136\"\u003e\u003ccode\u003eApp.vue\u003c/code\u003e でページ名を union literal として型定義しています。\u003cbr\u003e\n文字列リテラルを使うことで、\u003cstrong\u003eタイポがコンパイル時にエラーになる\u003c/strong\u003eという安全性が得られます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"44:1-67:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// App.vue（currentPage 型定義 - 実際のコード）\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentPage\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003efilms\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ecd-rentals\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003estreaming\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eranking\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003efeature\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003edetail\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eauth\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003emypage\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003emember-register\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003emember-register-confirm\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003emember-edit\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003emember-edit-confirm\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003emember-delete-confirm\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erental-history\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003epayment-history\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erecommendations\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ewatch-later\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003echeckout\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003echeckout-complete\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003efilms\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"69:1-70:49\"\u003e\u003ccode\u003eref\u0026lt;'films' | 'auth' | ...\u0026gt;('films')\u003c/code\u003e という形で型パラメータに union literal を渡します。\u003cbr\u003e\n初期値は \u003ccode\u003e'films'\u003c/code\u003e（作品一覧）です。\u003c/p\u003e\n\u003chr data-sourcepos=\"72:1-73:0\"\u003e\n\u003ch2 data-sourcepos=\"74:1-74:60\"\u003e\n\u003cspan id=\"テンプレートでのコンポーネント切り替え\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%81%A7%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eテンプレートでのコンポーネント切り替え\u003c/h2\u003e\n\u003cp data-sourcepos=\"76:1-76:72\"\u003e\u003ccode\u003ev-if\u003c/code\u003e / \u003ccode\u003ev-else-if\u003c/code\u003e を連鎖させてページを切り替えます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"78:1-112:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- App.vue（テンプレート 抜粋 - 実際のコード） --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;FilmsView\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage === 'films'\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eopen-detail=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"openFilmDetail\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eopen-auth=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage = 'auth'\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003edirect-checkout=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"handleDirectCheckout\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eadd-to-cart=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"handleAddToCart\"\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;FilmDetailView\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ev-else-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage === 'detail' \u0026amp;\u0026amp; selectedFilm\"\u003c/span\u003e\n  \u003cspan class=\"na\"\u003e:film=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"selectedFilm\"\u003c/span\u003e\n  \u003cspan class=\"na\"\u003e:is-authenticated=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"isAuthenticated\"\u003c/span\u003e\n  \u003cspan class=\"na\"\u003e:from-section=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"detailBackPage\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eadd-watch-later=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"handleAddWatchLater\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003edirect-checkout=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"handleDirectCheckout\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eadd-to-cart=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"handleAddToCart\"\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;AuthView\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ev-else-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage === 'auth'\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003elogin-success=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage = 'films'\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eopen-register=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"openMemberRegister\"\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;MemberRegistrationView\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ev-else-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage === 'member-register'\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003econfirm=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"openMemberRegisterConfirm\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eback-auth=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage = 'auth'\"\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;MemberRegistrationConfirmView\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ev-else-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage === 'member-register-confirm' \u0026amp;\u0026amp; registerDraft\"\u003c/span\u003e\n  \u003cspan class=\"na\"\u003e:payload=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"registerDraft\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eback-edit=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage = 'member-register'\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eregistered=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage = 'auth'\"\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"114:1-115:114\"\u003e\u003cstrong\u003eポイント\u003c/strong\u003e：各コンポーネントは「どこへ遷移するか」を知りません。\u003cbr\u003e\n\u003ccode\u003eemit\u003c/code\u003e でイベントを親に投げるだけで、遷移先の決定は \u003ccode\u003eApp.vue\u003c/code\u003e に集約されています。\u003c/p\u003e\n\u003chr data-sourcepos=\"117:1-118:0\"\u003e\n\u003ch2 data-sourcepos=\"119:1-119:27\"\u003e\n\u003cspan id=\"認証状態との連動\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%AA%8D%E8%A8%BC%E7%8A%B6%E6%85%8B%E3%81%A8%E3%81%AE%E9%80%A3%E5%8B%95\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e認証状態との連動\u003c/h2\u003e\n\u003cp data-sourcepos=\"121:1-121:108\"\u003eログイン状態は \u003ccode\u003euseAuthState\u003c/code\u003e composable で管理し、\u003ccode\u003eisAuthenticated\u003c/code\u003e を取得しています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"123:1-126:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// App.vue\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eisAuthenticated\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003elogoutLocal\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003euseAuthState\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"128:1-128:147\"\u003eグローバルタブのリンクは、認証状態によって「ログイン・会員登録」か「マイページ」に切り替わります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"130:1-145:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- グローバルタブ（認証状態で切り替え） --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;a\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"!isAuthenticated\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eclick=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage = 'auth'\"\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  ログイン・会員登録\n\u003cspan class=\"nt\"\u003e\u0026lt;/a\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;a\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ev-else\u003c/span\u003e\n  \u003cspan class=\"na\"\u003e:class=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"{ active: currentPage === 'mypage' || currentPage === 'member-edit' || currentPage === 'rental-history' }\"\u003c/span\u003e\n  \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eclick=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"currentPage = 'mypage'\"\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  マイページ\n\u003cspan class=\"nt\"\u003e\u0026lt;/a\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"147:1-148:0\"\u003e\n\u003ch2 data-sourcepos=\"149:1-149:33\"\u003e\n\u003cspan id=\"決済フローの特別処理\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%B1%BA%E6%B8%88%E3%83%95%E3%83%AD%E3%83%BC%E3%81%AE%E7%89%B9%E5%88%A5%E5%87%A6%E7%90%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e決済フローの特別処理\u003c/h2\u003e\n\u003cp data-sourcepos=\"151:1-152:71\"\u003e決済中はヘッダーやナビを非表示にします（Isolated Checkout）。\u003cbr\u003e\n\u003ccode\u003eisCheckoutFlow\u003c/code\u003e という \u003ccode\u003ecomputed\u003c/code\u003e でまとめて制御します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"154:1-159:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// App.vue\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eisCheckoutFlow\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003ecomputed\u003c/span\u003e\u003cspan class=\"p\"\u003e(()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentPage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003echeckout\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"nx\"\u003ecurrentPage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003echeckout-complete\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"161:1-173:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;!-- ヘッダー・ナビを決済中は非表示 --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;header\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"masthead\"\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"!isCheckoutFlow\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"c\"\u003e\u0026lt;!-- 通常ヘッダー --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/header\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;header\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"checkout-masthead\"\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"isCheckoutFlow\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"brand\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003eCINEMA DAYS\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;div\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"secure-note\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e🔒 安全な通信で決済処理を行っています\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/header\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;nav\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"global-tabs\"\u003c/span\u003e \u003cspan class=\"na\"\u003ev-if=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"!isCheckoutFlow\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"c\"\u003e\u0026lt;!-- 通常ナビ --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/nav\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"175:1-175:125\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4404749%2F24d4942e-c22e-4c6c-82c1-9673fc2a0951.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0e2f8f914bc256067c1d4764ea8841f3\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4404749%2F24d4942e-c22e-4c6c-82c1-9673fc2a0951.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0e2f8f914bc256067c1d4764ea8841f3\" alt=\"決済画面\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4404749%2F24d4942e-c22e-4c6c-82c1-9673fc2a0951.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=d97ccfd4a71ea9406f4415a5f3f35826 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4404749/24d4942e-c22e-4c6c-82c1-9673fc2a0951.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003chr data-sourcepos=\"177:1-178:0\"\u003e\n\u003ch2 data-sourcepos=\"179:1-179:27\"\u003e\n\u003cspan id=\"作品詳細への遷移\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%9C%E5%93%81%E8%A9%B3%E7%B4%B0%E3%81%B8%E3%81%AE%E9%81%B7%E7%A7%BB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e作品詳細への遷移\u003c/h2\u003e\n\u003cp data-sourcepos=\"181:1-182:129\"\u003e詳細画面への遷移は、選択した作品を \u003ccode\u003eselectedFilm\u003c/code\u003e に保存してから \u003ccode\u003ecurrentPage\u003c/code\u003e を切り替えます。\u003cbr\u003e\nどのセクションから来たかを \u003ccode\u003edetailBackPage\u003c/code\u003e で覚えておき、「戻る」ボタンの遷移先に使います。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"184:1-190:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// App.vue\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eselectedFilm\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003ePublicFilmSummary\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edetailBackPage\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\n  \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003efilms\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ecd-rentals\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003estreaming\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eranking\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003efeature\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003erecommendations\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ewatch-later\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003efilms\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"192:1-193:0\"\u003e\n\u003ch2 data-sourcepos=\"194:1-194:33\"\u003e\n\u003cspan id=\"メリットとデメリット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88%E3%81%A8%E3%83%87%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eメリットとデメリット\u003c/h2\u003e\n\u003cp data-sourcepos=\"196:1-196:16\"\u003e\u003cstrong\u003eメリット\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"197:1-201:0\"\u003e\n\u003cli data-sourcepos=\"197:1-197:77\"\u003eルートガードやナビゲーションガードを書かなくてよい\u003c/li\u003e\n\u003cli data-sourcepos=\"198:1-198:80\"\u003e認証状態とページ遷移を同じ場所（\u003ccode\u003eApp.vue\u003c/code\u003e）で管理できる\u003c/li\u003e\n\u003cli data-sourcepos=\"199:1-199:78\"\u003eURL が変わらないため「直接アクセス」の問題が起きない\u003c/li\u003e\n\u003cli data-sourcepos=\"200:1-201:0\"\u003eTypeScript の union literal で遷移先のタイポを防げる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"202:1-202:19\"\u003e\u003cstrong\u003eデメリット\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"203:1-206:0\"\u003e\n\u003cli data-sourcepos=\"203:1-203:66\"\u003eページ数が増えると \u003ccode\u003ev-else-if\u003c/code\u003e の連鎖が長くなる\u003c/li\u003e\n\u003cli data-sourcepos=\"204:1-204:92\"\u003eブラウザの「戻る」ボタンと連動しない（このアプリでは意図的）\u003c/li\u003e\n\u003cli data-sourcepos=\"205:1-206:0\"\u003eURL が変わらないためディープリンクができない\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"207:1-208:0\"\u003e\n\u003ch2 data-sourcepos=\"209:1-209:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"211:1-212:135\"\u003e「シングルページ・ワンウェイフロー」なアプリには、Vue Router を使わず \u003ccode\u003ecurrentPage\u003c/code\u003e で管理するアプローチが有効です。\u003cbr\u003e\nTypeScript の union literal 型を使うことで、\u003cstrong\u003e文字列管理でもタイポを防げる型安全な実装\u003c/strong\u003eになります。\u003c/p\u003e\n\u003cp data-sourcepos=\"214:1-214:177\"\u003eVue Router が必要になるのは、URL を公開してブックマーク・シェアを想定するか、ページ数が非常に多くなってきてからで十分です。\u003c/p\u003e\n","body":"## はじめに\n\nVue 3 + TypeScript でアプリを作るとき、最初に迷うのが「画面遷移をどう管理するか」です。  \nVue Router を使うのが定番ですが、このアプリでは **`App.vue` の `currentPage` という `ref` で全画面を管理する** 構成を選びました。\n\nなぜ Vue Router を使わなかったのか、どう実装しているのか、実際のコードで解説します。\n\n---\n\n## アプリの画面フロー\n\nこのアプリの画面遷移は大きく 2 つに分かれます。\n\n| ゾーン | 主な画面 |\n|---|---|\n| 公開エリア（ログイン不要） | 作品一覧・CDレンタル・VOD・ランキング・特集・作品詳細 |\n| 会員エリア（ログイン後） | マイページ・レンタル履歴・支払い履歴・おすすめ・あとで借りる・会員情報編集・退会 |\n| 認証フロー | ログイン・会員登録（入力→確認→完了） |\n| 決済フロー | カート→決済→完了 |\n\n![作品一覧画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4404749/6692ae96-7601-4035-8a60-0e6773bbcf7f.png)\n\n---\n\n## なぜ Vue Router を使わなかったか\n\nVue Router を入れると、以下の問題を意識する必要が出てきます。\n\n- **ルートガード** — 認証状態によってアクセスできるページを制限する実装が必要\n- **ブラウザの「戻る」ボタン** — 決済完了後に「戻る」で決済画面に戻れてしまう問題\n- **URL による直接アクセス** — `/checkout` を直打ちされると状態が壊れる\n\nこのアプリは「会員登録 → 一覧 → 詳細 → カート → 決済 → 完了」という一方向のフローが基本です。URL を公開して直接アクセスさせることを想定しておらず、Vue Router が提供する機能の多くが「余分な複雑さ」になります。\n\n**`currentPage` で管理することで、ルートガードもナビゲーションフックも不要になりました。**\n\n---\n\n## `currentPage` の型定義\n\n`App.vue` でページ名を union literal として型定義しています。  \n文字列リテラルを使うことで、**タイポがコンパイル時にエラーになる**という安全性が得られます。\n\n```typescript\n// App.vue（currentPage 型定義 - 実際のコード）\nconst currentPage = ref\u003c\n  'films' |\n  'cd-rentals' |\n  'streaming' |\n  'ranking' |\n  'feature' |\n  'detail' |\n  'auth' |\n  'mypage' |\n  'member-register' |\n  'member-register-confirm' |\n  'member-edit' |\n  'member-edit-confirm' |\n  'member-delete-confirm' |\n  'rental-history' |\n  'payment-history' |\n  'recommendations' |\n  'watch-later' |\n  'checkout' |\n  'checkout-complete'\n\u003e('films')\n```\n\n`ref\u003c'films' | 'auth' | ...\u003e('films')` という形で型パラメータに union literal を渡します。  \n初期値は `'films'`（作品一覧）です。\n\n---\n\n## テンプレートでのコンポーネント切り替え\n\n`v-if` / `v-else-if` を連鎖させてページを切り替えます。\n\n```vue\n\u003c!-- App.vue（テンプレート 抜粋 - 実際のコード） --\u003e\n\u003cFilmsView\n  v-if=\"currentPage === 'films'\"\n  @open-detail=\"openFilmDetail\"\n  @open-auth=\"currentPage = 'auth'\"\n  @direct-checkout=\"handleDirectCheckout\"\n  @add-to-cart=\"handleAddToCart\"\n/\u003e\n\u003cFilmDetailView\n  v-else-if=\"currentPage === 'detail' \u0026\u0026 selectedFilm\"\n  :film=\"selectedFilm\"\n  :is-authenticated=\"isAuthenticated\"\n  :from-section=\"detailBackPage\"\n  @add-watch-later=\"handleAddWatchLater\"\n  @direct-checkout=\"handleDirectCheckout\"\n  @add-to-cart=\"handleAddToCart\"\n/\u003e\n\u003cAuthView\n  v-else-if=\"currentPage === 'auth'\"\n  @login-success=\"currentPage = 'films'\"\n  @open-register=\"openMemberRegister\"\n/\u003e\n\u003cMemberRegistrationView\n  v-else-if=\"currentPage === 'member-register'\"\n  @confirm=\"openMemberRegisterConfirm\"\n  @back-auth=\"currentPage = 'auth'\"\n/\u003e\n\u003cMemberRegistrationConfirmView\n  v-else-if=\"currentPage === 'member-register-confirm' \u0026\u0026 registerDraft\"\n  :payload=\"registerDraft\"\n  @back-edit=\"currentPage = 'member-register'\"\n  @registered=\"currentPage = 'auth'\"\n/\u003e\n```\n\n**ポイント**：各コンポーネントは「どこへ遷移するか」を知りません。  \n`emit` でイベントを親に投げるだけで、遷移先の決定は `App.vue` に集約されています。\n\n---\n\n## 認証状態との連動\n\nログイン状態は `useAuthState` composable で管理し、`isAuthenticated` を取得しています。\n\n```typescript\n// App.vue\nconst { isAuthenticated, logoutLocal } = useAuthState()\n```\n\nグローバルタブのリンクは、認証状態によって「ログイン・会員登録」か「マイページ」に切り替わります。\n\n```vue\n\u003c!-- グローバルタブ（認証状態で切り替え） --\u003e\n\u003ca\n  v-if=\"!isAuthenticated\"\n  @click=\"currentPage = 'auth'\"\n\u003e\n  ログイン・会員登録\n\u003c/a\u003e\n\u003ca\n  v-else\n  :class=\"{ active: currentPage === 'mypage' || currentPage === 'member-edit' || currentPage === 'rental-history' }\"\n  @click=\"currentPage = 'mypage'\"\n\u003e\n  マイページ\n\u003c/a\u003e\n```\n\n---\n\n## 決済フローの特別処理\n\n決済中はヘッダーやナビを非表示にします（Isolated Checkout）。  \n`isCheckoutFlow` という `computed` でまとめて制御します。\n\n```typescript\n// App.vue\nconst isCheckoutFlow = computed(() =\u003e {\n  return currentPage.value === 'checkout' || currentPage.value === 'checkout-complete'\n})\n```\n\n```vue\n\u003c!-- ヘッダー・ナビを決済中は非表示 --\u003e\n\u003cheader class=\"masthead\" v-if=\"!isCheckoutFlow\"\u003e\n  \u003c!-- 通常ヘッダー --\u003e\n\u003c/header\u003e\n\u003cheader class=\"checkout-masthead\" v-if=\"isCheckoutFlow\"\u003e\n  \u003cdiv class=\"brand\"\u003eCINEMA DAYS\u003c/div\u003e\n  \u003cdiv class=\"secure-note\"\u003e🔒 安全な通信で決済処理を行っています\u003c/div\u003e\n\u003c/header\u003e\n\u003cnav class=\"global-tabs\" v-if=\"!isCheckoutFlow\"\u003e\n  \u003c!-- 通常ナビ --\u003e\n\u003c/nav\u003e\n```\n\n![決済画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4404749/24d4942e-c22e-4c6c-82c1-9673fc2a0951.png)\n\n---\n\n## 作品詳細への遷移\n\n詳細画面への遷移は、選択した作品を `selectedFilm` に保存してから `currentPage` を切り替えます。  \nどのセクションから来たかを `detailBackPage` で覚えておき、「戻る」ボタンの遷移先に使います。\n\n```typescript\n// App.vue\nconst selectedFilm = ref\u003cPublicFilmSummary | null\u003e(null)\nconst detailBackPage = ref\u003c\n  'films' | 'cd-rentals' | 'streaming' | 'ranking' | 'feature' | 'recommendations' | 'watch-later'\n\u003e('films')\n```\n\n---\n\n## メリットとデメリット\n\n**メリット**\n- ルートガードやナビゲーションガードを書かなくてよい\n- 認証状態とページ遷移を同じ場所（`App.vue`）で管理できる\n- URL が変わらないため「直接アクセス」の問題が起きない\n- TypeScript の union literal で遷移先のタイポを防げる\n\n**デメリット**\n- ページ数が増えると `v-else-if` の連鎖が長くなる\n- ブラウザの「戻る」ボタンと連動しない（このアプリでは意図的）\n- URL が変わらないためディープリンクができない\n\n---\n\n## まとめ\n\n「シングルページ・ワンウェイフロー」なアプリには、Vue Router を使わず `currentPage` で管理するアプローチが有効です。  \nTypeScript の union literal 型を使うことで、**文字列管理でもタイポを防げる型安全な実装**になります。\n\nVue Router が必要になるのは、URL を公開してブックマーク・シェアを想定するか、ページ数が非常に多くなってきてからで十分です。\n","coediting":false,"comments_count":0,"created_at":"2026-04-28T13:37:21+09:00","group":null,"id":"1809dce5acab8a571c76","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"TypeScript","versions":[]},{"name":"Vue.js","versions":[]},{"name":"SpringBoot","versions":[]}],"title":"App.vue のページ状態管理を、単純な currentPage で回した設計判断","updated_at":"2026-05-27T00:53:14+09:00","url":"https://qiita.com/y104autumn/items/1809dce5acab8a571c76","user":{"description":"約15年の開発経験を礎に、システムが形になり動くまでの全体像を重視した発信を目標としています。この業界の仕事に携わり始めた頃の情熱を胸に、論理的裏付けを持って皆さんと対話し、質の高いものづくりを追求したいです。知見の発信を通じ、技術で切磋琢磨できる関係を築ければと思います。「いいね」やコメントをいただけると大きな励みになります。","facebook_id":"","followees_count":38,"followers_count":3,"github_login_name":"y104autumn","id":"y104autumn","items_count":25,"linkedin_id":"","location":"東京都","name":"横塚 敏明","organization":"株式会社エンジョイ","permanent_id":4404749,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4404749/profile-images/1776747921","team_only":false,"twitter_screen_name":null,"website_url":"https://mint041223techblog.netlify.app/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:9\"\u003e\n\u003cspan id=\"概要\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A6%82%E8%A6%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e概要\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:172\"\u003e地図上にメモ、写真、動画、PDF、図形を置いて管理できる Windows 向けアプリケーション \u003cstrong\u003eGeoCode-Web\u003c/strong\u003e を OSS として公開しました。\u003c/p\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fitsuki-maru%2FGeoCode-Web-Single%2Fmain%2Fuserguide%2Fimages%2F1-1_geocode-web.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b8f6ee9d6996d6243f3a7d329d47af33\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fitsuki-maru%2FGeoCode-Web-Single%2Fmain%2Fuserguide%2Fimages%2F1-1_geocode-web.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b8f6ee9d6996d6243f3a7d329d47af33\" alt=\"GeoCode-Web\" width=\"700\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fraw.githubusercontent.com%2Fitsuki-maru%2FGeoCode-Web-Single%2Fmain%2Fuserguide%2Fimages%2F1-1_geocode-web.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=55b7845c94ab994c85801c8b025ce4e9 1x\" data-canonical-src=\"https://raw.githubusercontent.com/itsuki-maru/GeoCode-Web-Single/main/userguide/images/1-1_geocode-web.png\" loading=\"lazy\"\u003e\u003c/a\u003e\n\u003cp data-sourcepos=\"7:1-7:311\"\u003eこのアプリケーションは、内容をドキュメンタリーに寄せた Zenn では書ききれなかった「\u003cstrong\u003eアプリケーションとして何ができるのか\u003c/strong\u003e」「\u003cstrong\u003eどのような構成で動いているのか\u003c/strong\u003e」を、Qiita 向けに技術寄りで整理したのでご紹介します。\u003c/p\u003e\n\u003cp data-sourcepos=\"9:1-9:48\"\u003e\u003ciframe id=\"qiita-embed-content__277ee29207ad1d2b0ae8b1460b47d554\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__277ee29207ad1d2b0ae8b1460b47d554\" data-content=\"https%3A%2F%2Fzenn.dev%2Fmarudev%2Farticles%2F12e14d4f67c5b1\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"11:1-11:36\"\u003eリポジトリはこちらです。\u003c/p\u003e\n\u003cp data-sourcepos=\"13:1-13:49\"\u003e\u003ciframe id=\"qiita-embed-content__342ef3ae119931bc033c81cbe9a34260\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__342ef3ae119931bc033c81cbe9a34260\" data-content=\"https%3A%2F%2Fgithub.com%2Fitsuki-maru%2FGeoCode-Web-Single\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"15:1-15:66\"\u003eプロジェクトサイトとデモ版も用意しています。\u003c/p\u003e\n\u003cul data-sourcepos=\"17:1-21:0\"\u003e\n\u003cli data-sourcepos=\"17:1-17:103\"\u003eプロジェクトサイト: \u003ca href=\"https://geocode-web-site.pages.dev\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://geocode-web-site.pages.dev\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"18:1-18:102\"\u003ePC用ブラウザデモ版: \u003ca href=\"https://demo-geocode-web.pages.dev\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://demo-geocode-web.pages.dev\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"19:1-19:123\"\u003eスマホ用ブラウザデモ版: \u003ca href=\"https://demo-geocode-web-mobile.pages.dev\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://demo-geocode-web-mobile.pages.dev\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"20:1-21:0\"\u003eユーザーガイド: \u003ca href=\"https://geocode-web-single.pages.dev/user-guide.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://geocode-web-single.pages.dev/user-guide.html\u003c/a\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"22:1-22:18\"\u003e\n\u003cspan id=\"作ったもの\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e作ったもの\u003c/h2\u003e\n\u003cp data-sourcepos=\"24:1-24:165\"\u003eGeoCode-Web は、地図上にマーカーや図形を配置し、場所に紐付いた情報を管理するためのマッピングアプリケーションです。\u003c/p\u003e\n\u003cp data-sourcepos=\"26:1-26:263\"\u003eマーカーには マークダウン形式の詳細本文を書けるため、単なるピン管理ではなく、場所ごとの簡易 Wiki のようにも使えます。画像、PDF、MP4 もアップロードして、マーカー詳細に埋め込めます。\u003c/p\u003e\n\u003cp data-sourcepos=\"28:1-28:123\"\u003e主な用途としては、次のような「場所と情報を一緒に扱いたい」ケースを想定しています。\u003c/p\u003e\n\u003cul data-sourcepos=\"30:1-35:0\"\u003e\n\u003cli data-sourcepos=\"30:1-30:33\"\u003e\u003cstrong\u003e旅行や訪問先の記録\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"31:1-31:24\"\u003e\u003cstrong\u003e現地調査メモ\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"32:1-32:48\"\u003e\u003cstrong\u003e写真・PDF・動画つきの地点管理\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"33:1-33:54\"\u003e\u003cstrong\u003e小中規模チーム内での地図情報共有\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"34:1-35:0\"\u003e\u003cstrong\u003eオフラインまたはローカルネットワーク内で完結させたい地図メモ環境\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"36:1-36:106\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fgithub.com%2Fitsuki-maru%2FGeoCode-Web-Single%2Fraw%2Fmain%2Fuserguide%2Fimages%2F99-2_image.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=55789ff38f3378fe25ed3f58401134f6\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fgithub.com%2Fitsuki-maru%2FGeoCode-Web-Single%2Fraw%2Fmain%2Fuserguide%2Fimages%2F99-2_image.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=55789ff38f3378fe25ed3f58401134f6\" alt=\"GeoCode-Web\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fgithub.com%2Fitsuki-maru%2FGeoCode-Web-Single%2Fraw%2Fmain%2Fuserguide%2Fimages%2F99-2_image.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=793d2d203fdba720c591a533c0be2e4e 1x\" data-canonical-src=\"https://github.com/itsuki-maru/GeoCode-Web-Single/raw/main/userguide/images/99-2_image.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"38:1-38:45\"\u003e\n\u003cspan id=\"なぜ基本ローカル動作にしたか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C%E5%9F%BA%E6%9C%AC%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E5%8B%95%E4%BD%9C%E3%81%AB%E3%81%97%E3%81%9F%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜ基本ローカル動作にしたか\u003c/h2\u003e\n\u003cp data-sourcepos=\"40:1-40:163\"\u003e個人的に重視したのは、位置情報や写真などをできるだけ手元で管理できることです。詳しくはこちらをご覧ください👇\u003c/p\u003e\n\u003cp data-sourcepos=\"42:1-42:48\"\u003e\u003ciframe id=\"qiita-embed-content__fc88ae517f1f505c1aff49bdbd119920\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__fc88ae517f1f505c1aff49bdbd119920\" data-content=\"https%3A%2F%2Fzenn.dev%2Fmarudev%2Farticles%2F12e14d4f67c5b1\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"44:1-44:309\"\u003eGeoCode-Web は通常、Tauri デスクトップアプリとして起動し、内部でローカル HTTP サーバを立ち上げて UI を表示します。データベースには SQLite を使い、設定ファイルやアップロードファイルもユーザーのローカル領域に保存します。\u003c/p\u003e\n\u003cp data-sourcepos=\"46:1-46:42\"\u003e保存先は次のような構成です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"txt\" data-sourcepos=\"48:1-53:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e~/.geocode-web-single/\n  ├─ geocode-web-single.env.json\n  ├─ geocode-web.sqlite\n  └─ images/\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"55:1-55:255\"\u003eただし、初期状態では国土地理院の通常地図・航空写真タイルを利用するため、インターネット環境が必要です。同一端末内にタイルサーバを用意すれば、完全オフライン運用も可能です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"57:1-57:21\"\u003e\n\u003cspan id=\"技術スタック\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%8A%80%E8%A1%93%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e技術スタック\u003c/h2\u003e\n\u003cp data-sourcepos=\"59:1-59:33\"\u003e構成は次のとおりです。\u003c/p\u003e\n\u003ctable data-sourcepos=\"61:1-70:50\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"61:1-61:19\"\u003e\n\u003cth data-sourcepos=\"61:2-61:9\"\u003e領域\u003c/th\u003e\n\u003cth data-sourcepos=\"61:11-61:18\"\u003e技術\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"63:1-63:57\"\u003e\n\u003ctd data-sourcepos=\"63:2-63:21\"\u003eバックエンド\u003c/td\u003e\n\u003ctd data-sourcepos=\"63:23-63:56\"\u003eRust 2024 / Axum / SQLx / SQLite\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"64:1-64:61\"\u003e\n\u003ctd data-sourcepos=\"64:2-64:24\"\u003eフロントエンド\u003c/td\u003e\n\u003ctd data-sourcepos=\"64:26-64:60\"\u003eVue 3 / Vue Router / Pinia / Vite\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"65:1-65:32\"\u003e\n\u003ctd data-sourcepos=\"65:2-65:21\"\u003eデスクトップ\u003c/td\u003e\n\u003ctd data-sourcepos=\"65:23-65:31\"\u003eTauri 2\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"66:1-66:29\"\u003e\n\u003ctd data-sourcepos=\"66:2-66:21\"\u003eテンプレート\u003c/td\u003e\n\u003ctd data-sourcepos=\"66:23-66:28\"\u003eTera\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"67:1-67:23\"\u003e\n\u003ctd data-sourcepos=\"67:2-67:9\"\u003e認証\u003c/td\u003e\n\u003ctd data-sourcepos=\"67:11-67:22\"\u003eJWT / TOTP\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"68:1-68:43\"\u003e\n\u003ctd data-sourcepos=\"68:2-68:33\"\u003eマークダウンレンダラ\u003c/td\u003e\n\u003ctd data-sourcepos=\"68:35-68:42\"\u003emarked\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"69:1-69:47\"\u003e\n\u003ctd data-sourcepos=\"69:2-69:33\"\u003e静的ファイル埋め込み\u003c/td\u003e\n\u003ctd data-sourcepos=\"69:35-69:46\"\u003erust-embed\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"70:1-70:50\"\u003e\n\u003ctd data-sourcepos=\"70:2-70:9\"\u003e配布\u003c/td\u003e\n\u003ctd data-sourcepos=\"70:11-70:49\"\u003eGitHub Actions / NSIS / MSI Installer\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"72:1-72:162\"\u003eRust/Axum 製 API サーバと Vue 3 製フロントエンドを Tauri 2 でラップし、Windows 向けアプリケーションとして配布しています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"74:1-74:15\"\u003e\n\u003cspan id=\"主な機能\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%B8%BB%E3%81%AA%E6%A9%9F%E8%83%BD\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e主な機能\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"76:1-76:31\"\u003e\n\u003cspan id=\"地図マーカー管理\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%9C%B0%E5%9B%B3%E3%83%9E%E3%83%BC%E3%82%AB%E3%83%BC%E7%AE%A1%E7%90%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e地図・マーカー管理\u003c/h3\u003e\n\u003cp data-sourcepos=\"78:1-78:78\"\u003e地図上にマーカーを追加し、位置や内容を編集できます。\u003c/p\u003e\n\u003cul data-sourcepos=\"80:1-85:0\"\u003e\n\u003cli data-sourcepos=\"80:1-80:50\"\u003eマーカーの追加・移動・編集・削除\u003c/li\u003e\n\u003cli data-sourcepos=\"81:1-81:41\"\u003eMarkdown による詳細本文の記述\u003c/li\u003e\n\u003cli data-sourcepos=\"82:1-82:59\"\u003e指定マーカーへのフォーカス・ズームイン\u003c/li\u003e\n\u003cli data-sourcepos=\"83:1-83:20\"\u003eマーカー検索\u003c/li\u003e\n\u003cli data-sourcepos=\"84:1-85:0\"\u003e国土地理院の通常地図・航空写真を初期タイルとして利用\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"86:1-86:19\"\u003e\n\u003cspan id=\"レイヤ管理\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AC%E3%82%A4%E3%83%A4%E7%AE%A1%E7%90%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eレイヤ管理\u003c/h3\u003e\n\u003cp data-sourcepos=\"88:1-88:48\"\u003e情報はレイヤ単位で整理できます。\u003c/p\u003e\n\u003cul data-sourcepos=\"90:1-95:0\"\u003e\n\u003cli data-sourcepos=\"90:1-90:54\"\u003eユーザーごとの \u003ccode\u003emaster\u003c/code\u003e レイヤ自動作成\u003c/li\u003e\n\u003cli data-sourcepos=\"91:1-91:50\"\u003e任意レイヤの追加・名称変更・削除\u003c/li\u003e\n\u003cli data-sourcepos=\"92:1-92:47\"\u003eマスターレイヤでの全件横断検索\u003c/li\u003e\n\u003cli data-sourcepos=\"93:1-93:26\"\u003e個別レイヤ内検索\u003c/li\u003e\n\u003cli data-sourcepos=\"94:1-95:0\"\u003eレイヤ単位または全件の JSON エクスポート / インポート\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"96:1-96:188\"\u003eJSON エクスポートは、マーカーと図形を含む形式に対応で出力します。この機能で他の端末でも地図上のデータを復元することができます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"98:1-98:16\"\u003e\n\u003cspan id=\"図形描画\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%9B%B3%E5%BD%A2%E6%8F%8F%E7%94%BB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e図形描画\u003c/h3\u003e\n\u003cp data-sourcepos=\"100:1-100:63\"\u003eレイヤに紐付けて、地図上に図形を描けます。\u003c/p\u003e\n\u003cul data-sourcepos=\"102:1-106:0\"\u003e\n\u003cli data-sourcepos=\"102:1-102:14\"\u003eポリゴン\u003c/li\u003e\n\u003cli data-sourcepos=\"103:1-103:17\"\u003eポリライン\u003c/li\u003e\n\u003cli data-sourcepos=\"104:1-104:8\"\u003e矩形\u003c/li\u003e\n\u003cli data-sourcepos=\"105:1-106:0\"\u003e円\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"107:1-107:213\"\u003e図形には名前を付けられます。図形名は地図上のラベルとして表示され、後から名前や所属レイヤを変更できます。削除直後の取り消しにも対応しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"109:1-109:84\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fgeocode-web-single.pages.dev%2Fuserguide%2Fimages%2F13-3_geocode-web.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=8ade9836da12cf384a57c5a2857761c4\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fgeocode-web-single.pages.dev%2Fuserguide%2Fimages%2F13-3_geocode-web.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=8ade9836da12cf384a57c5a2857761c4\" alt=\"shape\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fgeocode-web-single.pages.dev%2Fuserguide%2Fimages%2F13-3_geocode-web.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=90ec878568f26f71c310dbd84c5232c9 1x\" data-canonical-src=\"https://geocode-web-single.pages.dev/userguide/images/13-3_geocode-web.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003ch3 data-sourcepos=\"111:1-111:31\"\u003e\n\u003cspan id=\"画像pdf動画管理\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%94%BB%E5%83%8Fpdf%E5%8B%95%E7%94%BB%E7%AE%A1%E7%90%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e画像・PDF・動画管理\u003c/h3\u003e\n\u003cp data-sourcepos=\"113:1-113:60\"\u003eアップロードできる形式は次のとおりです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"txt\" data-sourcepos=\"115:1-117:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ePNG / JPG / JPEG / GIF / WebP / PDF / MP4\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"119:1-119:58\"\u003eアップロード上限は 1 ファイル 100MB です。\u003c/p\u003e\n\u003cp data-sourcepos=\"121:1-121:266\"\u003e画像は再エンコードして保存し、EXIF などのメタデータを除去します。画像サムネイルも生成し、マークダウン表示時には \u003ccode\u003e?thumb=true\u003c/code\u003e を使ってサムネイルをバックエンドに要求し、軽量表示します。\u003c/p\u003e\n\u003cp data-sourcepos=\"123:1-123:253\"\u003eMP4 は可能な場合 poster 画像を生成し、動画タグの \u003ccode\u003eposter\u003c/code\u003e として利用します。Markdown 内の動画は \u003ccode\u003epreload=\"none\"\u003c/code\u003e で描画し、ポップアップ内の画像・動画は details 展開時に遅延読み込みします。\u003c/p\u003e\n\u003cp data-sourcepos=\"125:1-125:72\"\u003eマークダウンへの埋め込み文字列も自動生成します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"md\" data-sourcepos=\"127:1-131:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e![\u003c/span\u003e\u003cspan class=\"nv\"\u003e画像\u003c/span\u003e\u003cspan class=\"p\"\u003e](\u003c/span\u003e\u003cspan class=\"sx\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003ePDF\u003c/span\u003e\u003cspan class=\"p\"\u003e](\u003c/span\u003e\u003cspan class=\"sx\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n?\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003e動画\u003c/span\u003e\u003cspan class=\"p\"\u003e](\u003c/span\u003e\u003cspan class=\"sx\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"133:1-133:19\"\u003e\n\u003cspan id=\"一時共有url\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%B8%80%E6%99%82%E5%85%B1%E6%9C%89url\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e一時共有URL\u003c/h3\u003e\n\u003cp data-sourcepos=\"135:1-135:137\"\u003eサーバ単体モードや適切に公開された環境では、選択したレイヤを一時共有 URL として公開できます。\u003c/p\u003e\n\u003cul data-sourcepos=\"137:1-144:0\"\u003e\n\u003cli data-sourcepos=\"137:1-137:52\"\u003e共有対象レイヤを選択して URL を発行\u003c/li\u003e\n\u003cli data-sourcepos=\"138:1-138:23\"\u003e有効期限を指定\u003c/li\u003e\n\u003cli data-sourcepos=\"139:1-139:41\"\u003e任意の共有パスワードを設定\u003c/li\u003e\n\u003cli data-sourcepos=\"140:1-140:44\"\u003e図形を共有対象に含めるか選択\u003c/li\u003e\n\u003cli data-sourcepos=\"141:1-141:32\"\u003e既存リンクの内容更新\u003c/li\u003e\n\u003cli data-sourcepos=\"142:1-142:14\"\u003e共有停止\u003c/li\u003e\n\u003cli data-sourcepos=\"143:1-144:0\"\u003eデスクトップ / モバイル向け専用レイアウトで閲覧\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"145:1-145:156\"\u003e共有ページは live データではなく、発行時点のレイヤ・マーカー・図形情報を保持するスナップショット方式です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"147:1-147:39\"\u003e\n\u003cspan id=\"認証とセキュリティまわり\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%AA%8D%E8%A8%BC%E3%81%A8%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E3%81%BE%E3%82%8F%E3%82%8A\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e認証とセキュリティまわり\u003c/h2\u003e\n\u003cp data-sourcepos=\"149:1-149:135\"\u003eGeoCode-Web は、ローカル利用だけでなくサーバ単体モードも持つため、認証まわりも実装しています。\u003c/p\u003e\n\u003cul data-sourcepos=\"151:1-158:0\"\u003e\n\u003cli data-sourcepos=\"151:1-151:76\"\u003eアクセストークン + リフレッシュトークンを JWT で発行\u003c/li\u003e\n\u003cli data-sourcepos=\"152:1-152:117\"\u003e両トークンは HttpOnly Cookie で保持（http 環境では設定を変えないと動かないので注意）\u003c/li\u003e\n\u003cli data-sourcepos=\"153:1-153:64\"\u003eリフレッシュトークンは \u003ccode\u003e/account/refresh\u003c/code\u003e に限定\u003c/li\u003e\n\u003cli data-sourcepos=\"154:1-154:31\"\u003eTOTP による二段階認証\u003c/li\u003e\n\u003cli data-sourcepos=\"155:1-155:53\"\u003eログイン失敗回数に応じた再試行待ち\u003c/li\u003e\n\u003cli data-sourcepos=\"156:1-156:26\"\u003eアカウントロック\u003c/li\u003e\n\u003cli data-sourcepos=\"157:1-158:0\"\u003e管理者によるユーザー作成・パスワードリセット・ロック解除\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"159:1-159:199\"\u003e画像の参照可否はユーザー単位のプライバシーモードで制御します。プライバシーモードが ON の場合、自分以外からの画像アクセスを遮断します。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"161:1-161:18\"\u003e\n\u003cspan id=\"起動モード\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%B5%B7%E5%8B%95%E3%83%A2%E3%83%BC%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e起動モード\u003c/h2\u003e\n\u003cp data-sourcepos=\"163:1-163:130\"\u003eGeoCode-Web には、通常の Tauri デスクトップアプリとしての起動と、サーバ単体モードがあります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"165:1-165:37\"\u003e\n\u003cspan id=\"tauri-デスクトップアプリ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#tauri-%E3%83%87%E3%82%B9%E3%82%AF%E3%83%88%E3%83%83%E3%83%97%E3%82%A2%E3%83%97%E3%83%AA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eTauri デスクトップアプリ\u003c/h3\u003e\n\u003cp data-sourcepos=\"167:1-167:63\"\u003eインストーラで導入後、アプリを起動します。\u003c/p\u003e\n\u003cp data-sourcepos=\"169:1-169:93\"\u003e初回起動時はセットアップ画面が表示され、次の項目を設定します。\u003c/p\u003e\n\u003cul data-sourcepos=\"171:1-178:0\"\u003e\n\u003cli data-sourcepos=\"171:1-171:23\"\u003eアプリタイトル\u003c/li\u003e\n\u003cli data-sourcepos=\"172:1-172:44\"\u003e管理者ユーザー名 / パスワード\u003c/li\u003e\n\u003cli data-sourcepos=\"173:1-173:32\"\u003eアカウントロック回数\u003c/li\u003e\n\u003cli data-sourcepos=\"174:1-174:26\"\u003e待機制限開始回数\u003c/li\u003e\n\u003cli data-sourcepos=\"175:1-175:14\"\u003e待機時間\u003c/li\u003e\n\u003cli data-sourcepos=\"176:1-176:38\"\u003eアクセストークン有効期限\u003c/li\u003e\n\u003cli data-sourcepos=\"177:1-178:0\"\u003eリフレッシュトークン有効期限\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"179:1-179:144\"\u003eセットアップ完了後、設定ファイル、SQLite DB、アップロードファイル保存用ディレクトリが作成されます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"181:1-181:28\"\u003e\n\u003cspan id=\"サーバ単体モード\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B5%E3%83%BC%E3%83%90%E5%8D%98%E4%BD%93%E3%83%A2%E3%83%BC%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eサーバ単体モード\u003c/h3\u003e\n\u003cp data-sourcepos=\"183:1-183:78\"\u003eGUI を使わず、Axum サーバだけを起動することもできます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"185:1-191:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# ホストのみ指定する場合。ポートは 3000 を使用\u003c/span\u003e\ngeocode_web_single \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e 0.0.0.0\n\n\u003cspan class=\"c\"\u003e# ホストとポートを指定する場合\u003c/span\u003e\ngeocode_web_single \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e 0.0.0.0:9090\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"193:1-193:86\"\u003e事前に Tauri GUI でセットアップを完了しておく必要があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"195:1-195:208\"\u003eHTTP 環境で運用する場合、デフォルトでは \u003ccode\u003eSECURE_COOKIE=true\u003c/code\u003e のため Cookie が機能しません。その場合は、設定ファイルの \u003ccode\u003esecure_cookie\u003c/code\u003e を \u003ccode\u003efalse\u003c/code\u003e に変更します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"197:1-201:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"secure_cookie\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"false\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cblockquote data-sourcepos=\"203:1-203:173\"\u003e\n\u003cp data-sourcepos=\"203:3-203:173\"\u003e上記は一例です。規模によってはリバースプロキシなどの使用も検討したほうが良いですが、使う環境の状況によるでしょう。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 data-sourcepos=\"205:1-205:21\"\u003e\n\u003cspan id=\"モバイル対応\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A2%E3%83%90%E3%82%A4%E3%83%AB%E5%AF%BE%E5%BF%9C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eモバイル対応\u003c/h2\u003e\n\u003cp data-sourcepos=\"207:1-207:100\"\u003eUser-Agent に \u003ccode\u003eMobile\u003c/code\u003e が含まれるアクセスには、モバイル向け UI を返します。\u003c/p\u003e\n\u003cp data-sourcepos=\"209:1-209:230\"\u003eモバイル版では、ツール群をフローティングボタンと全画面モーダル中心の操作にしています。一時共有 URL のプレビューも、モバイル向けレイアウトに切り替わります。\u003c/p\u003e\n\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fgeocode-web-site.pages.dev%2Fproduct5.jpg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=68be939ecf0f911ec9cdf99e344d6f63\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fgeocode-web-site.pages.dev%2Fproduct5.jpg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=68be939ecf0f911ec9cdf99e344d6f63\" alt=\"GeoCode-Web mobile\" width=\"400\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fgeocode-web-site.pages.dev%2Fproduct5.jpg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=968f9ec09b9ef8242f37c5c05649debf 1x\" data-canonical-src=\"https://geocode-web-site.pages.dev/product5.jpg\" loading=\"lazy\"\u003e\u003c/a\u003e\n\u003ch2 data-sourcepos=\"213:1-213:14\"\u003e\n\u003cspan id=\"配布とci\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%85%8D%E5%B8%83%E3%81%A8ci\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e配布とCI\u003c/h2\u003e\n\u003cp data-sourcepos=\"215:1-215:60\"\u003e現時点の主なサポート対象は Windows 版です。\u003c/p\u003e\n\u003cp data-sourcepos=\"217:1-217:39\"\u003e配布形式は次のとおりです。\u003c/p\u003e\n\u003cul data-sourcepos=\"219:1-221:0\"\u003e\n\u003cli data-sourcepos=\"219:1-219:31\"\u003eMSI インストーラ \u003ccode\u003e.msi\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"220:1-221:0\"\u003ecargo が生成する単体実行ファイル \u003ccode\u003e.exe\u003c/code\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"222:1-222:125\"\u003eGitHub Actions では、\u003ccode\u003ev*\u003c/code\u003e タグ push または手動実行により Windows 向けリリースビルドを行います。\u003c/p\u003e\n\u003cp data-sourcepos=\"224:1-224:175\"\u003eCI では、フロントエンドの型チェック・ビルド、Tauri/Rust に埋め込む成果物生成、\u003ccode\u003ecargo fmt --check\u003c/code\u003e、\u003ccode\u003ecargo test --locked\u003c/code\u003e を実行します。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"226:1-226:24\"\u003e\n\u003cspan id=\"ossとしての方針\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#oss%E3%81%A8%E3%81%97%E3%81%A6%E3%81%AE%E6%96%B9%E9%87%9D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eOSSとしての方針\u003c/h2\u003e\n\u003cp data-sourcepos=\"228:1-228:207\"\u003eGeoCode-Web-Single は、ソースコードを OSS として公開しつつ、一般利用者には Windows 向けリリース成果物をフリーソフトのように利用できる形で配布します。\u003c/p\u003e\n\u003cul data-sourcepos=\"230:1-233:0\"\u003e\n\u003cli data-sourcepos=\"230:1-230:94\"\u003e通常の不具合報告、機能要望、改善提案は GitHub Issue で受け付けます\u003c/li\u003e\n\u003cli data-sourcepos=\"231:1-231:108\"\u003ePull Request は許可済みユーザーからのみ受け付けます（私のキャパの問題です）\u003c/li\u003e\n\u003cli data-sourcepos=\"232:1-233:0\"\u003eセキュリティ上の問題は公開 Issue に書かず、セキュリティポリシーの案内に従ってください\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"234:1-234:40\"\u003eライセンスは MIT License です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"236:1-236:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"238:1-238:141\"\u003eGeoCode-Web は、場所に紐付いた情報をローカル中心で管理するために開発した地図アプリケーションです。\u003c/p\u003e\n\u003cp data-sourcepos=\"240:1-240:236\"\u003e地図、マークダウン、ファイル添付、図形描画、レイヤ管理、一時共有 URL を組み合わせて、個人または小中規模チームで使える地図アプリケーション環境を目指しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"242:1-242:123\"\u003eブラウザだけで試せるデモ版もあるので、まずはそちらから触ってもらえると嬉しいです。\u003c/p\u003e\n\u003cul data-sourcepos=\"244:1-246:112\"\u003e\n\u003cli data-sourcepos=\"244:1-244:102\"\u003ePC用ブラウザデモ版: \u003ca href=\"https://demo-geocode-web.pages.dev\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://demo-geocode-web.pages.dev\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"245:1-245:123\"\u003eスマホ用ブラウザデモ版: \u003ca href=\"https://demo-geocode-web-mobile.pages.dev\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://demo-geocode-web-mobile.pages.dev\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"246:1-246:112\"\u003eGitHub: \u003ca href=\"https://github.com/itsuki-maru/GeoCode-Web-Single\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://github.com/itsuki-maru/GeoCode-Web-Single\u003c/a\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n","body":"## 概要\n\n地図上にメモ、写真、動画、PDF、図形を置いて管理できる Windows 向けアプリケーション **GeoCode-Web** を OSS として公開しました。\n\n\u003cimg src=\"https://raw.githubusercontent.com/itsuki-maru/GeoCode-Web-Single/main/userguide/images/1-1_geocode-web.png\" alt=\"GeoCode-Web\" width=\"700\"\u003e\n\nこのアプリケーションは、内容をドキュメンタリーに寄せた Zenn では書ききれなかった「**アプリケーションとして何ができるのか**」「**どのような構成で動いているのか**」を、Qiita 向けに技術寄りで整理したのでご紹介します。\n\nhttps://zenn.dev/marudev/articles/12e14d4f67c5b1\n\nリポジトリはこちらです。\n\nhttps://github.com/itsuki-maru/GeoCode-Web-Single\n\nプロジェクトサイトとデモ版も用意しています。\n\n- プロジェクトサイト: [https://geocode-web-site.pages.dev](https://geocode-web-site.pages.dev)\n- PC用ブラウザデモ版: [https://demo-geocode-web.pages.dev](https://demo-geocode-web.pages.dev)\n- スマホ用ブラウザデモ版: [https://demo-geocode-web-mobile.pages.dev](https://demo-geocode-web-mobile.pages.dev)\n- ユーザーガイド: [https://geocode-web-single.pages.dev/user-guide.html](https://geocode-web-single.pages.dev/user-guide.html)\n\n## 作ったもの\n\nGeoCode-Web は、地図上にマーカーや図形を配置し、場所に紐付いた情報を管理するためのマッピングアプリケーションです。\n\nマーカーには マークダウン形式の詳細本文を書けるため、単なるピン管理ではなく、場所ごとの簡易 Wiki のようにも使えます。画像、PDF、MP4 もアップロードして、マーカー詳細に埋め込めます。\n\n主な用途としては、次のような「場所と情報を一緒に扱いたい」ケースを想定しています。\n\n- **旅行や訪問先の記録**\n- **現地調査メモ**\n- **写真・PDF・動画つきの地点管理**\n- **小中規模チーム内での地図情報共有**\n- **オフラインまたはローカルネットワーク内で完結させたい地図メモ環境**\n\n![GeoCode-Web](https://github.com/itsuki-maru/GeoCode-Web-Single/raw/main/userguide/images/99-2_image.png)\n\n## なぜ基本ローカル動作にしたか\n\n個人的に重視したのは、位置情報や写真などをできるだけ手元で管理できることです。詳しくはこちらをご覧ください👇\n\nhttps://zenn.dev/marudev/articles/12e14d4f67c5b1\n\nGeoCode-Web は通常、Tauri デスクトップアプリとして起動し、内部でローカル HTTP サーバを立ち上げて UI を表示します。データベースには SQLite を使い、設定ファイルやアップロードファイルもユーザーのローカル領域に保存します。\n\n保存先は次のような構成です。\n\n```txt\n~/.geocode-web-single/\n  ├─ geocode-web-single.env.json\n  ├─ geocode-web.sqlite\n  └─ images/\n```\n\nただし、初期状態では国土地理院の通常地図・航空写真タイルを利用するため、インターネット環境が必要です。同一端末内にタイルサーバを用意すれば、完全オフライン運用も可能です。\n\n## 技術スタック\n\n構成は次のとおりです。\n\n| 領域 | 技術 |\n| --- | --- |\n| バックエンド | Rust 2024 / Axum / SQLx / SQLite |\n| フロントエンド | Vue 3 / Vue Router / Pinia / Vite |\n| デスクトップ | Tauri 2 |\n| テンプレート | Tera |\n| 認証 | JWT / TOTP |\n| マークダウンレンダラ | marked |\n| 静的ファイル埋め込み | rust-embed |\n| 配布 | GitHub Actions / NSIS / MSI Installer |\n\nRust/Axum 製 API サーバと Vue 3 製フロントエンドを Tauri 2 でラップし、Windows 向けアプリケーションとして配布しています。\n\n## 主な機能\n\n### 地図・マーカー管理\n\n地図上にマーカーを追加し、位置や内容を編集できます。\n\n- マーカーの追加・移動・編集・削除\n- Markdown による詳細本文の記述\n- 指定マーカーへのフォーカス・ズームイン\n- マーカー検索\n- 国土地理院の通常地図・航空写真を初期タイルとして利用\n\n### レイヤ管理\n\n情報はレイヤ単位で整理できます。\n\n- ユーザーごとの `master` レイヤ自動作成\n- 任意レイヤの追加・名称変更・削除\n- マスターレイヤでの全件横断検索\n- 個別レイヤ内検索\n- レイヤ単位または全件の JSON エクスポート / インポート\n\nJSON エクスポートは、マーカーと図形を含む形式に対応で出力します。この機能で他の端末でも地図上のデータを復元することができます。\n\n### 図形描画\n\nレイヤに紐付けて、地図上に図形を描けます。\n\n- ポリゴン\n- ポリライン\n- 矩形\n- 円\n\n図形には名前を付けられます。図形名は地図上のラベルとして表示され、後から名前や所属レイヤを変更できます。削除直後の取り消しにも対応しています。\n\n![shape](https://geocode-web-single.pages.dev/userguide/images/13-3_geocode-web.png)\n\n### 画像・PDF・動画管理\n\nアップロードできる形式は次のとおりです。\n\n```txt\nPNG / JPG / JPEG / GIF / WebP / PDF / MP4\n```\n\nアップロード上限は 1 ファイル 100MB です。\n\n画像は再エンコードして保存し、EXIF などのメタデータを除去します。画像サムネイルも生成し、マークダウン表示時には `?thumb=true` を使ってサムネイルをバックエンドに要求し、軽量表示します。\n\nMP4 は可能な場合 poster 画像を生成し、動画タグの `poster` として利用します。Markdown 内の動画は `preload=\"none\"` で描画し、ポップアップ内の画像・動画は details 展開時に遅延読み込みします。\n\nマークダウンへの埋め込み文字列も自動生成します。\n\n```md\n![画像](url)\n[PDF](url)\n?[動画](url)\n```\n\n### 一時共有URL\n\nサーバ単体モードや適切に公開された環境では、選択したレイヤを一時共有 URL として公開できます。\n\n- 共有対象レイヤを選択して URL を発行\n- 有効期限を指定\n- 任意の共有パスワードを設定\n- 図形を共有対象に含めるか選択\n- 既存リンクの内容更新\n- 共有停止\n- デスクトップ / モバイル向け専用レイアウトで閲覧\n\n共有ページは live データではなく、発行時点のレイヤ・マーカー・図形情報を保持するスナップショット方式です。\n\n## 認証とセキュリティまわり\n\nGeoCode-Web は、ローカル利用だけでなくサーバ単体モードも持つため、認証まわりも実装しています。\n\n- アクセストークン + リフレッシュトークンを JWT で発行\n- 両トークンは HttpOnly Cookie で保持（http 環境では設定を変えないと動かないので注意）\n- リフレッシュトークンは `/account/refresh` に限定\n- TOTP による二段階認証\n- ログイン失敗回数に応じた再試行待ち\n- アカウントロック\n- 管理者によるユーザー作成・パスワードリセット・ロック解除\n\n画像の参照可否はユーザー単位のプライバシーモードで制御します。プライバシーモードが ON の場合、自分以外からの画像アクセスを遮断します。\n\n## 起動モード\n\nGeoCode-Web には、通常の Tauri デスクトップアプリとしての起動と、サーバ単体モードがあります。\n\n### Tauri デスクトップアプリ\n\nインストーラで導入後、アプリを起動します。\n\n初回起動時はセットアップ画面が表示され、次の項目を設定します。\n\n- アプリタイトル\n- 管理者ユーザー名 / パスワード\n- アカウントロック回数\n- 待機制限開始回数\n- 待機時間\n- アクセストークン有効期限\n- リフレッシュトークン有効期限\n\nセットアップ完了後、設定ファイル、SQLite DB、アップロードファイル保存用ディレクトリが作成されます。\n\n### サーバ単体モード\n\nGUI を使わず、Axum サーバだけを起動することもできます。\n\n```bash\n# ホストのみ指定する場合。ポートは 3000 を使用\ngeocode_web_single -s 0.0.0.0\n\n# ホストとポートを指定する場合\ngeocode_web_single -s 0.0.0.0:9090\n```\n\n事前に Tauri GUI でセットアップを完了しておく必要があります。\n\nHTTP 環境で運用する場合、デフォルトでは `SECURE_COOKIE=true` のため Cookie が機能しません。その場合は、設定ファイルの `secure_cookie` を `false` に変更します。\n\n```json\n{\n  \"secure_cookie\": \"false\"\n}\n```\n\n\u003e 上記は一例です。規模によってはリバースプロキシなどの使用も検討したほうが良いですが、使う環境の状況によるでしょう。\n\n## モバイル対応\n\nUser-Agent に `Mobile` が含まれるアクセスには、モバイル向け UI を返します。\n\nモバイル版では、ツール群をフローティングボタンと全画面モーダル中心の操作にしています。一時共有 URL のプレビューも、モバイル向けレイアウトに切り替わります。\n\n\u003cimg src=\"https://geocode-web-site.pages.dev/product5.jpg\" alt=\"GeoCode-Web mobile\" width=\"400\"\u003e\n\n## 配布とCI\n\n現時点の主なサポート対象は Windows 版です。\n\n配布形式は次のとおりです。\n\n- MSI インストーラ `.msi`\n- cargo が生成する単体実行ファイル `.exe`\n\nGitHub Actions では、`v*` タグ push または手動実行により Windows 向けリリースビルドを行います。\n\nCI では、フロントエンドの型チェック・ビルド、Tauri/Rust に埋め込む成果物生成、`cargo fmt --check`、`cargo test --locked` を実行します。\n\n## OSSとしての方針\n\nGeoCode-Web-Single は、ソースコードを OSS として公開しつつ、一般利用者には Windows 向けリリース成果物をフリーソフトのように利用できる形で配布します。\n\n- 通常の不具合報告、機能要望、改善提案は GitHub Issue で受け付けます\n- Pull Request は許可済みユーザーからのみ受け付けます（私のキャパの問題です）\n- セキュリティ上の問題は公開 Issue に書かず、セキュリティポリシーの案内に従ってください\n\nライセンスは MIT License です。\n\n## まとめ\n\nGeoCode-Web は、場所に紐付いた情報をローカル中心で管理するために開発した地図アプリケーションです。\n\n地図、マークダウン、ファイル添付、図形描画、レイヤ管理、一時共有 URL を組み合わせて、個人または小中規模チームで使える地図アプリケーション環境を目指しています。\n\nブラウザだけで試せるデモ版もあるので、まずはそちらから触ってもらえると嬉しいです。\n\n- PC用ブラウザデモ版: [https://demo-geocode-web.pages.dev](https://demo-geocode-web.pages.dev)\n- スマホ用ブラウザデモ版: [https://demo-geocode-web-mobile.pages.dev](https://demo-geocode-web-mobile.pages.dev)\n- GitHub: [https://github.com/itsuki-maru/GeoCode-Web-Single](https://github.com/itsuki-maru/GeoCode-Web-Single)\n","coediting":false,"comments_count":0,"created_at":"2026-05-26T19:30:20+09:00","group":null,"id":"80f7ba278dfa69f67bb0","likes_count":2,"private":false,"reactions_count":0,"stocks_count":1,"tags":[{"name":"Rust","versions":[]},{"name":"Vue.js","versions":[]},{"name":"個人開発","versions":[]},{"name":"axum","versions":[]},{"name":"Tauri","versions":[]}],"title":"地図アプリ「GeoCode-Web」をOSSとして公開しました","updated_at":"2026-05-26T19:40:04+09:00","url":"https://qiita.com/maru_dev/items/80f7ba278dfa69f67bb0","user":{"description":"パソコンを少々。RustとかTypeScriptとかPythonとか","facebook_id":"","followees_count":10,"followers_count":2,"github_login_name":"itsuki-maru","id":"maru_dev","items_count":4,"linkedin_id":"","location":"","name":"maru itsuki","organization":"","permanent_id":2457409,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457409/profile-images/1690635573","team_only":false,"twitter_screen_name":"marudev3","website_url":"https://www.marudev.org/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003cp data-sourcepos=\"1:1-2:21\"\u003eVue公式のチュートリアル\u003cbr\u003e\nイベントの発行\u003c/p\u003e\n\u003cp data-sourcepos=\"4:1-4:38\"\u003e\u003ciframe id=\"qiita-embed-content__8fc807038cf031b8f5381aa6d58326b8\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__8fc807038cf031b8f5381aa6d58326b8\" data-content=\"https%3A%2F%2Fja.vuejs.org%2Ftutorial%2F%23step-13\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:42\"\u003eこれは初めて知ったかも・・。\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"8:1-8:144\"\u003e\n\u003cp data-sourcepos=\"8:3-8:144\"\u003eprops を受け取るだけでなく、子コンポーネントは親コンポーネントにイベントを発行することもできます:\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"10:1-18:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003echild側\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// 発行されるイベントを宣言します\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eemit\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003edefineEmits\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eresponse\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 引数つきで発行\u003c/span\u003e\n\u003cspan class=\"nf\"\u003eemit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eresponse\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ehello from child\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cblockquote data-sourcepos=\"19:1-19:117\"\u003e\n\u003cp data-sourcepos=\"19:3-19:117\"\u003eemit() の第一引数はイベント名です。追加の引数は、イベントリスナーに渡されます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote data-sourcepos=\"21:1-21:237\"\u003e\n\u003cp data-sourcepos=\"21:3-21:237\"\u003e親は v-on を使って子が発行するイベントを購読できます。ここでは、ハンドラーは子の emit 呼び出しから追加の引数を受け取り、それをローカルステートに割り当てています:\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"23:1-25:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003e親側\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;ChildComp\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eresponse=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"(msg) =\u0026gt; childMsg = msg\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"27:1-27:112\"\u003eこの \u003ccode\u003e@response\u003c/code\u003e って、一体、何を意味しているのか全然分からなかったけど・・・。\u003c/p\u003e\n\u003cp data-sourcepos=\"29:1-30:41\"\u003e\u003ccode\u003e@click\u003c/code\u003eとかはよく見るけど。。\u003cbr\u003e\nそもそも、\u003ccode\u003e@click\u003c/code\u003eは省略形で、\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"32:1-32:58\"\u003e\n\u003cp data-sourcepos=\"32:3-32:58\"\u003ev-on:click=\"handler\"、を省略して @click=\"handler\"\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"34:1-34:15\"\u003eと書くと。\u003c/p\u003e\n\u003cp data-sourcepos=\"36:1-36:52\"\u003e\u003ciframe id=\"qiita-embed-content__af0c0131c07b7c52687a6848af134e07\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__af0c0131c07b7c52687a6848af134e07\" data-content=\"https%3A%2F%2Fja.vuejs.org%2Fguide%2Fessentials%2Fevent-handling\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"38:1-39:69\"\u003eなので、\u003cbr\u003e\n\u003ccode\u003e@response\u003c/code\u003e は、略さず書くと、\u003ccode\u003ev-on:response\u003c/code\u003e　なのね。\u003c/p\u003e\n\u003cp data-sourcepos=\"41:1-44:72\"\u003eで、\u003cbr\u003e\n\u003ccode\u003e@click\u003c/code\u003e　とかは、特に自分で定義しなくても使えたけど、\u003cbr\u003e\n今回、emit で、response　という名前のイベントを定義しているので、\u003cbr\u003e\n\u003ccode\u003e@response\u003c/code\u003e で呼べるようになったということのようだ。\u003c/p\u003e\n\u003cp data-sourcepos=\"46:1-47:97\"\u003eだから、\u003cbr\u003e\nemit で、response1 という名前で定義すると、\u003ccode\u003e@response1\u003c/code\u003e で呼ぶことになる。\u003c/p\u003e\n\u003cp data-sourcepos=\"49:1-49:77\"\u003e以下、responseをresponse1としてみたけど、ちゃんと動いた。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"50:1-58:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003echild側\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// 発行されるイベントを宣言します\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eemit\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003edefineEmits\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eresponse1\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 引数つきで発行\u003c/span\u003e\n\u003cspan class=\"nf\"\u003eemit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eresponse1\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ehello from child\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"60:1-72:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003e親側\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"nx\"\u003eChildComp\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./ChildComp.vue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003echildMsg\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eNo child msg yet\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;ChildComp\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eresponse1=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"(msg) =\u0026gt; (childMsg = msg)\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;p\u0026gt;\u003c/span\u003e\u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003echildMsg\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/p\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"74:1-74:29\"\u003e\n\u003cspan id=\"stackblitzで書いた\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#stackblitz%E3%81%A7%E6%9B%B8%E3%81%84%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003estackblitzで書いた。\u003c/h3\u003e\n\u003cp data-sourcepos=\"76:1-76:86\"\u003e\u003ciframe id=\"qiita-embed-content__549feeff87a815528d176d79a228a6c6\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__549feeff87a815528d176d79a228a6c6\" data-content=\"https%3A%2F%2Fstackblitz.com%2Fedit%2Fvitejs-vite-tg2jgxce%3Ffile%3Dsrc%252Fcomponents%252FChildComp.vue\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n","body":"Vue公式のチュートリアル\nイベントの発行\n\nhttps://ja.vuejs.org/tutorial/#step-13\n\nこれは初めて知ったかも・・。\n\n\u003e props を受け取るだけでなく、子コンポーネントは親コンポーネントにイベントを発行することもできます:\n\n```vue:child側\n\u003cscript setup\u003e\n// 発行されるイベントを宣言します\nconst emit = defineEmits(['response'])\n\n// 引数つきで発行\nemit('response', 'hello from child')\n\u003c/script\u003e\n```\n\u003e emit() の第一引数はイベント名です。追加の引数は、イベントリスナーに渡されます。\n\n\u003e 親は v-on を使って子が発行するイベントを購読できます。ここでは、ハンドラーは子の emit 呼び出しから追加の引数を受け取り、それをローカルステートに割り当てています:\n\n```vue:親側\n\u003cChildComp @response=\"(msg) =\u003e childMsg = msg\" /\u003e\n```\n\nこの `@response` って、一体、何を意味しているのか全然分からなかったけど・・・。\n\n`@click`とかはよく見るけど。。\nそもそも、`@click`は省略形で、\n\n\u003e v-on:click=\"handler\"、を省略して @click=\"handler\" \n\nと書くと。\n\nhttps://ja.vuejs.org/guide/essentials/event-handling\n\nなので、\n`@response` は、略さず書くと、`v-on:response`　なのね。\n\nで、\n`@click`　とかは、特に自分で定義しなくても使えたけど、\n今回、emit で、response　という名前のイベントを定義しているので、\n`@response` で呼べるようになったということのようだ。\n\nだから、\nemit で、response1 という名前で定義すると、`@response1` で呼ぶことになる。\n\n以下、responseをresponse1としてみたけど、ちゃんと動いた。\n```vue:child側\n\u003cscript setup\u003e\n// 発行されるイベントを宣言します\nconst emit = defineEmits(['response1'])\n\n// 引数つきで発行\nemit('response1', 'hello from child')\n\u003c/script\u003e\n```\n\n```vue:親側\n\u003cscript setup\u003e\nimport { ref } from 'vue';\nimport ChildComp from './ChildComp.vue';\n\nconst childMsg = ref('No child msg yet');\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cChildComp @response1=\"(msg) =\u003e (childMsg = msg)\" /\u003e\n  \u003cp\u003e{{ childMsg }}\u003c/p\u003e\n\u003c/template\u003e\n```\n\n### stackblitzで書いた。\n\nhttps://stackblitz.com/edit/vitejs-vite-tg2jgxce?file=src%2Fcomponents%2FChildComp.vue\n","coediting":false,"comments_count":0,"created_at":"2026-05-26T14:40:49+09:00","group":null,"id":"5a43e20004d5995c30cd","likes_count":1,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"JavaScript","versions":[]},{"name":"Vue.js","versions":[]}],"title":"[Vue] コンポーネント イベントの発行","updated_at":"2026-05-26T14:40:49+09:00","url":"https://qiita.com/sasuke_sss/items/5a43e20004d5995c30cd","user":{"description":"","facebook_id":"","followees_count":9,"followers_count":13,"github_login_name":null,"id":"sasuke_sss","items_count":68,"linkedin_id":"","location":"","name":"","organization":"テクノフェイス","permanent_id":1284134,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/1284134/2e21ea1a85ea36f03c9c65376dcd3ed9f8cf71b9/x_large.png?1617674661","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":"technoface","slide":false},{"rendered_body":"\u003cdiv data-sourcepos=\"1:1-3:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"2:1-2:211\"\u003eこの記事は、\u003ca href=\"https://qiita.com/logue/items/21bb239fed2c4f4a3b2f\" id=\"reference-f258f1d2468beeb4dd20\"\u003eRsbuildで組むVueのVRMコンポーネント。ライセンスと配信の安全性を追求したライブラリ設計\u003c/a\u003eの一部です。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch1 data-sourcepos=\"6:1-6:64\"\u003e\n\u003cspan id=\"vue-vrmでなぜrsbuildスタックを採用したのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vue-vrm%E3%81%A7%E3%81%AA%E3%81%9Crsbuild%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%82%92%E6%8E%A1%E7%94%A8%E3%81%97%E3%81%9F%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003evue-vrmで、なぜRsbuildスタックを採用したのか？\u003c/h1\u003e\n\u003cp data-sourcepos=\"8:1-8:496\"\u003e現在、Viteスタックはコアエンジンこそ高速化しているものの、周辺を取り巻くeslintやstylelintなどは、古のwebpackの時代から使われてきた技術が使われており、依存関係がぐちゃぐちゃになっています。事実、eslintが9になったときは、設定ファイルの仕様が大幅に変わりトラブルが多発しました。今でもeslint8から移行できないプロジェクトも多いんじゃないでしょうか？\u003c/p\u003e\n\u003cp data-sourcepos=\"10:1-10:553\"\u003e一応、自分でも\u003ca href=\"https://github.com/logue/ol-slmap\" rel=\"nofollow noopener\" target=\"_blank\"\u003eol-slmap\u003c/a\u003eのような、vueのようなライブラリを含まない純粋なJavaScriptの新規プロジェクトでも部分的に使用していましたが、先月（2026/04/22）にRspack2.0がリリースされたことを受け、改めて調べてみると整形ツールのrslintやテストツールのrstest、ライブラリ向けビルドができるrslibなど独自にエコシステムが出来上がっているのを確認したため、使ってみることにしました。\u003c/p\u003e\n\u003cp data-sourcepos=\"12:1-12:279\"\u003e特筆するべき点として、公式のRsbuildのスターターテンプレートは、最初からAI駆動設計のためのスキルや指示書が含まれており、バイブコーディング時のガードレールが予め備わっているところがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"14:1-14:39\"\u003eここが決め手となりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"16:1-16:137\"\u003e\u003cem\u003e何でもできるということは「自由度が高い」ではなく、「どうとでもなってしまう」こと\u003c/em\u003eですしね。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"18:1-18:25\"\u003e\n\u003cspan id=\"rsbuildとviteの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#rsbuild%E3%81%A8vite%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eRsbuildとViteの違い\u003c/h1\u003e\n\u003cp data-sourcepos=\"20:1-20:65\"\u003eRsbuildとViteの違いをざっと箇条書きにしました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"22:1-22:39\"\u003e\n\u003cspan id=\"1-根本的な設計思想の違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E6%A0%B9%E6%9C%AC%E7%9A%84%E3%81%AA%E8%A8%AD%E8%A8%88%E6%80%9D%E6%83%B3%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 根本的な設計思想の違い\u003c/h2\u003e\n\u003ctable data-sourcepos=\"24:1-28:111\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"24:1-24:21\"\u003e\n\u003cth data-sourcepos=\"24:2-24:7\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"24:9-24:12\"\u003eVite\u003c/th\u003e\n\u003cth data-sourcepos=\"24:14-24:20\"\u003eRsbuild\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"26:1-26:157\"\u003e\n\u003ctd data-sourcepos=\"26:2-26:19\"\u003eコアエンジン\u003c/td\u003e\n\u003ctd data-sourcepos=\"26:21-26:126\"\u003eesbuild (Dev) + Rollup (Build)Rspack（全フェーズで統一）言語Go (esbuild) + JavaScript (Rollup)\u003c/td\u003e\n\u003ctd data-sourcepos=\"26:128-26:156\"\u003eRust（Rspack） + TypeScript\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"27:1-27:98\"\u003e\n\u003ctd data-sourcepos=\"27:2-27:23\"\u003eDev / Prodの一貫性\u003c/td\u003e\n\u003ctd data-sourcepos=\"27:25-27:63\"\u003e低い（DevはESM、ProdはBundling）\u003c/td\u003e\n\u003ctd data-sourcepos=\"27:65-27:97\"\u003e非常に高い（両方Rspack）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"28:1-28:111\"\u003e\n\u003ctd data-sourcepos=\"28:2-28:7\"\u003e哲学\u003c/td\u003e\n\u003ctd data-sourcepos=\"28:9-28:50\"\u003e「可能な限りバンドルしない」\u003c/td\u003e\n\u003ctd data-sourcepos=\"28:52-28:110\"\u003e高速Dev体験「Rustで全部高速化」＋Webpack互換\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"30:1-30:46\"\u003e\n\u003cspan id=\"2-開発モードdev-serverの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E9%96%8B%E7%99%BA%E3%83%A2%E3%83%BC%E3%83%89dev-server%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 開発モード（Dev Server）の違い\u003c/h2\u003e\n\u003cp data-sourcepos=\"32:1-32:5\"\u003eVite:\u003c/p\u003e\n\u003cul data-sourcepos=\"34:1-39:0\"\u003e\n\u003cli data-sourcepos=\"34:1-34:55\"\u003eNative ESM + on-demand compilation が最大の特徴\u003c/li\u003e\n\u003cli data-sourcepos=\"35:1-35:71\"\u003eブラウザが直接ESMを読み込む（\u003ccode\u003e\u0026lt;script type=\"module\"\u0026gt;\u003c/code\u003e）\u003c/li\u003e\n\u003cli data-sourcepos=\"36:1-36:69\"\u003e依存関係のPre-bundlingにesbuildを使用（非常に高速）\u003c/li\u003e\n\u003cli data-sourcepos=\"37:1-37:105\"\u003eソースコードの変換は必要最小限のみesbuildで行い、残りはブラウザに任せる\u003c/li\u003e\n\u003cli data-sourcepos=\"38:1-39:0\"\u003eHMRは非常に速いが、DevとProdの出力に差が出やすい（特にコード分割や最適化）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"40:1-40:17\"\u003eRsbuild (Rspack):\u003c/p\u003e\n\u003cul data-sourcepos=\"42:1-46:0\"\u003e\n\u003cli data-sourcepos=\"42:1-42:80\"\u003eDevでもフルバンドリングに近いアプローチ（Rspackが処理）\u003c/li\u003e\n\u003cli data-sourcepos=\"43:1-43:84\"\u003eRust並列処理により、大規模プロジェクトでもHMRが極めて高速\u003c/li\u003e\n\u003cli data-sourcepos=\"44:1-44:87\"\u003eDevとProdで同じエンジンを使うため、動作の一貫性が非常に高い\u003c/li\u003e\n\u003cli data-sourcepos=\"45:1-46:0\"\u003eプラグインやLoaderもWebpack/Rspack互換で動く\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"47:1-47:52\"\u003e\n\u003cspan id=\"3-本番ビルドproduction-buildの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E6%9C%AC%E7%95%AA%E3%83%93%E3%83%AB%E3%83%89production-build%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 本番ビルド（Production Build）の違い\u003c/h2\u003e\n\u003cp data-sourcepos=\"49:1-49:5\"\u003eVite:\u003c/p\u003e\n\u003cul data-sourcepos=\"51:1-54:0\"\u003e\n\u003cli data-sourcepos=\"51:1-51:41\"\u003eRollupをベースにバンドリング\u003c/li\u003e\n\u003cli data-sourcepos=\"52:1-54:0\"\u003eRollupの優れたTree-shaking、コード分割、チャンク制御が強み。\u003csup\u003e\u003ca href=\"#fn-1\" id=\"fnref-1\"\u003e1\u003c/a\u003e\u003c/sup\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"55:1-55:8\"\u003eRsbuild:\u003c/p\u003e\n\u003cul data-sourcepos=\"57:1-61:0\"\u003e\n\u003cli data-sourcepos=\"57:1-57:29\"\u003eRspackで一貫して処理\u003c/li\u003e\n\u003cli data-sourcepos=\"58:1-58:55\"\u003eRspackはWebpackのAPIをRustで再実装したもの\u003c/li\u003e\n\u003cli data-sourcepos=\"59:1-59:79\"\u003eWebpackエコシステム（Loader/Plugin）の多くがそのまま使える\u003c/li\u003e\n\u003cli data-sourcepos=\"60:1-61:0\"\u003e並列処理とRustの速度で、特に大規模プロジェクトでViteを上回るビルド速度を出すケースが多い\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"62:1-62:51\"\u003e\n\u003cspan id=\"4-プラグインエコシステムの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%A8%E3%82%B3%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. プラグイン・エコシステムの違い\u003c/h2\u003e\n\u003cp data-sourcepos=\"64:1-64:6\"\u003eVite:\u003c/p\u003e\n\u003cul data-sourcepos=\"65:1-66:0\"\u003e\n\u003cli data-sourcepos=\"65:1-66:0\"\u003eRollup互換プラグイン + Vite独自拡張。エコシステムが非常に成熟\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"67:1-67:8\"\u003eRsbuild:\u003c/p\u003e\n\u003cul data-sourcepos=\"69:1-72:0\"\u003e\n\u003cli data-sourcepos=\"69:1-69:110\"\u003eWebpack/Rspack互換（ほとんどのwebpack pluginが動く） + Rsbuild独自のシンプルなPlugin API\u003c/li\u003e\n\u003cli data-sourcepos=\"70:1-72:0\"\u003e既存のWebpackプロジェクトからの移行が比較的楽\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"73:1-73:48\"\u003e\n\u003cspan id=\"5-内部的な処理の流れ簡略化\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E5%86%85%E9%83%A8%E7%9A%84%E3%81%AA%E5%87%A6%E7%90%86%E3%81%AE%E6%B5%81%E3%82%8C%E7%B0%A1%E7%95%A5%E5%8C%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. 内部的な処理の流れ（簡略化）\u003c/h2\u003e\n\u003cp data-sourcepos=\"75:1-75:5\"\u003eVite:\u003c/p\u003e\n\u003col data-sourcepos=\"77:1-81:0\"\u003e\n\u003cli data-sourcepos=\"77:1-77:24\"\u003eDevサーバー起動\u003c/li\u003e\n\u003cli data-sourcepos=\"78:1-78:34\"\u003e依存Pre-bundling（esbuild）\u003c/li\u003e\n\u003cli data-sourcepos=\"79:1-79:87\"\u003eブラウザからリクエスト → 必要なファイルだけesbuildでtransform\u003c/li\u003e\n\u003cli data-sourcepos=\"80:1-81:0\"\u003eProduction: Rollupでフルバンドル\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"82:1-82:8\"\u003eRsbuild:\u003c/p\u003e\n\u003col data-sourcepos=\"84:1-87:0\"\u003e\n\u003cli data-sourcepos=\"84:1-84:35\"\u003eRspackがDev/Prod両方で動作\u003c/li\u003e\n\u003cli data-sourcepos=\"85:1-85:44\"\u003eRust並列処理でLoader/Pluginを実行\u003c/li\u003e\n\u003cli data-sourcepos=\"86:1-87:0\"\u003e統一されたPipelineで処理（Transformer → Optimizer → Bundler）\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 data-sourcepos=\"88:1-88:48\"\u003e\n\u003cspan id=\"まとめどちらが優れているか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81%E3%81%A9%E3%81%A1%E3%82%89%E3%81%8C%E5%84%AA%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ：どちらが優れているか？\u003c/h2\u003e\n\u003cul data-sourcepos=\"90:1-94:0\"\u003e\n\u003cli data-sourcepos=\"90:1-90:92\"\u003e開発体験・シンプルさ → Viteがまだ優勢（エコシステムの成熟度）\u003c/li\u003e\n\u003cli data-sourcepos=\"91:1-91:89\"\u003eパフォーマンス（特に大規模）・一貫性 → Rsbuild（Rspack）が優勢\u003c/li\u003e\n\u003cli data-sourcepos=\"92:1-92:57\"\u003eWebpack資産の活用 → Rsbuildが圧倒的に有利\u003c/li\u003e\n\u003cli data-sourcepos=\"93:1-94:0\"\u003e将来的 → ViteはRolldownでRust統一方向、RsbuildはすでにRust中心\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 data-sourcepos=\"95:1-95:17\"\u003e\n\u003cspan id=\"実際の設定\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E9%9A%9B%E3%81%AE%E8%A8%AD%E5%AE%9A\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実際の設定\u003c/h1\u003e\n\u003cp data-sourcepos=\"97:1-97:274\"\u003evue-vrmのリポジトリのソースコードは、デモプログラムと、ライブラリのコード両方が\u003ccode\u003esrc/\u003c/code\u003eディレクトリに入っているあたり、あまりいいディレクトリ構造ではありません。ここはおいおい直すとします。\u003c/p\u003e\n\u003cp data-sourcepos=\"99:1-99:158\"\u003e\u003ca href=\"https://rsbuild.rs/\" rel=\"nofollow noopener\" target=\"_blank\"\u003ersbuild\u003c/a\u003eでデモプログラムのビルドを行い、\u003ca href=\"https://rslib.rs/\" rel=\"nofollow noopener\" target=\"_blank\"\u003erslib\u003c/a\u003eでライブラリのビルドを行っています。\u003c/p\u003e\n\u003cp data-sourcepos=\"101:1-101:348\"\u003eデモプログラム部分の設定です。こちらは、rsbuildを用いてビルドします。出力されるコードは圧縮済みでwebpackの出力した圧縮コードに酷似したコードが出力されます。\u003ccode\u003esrc/meta.ts\u003c/code\u003eにビルド日時とバージョン情報を書き込むための定数はここで定義しています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"103:1-154:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003ersbuild.config.ts\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003ereadFileSync\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003enode:fs\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eresolve\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003enode:path\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003epackageJson\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eparse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003ereadFileSync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./package.json\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eutf-8\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ebuildDate\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eDate\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nf\"\u003etoISOString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\n\u003cspan class=\"nx\"\u003econsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eInjected version:\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003epackageJson\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eversion\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nx\"\u003econsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eInjected build date:\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ebuildDate\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003edefineConfig\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e@rsbuild/core\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003epluginVue\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e@rsbuild/plugin-vue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// Docs: https://rsbuild.rs/config/\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e// Demo build configuration - for library build, see rslib.config.ts\u003c/span\u003e\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"nf\"\u003edefineConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eplugins\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nf\"\u003epluginVue\u003c/span\u003e\u003cspan class=\"p\"\u003e()],\u003c/span\u003e\n  \u003cspan class=\"na\"\u003esource\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"na\"\u003e__DEMO_BUILD__\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n      \u003cspan class=\"na\"\u003e__APP_VERSION__\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epackageJson\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eversion\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n      \u003cspan class=\"na\"\u003e__BUILD_DATE__\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuildDate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eindex\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./src/index.ts\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eoutput\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edistPath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003edocs\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eassetPrefix\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003efilenameHash\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ecopy\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n      \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"na\"\u003efrom\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./src/assets\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eto\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eassets\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n      \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e]\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ehtml\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003etemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./src/index.html\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003etitle\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eVRM Viewer Demo - Vue VRM\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"na\"\u003etools\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ehtmlPlugin\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003eundefined\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eresolve\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ealias\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e@\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nf\"\u003eresolve\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003e__dirname\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003esrc\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"156:1-156:164\"\u003eライブラリ部分の設定です。こちらはrslibを使ってビルドしています。出力されるコードは圧縮済みのコードとなります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"158:1-226:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003erslib.config.ts\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003ereadFileSync\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003enode:fs\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003epluginVue\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e@rsbuild/plugin-vue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003edefineConfig\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e@rslib/core\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003epkg\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eparse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nf\"\u003ereadFileSync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./package.json\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eutf-8\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"kd\"\u003eas \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003edescription\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eauthor\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eemail\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003elicense\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eversion\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003ehomepage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ebuildDate\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eDate\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nf\"\u003etoISOString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"nf\"\u003edefineConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003elib\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e// Modern ESM build for modern bundlers and environments\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eformat\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eesm\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n      \u003cspan class=\"na\"\u003edts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n      \u003cspan class=\"na\"\u003esyntax\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eesnext\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e// Legacy CommonJS build for Node.js and older bundlers\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eformat\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ecjs\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n      \u003cspan class=\"na\"\u003edts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n      \u003cspan class=\"na\"\u003esyntax\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ees2015\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ebanner\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"na\"\u003ejs\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e`/**\n * \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003epkg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\n *\n * @description \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003epkg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003edescription\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\n * @author \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003epkg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eauthor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e \u0026lt;\u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003epkg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eauthor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eemail\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026gt;\n * @copyright 2026 By Masashi Yoshikawa All rights reserved.\n * @license \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003epkg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elicense\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\n * @version \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003epkg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eversion\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\n * @see {@link \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003epkg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ehomepage\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e}\n */\n`\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n      \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e],\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eplugins\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nf\"\u003epluginVue\u003c/span\u003e\u003cspan class=\"p\"\u003e()],\u003c/span\u003e\n  \u003cspan class=\"na\"\u003esource\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eindex\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./src/lib.ts\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"na\"\u003e__APP_VERSION__\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epkg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eversion\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n      \u003cspan class=\"na\"\u003e__BUILD_DATE__\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003estringify\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ebuildDate\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eoutput\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eweb\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eminify\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edistPath\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eroot\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./dist\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003ch1 data-sourcepos=\"228:1-228:29\"\u003e\n\u003cspan id=\"実際のテンプレート\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E9%9A%9B%E3%81%AE%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実際のテンプレート\u003c/h1\u003e\n\u003cp data-sourcepos=\"230:1-230:142\"\u003eVue向けにRsbuildを使用したスターターテンプレートを作成しました。これで色々テストしてみてください。\u003c/p\u003e\n\u003cp data-sourcepos=\"232:1-232:50\"\u003e\u003ca href=\"https://github.com/logue/rsbuild-vue3-ts-starter\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://github.com/logue/rsbuild-vue3-ts-starter\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"234:1-234:134\"\u003e日本語ドキュメントは\u003ca href=\"https://github.com/logue/rsbuild-vue3-ts-starter/blob/master/README.ja.md\" rel=\"nofollow noopener\" target=\"_blank\"\u003eこちら\u003c/a\u003eになります。\u003c/p\u003e\n\u003csection class=\"footnotes\"\u003e\n\u003col\u003e\n\u003cli id=\"fn-1\"\u003e\n\u003cp data-sourcepos=\"236:7-236:125\"\u003e（2026年現在）Rolldown（Rust版Rollup）への移行が進んでおり、徐々にRust統一されつつある。 \u003ca href=\"#fnref-1\" class=\"\"\u003e↩\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/section\u003e\n","body":":::note\nこの記事は、[Rsbuildで組むVueのVRMコンポーネント。ライセンスと配信の安全性を追求したライブラリ設計](https://qiita.com/logue/items/21bb239fed2c4f4a3b2f)の一部です。\n:::\n\n\n# vue-vrmで、なぜRsbuildスタックを採用したのか？\n\n現在、Viteスタックはコアエンジンこそ高速化しているものの、周辺を取り巻くeslintやstylelintなどは、古のwebpackの時代から使われてきた技術が使われており、依存関係がぐちゃぐちゃになっています。事実、eslintが9になったときは、設定ファイルの仕様が大幅に変わりトラブルが多発しました。今でもeslint8から移行できないプロジェクトも多いんじゃないでしょうか？\n\n一応、自分でも[ol-slmap](https://github.com/logue/ol-slmap)のような、vueのようなライブラリを含まない純粋なJavaScriptの新規プロジェクトでも部分的に使用していましたが、先月（2026/04/22）にRspack2.0がリリースされたことを受け、改めて調べてみると整形ツールのrslintやテストツールのrstest、ライブラリ向けビルドができるrslibなど独自にエコシステムが出来上がっているのを確認したため、使ってみることにしました。\n\n特筆するべき点として、公式のRsbuildのスターターテンプレートは、最初からAI駆動設計のためのスキルや指示書が含まれており、バイブコーディング時のガードレールが予め備わっているところがあります。\n\nここが決め手となりました。\n\n*何でもできるということは「自由度が高い」ではなく、「どうとでもなってしまう」こと*ですしね。\n\n# RsbuildとViteの違い\n\nRsbuildとViteの違いをざっと箇条書きにしました。\n\n## 1. 根本的な設計思想の違い\n\n|項目|Vite|Rsbuild|\n|--|--|--|\n|コアエンジン|esbuild (Dev) + Rollup (Build)Rspack（全フェーズで統一）言語Go (esbuild) + JavaScript (Rollup)|Rust（Rspack） + TypeScript|\n|Dev / Prodの一貫性|低い（DevはESM、ProdはBundling）|非常に高い（両方Rspack）|\n|哲学|「可能な限りバンドルしない」|高速Dev体験「Rustで全部高速化」＋Webpack互換|\n\n## 2. 開発モード（Dev Server）の違い\n\nVite:\n\n- Native ESM + on-demand compilation が最大の特徴\n- ブラウザが直接ESMを読み込む（`\u003cscript type=\"module\"\u003e`）\n- 依存関係のPre-bundlingにesbuildを使用（非常に高速）\n- ソースコードの変換は必要最小限のみesbuildで行い、残りはブラウザに任せる\n- HMRは非常に速いが、DevとProdの出力に差が出やすい（特にコード分割や最適化）\n\nRsbuild (Rspack):\n\n- Devでもフルバンドリングに近いアプローチ（Rspackが処理）\n- Rust並列処理により、大規模プロジェクトでもHMRが極めて高速\n- DevとProdで同じエンジンを使うため、動作の一貫性が非常に高い\n- プラグインやLoaderもWebpack/Rspack互換で動く\n\n## 3. 本番ビルド（Production Build）の違い\n\nVite:\n\n- Rollupをベースにバンドリング\n- Rollupの優れたTree-shaking、コード分割、チャンク制御が強み。[^1]\n\n\nRsbuild:\n\n- Rspackで一貫して処理\n- RspackはWebpackのAPIをRustで再実装したもの\n- Webpackエコシステム（Loader/Plugin）の多くがそのまま使える\n- 並列処理とRustの速度で、特に大規模プロジェクトでViteを上回るビルド速度を出すケースが多い\n\n## 4. プラグイン・エコシステムの違い\n\nVite: \n- Rollup互換プラグイン + Vite独自拡張。エコシステムが非常に成熟\n\nRsbuild:\n\n- Webpack/Rspack互換（ほとんどのwebpack pluginが動く） + Rsbuild独自のシンプルなPlugin API\n- 既存のWebpackプロジェクトからの移行が比較的楽\n\n\n## 5. 内部的な処理の流れ（簡略化）\n\nVite:\n\n1. Devサーバー起動\n2. 依存Pre-bundling（esbuild）\n3. ブラウザからリクエスト → 必要なファイルだけesbuildでtransform\n4. Production: Rollupでフルバンドル\n\nRsbuild:\n\n1. RspackがDev/Prod両方で動作\n2. Rust並列処理でLoader/Pluginを実行\n3. 統一されたPipelineで処理（Transformer → Optimizer → Bundler）\n\n## まとめ：どちらが優れているか？\n\n- 開発体験・シンプルさ → Viteがまだ優勢（エコシステムの成熟度）\n- パフォーマンス（特に大規模）・一貫性 → Rsbuild（Rspack）が優勢\n- Webpack資産の活用 → Rsbuildが圧倒的に有利\n- 将来的 → ViteはRolldownでRust統一方向、RsbuildはすでにRust中心\n\n# 実際の設定\n\nvue-vrmのリポジトリのソースコードは、デモプログラムと、ライブラリのコード両方が`src/`ディレクトリに入っているあたり、あまりいいディレクトリ構造ではありません。ここはおいおい直すとします。\n\n[rsbuild](https://rsbuild.rs/)でデモプログラムのビルドを行い、[rslib](https://rslib.rs/)でライブラリのビルドを行っています。\n\nデモプログラム部分の設定です。こちらは、rsbuildを用いてビルドします。出力されるコードは圧縮済みでwebpackの出力した圧縮コードに酷似したコードが出力されます。`src/meta.ts`にビルド日時とバージョン情報を書き込むための定数はここで定義しています。\n\n```ts:rsbuild.config.ts\nimport { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nconst packageJson = JSON.parse(readFileSync('./package.json', 'utf-8'));\nconst buildDate = new Date().toISOString();\n\nconsole.log('Injected version:', packageJson.version);\nconsole.log('Injected build date:', buildDate);\n\nimport { defineConfig } from '@rsbuild/core';\nimport { pluginVue } from '@rsbuild/plugin-vue';\n\n// Docs: https://rsbuild.rs/config/\n// Demo build configuration - for library build, see rslib.config.ts\nexport default defineConfig({\n  plugins: [pluginVue()],\n  source: {\n    define: {\n      __DEMO_BUILD__: JSON.stringify(true),\n      __APP_VERSION__: JSON.stringify(packageJson.version),\n      __BUILD_DATE__: JSON.stringify(buildDate)\n    },\n    entry: {\n      index: './src/index.ts'\n    }\n  },\n  output: {\n    distPath: 'docs',\n    assetPrefix: './',\n    filenameHash: true,\n    copy: [\n      {\n        from: './src/assets',\n        to: 'assets'\n      }\n    ]\n  },\n  html: {\n    template: './src/index.html',\n    title: 'VRM Viewer Demo - Vue VRM'\n  },\n  tools: {\n    htmlPlugin: undefined\n  },\n  resolve: {\n    alias: {\n      '@': resolve(__dirname, 'src')\n    }\n  }\n});\n```\n\nライブラリ部分の設定です。こちらはrslibを使ってビルドしています。出力されるコードは圧縮済みのコードとなります。\n\n```ts:rslib.config.ts\nimport { readFileSync } from 'node:fs';\n\nimport { pluginVue } from '@rsbuild/plugin-vue';\nimport { defineConfig } from '@rslib/core';\n\nconst pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) as {\n  name: string;\n  description: string;\n  author: {\n    name: string;\n    email: string;\n  };\n  license: string;\n  version: string;\n  homepage: string;\n};\n\nconst buildDate = new Date().toISOString();\n\nexport default defineConfig({\n  lib: [\n    {\n      // Modern ESM build for modern bundlers and environments\n      format: 'esm',\n      dts: false,\n      syntax: 'esnext',\n    },\n    {\n      // Legacy CommonJS build for Node.js and older bundlers\n      format: 'cjs',\n      dts: false,\n      syntax: 'es2015',\n    },\n    {\n      banner: {\n        js: `/**\n * ${pkg.name}\n *\n * @description ${pkg.description}\n * @author ${pkg.author.name} \u003c${pkg.author.email}\u003e\n * @copyright 2026 By Masashi Yoshikawa All rights reserved.\n * @license ${pkg.license}\n * @version ${pkg.version}\n * @see {@link ${pkg.homepage}}\n */\n`,\n      },\n    },\n  ],\n  plugins: [pluginVue()],\n  source: {\n    entry: {\n      index: './src/lib.ts',\n    },\n    define: {\n      __APP_VERSION__: JSON.stringify(pkg.version),\n      __BUILD_DATE__: JSON.stringify(buildDate),\n    },\n  },\n  output: {\n    target: 'web',\n    minify: true,\n    distPath: {\n      root: './dist',\n    },\n  },\n});\n```\n\n# 実際のテンプレート\n\nVue向けにRsbuildを使用したスターターテンプレートを作成しました。これで色々テストしてみてください。\n\n\u003chttps://github.com/logue/rsbuild-vue3-ts-starter\u003e\n\n日本語ドキュメントは[こちら](https://github.com/logue/rsbuild-vue3-ts-starter/blob/master/README.ja.md)になります。\n\n[^1]: （2026年現在）Rolldown（Rust版Rollup）への移行が進んでおり、徐々にRust統一されつつある。\n","coediting":false,"comments_count":0,"created_at":"2026-05-25T17:14:33+09:00","group":null,"id":"e191ed56e922b33e4c8f","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vue.js","versions":[]},{"name":"Rspack","versions":[]},{"name":"rsbuild","versions":[]},{"name":"rslint","versions":[]},{"name":"rslib","versions":[]}],"title":"なぜRsbuidスタックなのか？","updated_at":"2026-05-25T17:28:15+09:00","url":"https://qiita.com/logue/items/e191ed56e922b33e4c8f","user":{"description":"何でもできるということは、「自由度が高い」ではなく、「どうとでもなってしまう」ことである。\r\n\r\n設計図を読んだり、道具を揃えたり、生成ＡＩに解析させたからといって、同じ物が作れるとは限らない（ｗ\r\nそれくらいのことは、わかっているよな？\r\n\r\n客観性を欠いた想像はただの妄想に等しいのだ。","facebook_id":"logue256","followees_count":1,"followers_count":6,"github_login_name":"logue","id":"logue","items_count":19,"linkedin_id":"logue","location":"東京都","name":"Masashi Yoshikawa","organization":"403 Forbidden Colors","permanent_id":15020,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15020/profile-images/1775798799","team_only":false,"twitter_screen_name":"logue256","website_url":"https://logue.dev"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ciframe width=\"100%\" height=\"315\" src=\"https://www.youtube.com/embed/JAzlz17LpUI?si=Pdy1sJnJzsEjsdSA\" frameborder=\"0\" allowfullscreen\u003e\u003c/iframe\u003e\n\u003cp data-sourcepos=\"3:1-3:126\"\u003e本プロジェクトはVue向けのVRMアバター表示コンポーネントを開発したことに関する資料です。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-7:35\"\u003eNPM: \u003ca href=\"https://www.npmjs.com/package/vue-vrm\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://www.npmjs.com/package/vue-vrm\u003c/a\u003e\u003cbr\u003e\nGithub: \u003ca href=\"https://github.com/logue/vue-vrm\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://github.com/logue/vue-vrm\u003c/a\u003e\u003cbr\u003e\nデモ: \u003ca href=\"https://logue.dev/vue-vrm\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://logue.dev/vue-vrm\u003c/a\u003e\u003c/p\u003e\n\u003ch1 data-sourcepos=\"9:1-9:51\"\u003e\n\u003cspan id=\"1-ビルドスタックなぜrsbuildなのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E3%83%93%E3%83%AB%E3%83%89%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AA%E3%81%9Crsbuild%E3%81%AA%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. ビルドスタック：なぜRsbuildなのか\u003c/h1\u003e\n\u003cp data-sourcepos=\"11:1-11:95\"\u003e👉️\u003ca href=\"https://qiita.com/logue/items/e191ed56e922b33e4c8f\" id=\"reference-d30072e202f19b63891f\"\u003eなぜRsbuildスタックなのか\u003c/a\u003e\u003c/p\u003e\n\u003ch1 data-sourcepos=\"13:1-13:54\"\u003e\n\u003cspan id=\"2-ライセンスの防波堤arraybuffer設計\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%83%A9%E3%82%A4%E3%82%BB%E3%83%B3%E3%82%B9%E3%81%AE%E9%98%B2%E6%B3%A2%E5%A0%A4arraybuffer%E8%A8%AD%E8%A8%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. ライセンスの防波堤：\u003ccode\u003eArrayBuffer\u003c/code\u003e設計\u003c/h1\u003e\n\u003cp data-sourcepos=\"15:1-15:107\"\u003eVRMやVRMAを使う上で見落としがちなのは、\u003cstrong\u003eアセットのライセンスの問題\u003c/strong\u003eです。\u003c/p\u003e\n\u003cp data-sourcepos=\"17:1-17:347\"\u003e例えば\u003ca href=\"https://vroid.com/studio\" rel=\"nofollow noopener\" target=\"_blank\"\u003eVRoid Studio\u003c/a\u003eでアバターを作るときに、\u003ca href=\"https://booth.pm/ja/browse/VRoid\" rel=\"nofollow noopener\" target=\"_blank\"\u003eBooth\u003c/a\u003eや\u003ca href=\"https://www.etsy.com/jp/search?q=vroid\" rel=\"nofollow noopener\" target=\"_blank\"\u003eEtsy\u003c/a\u003e、\u003ca href=\"https://commons.nicovideo.jp/search?keywords=vrm\" rel=\"nofollow noopener\" target=\"_blank\"\u003eニコニ・コモンズ\u003c/a\u003eで配布／販売されているアセットやアバターを使っていたとします。\u003c/p\u003e\n\u003cp data-sourcepos=\"19:1-19:110\"\u003eこれを画像の\u003ccode\u003esrc\u003c/code\u003eのようにURL指定で受け取ったらどういうことになるでしょうか？\u003c/p\u003e\n\u003cp data-sourcepos=\"21:1-21:144\"\u003eブラウザの開発バーのネットワークタブを見ると、どこから何のファイルを取得したかのログが残ります。\u003c/p\u003e\n\u003cp data-sourcepos=\"23:1-23:426\"\u003eたとえ、そのアバターが自分に著作権があっても、そこで使用している第三者のアセットを使用している場合、\u003cstrong\u003e多くのアセットは再配布を禁じている\u003c/strong\u003eので、VRMやVRMAを画像と同じような感覚で扱うと、ブラウザのコンソールログからアバターのURLが漏れて、\u003cstrong\u003e意図せずとも再配布している\u003c/strong\u003e状態になってしまいます。\u003c/p\u003e\n\u003cp data-sourcepos=\"25:1-25:223\"\u003eVRMという規格自体にもメタ情報でライセンスを縛る仕組みがありますが、glTFという規格のサブセットという性質上、紳士協定に近く簡単に中身が見えてしまいます。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"27:1-29:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"28:1-28:279\"\u003eつまるところ、VRMアバターやVRMAを\u003cstrong\u003eURL指定で読み込むという実装は、簡単にダウンロードできる状態\u003c/strong\u003eということになり、もしもそこに第三者のアセットが含まれていた場合、\u003cstrong\u003eライセンス違反\u003c/strong\u003eになります。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"31:1-31:261\"\u003eそこで、Amazon S3やCloudflare R2のようなオブジェクトストレージにVRMやVRMAファイルを置き、Cloudflare Workersのようなワーカー使ってAPI経由でデータを\u003ccode\u003eArrayBuffer\u003c/code\u003e形式で受け取る実装を想定しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"33:1-33:264\"\u003eこうすることで、通信はオブジェクトストレージとAPI間のみになり、ブラウザに流れてくる情報は\u003ccode\u003eArrayBuffer\u003c/code\u003eとなるので、\u003cstrong\u003e開発バーに通信ログが残らず\u003c/strong\u003e安全にVRMやVRMAを使用することができます。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"35:1-35:20\"\u003e\n\u003cspan id=\"3-実装のキモ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E5%AE%9F%E8%A3%85%E3%81%AE%E3%82%AD%E3%83%A2\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 実装のキモ\u003c/h1\u003e\n\u003cp data-sourcepos=\"37:1-37:120\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F15020%2F2c918992-8d55-46c0-90c0-7238e48f3e7b.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b09a4b4801308fcc8fb49780887614be\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F15020%2F2c918992-8d55-46c0-90c0-7238e48f3e7b.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b09a4b4801308fcc8fb49780887614be\" alt=\"image.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F15020%2F2c918992-8d55-46c0-90c0-7238e48f3e7b.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=ed30ec8f295098e866934e89a414bd6f 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15020/2c918992-8d55-46c0-90c0-7238e48f3e7b.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"40:1-40:458\"\u003eまず、このVueコンポーネントは、\u003cstrong\u003eVRMアバターの表示に特化したものである\u003c/strong\u003eという点です。主に小規模フォーラムのユーザのアイコン代わりの３Dアバター、音声AIチャットの依代としての運用を想定しています。HTML文法に近いVueの手軽さと組み合わせて、タグを埋めこむだけでアバターが表示されるようにするというのがコンセプトです。\u003c/p\u003e\n\u003cp data-sourcepos=\"42:1-42:649\"\u003e\u003ca href=\"https://threejs.org/\" rel=\"nofollow noopener\" target=\"_blank\"\u003ethree.js\u003c/a\u003eはcanvasに描画する性質上、画面のサイズに追従しません。画面サイズが変わったときに、アスペクト比を保持しつつcanvasタグをリサイズさせ、再描画する処理は自力で書く必要があります。このためのイベント処理は、\u003ca href=\"https://developer.mozilla.org/ja/docs/Web/API/ResizeObserver\" rel=\"nofollow noopener\" target=\"_blank\"\u003e\u003ccode\u003eResizeObserver\u003c/code\u003e\u003c/a\u003eを使っています。一昔前までは\u003ccode\u003esetTimeout\u003c/code\u003e関数でスレッドを回して監視していましたが、この関数を使うとリサイズされたタイミングでイベントが発火するので処理が軽くなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"44:1-44:329\"\u003eまた、アスペクト比を入れるプロップ（デフォルトでは３：４）も用意しているため、ウィンドウのサイズを変えてもアバターが途切れるという現象を予め起きないようにしています。画面サイズ追従は\u003cem\u003eレスポンシブデザインの基本\u003c/em\u003eですしね。\u003c/p\u003e\n\u003cp data-sourcepos=\"46:1-46:547\"\u003eこれは00年代のマビノギの公式掲示板から着想を得ましたが、アバターにインタラクトできるプロップも用意しました。ズーム、パン（カメラの上下左右移動）に加えて、3D空間におけるカメラの3軸回転――すなわち、\u003cstrong\u003eピッチ（首を縦に振る）、ヨー（左右を見回す）、ロール（首をかしげる）、チルト（首を傾ける）\u003c/strong\u003e といった基本操作を、それぞれオプションで直感的に設定できるようにしています。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"48:1-48:23\"\u003e\n\u003cspan id=\"4-実際の使用例\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E5%AE%9F%E9%9A%9B%E3%81%AE%E4%BD%BF%E7%94%A8%E4%BE%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. 実際の使用例\u003c/h1\u003e\n\u003cp data-sourcepos=\"50:1-50:106\"\u003eまず、本コンポーネントとthreejs、vrm関連のライブラリをインストールします。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"52:1-54:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003epnpm add vue-vrm vue three @pixiv/three-vrm @pixiv/three-vrm-animation\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"56:1-56:193\"\u003eVue側のコードは下記のように記述します。このコードではファイルフォームにvrmファイルを入れると、そのアバターがTポーズで表示されます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"58:1-78:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e \u003cspan class=\"na\"\u003elang=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"ts\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eVrmCanvas\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003evue-vrm\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003emodelData\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nb\"\u003eArrayBuffer\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eonModelFile\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eevent\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e \u003cspan class=\"nb\"\u003ePromise\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003evoid\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003efile\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eevent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etarget\u003c/span\u003e \u003cspan class=\"nx\"\u003eas\u003c/span\u003e \u003cspan class=\"nx\"\u003eHTMLInputElement\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nx\"\u003efiles\u003c/span\u003e\u003cspan class=\"p\"\u003e?.[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003efile\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003emodelData\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003efile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003earrayBuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;div\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;input\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"file\"\u003c/span\u003e \u003cspan class=\"na\"\u003eaccept=\u003c/span\u003e\u003cspan class=\"s\"\u003e\".vrm,.glb\"\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003echange=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"onModelFile\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;VrmCanvas\u003c/span\u003e \u003cspan class=\"na\"\u003e:model-data=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"modelData\"\u003c/span\u003e \u003cspan class=\"na\"\u003e:show-grid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"true\"\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"80:1-80:155\"\u003eより詳しい情報は、\u003ca href=\"https://github.com/logue/vue-vrm/blob/master/README.ja.md\" rel=\"nofollow noopener\" target=\"_blank\"\u003evue-vrmの日本語公式ドキュメント\u003c/a\u003eを見てください。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"82:1-82:126\"\u003e\n\u003cspan id=\"5-バックエンドの防壁cloudflare-workersによるバイナリ配信\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89%E3%81%AE%E9%98%B2%E5%A3%81cloudflare-workers%E3%81%AB%E3%82%88%E3%82%8B%E3%83%90%E3%82%A4%E3%83%8A%E3%83%AA%E9%85%8D%E4%BF%A1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. バックエンドの防壁：\u003ca href=\"https://developers.cloudflare.com/workers/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eCloudflare Workers\u003c/a\u003eによるバイナリ配信\u003c/h1\u003e\n\u003cp data-sourcepos=\"84:1-84:161\"\u003e第2章で述べた「\u003ccode\u003eArrayBuffer\u003c/code\u003e設計」を成立させるためには、アセット（VRM）を安全に隠すバックエンド（API）が必要です。\u003c/p\u003e\n\u003cp data-sourcepos=\"86:1-86:406\"\u003e「Cloudflare Workersとかややこしそう」「インフラの設定面倒くさい」と思うかもしれませんが、やることはシンプルです。\u003ca href=\"https://aws.amazon.com/jp/s3/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAmazon S3\u003c/a\u003eや\u003ca href=\"https://developers.cloudflare.com/r2/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eCloudflare R2\u003c/a\u003eにアバターを格納し、Workersを経由させてブラウザに「生のバイナリ（Streaming）」として流し込むだけです。\u003c/p\u003e\n\u003cp data-sourcepos=\"88:1-88:187\"\u003eこれによって、フロントエンドのネットワークタブにはAPIのURLしか残らず、アセットの直リンク流出（意図せぬ再配布）を100%遮断します。\u003c/p\u003e\n\u003cp data-sourcepos=\"90:1-90:201\"\u003e以下に、\u003ca href=\"https://developers.cloudflare.com/workers/wrangler/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eWrangler\u003c/a\u003e（Wrangler v3以降）で即座にデプロイできる最小限のWorkerコード（TypeScript）を置いておきます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"92:1-123:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003eindex.ts\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// JSDocはAIへの指示書であり、未来の自分への仕様書である。\u003c/span\u003e\n\u003cspan class=\"kr\"\u003einterface\u003c/span\u003e \u003cspan class=\"nx\"\u003eEnv\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eBUCKET\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eR2Bucket\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// あなたのR2バケット\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"nf\"\u003efetch\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"na\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003eenv\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eEnv\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e \u003cspan class=\"nb\"\u003ePromise\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eResponse\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eurl\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eURL\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ekey\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epathname\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eslice\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// URLからファイル名を取得\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e// 1. レガシーな世界からの不正アクセスを弾く最低限のバリデーション\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eendsWith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e.vrm\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eendsWith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e.vrma\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eInvalid Asset Type\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e400\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n    \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eobject\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003eenv\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eBUCKET\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nx\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eAsset Not Found\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e404\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n    \u003cspan class=\"c1\"\u003e// 2. フロントエンドのArrayBuffer通信を許可するCORSヘッダーのコンパイル\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eHeaders\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ewriteHttpMetadata\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eAccess-Control-Allow-Origin\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e[https://your-frontend-domain.com](https://your-frontend-domain.com)\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eAccess-Control-Allow-Methods\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eGET\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \n    \u003cspan class=\"c1\"\u003e// 3. バイナリをストリーミングとして安全に出力\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eheaders\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"125:1-125:164\"\u003eこのあたりの実装はかなりややこしいので、下記ページのドキュメントを読みつつ、生成AIを活用して作業してください。\u003c/p\u003e\n\u003cul data-sourcepos=\"127:1-129:0\"\u003e\n\u003cli data-sourcepos=\"127:1-127:79\"\u003e\u003ca href=\"https://github.com/logue/vue-vrm/blob/master/examples/cloudflare-workers.md\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://github.com/logue/vue-vrm/blob/master/examples/cloudflare-workers.md\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"128:1-129:0\"\u003e\u003ca href=\"https://github.com/logue/vue-vrm/blob/master/examples/vroid-hub-api.md\" rel=\"nofollow noopener\" target=\"_blank\"\u003ehttps://github.com/logue/vue-vrm/blob/master/examples/vroid-hub-api.md\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 data-sourcepos=\"130:1-130:20\"\u003e\n\u003cspan id=\"6-今後の展望\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6-%E4%BB%8A%E5%BE%8C%E3%81%AE%E5%B1%95%E6%9C%9B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. 今後の展望\u003c/h1\u003e\n\u003cp data-sourcepos=\"132:1-132:262\"\u003e現在、VRMファイルを読み込みアバターをVueコンポーネントで表示し、そこにVRMA形式のアニメを反映するところと、カメラの位置や、ライティングの設定など最小限のことしか実装していません。\u003c/p\u003e\n\u003cp data-sourcepos=\"134:1-134:286\"\u003e\u003ccode\u003ethree.js\u003c/code\u003eのインスタンスや、\u003ccode\u003e@pixiv/vrm\u003c/code\u003eのインスタンスへ直接アクセスできる設計にしていますが、まだまだ発展途上です。今後の\u003ca href=\"https://github.com/logue/vue-vrm/issues\" rel=\"nofollow noopener\" target=\"_blank\"\u003eIssue\u003c/a\u003eなどの様子を見て実装していきたいと思います。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"136:1-136:73\"\u003e\n\u003cspan id=\"7-おわりに単に行儀の良いコードならaiでも書ける\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#7-%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB%E5%8D%98%E3%81%AB%E8%A1%8C%E5%84%80%E3%81%AE%E8%89%AF%E3%81%84%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AA%E3%82%89ai%E3%81%A7%E3%82%82%E6%9B%B8%E3%81%91%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e7. おわりに：単に行儀の良いコードならAIでも書ける\u003c/h1\u003e\n\u003cp data-sourcepos=\"138:1-138:384\"\u003eこのRsbuild×Vue3×VRMという尖ったスタックを組んでいる最中、生成AIにコードのアシストを頼んでみたところ、AIはRsbuildのピュアな美しさが理解できず、途中で脳死のViteを混ぜてきたり、RslintがあるのにESLintをインポートしようとしてハングアップ（コンパイルエラー）を起こしました。\u003c/p\u003e\n\u003cp data-sourcepos=\"140:1-140:332\"\u003eAIは「世の中の凡庸なコピペコード（What）」を真似ることは得意ですが、「\u003cstrong\u003eなぜこの依存関係を引き剥がし、アセットのライセンスを守るために\u003ccode\u003eArrayBuffer\u003c/code\u003eで縛るのか\u003c/strong\u003e」という、人間の生々しいコンテキスト（Why）を理解した設計はできません。\u003c/p\u003e\n\u003cp data-sourcepos=\"142:1-142:350\"\u003eこれは、フリーランス市場で「技術選定の立場」を求めている人間に対して、「Reactの経験年数は？」と聞いてきたり、「フリーランスがダメなら正社員になれば？」と的外れなバグを吐き出す、どこぞの仲介エージェントの処理能力の低さに酷似しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"144:1-144:210\"\u003e単に行儀が良く、誰の感情も逆撫でしない代わりに、脆弱性だらけでパッと見で意図のわからない負債コードを量産する時代は、もう終わりにしましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"146:1-146:181\"\u003e本ライブラリ（vue-vrm）が、あなたのアバターを、美しく安全にインターネットへデプロイするための堅牢な防波堤になれば幸いです。\u003c/p\u003e\n","body":"\u003ciframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/JAzlz17LpUI?si=Pdy1sJnJzsEjsdSA\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen\u003e\u003c/iframe\u003e\n\n本プロジェクトはVue向けのVRMアバター表示コンポーネントを開発したことに関する資料です。\n\nNPM: \u003chttps://www.npmjs.com/package/vue-vrm\u003e\nGithub: \u003chttps://github.com/logue/vue-vrm\u003e\nデモ: \u003chttps://logue.dev/vue-vrm\u003e\n\n# 1. ビルドスタック：なぜRsbuildなのか\n\n👉️[なぜRsbuildスタックなのか](https://qiita.com/logue/items/e191ed56e922b33e4c8f)\n\n# 2. ライセンスの防波堤：`ArrayBuffer`設計\n\nVRMやVRMAを使う上で見落としがちなのは、**アセットのライセンスの問題**です。\n\n例えば[VRoid Studio](https://vroid.com/studio)でアバターを作るときに、[Booth](https://booth.pm/ja/browse/VRoid)や[Etsy](https://www.etsy.com/jp/search?q=vroid)、[ニコニ・コモンズ](https://commons.nicovideo.jp/search?keywords=vrm)で配布／販売されているアセットやアバターを使っていたとします。\n\nこれを画像の`src`のようにURL指定で受け取ったらどういうことになるでしょうか？\n\nブラウザの開発バーのネットワークタブを見ると、どこから何のファイルを取得したかのログが残ります。\n\nたとえ、そのアバターが自分に著作権があっても、そこで使用している第三者のアセットを使用している場合、**多くのアセットは再配布を禁じている**ので、VRMやVRMAを画像と同じような感覚で扱うと、ブラウザのコンソールログからアバターのURLが漏れて、**意図せずとも再配布している**状態になってしまいます。\n\nVRMという規格自体にもメタ情報でライセンスを縛る仕組みがありますが、glTFという規格のサブセットという性質上、紳士協定に近く簡単に中身が見えてしまいます。\n\n:::note warn\nつまるところ、VRMアバターやVRMAを**URL指定で読み込むという実装は、簡単にダウンロードできる状態**ということになり、もしもそこに第三者のアセットが含まれていた場合、**ライセンス違反**になります。\n:::\n\nそこで、Amazon S3やCloudflare R2のようなオブジェクトストレージにVRMやVRMAファイルを置き、Cloudflare Workersのようなワーカー使ってAPI経由でデータを`ArrayBuffer`形式で受け取る実装を想定しています。\n\nこうすることで、通信はオブジェクトストレージとAPI間のみになり、ブラウザに流れてくる情報は`ArrayBuffer`となるので、**開発バーに通信ログが残らず**安全にVRMやVRMAを使用することができます。\n\n# 3. 実装のキモ\n\n![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15020/2c918992-8d55-46c0-90c0-7238e48f3e7b.png)\n\n\nまず、このVueコンポーネントは、**VRMアバターの表示に特化したものである**という点です。主に小規模フォーラムのユーザのアイコン代わりの３Dアバター、音声AIチャットの依代としての運用を想定しています。HTML文法に近いVueの手軽さと組み合わせて、タグを埋めこむだけでアバターが表示されるようにするというのがコンセプトです。\n\n[three.js](https://threejs.org/)はcanvasに描画する性質上、画面のサイズに追従しません。画面サイズが変わったときに、アスペクト比を保持しつつcanvasタグをリサイズさせ、再描画する処理は自力で書く必要があります。このためのイベント処理は、[`ResizeObserver`](https://developer.mozilla.org/ja/docs/Web/API/ResizeObserver)を使っています。一昔前までは`setTimeout`関数でスレッドを回して監視していましたが、この関数を使うとリサイズされたタイミングでイベントが発火するので処理が軽くなります。\n\nまた、アスペクト比を入れるプロップ（デフォルトでは３：４）も用意しているため、ウィンドウのサイズを変えてもアバターが途切れるという現象を予め起きないようにしています。画面サイズ追従は*レスポンシブデザインの基本*ですしね。\n\nこれは00年代のマビノギの公式掲示板から着想を得ましたが、アバターにインタラクトできるプロップも用意しました。ズーム、パン（カメラの上下左右移動）に加えて、3D空間におけるカメラの3軸回転――すなわち、**ピッチ（首を縦に振る）、ヨー（左右を見回す）、ロール（首をかしげる）、チルト（首を傾ける）** といった基本操作を、それぞれオプションで直感的に設定できるようにしています。\n\n# 4. 実際の使用例\n\nまず、本コンポーネントとthreejs、vrm関連のライブラリをインストールします。\n\n```zsh\npnpm add vue-vrm vue three @pixiv/three-vrm @pixiv/three-vrm-animation\n```\n\nVue側のコードは下記のように記述します。このコードではファイルフォームにvrmファイルを入れると、そのアバターがTポーズで表示されます。\n\n```vue\n\u003cscript setup lang=\"ts\"\u003e\nimport { ref } from 'vue';\nimport { VrmCanvas } from 'vue-vrm';\n\nconst modelData = ref\u003cArrayBuffer | null\u003e(null);\n\nasync function onModelFile(event: Event): Promise\u003cvoid\u003e {\n  const file = (event.target as HTMLInputElement).files?.[0];\n  if (!file) return;\n  modelData.value = await file.arrayBuffer();\n}\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cdiv\u003e\n    \u003cinput type=\"file\" accept=\".vrm,.glb\" @change=\"onModelFile\" /\u003e\n    \u003cVrmCanvas :model-data=\"modelData\" :show-grid=\"true\" /\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n\nより詳しい情報は、[vue-vrmの日本語公式ドキュメント](https://github.com/logue/vue-vrm/blob/master/README.ja.md)を見てください。\n\n# 5. バックエンドの防壁：[Cloudflare Workers](https://developers.cloudflare.com/workers/)によるバイナリ配信\n\n第2章で述べた「`ArrayBuffer`設計」を成立させるためには、アセット（VRM）を安全に隠すバックエンド（API）が必要です。\n\n「Cloudflare Workersとかややこしそう」「インフラの設定面倒くさい」と思うかもしれませんが、やることはシンプルです。[Amazon S3](https://aws.amazon.com/jp/s3/)や[Cloudflare R2](https://developers.cloudflare.com/r2/)にアバターを格納し、Workersを経由させてブラウザに「生のバイナリ（Streaming）」として流し込むだけです。\n\nこれによって、フロントエンドのネットワークタブにはAPIのURLしか残らず、アセットの直リンク流出（意図せぬ再配布）を100%遮断します。\n\n以下に、[Wrangler](https://developers.cloudflare.com/workers/wrangler/)（Wrangler v3以降）で即座にデプロイできる最小限のWorkerコード（TypeScript）を置いておきます。\n\n```typescript:index.ts\n// JSDocはAIへの指示書であり、未来の自分への仕様書である。\ninterface Env {\n  BUCKET: R2Bucket; // あなたのR2バケット\n}\n\nexport default {\n  async fetch(request: Request, env: Env): Promise\u003cResponse\u003e {\n    const url = new URL(request.url);\n    const key = url.pathname.slice(1); // URLからファイル名を取得\n\n    // 1. レガシーな世界からの不正アクセスを弾く最低限のバリデーション\n    if (!key.endsWith('.vrm') \u0026\u0026 !key.endsWith('.vrma')) {\n      return new Response('Invalid Asset Type', { status: 400 });\n    }\n\n    const object = await env.BUCKET.get(key);\n    if (!object) {\n      return new Response('Asset Not Found', { status: 404 });\n    }\n\n    // 2. フロントエンドのArrayBuffer通信を許可するCORSヘッダーのコンパイル\n    const headers = new Headers();\n    object.writeHttpMetadata(headers);\n    headers.set('Access-Control-Allow-Origin', '[https://your-frontend-domain.com](https://your-frontend-domain.com)');\n    headers.set('Access-Control-Allow-Methods', 'GET');\n    \n    // 3. バイナリをストリーミングとして安全に出力\n    return new Response(object.body, { headers });\n  },\n};\n```\n\nこのあたりの実装はかなりややこしいので、下記ページのドキュメントを読みつつ、生成AIを活用して作業してください。\n\n- \u003chttps://github.com/logue/vue-vrm/blob/master/examples/cloudflare-workers.md\u003e\n- \u003chttps://github.com/logue/vue-vrm/blob/master/examples/vroid-hub-api.md\u003e\n\n# 6. 今後の展望\n\n現在、VRMファイルを読み込みアバターをVueコンポーネントで表示し、そこにVRMA形式のアニメを反映するところと、カメラの位置や、ライティングの設定など最小限のことしか実装していません。\n\n`three.js`のインスタンスや、`@pixiv/vrm`のインスタンスへ直接アクセスできる設計にしていますが、まだまだ発展途上です。今後の[Issue](https://github.com/logue/vue-vrm/issues)などの様子を見て実装していきたいと思います。\n\n# 7. おわりに：単に行儀の良いコードならAIでも書ける\n\nこのRsbuild×Vue3×VRMという尖ったスタックを組んでいる最中、生成AIにコードのアシストを頼んでみたところ、AIはRsbuildのピュアな美しさが理解できず、途中で脳死のViteを混ぜてきたり、RslintがあるのにESLintをインポートしようとしてハングアップ（コンパイルエラー）を起こしました。\n\nAIは「世の中の凡庸なコピペコード（What）」を真似ることは得意ですが、「**なぜこの依存関係を引き剥がし、アセットのライセンスを守るために`ArrayBuffer`で縛るのか**」という、人間の生々しいコンテキスト（Why）を理解した設計はできません。\n\nこれは、フリーランス市場で「技術選定の立場」を求めている人間に対して、「Reactの経験年数は？」と聞いてきたり、「フリーランスがダメなら正社員になれば？」と的外れなバグを吐き出す、どこぞの仲介エージェントの処理能力の低さに酷似しています。\n\n単に行儀が良く、誰の感情も逆撫でしない代わりに、脆弱性だらけでパッと見で意図のわからない負債コードを量産する時代は、もう終わりにしましょう。\n\n本ライブラリ（vue-vrm）が、あなたのアバターを、美しく安全にインターネットへデプロイするための堅牢な防波堤になれば幸いです。\n","coediting":false,"comments_count":0,"created_at":"2026-05-25T14:20:42+09:00","group":null,"id":"21bb239fed2c4f4a3b2f","likes_count":0,"private":false,"reactions_count":0,"stocks_count":1,"tags":[{"name":"three.js","versions":[]},{"name":"Vue.js","versions":[]},{"name":"VRM","versions":[]},{"name":"VRMA","versions":[]},{"name":"Rspack","versions":[]}],"title":"Rsbuildで組むVueのVRMコンポーネント。ライセンスと配信の安全性を追求したライブラリ設計","updated_at":"2026-05-28T13:19:36+09:00","url":"https://qiita.com/logue/items/21bb239fed2c4f4a3b2f","user":{"description":"何でもできるということは、「自由度が高い」ではなく、「どうとでもなってしまう」ことである。\r\n\r\n設計図を読んだり、道具を揃えたり、生成ＡＩに解析させたからといって、同じ物が作れるとは限らない（ｗ\r\nそれくらいのことは、わかっているよな？\r\n\r\n客観性を欠いた想像はただの妄想に等しいのだ。","facebook_id":"logue256","followees_count":1,"followers_count":6,"github_login_name":"logue","id":"logue","items_count":19,"linkedin_id":"logue","location":"東京都","name":"Masashi Yoshikawa","organization":"403 Forbidden Colors","permanent_id":15020,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15020/profile-images/1775798799","team_only":false,"twitter_screen_name":"logue256","website_url":"https://logue.dev"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"2:1-2:21\"\u003e\n\u003cspan id=\"結論先に\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%B5%90%E8%AB%96%E5%85%88%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e結論（先に）\u003c/h2\u003e\n\u003cp data-sourcepos=\"4:1-4:33\"\u003evue-i18n の初期ロケールを\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"6:1-8:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003elocale\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003elocalStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003elocale\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eja\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"10:1-10:360\"\u003eのように「保存が無ければ 'ja'」で決めていると、\u003cstrong\u003eOS言語を一切見ずに常に日本語で起動\u003c/strong\u003eします。App Store のプライマリ言語を英語にした途端、英語圏のユーザーがアプリを開くと日本語UIが出て事故ります。\u003ccode\u003enavigator.languages\u003c/code\u003e から推定する初期化に変えて直しました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"12:1-12:9\"\u003e\n\u003cspan id=\"環境\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%92%B0%E5%A2%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e環境\u003c/h2\u003e\n\u003cul data-sourcepos=\"14:1-17:0\"\u003e\n\u003cli data-sourcepos=\"14:1-14:29\"\u003eTauri v2 + Vue 3 + vue-i18n\u003c/li\u003e\n\u003cli data-sourcepos=\"15:1-15:33\"\u003e対応言語: ja / en / de / fr\u003c/li\u003e\n\u003cli data-sourcepos=\"16:1-17:0\"\u003e個人開発の diff ツール「Diff Pro Max」での実話\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"18:1-18:21\"\u003e\n\u003cspan id=\"何が起きたか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%95%E3%81%8C%E8%B5%B7%E3%81%8D%E3%81%9F%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e何が起きたか\u003c/h2\u003e\n\u003cp data-sourcepos=\"20:1-20:291\"\u003eApp Store Connect のプライマリ言語を日本語から英語に変更しました。その状態で英語圏のユーザーがアプリを開くと、\u003cstrong\u003eUIが日本語で表示される\u003c/strong\u003e。ストアページは英語なのに、開いたら日本語です。これは離脱します。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"22:1-22:54\"\u003e\n\u003cspan id=\"原因初期ロケールが-ja-固定だった\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8E%9F%E5%9B%A0%E5%88%9D%E6%9C%9F%E3%83%AD%E3%82%B1%E3%83%BC%E3%83%AB%E3%81%8C-ja-%E5%9B%BA%E5%AE%9A%E3%81%A0%E3%81%A3%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e原因：初期ロケールが 'ja' 固定だった\u003c/h2\u003e\n\u003cp data-sourcepos=\"24:1-24:50\"\u003ei18n の初期化がこうなっていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"26:1-38:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// before\u003c/span\u003e\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003ecreateI18nInstance\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003elocale\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003elocalStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003elocale\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"kd\"\u003eas \u003c/span\u003e\u003cspan class=\"nx\"\u003eSupportedLocale\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eja\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ei18n\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003ecreateI18n\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n    \u003cspan class=\"na\"\u003elegacy\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003elocale\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003efallbackLocale\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eja\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"40:1-40:331\"\u003e\u003ccode\u003elocalStorage\u003c/code\u003e に保存された言語が無いとき、つまり\u003cstrong\u003eインストール直後の初回起動\u003c/strong\u003eで、問答無用に \u003ccode\u003e'ja'\u003c/code\u003e になります。OS やブラウザの言語設定を全く見ていません。fallback も \u003ccode\u003e'ja'\u003c/code\u003e。日本語前提のままストアだけ英語にしたので、当然こうなります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"42:1-42:74\"\u003e\n\u003cspan id=\"直し方navigatorlanguages-から初期ロケールを推定する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%9B%B4%E3%81%97%E6%96%B9navigatorlanguages-%E3%81%8B%E3%82%89%E5%88%9D%E6%9C%9F%E3%83%AD%E3%82%B1%E3%83%BC%E3%83%AB%E3%82%92%E6%8E%A8%E5%AE%9A%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e直し方：navigator.languages から初期ロケールを推定する\u003c/h2\u003e\n\u003cp data-sourcepos=\"44:1-44:116\"\u003e「明示選択 \u0026gt; OS言語推定 \u0026gt; 英語」の優先順で初期ロケールを決める関数を足しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"46:1-69:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eSupportedLocale\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eja\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003een\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ede\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003efr\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eSUPPORTED\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003eja\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003een\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003ede\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003efr\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"kd\"\u003eas const\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003edetectInitialLocale\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e \u003cspan class=\"nx\"\u003eSupportedLocale\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// 1. ユーザーが明示的に選んだ言語があれば最優先で尊重\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003esaved\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003elocalStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetItem\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003elocale\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esaved\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eSUPPORTED\u003c/span\u003e \u003cspan class=\"kd\"\u003eas \u003c/span\u003e\u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e[]).\u003c/span\u003e\u003cspan class=\"nf\"\u003eincludes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003esaved\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003esaved\u003c/span\u003e \u003cspan class=\"kd\"\u003eas \u003c/span\u003e\u003cspan class=\"nx\"\u003eSupportedLocale\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// 2. 無ければ OS / ブラウザの優先言語から推定\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eprefs\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003enavigator\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elanguages\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"nx\"\u003elength\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"nb\"\u003enavigator\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elanguages\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nb\"\u003enavigator\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003elanguage\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003een\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n  \u003cspan class=\"k\"\u003efor \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e \u003cspan class=\"k\"\u003eof\u003c/span\u003e \u003cspan class=\"nx\"\u003eprefs\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ecode\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003etoLowerCase\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nf\"\u003esplit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e-\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 'en-US' -\u0026gt; 'en'\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"nx\"\u003eSUPPORTED\u003c/span\u003e \u003cspan class=\"kd\"\u003eas \u003c/span\u003e\u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kr\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e[]).\u003c/span\u003e\u003cspan class=\"nf\"\u003eincludes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ecode\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003ecode\u003c/span\u003e \u003cspan class=\"kd\"\u003eas \u003c/span\u003e\u003cspan class=\"nx\"\u003eSupportedLocale\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// 3. 該当が無ければ英語（ストアのプライマリ言語に合わせる）\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003een\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"71:1-71:42\"\u003e呼び出し側はこう変えました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"73:1-82:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// after\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003elocale\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003edetectInitialLocale\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ei18n\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003ecreateI18n\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"na\"\u003elegacy\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003elocale\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"na\"\u003efallbackLocale\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003een\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 'ja' から変更\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"84:1-84:28\"\u003eポイントは3つです。\u003c/p\u003e\n\u003cul data-sourcepos=\"86:1-89:0\"\u003e\n\u003cli data-sourcepos=\"86:1-86:240\"\u003e\n\u003cstrong\u003e\u003ccode\u003enavigator.languages\u003c/code\u003e は Tauri の WebView でも OS の言語設定を反映します\u003c/strong\u003e。\u003ccode\u003een-US\u003c/code\u003e のような地域コード付きで来るので、\u003ccode\u003esplit('-')[0]\u003c/code\u003e で言語コードだけ取り出して対応言語に当てます。\u003c/li\u003e\n\u003cli data-sourcepos=\"87:1-87:209\"\u003e\n\u003cstrong\u003elocalStorage への保存はしません\u003c/strong\u003e。これで、ユーザーが言語を明示選択するまでは OS 言語の変更に追従します（設定画面で選べば、その時点から固定）。\u003c/li\u003e\n\u003cli data-sourcepos=\"88:1-89:0\"\u003e\n\u003cstrong\u003efallbackLocale も \u003ccode\u003e'en'\u003c/code\u003e に\u003c/strong\u003e。対応4言語は翻訳が揃っているので、未知の言語は英語へ寄せます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"90:1-90:18\"\u003e\n\u003cspan id=\"おまけの罠\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%8A%E3%81%BE%E3%81%91%E3%81%AE%E7%BD%A0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eおまけの罠\u003c/h2\u003e\n\u003cul data-sourcepos=\"92:1-94:0\"\u003e\n\u003cli data-sourcepos=\"92:1-92:261\"\u003e\n\u003cstrong\u003e既存ユーザーには影響なし\u003c/strong\u003eです。すでに \u003ccode\u003elocalStorage\u003c/code\u003e に言語が保存されているので、優先順1で尊重されます。挙動が変わるのは「保存が無い＝新規インストール or 未選択」のユーザーだけ。\u003c/li\u003e\n\u003cli data-sourcepos=\"93:1-94:0\"\u003eネイティブ側も整合を取ります。macOS の \u003ccode\u003eCFBundleDevelopmentRegion\u003c/code\u003e（Info.plist）を \u003ccode\u003een\u003c/code\u003e にしておかないと、OSレベルの表示名などで日本語が顔を出すことがあります。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"95:1-95:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cul data-sourcepos=\"97:1-101:0\"\u003e\n\u003cli data-sourcepos=\"97:1-97:102\"\u003evue-i18n の初期ロケールを \u003ccode\u003e|| 'ja'\u003c/code\u003e で決めると、初回起動がOS言語を無視する\u003c/li\u003e\n\u003cli data-sourcepos=\"98:1-98:98\"\u003e\n\u003ccode\u003enavigator.languages\u003c/code\u003e から推定し、「明示選択 \u0026gt; OS推定 \u0026gt; 英語」の順で決める\u003c/li\u003e\n\u003cli data-sourcepos=\"99:1-99:76\"\u003e保存しないことで、未選択のうちはOS言語に追従できる\u003c/li\u003e\n\u003cli data-sourcepos=\"100:1-101:0\"\u003eストアのプライマリ言語を切り替えるときは、アプリ内の既定言語と fallback もセットで見直す\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"102:1-103:0\"\u003e\n\u003cp data-sourcepos=\"104:1-104:251\"\u003e個人開発の diff ツール「Diff Pro Max」（Tauri v2 + Vue）での実話でした。同じく多言語アプリをストア配信する方の参考になれば。開発の話は X（\u003ca href=\"https://x.com/rkpg10\" rel=\"nofollow noopener\" target=\"_blank\"\u003e@rkpg10\u003c/a\u003e）でも書いています。\u003c/p\u003e\n","body":"\n## 結論（先に）\n\nvue-i18n の初期ロケールを\n\n```ts\nconst locale = localStorage.getItem('locale') || 'ja';\n```\n\nのように「保存が無ければ 'ja'」で決めていると、**OS言語を一切見ずに常に日本語で起動**します。App Store のプライマリ言語を英語にした途端、英語圏のユーザーがアプリを開くと日本語UIが出て事故ります。`navigator.languages` から推定する初期化に変えて直しました。\n\n## 環境\n\n- Tauri v2 + Vue 3 + vue-i18n\n- 対応言語: ja / en / de / fr\n- 個人開発の diff ツール「Diff Pro Max」での実話\n\n## 何が起きたか\n\nApp Store Connect のプライマリ言語を日本語から英語に変更しました。その状態で英語圏のユーザーがアプリを開くと、**UIが日本語で表示される**。ストアページは英語なのに、開いたら日本語です。これは離脱します。\n\n## 原因：初期ロケールが 'ja' 固定だった\n\ni18n の初期化がこうなっていました。\n\n```ts\n// before\nexport async function createI18nInstance() {\n  const locale = (localStorage.getItem('locale') as SupportedLocale) || 'ja';\n  // ...\n  const i18n = createI18n({\n    legacy: false,\n    locale,\n    fallbackLocale: 'ja',\n    // ...\n  });\n}\n```\n\n`localStorage` に保存された言語が無いとき、つまり**インストール直後の初回起動**で、問答無用に `'ja'` になります。OS やブラウザの言語設定を全く見ていません。fallback も `'ja'`。日本語前提のままストアだけ英語にしたので、当然こうなります。\n\n## 直し方：navigator.languages から初期ロケールを推定する\n\n「明示選択 \u003e OS言語推定 \u003e 英語」の優先順で初期ロケールを決める関数を足しました。\n\n```ts\nexport type SupportedLocale = 'ja' | 'en' | 'de' | 'fr';\nconst SUPPORTED = ['ja', 'en', 'de', 'fr'] as const;\n\nfunction detectInitialLocale(): SupportedLocale {\n  // 1. ユーザーが明示的に選んだ言語があれば最優先で尊重\n  const saved = localStorage.getItem('locale');\n  if (saved \u0026\u0026 (SUPPORTED as readonly string[]).includes(saved)) {\n    return saved as SupportedLocale;\n  }\n  // 2. 無ければ OS / ブラウザの優先言語から推定\n  const prefs = navigator.languages?.length\n    ? navigator.languages\n    : [navigator.language || 'en'];\n  for (const p of prefs) {\n    const code = p.toLowerCase().split('-')[0]; // 'en-US' -\u003e 'en'\n    if ((SUPPORTED as readonly string[]).includes(code)) {\n      return code as SupportedLocale;\n    }\n  }\n  // 3. 該当が無ければ英語（ストアのプライマリ言語に合わせる）\n  return 'en';\n}\n```\n\n呼び出し側はこう変えました。\n\n```ts\n// after\nconst locale = detectInitialLocale();\nconst i18n = createI18n({\n  legacy: false,\n  locale,\n  fallbackLocale: 'en', // 'ja' から変更\n  // ...\n});\n```\n\nポイントは3つです。\n\n- **`navigator.languages` は Tauri の WebView でも OS の言語設定を反映します**。`en-US` のような地域コード付きで来るので、`split('-')[0]` で言語コードだけ取り出して対応言語に当てます。\n- **localStorage への保存はしません**。これで、ユーザーが言語を明示選択するまでは OS 言語の変更に追従します（設定画面で選べば、その時点から固定）。\n- **fallbackLocale も `'en'` に**。対応4言語は翻訳が揃っているので、未知の言語は英語へ寄せます。\n\n## おまけの罠\n\n- **既存ユーザーには影響なし**です。すでに `localStorage` に言語が保存されているので、優先順1で尊重されます。挙動が変わるのは「保存が無い＝新規インストール or 未選択」のユーザーだけ。\n- ネイティブ側も整合を取ります。macOS の `CFBundleDevelopmentRegion`（Info.plist）を `en` にしておかないと、OSレベルの表示名などで日本語が顔を出すことがあります。\n\n## まとめ\n\n- vue-i18n の初期ロケールを `|| 'ja'` で決めると、初回起動がOS言語を無視する\n- `navigator.languages` から推定し、「明示選択 \u003e OS推定 \u003e 英語」の順で決める\n- 保存しないことで、未選択のうちはOS言語に追従できる\n- ストアのプライマリ言語を切り替えるときは、アプリ内の既定言語と fallback もセットで見直す\n\n---\n\n個人開発の diff ツール「Diff Pro Max」（Tauri v2 + Vue）での実話でした。同じく多言語アプリをストア配信する方の参考になれば。開発の話は X（[@rkpg10](https://x.com/rkpg10)）でも書いています。\n","coediting":false,"comments_count":0,"created_at":"2026-05-25T01:41:08+09:00","group":null,"id":"d04d1ac83fd98309525a","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"I18n","versions":[]},{"name":"Vue.js","versions":[]},{"name":"個人開発","versions":[]},{"name":"vue-i18n","versions":[]},{"name":"Tauri","versions":[]}],"title":"Tauri + vue-i18n アプリが「英語ストアからDLされると日本語UI」になる問題をOS言語判定で直す","updated_at":"2026-05-25T01:48:09+09:00","url":"https://qiita.com/rkpg/items/d04d1ac83fd98309525a","user":{"description":"AIを活用した制作を実験中\r\n観察と設計で“見えないもの”を可視化する\r\nWeb制作17年／個人開発4アプリ\r\nブログ「LifeOS 個人未来観測所」の人","facebook_id":"","followees_count":1,"followers_count":0,"github_login_name":"rkpg","id":"rkpg","items_count":5,"linkedin_id":"","location":"","name":"R | AI ×個人開発","organization":"LifeOS 個人未来観測所 ","permanent_id":40262,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/40262/be83bebd6afaefc54eec37e193ce1147c2afa8ff/x_large.png?1779502640","team_only":false,"twitter_screen_name":null,"website_url":"https://rkpg.net/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"3:1-3:43\"\u003e\n\u003cspan id=\"ウェブ開発で一番人気のnextjs\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A6%E3%82%A7%E3%83%96%E9%96%8B%E7%99%BA%E3%81%A7%E4%B8%80%E7%95%AA%E4%BA%BA%E6%B0%97%E3%81%AEnextjs\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eウェブ開発で一番人気のNext.js\u003c/h2\u003e\n\u003cp data-sourcepos=\"5:1-5:66\"\u003eNext.jsはReactをベースにしたフレームワークです。\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:83\"\u003eReact単体では使うのが難しいさまざまな機能が入っています。\u003c/p\u003e\n\u003cp data-sourcepos=\"9:1-9:133\"\u003e世界的に非常に人気のツールで、近年のウェブアプリ開発で最も使われているのがこのNext.jsです。\u003c/p\u003e\n\u003cp data-sourcepos=\"11:1-11:90\"\u003e本記事ではそのインストール方法と公開方法を時短で紹介します。\u003c/p\u003e\n\u003cp data-sourcepos=\"13:1-13:34\"\u003e使う言語はTypeScriptです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"15:1-15:55\"\u003e\n\u003cspan id=\"nextjs-typescriptのインストール方法\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#nextjs-typescript%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%96%B9%E6%B3%95\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eNext.js（+ TypeScript）のインストール方法\u003c/h2\u003e\n\u003cp data-sourcepos=\"17:1-17:94\"\u003eターミナル上で、Next.jsをインストールしたいフォルダに移動します。\u003c/p\u003e\n\u003cp data-sourcepos=\"19:1-19:78\"\u003eここでは「ダウンロード」フォルダにいるものとします。\u003c/p\u003e\n\u003cp data-sourcepos=\"21:1-21:95\"\u003e次のコマンドをターミナルに打ち、「Enter」キーで実行してください。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"shell\" data-sourcepos=\"23:1-25:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003enpx create-next-app\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"27:1-27:155\"\u003eここで次のような表示が出ることがありますが、特に問題ではないので「Enter」キーを押して次に進んでください。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"shell\" data-sourcepos=\"29:1-33:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eNeed to \u003cspan class=\"nb\"\u003einstall \u003c/span\u003ethe following packages:\n    create-next-app@16.2.4\nOk to proceed? \u003cspan class=\"o\"\u003e(\u003c/span\u003ey\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"35:1-35:87\"\u003eこれ以降、質問がいくつか出てくるので回答していきましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"37:1-37:90\"\u003e最初はこのアプリの名前で、これがフォルダの名前に使われます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"shell\" data-sourcepos=\"39:1-41:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e? What is your project named? ›\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"43:1-43:42\"\u003e名前は好きなものが使えます。\u003c/p\u003e\n\u003cp data-sourcepos=\"45:1-45:58\"\u003eここでは「first-nextjs-app」と書きましょう。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"shell\" data-sourcepos=\"47:1-49:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e? What is your project named? › first-nextjs-app\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"51:1-51:105\"\u003e「Enter」キーを押すと次の質問が出ます。Next.jsの初期設定に関する質問です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"shell\" data-sourcepos=\"53:1-59:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e? Would you like to use the recommended Next.js defaults? › - Use arrow-keys. Return to submit.\n    Yes, use recommended defaults\n    No, reuse previous settings\n❯   No, customize settings\n    Choose your own preferences\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"61:1-61:31\"\u003e選択肢は3つあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"63:1-64:97\"\u003e【1】\u003cbr\u003e\nTypeScriptやTailwind CSSなども一緒にインストールする（use recommended defaults）\u003c/p\u003e\n\u003cp data-sourcepos=\"66:1-67:62\"\u003e【2】\u003cbr\u003e\n前回の設定を再利用する（reuse previous settings）\u003c/p\u003e\n\u003cp data-sourcepos=\"69:1-70:42\"\u003e【3】\u003cbr\u003e\n自分で決める（customize settings）\u003c/p\u003e\n\u003chr data-sourcepos=\"72:1-73:0\"\u003e\n\u003cp data-sourcepos=\"74:1-74:208\"\u003eここではひとつひとつ自分で決めていきたいので、キーボードの矢印キーを使って3つ目の「No, customize settings」を選び、「Enter」キーで実行してください。\u003c/p\u003e\n\u003cp data-sourcepos=\"76:1-76:79\"\u003e次の質問が出ます。Next.jsの具体的な初期設定の質問です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"shell\" data-sourcepos=\"78:1-87:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e? Would you like to use TypeScript? › No / Yes\n? Which linter would you like to use? › - Use arrow-keys. Return to submit.\n? Would you like to use React Compiler? › No / Yes\n? Would you like to use Tailwind CSS? › No / Yes\n? Would you like to use \u003cspan class=\"sb\"\u003e`\u003c/span\u003esrc/\u003cspan class=\"sb\"\u003e`\u003c/span\u003e directory? › No / Yes\n? Would you like to use App Router? \u003cspan class=\"o\"\u003e(\u003c/span\u003erecommended\u003cspan class=\"o\"\u003e)\u003c/span\u003e › No / Yes\n? Would you like to customize the default import \u003cspan class=\"nb\"\u003ealias\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003e@/\u003cspan class=\"k\"\u003e*\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e? › No / Yes\n? Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? › No / Yes\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"89:1-89:82\"\u003e「Would you like to〜」とは「Do you want〜」の丁寧な聞き方です。\u003c/p\u003e\n\u003cp data-sourcepos=\"91:1-91:148\"\u003eこれらの質問は「TypeScriptやTailwind CSSなども一緒にインストールしますか？」と聞いているのだとわかります。\u003c/p\u003e\n\u003cp data-sourcepos=\"93:1-93:111\"\u003eここでは最小限の設定で進めたいので、各質問には次のように回答してください。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"shell\" data-sourcepos=\"95:1-104:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e✔ Would you like to use TypeScript?  → 「Yes」\n✔ Which linter would you like to use?  → 「None」\n✔ Would you like to use React Compiler?  → 「Yes」\n✔ Would you like to use Tailwind CSS?  →  「No」\n✔ Would you like your code inside a \u003cspan class=\"sb\"\u003e`\u003c/span\u003esrc/\u003cspan class=\"sb\"\u003e`\u003c/span\u003e directory?  →  「No」\n✔ Would you like to use App Router? \u003cspan class=\"o\"\u003e(\u003c/span\u003erecommended\u003cspan class=\"o\"\u003e)\u003c/span\u003e →  「Yes」\n✔ Would you like to customize the import \u003cspan class=\"nb\"\u003ealias\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"sb\"\u003e`\u003c/span\u003e@/\u003cspan class=\"k\"\u003e*\u003c/span\u003e\u003cspan class=\"sb\"\u003e`\u003c/span\u003e by default\u003cspan class=\"o\"\u003e)\u003c/span\u003e?  →  「No」\n✔ Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code?  →  「No」\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"106:1-106:57\"\u003eいくつかの質問について少し補足します。\u003c/p\u003e\n\u003cp data-sourcepos=\"108:1-108:79\"\u003eこの中で一番重要な質問は「App Router」に関するものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"110:1-110:51\"\u003eかならず「Yes」を選択してください。\u003c/p\u003e\n\u003cp data-sourcepos=\"112:1-112:132\"\u003eこれによって、Next.jsバージョン13以降でデフォルトになっている「Appフォルダ」が利用できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"114:1-114:76\"\u003e「TypeScript」に関する質問も必ず「Yes」を選びましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"116:1-116:69\"\u003e「No」にするとJavaScript版がインストールされます。\u003c/p\u003e\n\u003cp data-sourcepos=\"118:1-118:142\"\u003eReact CompilerはReactのコードを最適化するツールで、Next.jsバージョン16から利用できるようになっています。\u003c/p\u003e\n\u003cp data-sourcepos=\"120:1-120:209\"\u003e本記事でインストールしてもしなくても構いませんが、今後はReact Compilerの利用が標準になっていくと考えられるので、ここでは「Yes」を選択しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"122:1-122:89\"\u003eまた、linterの質問の選択肢には「Biome」と「ESLint」が出てきます。\u003c/p\u003e\n\u003cp data-sourcepos=\"124:1-124:57\"\u003eこれらはコードの品質を高めるものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"126:1-126:168\"\u003e本記事では「None」を選択しましたが、特にBiomeは近年注目を集めているので、使い方を確認しておくことをおすすめします。\u003c/p\u003e\n\u003cp data-sourcepos=\"128:1-128:96\"\u003eインストールが完了したら、ダウンロードフォルダを開いてください。\u003c/p\u003e\n\u003cp data-sourcepos=\"130:1-130:84\"\u003e次のように、新しいフォルダができているのを確認できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"132:1-132:127\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F3e261e14-e4db-4d10-a1dc-21f8704ad66e.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=e5634c838a9aae6e4886917c5de2efde\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F3e261e14-e4db-4d10-a1dc-21f8704ad66e.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=e5634c838a9aae6e4886917c5de2efde\" alt=\"dw-folder.jpg\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F3e261e14-e4db-4d10-a1dc-21f8704ad66e.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=70239edf59e451be4ff74f4b7549ceee 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/3e261e14-e4db-4d10-a1dc-21f8704ad66e.jpeg\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"134:1-134:123\"\u003eこれをVS Codeで開きましょう（VS Codeにフォルダを直接ドラッグ\u0026amp;ドロップすれば開けます）。\u003c/p\u003e\n\u003cp data-sourcepos=\"136:1-136:45\"\u003e中身は次のようになっています。\u003c/p\u003e\n\u003cp data-sourcepos=\"138:1-138:123\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Fabddae56-4b88-4b96-aac5-6b9a99da93a4.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b61f3aa5340d0e9c49334dbabf9f1fb2\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Fabddae56-4b88-4b96-aac5-6b9a99da93a4.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b61f3aa5340d0e9c49334dbabf9f1fb2\" alt=\"pic-1.jpg\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Fabddae56-4b88-4b96-aac5-6b9a99da93a4.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=301794ac2e5cc3b59588c257fa813e66 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/abddae56-4b88-4b96-aac5-6b9a99da93a4.jpeg\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"140:1-140:117\"\u003eこの中には必要のないフォルダやコードがあるので、まずは整理をしていきましょう。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"142:1-142:34\"\u003e\n\u003cspan id=\"nextjsのクリーンアップ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#nextjs%E3%81%AE%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%A2%E3%83%83%E3%83%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eNext.jsのクリーンアップ\u003c/h2\u003e\n\u003cp data-sourcepos=\"144:1-144:90\"\u003eまっさらな状態で始めたいので、不要なものを削除していきます。\u003c/p\u003e\n\u003cp data-sourcepos=\"146:1-146:79\"\u003e\u003ccode\u003eapp\u003c/code\u003eフォルダの中にある\u003ccode\u003epage.module.css\u003c/code\u003eを削除してください。\u003c/p\u003e\n\u003cp data-sourcepos=\"148:1-148:129\"\u003e次は\u003ccode\u003eapp\u003c/code\u003eフォルダの\u003ccode\u003eglobals.css\u003c/code\u003eファイルを開き、中に書かれているコードをすべて消しましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"150:1-150:108\"\u003e次に\u003ccode\u003elayout.tsx\u003c/code\u003eを開き、中のコードをすべて消し、次のコードを書いてください。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"152:1-168:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// app/layout.tsx\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e./globals.css\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eRootLayout\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"nx\"\u003echildren\u003c/span\u003e \u003cspan class=\"p\"\u003e}:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nl\"\u003echildren\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eReact\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eReactNode\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n        \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003ehtml\u003c/span\u003e \u003cspan class=\"nx\"\u003elang\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003een\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003ebody\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n                \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"nx\"\u003echildren\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"sr\"\u003e/body\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026gt;\n\u003c/span\u003e        \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"sr\"\u003e/html\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026gt;\n\u003c/span\u003e    \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"nx\"\u003eRootLayout\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"170:1-170:109\"\u003e次は\u003ccode\u003epage.tsx\u003c/code\u003eを開き、ここでも同様に書かれているコードをすべて消しましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"172:1-172:51\"\u003eそして次のコードを書いてください。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"174:1-186:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// app/page.tsx\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eHome\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n        \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003ediv\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eh1\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eこんにちは\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"sr\"\u003e/h1\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026gt;\n\u003c/span\u003e        \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"sr\"\u003e/div\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026gt;\n\u003c/span\u003e    \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e \n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"nx\"\u003eHome\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"188:1-188:159\"\u003eVS Code上部メニューバーの「File」→「Save」、もしくは「Command」+「S」で、各ファイルに加えた変更を保存しましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"190:1-190:106\"\u003eこれでクリーンアップが完了して、Next.js開発を始める地ならしができました。\u003c/p\u003e\n\u003cp data-sourcepos=\"192:1-192:43\"\u003e次はNext.jsを起動させましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"194:1-194:111\"\u003eターミナルに\u003ccode\u003enpm run dev\u003c/code\u003eを打ち、「Enter」で実行すると、次のように表示されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"196:1-196:123\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F3855f7b9-460b-45b9-898b-5a829cfb8af3.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=77dad1f4acc4f9bc23bfc4f7b80283b4\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F3855f7b9-460b-45b9-898b-5a829cfb8af3.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=77dad1f4acc4f9bc23bfc4f7b80283b4\" alt=\"pic-2.jpg\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F3855f7b9-460b-45b9-898b-5a829cfb8af3.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=91554467c85cb3c1020b986da0e10bb8 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/3855f7b9-460b-45b9-898b-5a829cfb8af3.jpeg\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"198:1-198:74\"\u003e指定されている\u003ccode\u003ehttp://localhost:3000\u003c/code\u003eを開いてみましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"200:1-200:36\"\u003e次のように表示されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"202:1-202:123\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Fd3a7183a-5f31-488b-9956-89bbeb3b009e.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=341c2304d764f5bd2dc63436f4efc3aa\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Fd3a7183a-5f31-488b-9956-89bbeb3b009e.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=341c2304d764f5bd2dc63436f4efc3aa\" alt=\"pic-3.jpg\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Fd3a7183a-5f31-488b-9956-89bbeb3b009e.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=a8feb050bdd896cd01e3b1c06dd3bb46 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/d3a7183a-5f31-488b-9956-89bbeb3b009e.jpeg\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"204:1-204:125\"\u003eVS Codeに戻り、\u003ccode\u003epage.tsx\u003c/code\u003e内の\u003ccode\u003e\u0026lt;h1\u0026gt;\u003c/code\u003eタグの文字列を「さようなら」に変えて保存してみましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"206:1-206:60\"\u003eブラウザを見ると、表示も変わっています。\u003c/p\u003e\n\u003cp data-sourcepos=\"208:1-208:123\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F1b80088f-8d91-4ed1-88e8-6c540b5614b8.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=83a939f8188625b3a005f60abf3a3f31\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F1b80088f-8d91-4ed1-88e8-6c540b5614b8.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=83a939f8188625b3a005f60abf3a3f31\" alt=\"pic-4.jpg\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F1b80088f-8d91-4ed1-88e8-6c540b5614b8.jpeg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=c932d9c5fa24a58588f4237e48c236c8 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/1b80088f-8d91-4ed1-88e8-6c540b5614b8.jpeg\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"210:1-210:123\"\u003eつまり\u003ccode\u003epage.tsx\u003c/code\u003eの\u003ccode\u003ereturn\u003c/code\u003e横のカッコ\u003ccode\u003e( )\u003c/code\u003e内は、HTMLと同じ要領で編集できることがわかります。\u003c/p\u003e\n\u003cp data-sourcepos=\"212:1-212:55\"\u003eこれがNext.jsの使い方の初歩の初歩です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"214:1-214:22\"\u003e\n\u003cspan id=\"layouttsxの役割\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#layouttsx%E3%81%AE%E5%BD%B9%E5%89%B2\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003elayout.tsxの役割\u003c/h2\u003e\n\u003cp data-sourcepos=\"215:1-215:73\"\u003e\u003ccode\u003elayout.tsx\u003c/code\u003eはNext.jsが用意している特殊なファイルです。\u003c/p\u003e\n\u003cp data-sourcepos=\"217:1-217:102\"\u003eアプリ全体で適用したいスタイルやコンポーネントなどをここに書きます。\u003c/p\u003e\n\u003cp data-sourcepos=\"219:1-219:189\"\u003e\u003ccode\u003elayout.tsx\u003c/code\u003eという名前のファイルに書いたコードは、次図のように\u003ccode\u003eapp\u003c/code\u003eフォルダ内に作ったすべての\u003ccode\u003epage.tsx\u003c/code\u003eを包み込むように機能するのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"221:1-221:127\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F570e2fa8-1619-4c1c-becd-0c8b9d5179cf.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=f7b90b3e49f5e6d73dd460d832467170\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F570e2fa8-1619-4c1c-becd-0c8b9d5179cf.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=f7b90b3e49f5e6d73dd460d832467170\" alt=\"layout-tsx.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F570e2fa8-1619-4c1c-becd-0c8b9d5179cf.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=80f25136d27fcaa714e78b2a9295ce77 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/570e2fa8-1619-4c1c-becd-0c8b9d5179cf.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"223:1-223:25\"\u003e\n\u003cspan id=\"nextjsの公開方法\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#nextjs%E3%81%AE%E5%85%AC%E9%96%8B%E6%96%B9%E6%B3%95\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eNext.jsの公開方法\u003c/h2\u003e\n\u003cp data-sourcepos=\"224:1-224:55\"\u003eNext.jsはVercelで公開するのが一般的です。\u003c/p\u003e\n\u003cp data-sourcepos=\"226:1-226:164\"\u003eVercelはNext.jsの開発元が運営しているプラットフォームなので、Next.jsのさまざまな機能を制約なしに実行できるからです。\u003c/p\u003e\n\u003cp data-sourcepos=\"228:1-228:202\"\u003eVercelでの公開には、コードをGitで管理していることが前提なので本記事では触れませんが、「Next.js + Vercel」が一般的な運用だと知っておきましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"230:1-230:152\"\u003eVercel以外で公開するには、buildとexportという作業を、あらかじめ手元のコンピューター内で行う必要があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"232:1-232:76\"\u003eexportを行う下記コードを\u003ccode\u003enext.config.ts\u003c/code\u003eに追加しましょう。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"234:1-245:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// next.config.ts\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"nx\"\u003etype\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eNextConfig\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003enext\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003enextConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eNextConfig\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"cm\"\u003e/* config options here */\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eoutput\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eexport\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e            \u003cspan class=\"c1\"\u003e// 追加\u003c/span\u003e\n\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"nx\"\u003enextConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"247:1-247:33\"\u003e変更を保存しましょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"249:1-249:114\"\u003eターミナル上で「control」を押しながら「C」を押して、Next.jsを停止させてください。\u003c/p\u003e\n\u003cp data-sourcepos=\"251:1-251:68\"\u003eそしてbuildを行う下記コマンドを実行しましょう。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"shell\" data-sourcepos=\"253:1-255:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003enpm run build\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"257:1-257:59\"\u003e\u003ccode\u003eout\u003c/code\u003eという新しいフォルダが生成されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"259:1-259:123\"\u003eこの\u003ccode\u003eout\u003c/code\u003eフォルダをNetlifyなどにアップロードすれば、アプリをオンラインで公開可能です。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"261:1-263:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"262:1-262:274\"\u003eVercel以外のプラットフォームで公開するときには、いくつか注意点があります。詳しくは\u003ca href=\"https://monotein.com/blog/deploy-nextjs-to-other-hosting-platforms/?utm_source=14727dad8c284cd14f86-qiita\" rel=\"nofollow noopener\" target=\"_blank\"\u003eこちらの記事\u003c/a\u003eで紹介しています。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"265:1-265:30\"\u003e\n\u003cspan id=\"よくある質問faq\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%88%E3%81%8F%E3%81%82%E3%82%8B%E8%B3%AA%E5%95%8Ffaq\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eよくある質問（FAQ）\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"267:1-267:57\"\u003e\n\u003cspan id=\"q1-nextjsとreact--vite選ぶ基準を教えて\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#q1-nextjs%E3%81%A8react--vite%E9%81%B8%E3%81%B6%E5%9F%BA%E6%BA%96%E3%82%92%E6%95%99%E3%81%88%E3%81%A6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eQ1. Next.jsとReact + Vite、選ぶ基準を教えて\u003c/h3\u003e\n\u003cp data-sourcepos=\"269:1-269:96\"\u003e現在のReact開発のツールは「Next.js」もしくは「React + Vite」の二択です。\u003c/p\u003e\n\u003cp data-sourcepos=\"271:1-271:57\"\u003e大まかな使い分けは次のようになります。\u003c/p\u003e\n\u003chr data-sourcepos=\"273:1-274:0\"\u003e\n\u003cul data-sourcepos=\"275:1-279:0\"\u003e\n\u003cli data-sourcepos=\"275:1-275:82\"\u003e社内用アプリや管理画面のダッシュボードなど → React + Vite\u003c/li\u003e\n\u003cli data-sourcepos=\"276:1-276:26\"\u003eSEOが重要 → Next.js\u003c/li\u003e\n\u003cli data-sourcepos=\"277:1-277:56\"\u003e規模が大きく複雑な機能が必要 → Next.js\u003c/li\u003e\n\u003cli data-sourcepos=\"278:1-279:0\"\u003e軽量に運用したい → React + Vite\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"280:1-281:0\"\u003e\n\u003cp data-sourcepos=\"282:1-282:186\"\u003eより詳しい使い分けは\u003ca href=\"https://monotein.com/blog/react-vite-or-nextjs-which-to-choose/?utm_source=14727dad8c284cd14f86-qiita\" rel=\"nofollow noopener\" target=\"_blank\"\u003eこちらの記事\u003c/a\u003eを参考にしてください。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"284:1-284:52\"\u003e\n\u003cspan id=\"q2-npm-run-dev以外のコマンドを教えて\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#q2-npm-run-dev%E4%BB%A5%E5%A4%96%E3%81%AE%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%82%92%E6%95%99%E3%81%88%E3%81%A6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eQ2. npm run dev以外のコマンドを教えて\u003c/h3\u003e\n\u003cp data-sourcepos=\"285:1-285:130\"\u003eNext.jsは、手元のパソコンでの開発時と、オンラインでの公開時の挙動が異なる場合があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"287:1-287:90\"\u003e開発環境と本番環境での動きに違いが生じる可能性があるのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"289:1-289:99\"\u003eそのため、本番環境の挙動もテストできるコマンドが用意されています。\u003c/p\u003e\n\u003cp data-sourcepos=\"291:1-291:117\"\u003e先ほど紹介したビルドコマンド\u003ccode\u003enpm run build\u003c/code\u003e完了後に、次のコマンドを実行しましょう。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"shell\" data-sourcepos=\"293:1-295:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003enpm run start\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"297:1-297:63\"\u003eこれで本番環境での動きをチェックできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"299:1-299:45\"\u003eまとめると次のようになります。\u003c/p\u003e\n\u003ctable data-sourcepos=\"301:1-303:49\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"301:1-301:55\"\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"301:2-301:27\"\u003e開発環境での挙動\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"301:29-301:54\"\u003e本番環境での挙動\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"303:1-303:49\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"303:2-303:14\"\u003enpm run dev\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"303:16-303:48\"\u003enpm run build → npm run start\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr data-sourcepos=\"305:1-306:0\"\u003e\n\u003cp data-sourcepos=\"307:1-307:103\"\u003e難しいと思っている人が多いNetx.jsも、本記事のように実は簡単に使えます。\u003c/p\u003e\n\u003cp data-sourcepos=\"309:1-309:79\"\u003e最初からNext.jsの全機能をマスターする必要はないのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"311:1-311:105\"\u003e最低限の機能から使ってみて、そして必要があれば高度な機能を学んでみる。\u003c/p\u003e\n\u003cp data-sourcepos=\"313:1-313:54\"\u003eこれがもっとも失敗しない学習法です。\u003c/p\u003e\n\u003cp data-sourcepos=\"315:1-315:51\"\u003e本記事がその助けとなれば幸いです。\u003c/p\u003e\n","body":"\n\n## ウェブ開発で一番人気のNext.js\n\nNext.jsはReactをベースにしたフレームワークです。\n\nReact単体では使うのが難しいさまざまな機能が入っています。\n\n世界的に非常に人気のツールで、近年のウェブアプリ開発で最も使われているのがこのNext.jsです。\n\n本記事ではそのインストール方法と公開方法を時短で紹介します。\n\n使う言語はTypeScriptです。\n\n## Next.js（+ TypeScript）のインストール方法\n\nターミナル上で、Next.jsをインストールしたいフォルダに移動します。\n\nここでは「ダウンロード」フォルダにいるものとします。\n\n次のコマンドをターミナルに打ち、「Enter」キーで実行してください。\n\n```shell\nnpx create-next-app\n```\n\nここで次のような表示が出ることがありますが、特に問題ではないので「Enter」キーを押して次に進んでください。\n\n```shell\nNeed to install the following packages:\n    create-next-app@16.2.4\nOk to proceed? (y)\n```\n\nこれ以降、質問がいくつか出てくるので回答していきましょう。\n\n最初はこのアプリの名前で、これがフォルダの名前に使われます。\n\n```shell\n? What is your project named? ›\n```\n\n名前は好きなものが使えます。\n\nここでは「first-nextjs-app」と書きましょう。\n\n```shell\n? What is your project named? › first-nextjs-app\n```\n\n「Enter」キーを押すと次の質問が出ます。Next.jsの初期設定に関する質問です。\n\n```shell\n? Would you like to use the recommended Next.js defaults? › - Use arrow-keys. Return to submit.\n    Yes, use recommended defaults\n    No, reuse previous settings\n❯   No, customize settings\n    Choose your own preferences\n```\n\n選択肢は3つあります。\n\n【1】\\\nTypeScriptやTailwind CSSなども一緒にインストールする（use recommended defaults）\n\n【2】\\\n前回の設定を再利用する（reuse previous settings）\n\n【3】\\\n自分で決める（customize settings）\n\n---\n\nここではひとつひとつ自分で決めていきたいので、キーボードの矢印キーを使って3つ目の「No, customize settings」を選び、「Enter」キーで実行してください。\n\n次の質問が出ます。Next.jsの具体的な初期設定の質問です。\n\n```shell\n? Would you like to use TypeScript? › No / Yes\n? Which linter would you like to use? › - Use arrow-keys. Return to submit.\n? Would you like to use React Compiler? › No / Yes\n? Would you like to use Tailwind CSS? › No / Yes\n? Would you like to use `src/` directory? › No / Yes\n? Would you like to use App Router? (recommended) › No / Yes\n? Would you like to customize the default import alias (@/*)? › No / Yes\n? Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? › No / Yes\n```\n\n「Would you like to〜」とは「Do you want〜」の丁寧な聞き方です。\n\nこれらの質問は「TypeScriptやTailwind CSSなども一緒にインストールしますか？」と聞いているのだとわかります。\n\nここでは最小限の設定で進めたいので、各質問には次のように回答してください。\n\n```shell\n✔ Would you like to use TypeScript?  → 「Yes」\n✔ Which linter would you like to use?  → 「None」\n✔ Would you like to use React Compiler?  → 「Yes」\n✔ Would you like to use Tailwind CSS?  →  「No」\n✔ Would you like your code inside a `src/` directory?  →  「No」\n✔ Would you like to use App Router? (recommended) →  「Yes」\n✔ Would you like to customize the import alias (`@/*` by default)?  →  「No」\n✔ Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code?  →  「No」\n```\n\nいくつかの質問について少し補足します。\n\nこの中で一番重要な質問は「App Router」に関するものです。\n\nかならず「Yes」を選択してください。\n\nこれによって、Next.jsバージョン13以降でデフォルトになっている「Appフォルダ」が利用できます。\n\n「TypeScript」に関する質問も必ず「Yes」を選びましょう。\n\n「No」にするとJavaScript版がインストールされます。\n\nReact CompilerはReactのコードを最適化するツールで、Next.jsバージョン16から利用できるようになっています。\n\n本記事でインストールしてもしなくても構いませんが、今後はReact Compilerの利用が標準になっていくと考えられるので、ここでは「Yes」を選択しています。\n\nまた、linterの質問の選択肢には「Biome」と「ESLint」が出てきます。\n\nこれらはコードの品質を高めるものです。\n\n本記事では「None」を選択しましたが、特にBiomeは近年注目を集めているので、使い方を確認しておくことをおすすめします。\n\nインストールが完了したら、ダウンロードフォルダを開いてください。\n\n次のように、新しいフォルダができているのを確認できます。\n\n![dw-folder.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/3e261e14-e4db-4d10-a1dc-21f8704ad66e.jpeg)\n\nこれをVS Codeで開きましょう（VS Codeにフォルダを直接ドラッグ\u0026ドロップすれば開けます）。\n\n中身は次のようになっています。\n\n![pic-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/abddae56-4b88-4b96-aac5-6b9a99da93a4.jpeg)\n\nこの中には必要のないフォルダやコードがあるので、まずは整理をしていきましょう。\n\n## Next.jsのクリーンアップ\n\nまっさらな状態で始めたいので、不要なものを削除していきます。\n\n`app`フォルダの中にある`page.module.css`を削除してください。\n\n次は`app`フォルダの`globals.css`ファイルを開き、中に書かれているコードをすべて消しましょう。\n\n次に`layout.tsx`を開き、中のコードをすべて消し、次のコードを書いてください。\n\n```js\n// app/layout.tsx\n\nimport \"./globals.css\"\n\nconst RootLayout = ({ children }: { children: React.ReactNode }) =\u003e {\n    return (\n        \u003chtml lang=\"en\"\u003e\n            \u003cbody\u003e\n                {children}\n            \u003c/body\u003e\n        \u003c/html\u003e\n    )\n}\n\nexport default RootLayout\n```\n\n次は`page.tsx`を開き、ここでも同様に書かれているコードをすべて消しましょう。\n\nそして次のコードを書いてください。\n\n```js\n// app/page.tsx\n\nconst Home = () =\u003e {\n    return (\n        \u003cdiv\u003e\n            \u003ch1\u003eこんにちは\u003c/h1\u003e\n        \u003c/div\u003e\n    )\n} \n\nexport default Home\n```\n\nVS Code上部メニューバーの「File」→「Save」、もしくは「Command」+「S」で、各ファイルに加えた変更を保存しましょう。\n\nこれでクリーンアップが完了して、Next.js開発を始める地ならしができました。\n\n次はNext.jsを起動させましょう。\n\nターミナルに`npm run dev`を打ち、「Enter」で実行すると、次のように表示されます。\n\n![pic-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/3855f7b9-460b-45b9-898b-5a829cfb8af3.jpeg)\n\n指定されている`http://localhost:3000`を開いてみましょう。\n\n次のように表示されます。\n\n![pic-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/d3a7183a-5f31-488b-9956-89bbeb3b009e.jpeg)\n\nVS Codeに戻り、`page.tsx`内の`\u003ch1\u003e`タグの文字列を「さようなら」に変えて保存してみましょう。\n\nブラウザを見ると、表示も変わっています。\n\n![pic-4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/1b80088f-8d91-4ed1-88e8-6c540b5614b8.jpeg)\n\nつまり`page.tsx`の`return`横のカッコ`( )`内は、HTMLと同じ要領で編集できることがわかります。\n\nこれがNext.jsの使い方の初歩の初歩です。\n\n## layout.tsxの役割\n`layout.tsx`はNext.jsが用意している特殊なファイルです。\n\nアプリ全体で適用したいスタイルやコンポーネントなどをここに書きます。\n\n`layout.tsx`という名前のファイルに書いたコードは、次図のように`app`フォルダ内に作ったすべての`page.tsx`を包み込むように機能するのです。\n\n![layout-tsx.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/570e2fa8-1619-4c1c-becd-0c8b9d5179cf.png)\n\n## Next.jsの公開方法\nNext.jsはVercelで公開するのが一般的です。\n\nVercelはNext.jsの開発元が運営しているプラットフォームなので、Next.jsのさまざまな機能を制約なしに実行できるからです。\n\nVercelでの公開には、コードをGitで管理していることが前提なので本記事では触れませんが、「Next.js + Vercel」が一般的な運用だと知っておきましょう。\n\nVercel以外で公開するには、buildとexportという作業を、あらかじめ手元のコンピューター内で行う必要があります。\n\nexportを行う下記コードを`next.config.ts`に追加しましょう。\n\n```js\n// next.config.ts\n\nimport type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n  output: \"export\",            // 追加\n};\n\nexport default nextConfig;\n```\n\n変更を保存しましょう。\n\nターミナル上で「control」を押しながら「C」を押して、Next.jsを停止させてください。\n\nそしてbuildを行う下記コマンドを実行しましょう。\n\n```shell\nnpm run build\n```\n\n`out`という新しいフォルダが生成されます。\n\nこの`out`フォルダをNetlifyなどにアップロードすれば、アプリをオンラインで公開可能です。\n\n:::note info\nVercel以外のプラットフォームで公開するときには、いくつか注意点があります。詳しくは[こちらの記事](https://monotein.com/blog/deploy-nextjs-to-other-hosting-platforms/?utm_source=14727dad8c284cd14f86-qiita)で紹介しています。\n:::\n\n## よくある質問（FAQ）\n\n### Q1. Next.jsとReact + Vite、選ぶ基準を教えて\n\n現在のReact開発のツールは「Next.js」もしくは「React + Vite」の二択です。\n\n大まかな使い分けは次のようになります。\n\n---\n\n- 社内用アプリや管理画面のダッシュボードなど → React + Vite\n- SEOが重要 → Next.js\n- 規模が大きく複雑な機能が必要 → Next.js\n- 軽量に運用したい → React + Vite\n\n---\n\nより詳しい使い分けは[こちらの記事](https://monotein.com/blog/react-vite-or-nextjs-which-to-choose/?utm_source=14727dad8c284cd14f86-qiita)を参考にしてください。\n\n### Q2. npm run dev以外のコマンドを教えて\nNext.jsは、手元のパソコンでの開発時と、オンラインでの公開時の挙動が異なる場合があります。\n\n開発環境と本番環境での動きに違いが生じる可能性があるのです。\n\nそのため、本番環境の挙動もテストできるコマンドが用意されています。\n\n先ほど紹介したビルドコマンド`npm run build`完了後に、次のコマンドを実行しましょう。\n\n```shell\nnpm run start\n```\n\nこれで本番環境での動きをチェックできます。\n\nまとめると次のようになります。\n\n| 開発環境での挙動 | 本番環境での挙動 |\n|:----:|:----:|\n| npm run dev | npm run build → npm run start |\n\n---\n\n難しいと思っている人が多いNetx.jsも、本記事のように実は簡単に使えます。\n\n最初からNext.jsの全機能をマスターする必要はないのです。\n\n最低限の機能から使ってみて、そして必要があれば高度な機能を学んでみる。\n\nこれがもっとも失敗しない学習法です。\n\n本記事がその助けとなれば幸いです。\n","coediting":false,"comments_count":0,"created_at":"2026-05-23T12:51:27+09:00","group":null,"id":"14727dad8c284cd14f86","likes_count":1,"private":false,"reactions_count":0,"stocks_count":2,"tags":[{"name":"JavaScript","versions":[]},{"name":"Vue.js","versions":[]},{"name":"フロントエンド","versions":[]},{"name":"React","versions":[]},{"name":"Next.js","versions":[]}],"title":"【Next.js × TypeScript】3分で使えるようになる入門ガイド（2026年版）","updated_at":"2026-05-23T12:51:27+09:00","url":"https://qiita.com/monotein/items/14727dad8c284cd14f86","user":{"description":"非IT出身。専門用語の壁でプログラミングに挫折した経験から「専門用語なし」メソッドを確立。1200人以上の初心者をフロントエンド開発へ導く。『はじめてつくるReactアプリ with TypeScript』、『動かして学ぶ！Next.js/React開発入門（翔泳社／*韓国でも発売）』など著書多数。","facebook_id":"","followees_count":1,"followers_count":1,"github_login_name":null,"id":"monotein","items_count":17,"linkedin_id":"","location":"","name":"三好 アキ｜専門用語なしでプログラミング","organization":"","permanent_id":4429330,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/4429330/6d0af74f901b2b635b18a7d5878a099040933d53/x_large.png?1778461148","team_only":false,"twitter_screen_name":null,"website_url":"https://monotein.com/business/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:10\"\u003e\n\u003cspan id=\"appvue\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#appvue\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eApp.vue\u003c/h2\u003e\n\u003cp data-sourcepos=\"2:1-7:87\"\u003eVueでは\u003ccode\u003esrc/App.vue\u003c/code\u003eがメインコンポーネント。\u003cbr\u003e\n\u003ccode\u003escript\u003c/code\u003eでTypeScriptを定義し、\u003ccode\u003etemplate\u003c/code\u003eでHTMLブロックを定義する。\u003cbr\u003e\n\u003ccode\u003escript\u003c/code\u003eブロックでは、\u003ccode\u003etemplate\u003c/code\u003eブロックで参照できるコンポーネントをimportできる。\u003cbr\u003e\n\u003ccode\u003etemplate\u003c/code\u003eブロックでは、importしたvueコンポーネントを参照し、HTMLテンプレートに埋め込む事ができる。\u003cbr\u003e\nコンポーネントにはプロパティを指定する事ができる。\u003cbr\u003e\nプロパティにより、コンポーネント間での値の受け渡しが可能。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"8:1-16:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e \u003cspan class=\"na\"\u003elang=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"ts\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"nx\"\u003eCounter\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003e./components/Counter.vue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e \u003cspan class=\"c1\"\u003e// コンポーネントのimport.\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;Counter\u003c/span\u003e \u003cspan class=\"na\"\u003emsg=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"Vite + Vue\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e \u003cspan class=\"c\"\u003e\u0026lt;!-- コンポーネントを埋め込み、msgプロパティで値を指定 --\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"18:1-18:24\"\u003e\n\u003cspan id=\"コンポーネント\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eコンポーネント\u003c/h2\u003e\n\u003cp data-sourcepos=\"19:1-20:289\"\u003eApp.vueから参照される。\u003cbr\u003e\n非常に簡単なコンポーネントとして、msgというプロパティを受け取った値を表示し、counterというリアクティブ値を定義して、アクションにより値の変化が動的に反映できることを確認するvueコンポーネントを定義。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"21:1-44:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e \u003cspan class=\"na\"\u003elang=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"ts\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ecomputed\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"nx\"\u003edefineProps\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003emsg\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003estring\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"c1\"\u003e// msgというstring型のプロパティを宣言\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ecounter\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// counterというリアクティブオブジェクトを定義（初期値は0）\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edoubleCounter\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003ecomputed\u003c/span\u003e\u003cspan class=\"p\"\u003e(()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"nx\"\u003ecounter\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003evalue\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// リアクティブオブジェクトが変化することにより自動実行される関数をcomputed関数で定義\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;section\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"center\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"c\"\u003e\u0026lt;!-- msgプロパティを表示 --\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"nt\"\u003e\u0026lt;div\u0026gt;\u003c/span\u003e\u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003emsg\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n\n        \u003cspan class=\"c\"\u003e\u0026lt;!-- counterを+1するボタン --\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"button\"\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"counter\"\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eclick=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"counter++\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n            Count is \u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003ecounter\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\n        \u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n\n        \u003cspan class=\"c\"\u003e\u0026lt;!-- counter が変化することで自動計算されたdoubleCounterを表示 --\u0026gt;\u003c/span\u003e\n        \u003cspan class=\"nt\"\u003e\u0026lt;div\u0026gt;\u003c/span\u003eDouble count is \u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003edoubleCounter\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/div\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"nt\"\u003e\u0026lt;/section\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"46:1-46:18\"\u003e\n\u003cspan id=\"コンセプト\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B3%E3%83%B3%E3%82%BB%E3%83%97%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eコンセプト\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"47:1-47:33\"\u003e\n\u003cspan id=\"マスタッシュ-mustache\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%9E%E3%82%B9%E3%82%BF%E3%83%83%E3%82%B7%E3%83%A5-mustache\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eマスタッシュ (mustache)\u003c/h3\u003e\n\u003cp data-sourcepos=\"48:1-49:99\"\u003eHTMLベースのテンプレート構文を採用しているVueでは、マスタッシュという二重中括弧で囲った変数をHTMLテンプレートに定義できるようになっている。\u003cbr\u003e\nコンパイルすると、マスタッシュ変数が実際の変数にマッピングされる。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vuejs\" data-sourcepos=\"51:1-59:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e \u003cspan class=\"na\"\u003elang=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"ts\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ecount\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\nCount is \u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003ecount\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"61:1-61:28\"\u003e\n\u003cspan id=\"リアクティビティ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AA%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%93%E3%83%86%E3%82%A3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eリアクティビティ\u003c/h3\u003e\n\u003cp data-sourcepos=\"62:1-64:268\"\u003eHTMLテンプレートに定義したマスタッシュ変数は、そのままではプログラムにより値が変化しても画面上では変化しない。\u003cbr\u003e\nVueでは、プログラムの状態を監視し、値が変化したことを検出した場合に値を更新するという仕組みがある。これをリアクティビティという。\u003cbr\u003e\n\u003ccode\u003eref()\u003c/code\u003eを使ってリアクティブ値としてオブジェクトを生成することで、このオブジェクト値が変化した時に関連する計算処理が自動実行され、マスタッシュで定義された値が画面上で自動更新される。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"vue\" data-sourcepos=\"65:1-76:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e \u003cspan class=\"na\"\u003esetup\u003c/span\u003e \u003cspan class=\"na\"\u003elang=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"ts\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eimport\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ecomputed\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003evue\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ecounter\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eref\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003escript\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"nt\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;button\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"button\"\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"counter\"\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eclick=\u003c/span\u003e\u003cspan class=\"s\"\u003e\"counter++\"\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n  Count is \u003cspan class=\"si\"\u003e{{\u003c/span\u003e \u003cspan class=\"nx\"\u003ecounter\u003c/span\u003e \u003cspan class=\"si\"\u003e}}\u003c/span\u003e\n  \u003cspan class=\"nt\"\u003e\u0026lt;/button\u0026gt;\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n","body":"## App.vue\nVueでは`src/App.vue`がメインコンポーネント。\n`script`でTypeScriptを定義し、`template`でHTMLブロックを定義する。\n`script`ブロックでは、`template`ブロックで参照できるコンポーネントをimportできる。\n`template`ブロックでは、importしたvueコンポーネントを参照し、HTMLテンプレートに埋め込む事ができる。\nコンポーネントにはプロパティを指定する事ができる。\nプロパティにより、コンポーネント間での値の受け渡しが可能。\n``` vue\n\u003cscript setup lang=\"ts\"\u003e\nimport Counter from './components/Counter.vue' // コンポーネントのimport.\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cCounter msg=\"Vite + Vue\"/\u003e \u003c!-- コンポーネントを埋め込み、msgプロパティで値を指定 --\u003e\n\u003c/template\u003e\n```\n\n## コンポーネント\nApp.vueから参照される。\n非常に簡単なコンポーネントとして、msgというプロパティを受け取った値を表示し、counterというリアクティブ値を定義して、アクションにより値の変化が動的に反映できることを確認するvueコンポーネントを定義。\n```vue\n\u003cscript setup lang=\"ts\"\u003e\nimport { ref, computed } from 'vue';\ndefineProps\u003c{ msg: string }\u003e() // msgというstring型のプロパティを宣言\n\nconst counter = ref(0); // counterというリアクティブオブジェクトを定義（初期値は0）\nconst doubleCounter = computed(() =\u003e counter.value * 2); // リアクティブオブジェクトが変化することにより自動実行される関数をcomputed関数で定義\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n    \u003csection id=\"center\"\u003e\n        \u003c!-- msgプロパティを表示 --\u003e\n        \u003cdiv\u003e{{ msg }}\u003c/div\u003e\n\n        \u003c!-- counterを+1するボタン --\u003e\n        \u003cbutton type=\"button\" class=\"counter\" @click=\"counter++\"\u003e\n            Count is {{ counter }}\n        \u003c/button\u003e\n\n        \u003c!-- counter が変化することで自動計算されたdoubleCounterを表示 --\u003e\n        \u003cdiv\u003eDouble count is {{ doubleCounter }}\u003c/div\u003e\n    \u003c/section\u003e\n\u003c/template\u003e\n```\n\n## コンセプト\n### マスタッシュ (mustache)\nHTMLベースのテンプレート構文を採用しているVueでは、マスタッシュという二重中括弧で囲った変数をHTMLテンプレートに定義できるようになっている。\nコンパイルすると、マスタッシュ変数が実際の変数にマッピングされる。\n\n```vuejs\n\u003cscript setup lang=\"ts\"\u003e\nconst count = 0;\n\u003c/script\u003e\n\n\u003ctemplate\u003e\nCount is {{ count }}\n\u003c/template\u003e\n```\n\n### リアクティビティ\nHTMLテンプレートに定義したマスタッシュ変数は、そのままではプログラムにより値が変化しても画面上では変化しない。\nVueでは、プログラムの状態を監視し、値が変化したことを検出した場合に値を更新するという仕組みがある。これをリアクティビティという。\n`ref()`を使ってリアクティブ値としてオブジェクトを生成することで、このオブジェクト値が変化した時に関連する計算処理が自動実行され、マスタッシュで定義された値が画面上で自動更新される。\n```vue\n\u003cscript setup lang=\"ts\"\u003e\nimport { ref, computed } from 'vue';\nconst counter = ref(0);\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cbutton type=\"button\" class=\"counter\" @click=\"counter++\"\u003e\n  Count is {{ counter }}\n  \u003c/button\u003e\n\u003c/template\u003e\n```\n","coediting":false,"comments_count":0,"created_at":"2026-05-23T09:29:49+09:00","group":null,"id":"e3d7f0d900e586763ad2","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Node.js","versions":[]},{"name":"TypeScript","versions":[]},{"name":"Vue.js","versions":[]}],"title":"VueコンポーネントとHTML構文","updated_at":"2026-05-23T09:29:49+09:00","url":"https://qiita.com/masafullversion/items/e3d7f0d900e586763ad2","user":{"description":"掲載している記事は、個人の意見および個人的活動を記したものであり、会社を代表するものではありません","facebook_id":"","followees_count":0,"followers_count":2,"github_login_name":null,"id":"masafullversion","items_count":59,"linkedin_id":"masatoshi-sato-57526b40","location":"","name":"","organization":"","permanent_id":466853,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/profile-images/1563716185","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:83\"\u003e\n\u003cspan id=\"vitevueプロジェクト用テンプレートからプロジェクトを作成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vitevue%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E7%94%A8%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%81%8B%E3%82%89%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eVite+Vueプロジェクト用テンプレートからプロジェクトを作成\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"2:1-4:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003enpm create vite@latest vitevue\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch5 data-sourcepos=\"5:1-5:9\"\u003e\n\u003cspan id=\"npm\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#npm\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003enpm\u003c/h5\u003e\n\u003cul data-sourcepos=\"6:1-8:0\"\u003e\n\u003cli data-sourcepos=\"6:1-6:92\"\u003eNode.jsに付属するパッケージマネージャー（Node Package Manager）の実行\u003c/li\u003e\n\u003cli data-sourcepos=\"7:1-8:0\"\u003e「今からnpmの機能を使うよ」宣言\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch5 data-sourcepos=\"9:1-9:12\"\u003e\n\u003cspan id=\"create\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#create\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003ecreate\u003c/h5\u003e\n\u003cul data-sourcepos=\"10:1-12:0\"\u003e\n\u003cli data-sourcepos=\"10:1-10:104\"\u003eプロジェクト初期化コマンドの実行（npm init というコマンドのエイリアス）\u003c/li\u003e\n\u003cli data-sourcepos=\"11:1-12:0\"\u003e\n\u003ccode\u003enpm create xx\u003c/code\u003e と書くと、npmは自動的に create-xx という名前のプロジェクト作成ツールを検索して実行する\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch5 data-sourcepos=\"13:1-13:17\"\u003e\n\u003cspan id=\"vitelatest\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vitelatest\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003evite@latest\u003c/h5\u003e\n\u003cul data-sourcepos=\"14:1-16:0\"\u003e\n\u003cli data-sourcepos=\"14:1-14:62\"\u003eプロジェクト作成ツールとバージョンの指定\u003c/li\u003e\n\u003cli data-sourcepos=\"15:1-16:0\"\u003e\n\u003ccode\u003enpm create\u003c/code\u003eの仕組みによって、npmはインターネット上から create-vite という「Viteプロジェクト作成専用ツール」の最新版を一時的にダウンロード\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch5 data-sourcepos=\"17:1-17:13\"\u003e\n\u003cspan id=\"vitevue\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#vitevue\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003evitevue\u003c/h5\u003e\n\u003cul data-sourcepos=\"18:1-19:0\"\u003e\n\u003cli data-sourcepos=\"18:1-19:0\"\u003e作成するプロジェクトのディレクトリ名\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"20:1-20:15\"\u003e\n\u003cspan id=\"実行結果\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実行結果\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"21:1-43:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# UIライブラリとしてVueを選択\u003c/span\u003e\nsato@[8:00:11]:~/proj/try% npm create vite@latest vitevue\n\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e npx\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e create-vite vitevue\n\n│\n◆  Select a framework:\n│  ○ Vanilla\n│  ● Vue\n│  ○ React\n│  ○ Preact\n│  ○ Lit\n│  ○ Svelte\n│  ○ Solid\n│  ○ Ember\n│  ○ Qwik\n│  ○ Angular\n│  ○ Marko\n│  ○ Others\n└\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"44:1-63:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# 実行環境や言語の選択でTypeScriptを選択\u003c/span\u003e\nsato@[8:00:11]:~/proj/try/vitevue% npm create vite@latest vitevue\n\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e npx\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e create-vite vitevue\n\n│\n◇  Select a framework:\n│  Vue\n│\n◆  Select a variant:\n│  ● TypeScript\n│  ○ JavaScript\n│  ○ Official Vue Starter ↗\n│  ○ Nuxt ↗ https://nuxt.com\n│  ○ Vike ↗ https://vike.dev\n└\n\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"64:1-81:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# npm installを実行して依存関係のあるライブラリをインストールして実行するかどうか\u003c/span\u003e\nsato@[8:00:11]:~/proj/try/vitevue% npm create vite@latest vitevue\n\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e npx\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e create-vite vitevue\n\n│\n◇  Select a framework:\n│  Vue\n│\n◇  Select a variant:\n│  TypeScript\n│\n◆  Install with npm and start now?\n│  ● Yes / ○ No\n└\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"82:1-131:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# プロジェクトディレクトリを作成し、npm installで依存関係をインストールし、viteを実行\u003c/span\u003e\nsato@[8:00:11]:~/proj/try/vitevue% npm create vite@latest vitevue\n\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e npx\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e create-vite vitevue\n\n│\n◇  Select a framework:\n│  Vue\n│\n◇  Select a variant:\n│  TypeScript\n│\n◇  Install with npm and start now?\n│  Yes\n│\n◇  Scaffolding project \u003cspan class=\"k\"\u003ein\u003c/span\u003e /Users/sato/proj/try/vitevue/vitevue...\n│\n◇  Installing dependencies with npm...\n\nadded 48 packages, and audited 49 packages \u003cspan class=\"k\"\u003ein \u003c/span\u003e8s\n\n9 packages are looking \u003cspan class=\"k\"\u003efor \u003c/span\u003efunding\n  run \u003cspan class=\"sb\"\u003e`\u003c/span\u003enpm fund\u003cspan class=\"sb\"\u003e`\u003c/span\u003e \u003cspan class=\"k\"\u003efor \u003c/span\u003edetails\n\nfound 0 vulnerabilities\n│\n◇  Starting dev server...\n\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e vitevue@0.0.0 dev\n\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e vite\n\n\n  VITE v8.0.13  ready \u003cspan class=\"k\"\u003ein \u003c/span\u003e1200 ms\n\n  ➜  Local:   http://localhost:5173/\n  ➜  Network: use \u003cspan class=\"nt\"\u003e--host\u003c/span\u003e to expose\n  ➜  press h + enter to show \u003cspan class=\"nb\"\u003ehelp\n\u003c/span\u003eh     \n\n  Shortcuts\n  press r + enter to restart the server\n  press u + enter to show server url\n  press o + enter to open \u003cspan class=\"k\"\u003ein \u003c/span\u003ebrowser\n  press c + enter to clear console\n  press q + enter to quit\n\n\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"132:1-132:121\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F466853%2Fbd29d86c-1732-451f-88ae-b2b66629e628.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1aa6e1447c08a8b7c4e9ea641038202f\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F466853%2Fbd29d86c-1732-451f-88ae-b2b66629e628.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=1aa6e1447c08a8b7c4e9ea641038202f\" alt=\"image.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F466853%2Fbd29d86c-1732-451f-88ae-b2b66629e628.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=5e0b0f55470c64c60153e7175f226038 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/bd29d86c-1732-451f-88ae-b2b66629e628.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n","body":"## Vite+Vueプロジェクト用テンプレートからプロジェクトを作成\n```\nnpm create vite@latest vitevue\n```\n##### npm\n* Node.jsに付属するパッケージマネージャー（Node Package Manager）の実行\n* 「今からnpmの機能を使うよ」宣言\n\n##### create\n* プロジェクト初期化コマンドの実行（npm init というコマンドのエイリアス）\n* `npm create xx` と書くと、npmは自動的に create-xx という名前のプロジェクト作成ツールを検索して実行する\n\n##### vite@latest\n* プロジェクト作成ツールとバージョンの指定\n* ```npm create```の仕組みによって、npmはインターネット上から create-vite という「Viteプロジェクト作成専用ツール」の最新版を一時的にダウンロード\n\n##### vitevue\n* 作成するプロジェクトのディレクトリ名\n\n## 実行結果\n```bash\n# UIライブラリとしてVueを選択\nsato@[8:00:11]:~/proj/try% npm create vite@latest vitevue\n\n\u003e npx\n\u003e create-vite vitevue\n\n│\n◆  Select a framework:\n│  ○ Vanilla\n│  ● Vue\n│  ○ React\n│  ○ Preact\n│  ○ Lit\n│  ○ Svelte\n│  ○ Solid\n│  ○ Ember\n│  ○ Qwik\n│  ○ Angular\n│  ○ Marko\n│  ○ Others\n└\n```\n```bash\n# 実行環境や言語の選択でTypeScriptを選択\nsato@[8:00:11]:~/proj/try/vitevue% npm create vite@latest vitevue\n\n\u003e npx\n\u003e create-vite vitevue\n\n│\n◇  Select a framework:\n│  Vue\n│\n◆  Select a variant:\n│  ● TypeScript\n│  ○ JavaScript\n│  ○ Official Vue Starter ↗\n│  ○ Nuxt ↗ https://nuxt.com\n│  ○ Vike ↗ https://vike.dev\n└\n\n```\n```bash\n# npm installを実行して依存関係のあるライブラリをインストールして実行するかどうか\nsato@[8:00:11]:~/proj/try/vitevue% npm create vite@latest vitevue\n\n\u003e npx\n\u003e create-vite vitevue\n\n│\n◇  Select a framework:\n│  Vue\n│\n◇  Select a variant:\n│  TypeScript\n│\n◆  Install with npm and start now?\n│  ● Yes / ○ No\n└\n```\n```bash\n# プロジェクトディレクトリを作成し、npm installで依存関係をインストールし、viteを実行\nsato@[8:00:11]:~/proj/try/vitevue% npm create vite@latest vitevue\n\n\u003e npx\n\u003e create-vite vitevue\n\n│\n◇  Select a framework:\n│  Vue\n│\n◇  Select a variant:\n│  TypeScript\n│\n◇  Install with npm and start now?\n│  Yes\n│\n◇  Scaffolding project in /Users/sato/proj/try/vitevue/vitevue...\n│\n◇  Installing dependencies with npm...\n\nadded 48 packages, and audited 49 packages in 8s\n\n9 packages are looking for funding\n  run `npm fund` for details\n\nfound 0 vulnerabilities\n│\n◇  Starting dev server...\n\n\u003e vitevue@0.0.0 dev\n\u003e vite\n\n\n  VITE v8.0.13  ready in 1200 ms\n\n  ➜  Local:   http://localhost:5173/\n  ➜  Network: use --host to expose\n  ➜  press h + enter to show help\nh     \n\n  Shortcuts\n  press r + enter to restart the server\n  press u + enter to show server url\n  press o + enter to open in browser\n  press c + enter to clear console\n  press q + enter to quit\n\n\n```\n![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/bd29d86c-1732-451f-88ae-b2b66629e628.png)\n","coediting":false,"comments_count":0,"created_at":"2026-05-20T08:50:41+09:00","group":null,"id":"6a681a5f8b7508c58d24","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Node.js","versions":[]},{"name":"TypeScript","versions":[]},{"name":"Vue.js","versions":[]},{"name":"vite","versions":[]}],"title":"ViteでVueプロジェクトの開始コマンド","updated_at":"2026-05-20T08:50:41+09:00","url":"https://qiita.com/masafullversion/items/6a681a5f8b7508c58d24","user":{"description":"掲載している記事は、個人の意見および個人的活動を記したものであり、会社を代表するものではありません","facebook_id":"","followees_count":0,"followers_count":2,"github_login_name":null,"id":"masafullversion","items_count":59,"linkedin_id":"masatoshi-sato-57526b40","location":"","name":"","organization":"","permanent_id":466853,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466853/profile-images/1563716185","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:38\"\u003e\n\u003cspan id=\"reactの第一印象は簡単\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#react%E3%81%AE%E7%AC%AC%E4%B8%80%E5%8D%B0%E8%B1%A1%E3%81%AF%E7%B0%A1%E5%8D%98\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eReactの第一印象は「簡単」\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:133\"\u003eReactを一度でも触った人は、「Reactって思ってたほどは難しくないな」と感じる人が多いようです。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:134\"\u003eその理由は下記コードのように、Reactの\u003ccode\u003ereturn\u003c/code\u003e内はほとんどHTMLと同じように書けてしまうからです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"7:1-17:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eApp\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n        \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003ediv\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n            \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eh1\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eこんにちは\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"sr\"\u003e/h1\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026gt;\n\u003c/span\u003e        \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"sr\"\u003e/div\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026gt;\n\u003c/span\u003e    \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"nx\"\u003eApp\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"19:1-19:62\"\u003e\u003ccode\u003ereturn\u003c/code\u003e以下のコードを取り出してみましょう。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"21:1-25:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003ediv\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n    \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eh1\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"nx\"\u003eこんにちは\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"sr\"\u003e/h1\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026gt;\n\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"sr\"\u003e/div\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026gt;\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"27:1-27:55\"\u003eこれだけ見ればHTMLとまったく同じです。\u003c/p\u003e\n\u003cp data-sourcepos=\"29:1-29:51\"\u003e\u003ccode\u003eindex.html\u003c/code\u003eにコピペしたら機能します。\u003c/p\u003e\n\u003cp data-sourcepos=\"31:1-31:117\"\u003eCSSを書くときも、HTMLの\u003ccode\u003eclass\u003c/code\u003eがReactでは\u003ccode\u003eclassName\u003c/code\u003eになるだけで、他はまったく同じです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"33:1-39:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// HTMLの場合\u003c/span\u003e\n\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003ediv\u003c/span\u003e \u003cspan class=\"kd\"\u003eclass\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003econtainer\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// Reactの場合\u003c/span\u003e\n\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nx\"\u003ediv\u003c/span\u003e \u003cspan class=\"nx\"\u003eclassName\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003econtainer\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"41:1-41:115\"\u003e「Reactは難しい」と思っている人の多くは、実はまだReactを触っていない人たちです。\u003c/p\u003e\n\u003cp data-sourcepos=\"43:1-43:89\"\u003e一度でもReactを触れば「自分にもできそうだ」と思えるでしょう。\u003c/p\u003e\n\u003cp data-sourcepos=\"45:1-45:154\"\u003eしかしそうやってReact入門が無事成功したあとに待っているのが、「やっぱりReactは難しい」というハードルです。\u003c/p\u003e\n\u003cp data-sourcepos=\"47:1-47:36\"\u003eどういうことでしょうか？\u003c/p\u003e\n\u003ch2 data-sourcepos=\"49:1-49:32\"\u003e\n\u003cspan id=\"やっぱりreactは難しい\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%84%E3%81%A3%E3%81%B1%E3%82%8Areact%E3%81%AF%E9%9B%A3%E3%81%97%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eやっぱりReactは難しい\u003c/h2\u003e\n\u003cp data-sourcepos=\"51:1-51:176\"\u003e実は私も、使い始めの時は「Reactって意外に簡単だな」と思ったものの、「いや、やっぱり難しい……」と意見を変えた人間です。\u003c/p\u003e\n\u003cp data-sourcepos=\"53:1-53:100\"\u003eこの理由は「JavaScriptの理解が足りてない」というありがちなものでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"55:1-55:150\"\u003eReactはJavaScriptをベースに作られているので、その基礎部分の知識がないとすぐに壁にぶつかってしまうのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"57:1-57:64\"\u003eなので私はJavaScriptにもどって勉強をしました。\u003c/p\u003e\n\u003cp data-sourcepos=\"59:1-59:223\"\u003eその中で気がついた「Reactを理解する上でもっとも足りていなかったJavaScriptの知識」、逆からいうと「Reactを構成するもっとも重要なJavaScriptの項目」は次の2つです。\u003c/p\u003e\n\u003chr data-sourcepos=\"61:1-62:0\"\u003e\n\u003cul data-sourcepos=\"63:1-65:0\"\u003e\n\u003cli data-sourcepos=\"63:1-63:50\"\u003e\u003cstrong\u003efunction（コンポーネント）の記法\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"64:1-65:0\"\u003e\u003cstrong\u003eイベントの記法\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"66:1-66:35\"\u003e\n\u003cspan id=\"reactをマスターする方法\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#react%E3%82%92%E3%83%9E%E3%82%B9%E3%82%BF%E3%83%BC%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eReactをマスターする方法\u003c/h2\u003e\n\u003cp data-sourcepos=\"68:1-68:112\"\u003eHTML／CSSでもそうですが、同じ結果を得るための方法は一つではなく複数あります。\u003c/p\u003e\n\u003cp data-sourcepos=\"70:1-70:191\"\u003eたとえばCSSを適用するときには、\u003ccode\u003estyle.css\u003c/code\u003eと別ファイルに書く方法もあれば、\u003ccode\u003e\u0026lt;div style=\"color: red;\"\u0026gt;\u003c/code\u003eのようにインラインで書く方法もあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"72:1-72:35\"\u003eこれはReactでも同じです。\u003c/p\u003e\n\u003cp data-sourcepos=\"74:1-74:108\"\u003eたとえばReactコンポーネントの下記2つは、記法が違っていても働きは同じです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"76:1-86:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// 書き方 1\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eApp\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e...\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 書き方 2\u003c/span\u003e\n\u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eApp\u003c/span\u003e\u003cspan class=\"p\"\u003e(){\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n        \u003cspan class=\"p\"\u003e...\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"88:1-88:183\"\u003eしかし、「書き方（記法）が違うだけで、働きや結果は同じなんだ」ということを知らないと、自分の書くコードに自信が持てません。\u003c/p\u003e\n\u003cp data-sourcepos=\"90:1-90:132\"\u003e「なんとなく」の感覚では書けても、本当にそれが正しいのかという確信を得られないからです。\u003c/p\u003e\n\u003cp data-sourcepos=\"92:1-92:171\"\u003e本記事はここで一旦終わりにしますが、本記事の後編では上記の「functionの記法」と「イベントの記法」について紹介します ▼\u003c/p\u003e\n\u003cp data-sourcepos=\"94:1-94:155\"\u003e\u003cem\u003e\u003ca href=\"https://monotein.com/blog/why-react-is-difficult-to-learn-2/?utm_source=72daf1a2ee470319a86d-qiita\" rel=\"nofollow noopener\" target=\"_blank\"\u003emonotein.com/blog/why-react-is-difficult-to-learn-2\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"96:1-96:81\"\u003eこの2つが分かれば、Reactに感じる難しさはグッと減ります。\u003c/p\u003e\n","body":"## Reactの第一印象は「簡単」\n\nReactを一度でも触った人は、「Reactって思ってたほどは難しくないな」と感じる人が多いようです。\n\nその理由は下記コードのように、Reactの`return`内はほとんどHTMLと同じように書けてしまうからです。\n\n```js\nconst App = () =\u003e {\n    return (\n        \u003cdiv\u003e\n            \u003ch1\u003eこんにちは\u003c/h1\u003e\n        \u003c/div\u003e\n    )\n}\n\nexport default App\n```\n\n`return`以下のコードを取り出してみましょう。\n\n```js\n\u003cdiv\u003e\n    \u003ch1\u003eこんにちは\u003c/h1\u003e\n\u003c/div\u003e\n```\n\nこれだけ見ればHTMLとまったく同じです。\n\n`index.html`にコピペしたら機能します。\n\nCSSを書くときも、HTMLの`class`がReactでは`className`になるだけで、他はまったく同じです。\n\n```js\n// HTMLの場合\n\u003cdiv class=\"container\"\u003e\n\n// Reactの場合\n\u003cdiv className=\"container\"\u003e\n```\n\n「Reactは難しい」と思っている人の多くは、実はまだReactを触っていない人たちです。\n\n一度でもReactを触れば「自分にもできそうだ」と思えるでしょう。\n\nしかしそうやってReact入門が無事成功したあとに待っているのが、「やっぱりReactは難しい」というハードルです。\n\nどういうことでしょうか？\n\n## やっぱりReactは難しい\n\n実は私も、使い始めの時は「Reactって意外に簡単だな」と思ったものの、「いや、やっぱり難しい……」と意見を変えた人間です。\n\nこの理由は「JavaScriptの理解が足りてない」というありがちなものでした。\n\nReactはJavaScriptをベースに作られているので、その基礎部分の知識がないとすぐに壁にぶつかってしまうのです。\n\nなので私はJavaScriptにもどって勉強をしました。\n\nその中で気がついた「Reactを理解する上でもっとも足りていなかったJavaScriptの知識」、逆からいうと「Reactを構成するもっとも重要なJavaScriptの項目」は次の2つです。\n\n---\n\n- **function（コンポーネント）の記法**\n- **イベントの記法**\n\n## Reactをマスターする方法\n\nHTML／CSSでもそうですが、同じ結果を得るための方法は一つではなく複数あります。\n\nたとえばCSSを適用するときには、`style.css`と別ファイルに書く方法もあれば、`\u003cdiv style=\"color: red;\"\u003e`のようにインラインで書く方法もあります。\n\nこれはReactでも同じです。\n\nたとえばReactコンポーネントの下記2つは、記法が違っていても働きは同じです。\n\n```js\n// 書き方 1\nconst App = () =\u003e {\n    return(\n        ...\n\n// 書き方 2\nfunction App(){\n    return(\n        ...\n```\n\nしかし、「書き方（記法）が違うだけで、働きや結果は同じなんだ」ということを知らないと、自分の書くコードに自信が持てません。\n\n「なんとなく」の感覚では書けても、本当にそれが正しいのかという確信を得られないからです。\n\n本記事はここで一旦終わりにしますが、本記事の後編では上記の「functionの記法」と「イベントの記法」について紹介します ▼\n\n*[monotein.com/blog/why-react-is-difficult-to-learn-2](https://monotein.com/blog/why-react-is-difficult-to-learn-2/?utm_source=72daf1a2ee470319a86d-qiita)*\n\nこの2つが分かれば、Reactに感じる難しさはグッと減ります。\n","coediting":false,"comments_count":0,"created_at":"2026-05-20T08:50:23+09:00","group":null,"id":"72daf1a2ee470319a86d","likes_count":4,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"JavaScript","versions":[]},{"name":"Vue.js","versions":[]},{"name":"フロントエンド","versions":[]},{"name":"React","versions":[]},{"name":"Next.js","versions":[]}],"title":"Reactを難しく感じる理由と、その解決法【前編】","updated_at":"2026-05-22T19:59:47+09:00","url":"https://qiita.com/monotein/items/72daf1a2ee470319a86d","user":{"description":"非IT出身。専門用語の壁でプログラミングに挫折した経験から「専門用語なし」メソッドを確立。1200人以上の初心者をフロントエンド開発へ導く。『はじめてつくるReactアプリ with TypeScript』、『動かして学ぶ！Next.js/React開発入門（翔泳社／*韓国でも発売）』など著書多数。","facebook_id":"","followees_count":1,"followers_count":1,"github_login_name":null,"id":"monotein","items_count":17,"linkedin_id":"","location":"","name":"三好 アキ｜専門用語なしでプログラミング","organization":"","permanent_id":4429330,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/4429330/6d0af74f901b2b635b18a7d5878a099040933d53/x_large.png?1778461148","team_only":false,"twitter_screen_name":null,"website_url":"https://monotein.com/business/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:14\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h1\u003e\n\u003cp data-sourcepos=\"2:1-3:193\"\u003eこんにちは！普段は実務でJavaやjQueryといった、いわゆるレガシーな技術に触れているエンジニアです。\u003cbr\u003e\n今回は、個人開発でモダンな技術に挑戦しようと思い、\u003cstrong\u003eGo（バックエンド） + Vue.js（フロントエンド）の環境をDocker\u003c/strong\u003e を使って構築しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:282\"\u003eDocker初心者の私が、複数コンテナの連携や、開発を爆速にするための「ホットリロード対応」でつまずいたポイントも含めて、設定手順をまとめました。これから個人開発を始める方の参考になれば幸いです！\u003c/p\u003e\n\u003ch2 data-sourcepos=\"7:1-7:39\"\u003e\n\u003cspan id=\"1-今回構築する環境の構成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E4%BB%8A%E5%9B%9E%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B%E7%92%B0%E5%A2%83%E3%81%AE%E6%A7%8B%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 今回構築する環境の構成\u003c/h2\u003e\n\u003cp data-sourcepos=\"8:1-8:91\"\u003e今回は \u003ccode\u003edocker-compose\u003c/code\u003e を使い、以下の3つのコンテナを立ち上げます。\u003c/p\u003e\n\u003cul data-sourcepos=\"10:1-13:0\"\u003e\n\u003cli data-sourcepos=\"10:1-10:52\"\u003e\n\u003cstrong\u003efrontendコンテナ\u003c/strong\u003e : Vue.js (Vite) / Node.js\u003c/li\u003e\n\u003cli data-sourcepos=\"11:1-11:66\"\u003e\n\u003cstrong\u003ebackendコンテナ\u003c/strong\u003e : Go (Airによるホットリロード)\u003c/li\u003e\n\u003cli data-sourcepos=\"12:1-13:0\"\u003e\n\u003cstrong\u003edbコンテナ\u003c/strong\u003e : PostgreSQL（今回は例として用意。任意でMySQL等に変更可）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"14:1-14:28\"\u003e\n\u003cspan id=\"ディレクトリ構成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E6%A7%8B%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eディレクトリ構成\u003c/h3\u003e\n\u003cp data-sourcepos=\"15:1-15:105\"\u003e全体の見通しを良くするため、以下のようなディレクトリ構造にしています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"Plaintext\" data-sourcepos=\"17:1-29:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003emy-app/\n├── docker-compose.yml\n├── backend/\n│   ├── Dockerfile\n│   ├── main.go\n│   ├── go.mod\n│   └── .air.toml       # Goのホットリロード用設定\n└── frontend/\n    ├── Dockerfile\n    ├── package.json\n    └── (Vueのソースコード一式)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"31:1-31:39\"\u003e\n\u003cspan id=\"2-各種設定ファイルの作成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E5%90%84%E7%A8%AE%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 各種設定ファイルの作成\u003c/h2\u003e\n\u003cp data-sourcepos=\"32:1-33:83\"\u003e\u003cstrong\u003e① backend（Go）の設定\u003c/strong\u003e\u003cbr\u003e\nGoのホットリロードには、定番ツールの \u003cstrong\u003eAir\u003c/strong\u003e を使用します。\u003c/p\u003e\n\u003cp data-sourcepos=\"35:1-35:20\"\u003e\u003ccode\u003ebackend/Dockerfile\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"Dockerfile\" data-sourcepos=\"37:1-53:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"s\"\u003e golang:1.26-alpine\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eWORKDIR\u003c/span\u003e\u003cspan class=\"s\"\u003e /app\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# ライブリロード用のツール「Air」をインストール\u003c/span\u003e\n\u003cspan class=\"k\"\u003eRUN \u003c/span\u003ego \u003cspan class=\"nb\"\u003einstall \u003c/span\u003egithub.com/air-verse/air@latest\n\n\u003cspan class=\"c\"\u003e# go.sumがまだなくてもビルドが通る\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e go.mod go.sum* ./\u003c/span\u003e\n\u003cspan class=\"k\"\u003eRUN \u003c/span\u003ego mod download\n\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e . .\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# Air経由でアプリを起動\u003c/span\u003e\n\u003cspan class=\"k\"\u003eCMD\u003c/span\u003e\u003cspan class=\"s\"\u003e [\"air\", \"-c\", \".air.toml\"]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"54:1-57:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"55:1-56:207\"\u003e\u003cstrong\u003e💡 補足 (.air.toml について)\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003eair init\u003c/code\u003e コマンドで生成されるデフォルトの設定ファイルでOKです。これにより、Goのファイルを書き換えた瞬間に自動で再ビルドが走るようになります。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"59:1-60:47\"\u003e\u003cstrong\u003e② frontend（Vue.js / Vite）の設定\u003c/strong\u003e\u003cbr\u003e\nVue3 + Viteの環境を想定しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"62:1-62:21\"\u003e\u003ccode\u003efrontend/Dockerfile\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"Dockerfile\" data-sourcepos=\"64:1-77:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"s\"\u003e node:lts-alpine\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eWORKDIR\u003c/span\u003e\u003cspan class=\"s\"\u003e /app\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e package*.json ./\u003c/span\u003e\n\u003cspan class=\"k\"\u003eRUN \u003c/span\u003enpm \u003cspan class=\"nb\"\u003einstall\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e\u003cspan class=\"s\"\u003e . .\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eEXPOSE\u003c/span\u003e\u003cspan class=\"s\"\u003e 5173\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eCMD\u003c/span\u003e\u003cspan class=\"s\"\u003e [\"npm\", \"run\", \"dev\"]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"79:1-79:36\"\u003e\n\u003cspan id=\"3-docker-composeyml-の作成\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-docker-composeyml-%E3%81%AE%E4%BD%9C%E6%88%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. docker-compose.yml の作成**\u003c/h2\u003e\n\u003cp data-sourcepos=\"80:1-80:75\"\u003eここが今回の肝となる、全体を繋ぐ設定ファイルです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"YAML\" data-sourcepos=\"82:1-119:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"na\"\u003eservices\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edb\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eimage\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003epostgres:18-alpine\u003c/span\u003e\n    \u003cspan class=\"na\"\u003econtainer_name\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003emy-postgres\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eenvironment\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ePOSTGRES_USER\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003euser\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ePOSTGRES_PASSWORD\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003epassword\u003c/span\u003e\n      \u003cspan class=\"na\"\u003ePOSTGRES_DB\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003emydata\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eports\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e5432:5432\"\u003c/span\u003e\n    \u003cspan class=\"na\"\u003evolumes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003edb-store:/var/lib/postgresql/data\u003c/span\u003e\n\n  \u003cspan class=\"na\"\u003ebackend\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ebuild\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e./backend\u003c/span\u003e\n    \u003cspan class=\"na\"\u003econtainer_name\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003emy-go-backend\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eports\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e8080:8080\"\u003c/span\u003e\n    \u003cspan class=\"na\"\u003evolumes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003e./backend:/app\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edepends_on\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003edb\u003c/span\u003e\n\n  \u003cspan class=\"na\"\u003efrontend\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ebuild\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e./frontend\u003c/span\u003e\n    \u003cspan class=\"na\"\u003econtainer_name\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003emy-vue-frontend\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eports\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e5173:5173\"\u003c/span\u003e\n    \u003cspan class=\"na\"\u003evolumes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003e./frontend:/app\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003e/app/node_modules\u003c/span\u003e \u003cspan class=\"c1\"\u003e# ホスト側のnode_modulesで上書きされるのを防ぐ\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edepends_on\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s\"\u003ebackend\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003evolumes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edb-store\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"120:1-123:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"121:1-122:343\"\u003e\u003cstrong\u003eホットリロードを実現するポイント（Volumes）\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003evolumes\u003c/code\u003e でホスト（自分のPC）のディレクトリとコンテナ内の \u003ccode\u003e/app\u003c/code\u003e をマウント（同期）しています。これにより、自分がPC上でコードを書き換えると、コンテナ内のファイルも瞬時に変更され、VueのViteやGoのAirがそれを検知して自動リロードしてくれます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"125:1-125:85\"\u003e\n\u003cspan id=\"初心者が複数コンテナを繋ぐときにつまずいた3つのポイント\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%88%9D%E5%BF%83%E8%80%85%E3%81%8C%E8%A4%87%E6%95%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%82%92%E7%B9%8B%E3%81%90%E3%81%A8%E3%81%8D%E3%81%AB%E3%81%A4%E3%81%BE%E3%81%9A%E3%81%84%E3%81%9F3%E3%81%A4%E3%81%AE%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e初心者が複数コンテナを繋ぐときにつまずいた3つのポイント\u003c/h2\u003e\n\u003cp data-sourcepos=\"126:1-126:168\"\u003e実務でDocker環境が用意されているときは気づきませんでしたが、一から自分で作ると以下のポイントで派手にハマりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"128:1-130:189\"\u003e\u003cstrong\u003e① Viteのホットリロードが効かない問題（解決策：Vite側の設定）\u003c/strong\u003e\u003cbr\u003e\nDockerにVueを閉じ込めた際、ブラウザをいくら更新してもコードの変更が反映されない現象が起きました。\u003cbr\u003e\n原因は、Viteがファイルの変更を監視（ポーリング）できていなかったためです。\u003ccode\u003evite.config.js\u003c/code\u003e に以下の設定を追加することで解決しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"JavaScript\" data-sourcepos=\"132:1-143:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// frontend/vite.config.js\u003c/span\u003e\n\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"nf\"\u003edefineConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// ...省略...\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eserver\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ewatch\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eusePolling\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Docker環境で変更を検知するために必須\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ehost\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"c1\"\u003e// ホストマシンからのアクセスを許可\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"145:1-148:101\"\u003e\u003cstrong\u003e② フロントエンドからバックエンドへの接続先（localhostの罠）\u003c/strong\u003e\u003cbr\u003e\nVueのコード内からGoのAPIを叩く際、URLを \u003ccode\u003ehttp://localhost:8080/api\u003c/code\u003e と指定していたのですが、これが繋がりませんでした。\u003cbr\u003e\n\u003cstrong\u003e「Dockerコンテナ内における \u003ccode\u003elocalhost\u003c/code\u003e は、そのコンテナ自身を指す」\u003c/strong\u003e という基本を見落としていました。\u003cbr\u003e\n\u003ccode\u003edocker-compose\u003c/code\u003e 内で動いているコンテナ同士は、サービス名で通信できます。\u003c/p\u003e\n\u003cul data-sourcepos=\"150:1-153:0\"\u003e\n\u003cli data-sourcepos=\"150:1-150:35\"\u003e❌ 誤：\u003ccode\u003ehttp://localhost:8080\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"151:1-153:0\"\u003e⭕ 正：\u003ccode\u003ehttp://backend:8080\u003c/code\u003e （コンテナ間通信の場合）\u003cbr\u003e\n※ただし、フロントエンドが「ブラウザ側（クライアントサイド）」で実行されてAPIを叩く場合は、ホスト（PC）から見た \u003ccode\u003ehttp://localhost:8080\u003c/code\u003e で良いケースもあります。自分がどちらの文脈でリクエストを送っているかの整理が大切でした。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"154:1-156:181\"\u003e\u003cstrong\u003e③ node_modules が消える・バグる問題\u003c/strong\u003e\u003cbr\u003e\nローカル環境（ホスト側）に \u003ccode\u003enode_modules\u003c/code\u003e がない状態でボリュームマウントをすると、コンテナ内で \u003ccode\u003enpm install\u003c/code\u003e した綺麗な \u003ccode\u003enode_modules\u003c/code\u003e が空のディレクトリで上書きされて消えてしまう罠があります。\u003cbr\u003e\n\u003ccode\u003edocker-compose.yml\u003c/code\u003e で \u003ccode\u003e- /app/node_modules\u003c/code\u003e という「匿名ボリューム」を1行足すことで、コンテナ内の \u003ccode\u003enode_modules\u003c/code\u003e を保護することができます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"158:1-158:24\"\u003e\n\u003cspan id=\"4-起動してみる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E8%B5%B7%E5%8B%95%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. 起動してみる\u003c/h2\u003e\n\u003cp data-sourcepos=\"159:1-159:120\"\u003e設定が完了したら、プロジェクトのルートディレクトリで以下のコマンドを実行します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"Bash\" data-sourcepos=\"161:1-163:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003edocker-compose up \u003cspan class=\"nt\"\u003e--build\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"164:1-165:157\"\u003eこれだけで、PostgreSQL、Go、Vue.jsのすべてのコンテナが立ち上がります。\u003cbr\u003e\nブラウザで \u003ccode\u003ehttp://localhost:5173\u003c/code\u003e にアクセスし、Vueのコードを書き換えて一瞬で画面が変わったときは感動モノでした！\u003c/p\u003e\n\u003ch2 data-sourcepos=\"167:1-167:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"168:1-168:293\"\u003e実務のJavaやjQueryの環境では、Weblogicを再起動したり、画面をF5で手動リロードしたりするのが当たり前になっていましたが、Docker + モダン技術による「コードを書いたら即座に反映される開発体験」は本当に最高です。\u003c/p\u003e\n\u003cp data-sourcepos=\"170:1-170:180\"\u003e環境構築でつまずくポイントさえ押さえれば、個人開発のスタートダッシュが非常にスムーズになります。ぜひ試してみてください！\u003c/p\u003e\n","body":"# はじめに\nこんにちは！普段は実務でJavaやjQueryといった、いわゆるレガシーな技術に触れているエンジニアです。\n今回は、個人開発でモダンな技術に挑戦しようと思い、**Go（バックエンド） + Vue.js（フロントエンド）の環境をDocker** を使って構築しました。\n\nDocker初心者の私が、複数コンテナの連携や、開発を爆速にするための「ホットリロード対応」でつまずいたポイントも含めて、設定手順をまとめました。これから個人開発を始める方の参考になれば幸いです！\n\n## 1. 今回構築する環境の構成\n今回は `docker-compose` を使い、以下の3つのコンテナを立ち上げます。\n\n- **frontendコンテナ** : Vue.js (Vite) / Node.js\n- **backendコンテナ** : Go (Airによるホットリロード)\n- **dbコンテナ** : PostgreSQL（今回は例として用意。任意でMySQL等に変更可）\n\n### ディレクトリ構成\n全体の見通しを良くするため、以下のようなディレクトリ構造にしています。\n\n```Plaintext\nmy-app/\n├── docker-compose.yml\n├── backend/\n│   ├── Dockerfile\n│   ├── main.go\n│   ├── go.mod\n│   └── .air.toml       # Goのホットリロード用設定\n└── frontend/\n    ├── Dockerfile\n    ├── package.json\n    └── (Vueのソースコード一式)\n```\n\n## 2. 各種設定ファイルの作成\n**① backend（Go）の設定**\nGoのホットリロードには、定番ツールの **Air** を使用します。\n\n`backend/Dockerfile`\n\n```Dockerfile\nFROM golang:1.26-alpine\n\nWORKDIR /app\n\n# ライブリロード用のツール「Air」をインストール\nRUN go install github.com/air-verse/air@latest\n\n# go.sumがまだなくてもビルドが通る\nCOPY go.mod go.sum* ./\nRUN go mod download\n\nCOPY . .\n\n# Air経由でアプリを起動\nCMD [\"air\", \"-c\", \".air.toml\"]\n```\n:::note info\n**💡 補足 (.air.toml について)**\n`air init` コマンドで生成されるデフォルトの設定ファイルでOKです。これにより、Goのファイルを書き換えた瞬間に自動で再ビルドが走るようになります。\n:::\n\n**② frontend（Vue.js / Vite）の設定**\nVue3 + Viteの環境を想定しています。\n\n`frontend/Dockerfile`\n\n```Dockerfile\nFROM node:lts-alpine\n\nWORKDIR /app\n\nCOPY package*.json ./\nRUN npm install\n\nCOPY . .\n\nEXPOSE 5173\n\nCMD [\"npm\", \"run\", \"dev\"]\n```\n\n## 3. docker-compose.yml の作成**\nここが今回の肝となる、全体を繋ぐ設定ファイルです。\n\n```YAML\nservices:\n  db:\n    image: postgres:18-alpine\n    container_name: my-postgres\n    environment:\n      POSTGRES_USER: user\n      POSTGRES_PASSWORD: password\n      POSTGRES_DB: mydata\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - db-store:/var/lib/postgresql/data\n\n  backend:\n    build: ./backend\n    container_name: my-go-backend\n    ports:\n      - \"8080:8080\"\n    volumes:\n      - ./backend:/app\n    depends_on:\n      - db\n\n  frontend:\n    build: ./frontend\n    container_name: my-vue-frontend\n    ports:\n      - \"5173:5173\"\n    volumes:\n      - ./frontend:/app\n      - /app/node_modules # ホスト側のnode_modulesで上書きされるのを防ぐ\n    depends_on:\n      - backend\n\nvolumes:\n  db-store:\n```\n:::note info\n**ホットリロードを実現するポイント（Volumes）**\n`volumes` でホスト（自分のPC）のディレクトリとコンテナ内の `/app` をマウント（同期）しています。これにより、自分がPC上でコードを書き換えると、コンテナ内のファイルも瞬時に変更され、VueのViteやGoのAirがそれを検知して自動リロードしてくれます。\n:::\n\n## 初心者が複数コンテナを繋ぐときにつまずいた3つのポイント\n実務でDocker環境が用意されているときは気づきませんでしたが、一から自分で作ると以下のポイントで派手にハマりました。\n\n**① Viteのホットリロードが効かない問題（解決策：Vite側の設定）**\nDockerにVueを閉じ込めた際、ブラウザをいくら更新してもコードの変更が反映されない現象が起きました。\n原因は、Viteがファイルの変更を監視（ポーリング）できていなかったためです。`vite.config.js` に以下の設定を追加することで解決しました。\n\n```JavaScript\n// frontend/vite.config.js\nexport default defineConfig({\n  // ...省略...\n  server: {\n    watch: {\n      usePolling: true, // Docker環境で変更を検知するために必須\n    },\n    host: true, // ホストマシンからのアクセスを許可\n  },\n})\n```\n\n**② フロントエンドからバックエンドへの接続先（localhostの罠）**\nVueのコード内からGoのAPIを叩く際、URLを `http://localhost:8080/api` と指定していたのですが、これが繋がりませんでした。\n**「Dockerコンテナ内における `localhost` は、そのコンテナ自身を指す」** という基本を見落としていました。\n`docker-compose` 内で動いているコンテナ同士は、サービス名で通信できます。\n\n- ❌ 誤：`http://localhost:8080`\n- ⭕ 正：`http://backend:8080` （コンテナ間通信の場合）\n※ただし、フロントエンドが「ブラウザ側（クライアントサイド）」で実行されてAPIを叩く場合は、ホスト（PC）から見た `http://localhost:8080` で良いケースもあります。自分がどちらの文脈でリクエストを送っているかの整理が大切でした。\n\n**③ node_modules が消える・バグる問題**\nローカル環境（ホスト側）に `node_modules` がない状態でボリュームマウントをすると、コンテナ内で `npm install` した綺麗な `node_modules` が空のディレクトリで上書きされて消えてしまう罠があります。\n`docker-compose.yml` で `- /app/node_modules` という「匿名ボリューム」を1行足すことで、コンテナ内の `node_modules` を保護することができます。\n\n## 4. 起動してみる\n設定が完了したら、プロジェクトのルートディレクトリで以下のコマンドを実行します。\n\n```Bash\ndocker-compose up --build\n```\nこれだけで、PostgreSQL、Go、Vue.jsのすべてのコンテナが立ち上がります。\nブラウザで `http://localhost:5173` にアクセスし、Vueのコードを書き換えて一瞬で画面が変わったときは感動モノでした！\n\n## まとめ\n実務のJavaやjQueryの環境では、Weblogicを再起動したり、画面をF5で手動リロードしたりするのが当たり前になっていましたが、Docker + モダン技術による「コードを書いたら即座に反映される開発体験」は本当に最高です。\n\n環境構築でつまずくポイントさえ押さえれば、個人開発のスタートダッシュが非常にスムーズになります。ぜひ試してみてください！\n","coediting":false,"comments_count":0,"created_at":"2026-05-19T17:24:13+09:00","group":null,"id":"12d3738a6c5bfab75b23","likes_count":2,"private":false,"reactions_count":0,"stocks_count":2,"tags":[{"name":"Go","versions":[]},{"name":"Docker","versions":[]},{"name":"Vue.js","versions":[]},{"name":"docker-compose","versions":[]},{"name":"個人開発","versions":[]}],"title":"【個人開発】Go + Vue.js の開発環境をDockerでサクッと構築する（ホットリロード対応）","updated_at":"2026-05-19T17:24:13+09:00","url":"https://qiita.com/doc_lab/items/12d3738a6c5bfab75b23","user":{"description":null,"facebook_id":null,"followees_count":4,"followers_count":3,"github_login_name":null,"id":"doc_lab","items_count":34,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4295959,"profile_image_url":"https://lh3.googleusercontent.com/a/ACg8ocJiYTRqCm5LhnBc5n5i0nHw2f3q4i-MZUmJudqd17DyFWHQ5A=s96-c","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:45\"\u003e\n\u003cspan id=\"react現場ではtypescriptが当たり前\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#react%E7%8F%BE%E5%A0%B4%E3%81%A7%E3%81%AFtypescript%E3%81%8C%E5%BD%93%E3%81%9F%E3%82%8A%E5%89%8D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eReact現場ではTypeScriptが当たり前\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:105\"\u003e実際の開発現場では、ReactとTypeScriptを使うことがデフォルトになっています。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:34\"\u003eJavaScriptではありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:160\"\u003eしかし、これまで私が教えてきたビギナーの方々を見ていて、TypeScriptを初心者が使うのはハードルが高いと感じます。\u003c/p\u003e\n\u003cp data-sourcepos=\"9:1-9:34\"\u003e次の2つの理由からです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"11:1-11:51\"\u003e\n\u003cspan id=\"理由１メリットがわかりにくい\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%90%86%E7%94%B1%EF%BC%91%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88%E3%81%8C%E3%82%8F%E3%81%8B%E3%82%8A%E3%81%AB%E3%81%8F%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【理由１】メリットがわかりにくい\u003c/h2\u003e\n\u003cp data-sourcepos=\"13:1-13:113\"\u003eJavaScriptではなくTypeScriptが使われるのは、当然なんらかのメリットがあるからです。\u003c/p\u003e\n\u003cp data-sourcepos=\"15:1-15:139\"\u003eそこでネット記事などに目を通すと、「TypeScriptのメリット」として次のようなことが書かれています。\u003c/p\u003e\n\u003chr data-sourcepos=\"17:1-18:0\"\u003e\n\u003cul data-sourcepos=\"19:1-22:0\"\u003e\n\u003cli data-sourcepos=\"19:1-19:29\"\u003eアプリが堅牢になる\u003c/li\u003e\n\u003cli data-sourcepos=\"20:1-20:32\"\u003e開発スピードが上がる\u003c/li\u003e\n\u003cli data-sourcepos=\"21:1-22:0\"\u003eエラーが減る\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"23:1-24:0\"\u003e\n\u003cp data-sourcepos=\"25:1-25:114\"\u003eビギナー向けを謳（うた）う記事を見ても、このようなことばかり書いてあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"27:1-27:96\"\u003eしかしこういうメリットは、ビギナーには一番理解しにくいものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"29:1-29:207\"\u003e私自身もかつてそうでしたが、ビギナーのうちから「バグがないように」や「エラーがないように」といったことを意識している人はほとんどいません。\u003c/p\u003e\n\u003cp data-sourcepos=\"31:1-31:160\"\u003e初心者のうちは、「とにかくアプリが完成すればOK」「とにかくアプリが動けばOK」というのがゴール地点になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"33:1-33:42\"\u003eそして実際それでいいのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"35:1-35:264\"\u003eビギナーにとって優先事項は、まずはとにかく完成させること、教材をひとつ終えること、そして「ここまで自力でできた！」という達成感と「もっと知りたい！」という向上心を得ることです。\u003c/p\u003e\n\u003cp data-sourcepos=\"37:1-37:175\"\u003eしかしTypeScriptを使っても、アプリの見た目が良くなるわけでもなければ、アプリの動くスピードが速くなるわけでもありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"39:1-39:144\"\u003eその一方で、書くコードの量は増え、エラーの発生も多くなるため、開発に時間がかかるようになります。\u003c/p\u003e\n\u003cp data-sourcepos=\"41:1-41:45\"\u003eメリットがわかりづらいのです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"43:1-43:57\"\u003e\n\u003cspan id=\"理由２型の概念に馴染みがない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%90%86%E7%94%B1%EF%BC%92%E5%9E%8B%E3%81%AE%E6%A6%82%E5%BF%B5%E3%81%AB%E9%A6%B4%E6%9F%93%E3%81%BF%E3%81%8C%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e【理由２】「型」の概念に馴染みがない\u003c/h2\u003e\n\u003cp data-sourcepos=\"45:1-45:119\"\u003eTypeScriptは、「静的型付け言語」や「型（かた）のついたJavaScript」とよく紹介されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"47:1-47:84\"\u003eしかし、これだけを聞いて意味をつかめる初心者はいません。\u003c/p\u003e\n\u003cp data-sourcepos=\"49:1-49:103\"\u003e「型」という概念が、HTML／CSS開発ではまったく出てこないものだからです。\u003c/p\u003e\n\u003cp data-sourcepos=\"51:1-51:169\"\u003eまた、JavaScript（or jQuery）を使ったことが多少はあっても、「型」を意識する機会がほとんどないことも理由としてあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"53:1-53:131\"\u003e実はこの「型」は、JavaやC言語など他の言語でも出てくるプログラミングの基本コンセプトです。\u003c/p\u003e\n\u003cp data-sourcepos=\"55:1-55:110\"\u003eそして、これを意識して開発していくことが今のReact開発の潮流になっています。\u003c/p\u003e\n\u003cp data-sourcepos=\"57:1-57:137\"\u003eJavaScriptの「型」をより意識してコードを書けるTypeScriptの導入が進んでいるのは、そのためなのです。\u003c/p\u003e\n\u003chr data-sourcepos=\"59:1-60:0\"\u003e\n\u003cp data-sourcepos=\"61:1-61:97\"\u003e型について少し見てみましょう（*ここは読み飛ばしても大丈夫です）。\u003c/p\u003e\n\u003cp data-sourcepos=\"63:1-63:61\"\u003eたとえばJavaScriptでは、次のように書けます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"65:1-70:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// JavaScriptの場合\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003edata\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eHello\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"nx\"\u003edata\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e123\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e        \u003cspan class=\"c1\"\u003e// エラーなし\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"72:1-72:64\"\u003eTypeScriptで同じように書くと、エラーが出ます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"74:1-79:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e// TypeScriptの場合\u003c/span\u003e\n\n\u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003estring\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eHello\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"nx\"\u003edata\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e123\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e        \u003cspan class=\"c1\"\u003e// エラー: string型にnumber型は代入不可\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"81:1-81:61\"\u003eTypeScriptでは型を合致させる必要があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"83:1-83:81\"\u003eこの例でエラーを消すには、たとえば次のように書けます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"js\" data-sourcepos=\"85:1-88:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003estring\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eHello\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"nx\"\u003edata\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e123\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e       \u003cspan class=\"c1\"\u003e// \"\"を使うことでnumber型をstring型に変換\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"90:1-90:34\"\u003e\n\u003cspan id=\"typescriptを楽に学ぶコツ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#typescript%E3%82%92%E6%A5%BD%E3%81%AB%E5%AD%A6%E3%81%B6%E3%82%B3%E3%83%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eTypeScriptを楽に学ぶコツ\u003c/h2\u003e\n\u003cp data-sourcepos=\"92:1-92:157\"\u003eTypeScriptは、コード量が多い中〜大規模アプリ、さらに複数の開発者が関わるチーム開発時にメリットを発揮します。\u003c/p\u003e\n\u003cp data-sourcepos=\"94:1-94:165\"\u003e商業用のアプリはチームで開発するのが一般的なので、TypeScriptが実際のReact開発現場ではデフォルトとなっているのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"96:1-96:172\"\u003eでは、大規模アプリやチーム開発で真価を発揮するTypeScriptを、ビギナーがひとりで勉強するにはどうすればいいのでしょうか？\u003c/p\u003e\n\u003cp data-sourcepos=\"98:1-98:30\"\u003e「慣れること」です。\u003c/p\u003e\n\u003cp data-sourcepos=\"100:1-100:100\"\u003eコード量が少ない簡単なアプリでTypeScriptを使って、慣れていくことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"102:1-102:103\"\u003eそして徐々に、より規模の大きなアプリでもTypeScriptを使っていきましょう。\u003c/p\u003e\n","body":"## React現場ではTypeScriptが当たり前\n\n実際の開発現場では、ReactとTypeScriptを使うことがデフォルトになっています。\n\nJavaScriptではありません。\n\nしかし、これまで私が教えてきたビギナーの方々を見ていて、TypeScriptを初心者が使うのはハードルが高いと感じます。\n\n次の2つの理由からです。\n\n## 【理由１】メリットがわかりにくい\n\nJavaScriptではなくTypeScriptが使われるのは、当然なんらかのメリットがあるからです。\n\nそこでネット記事などに目を通すと、「TypeScriptのメリット」として次のようなことが書かれています。\n\n---\n\n- アプリが堅牢になる\n- 開発スピードが上がる\n- エラーが減る\n\n---\n\nビギナー向けを謳（うた）う記事を見ても、このようなことばかり書いてあります。\n\nしかしこういうメリットは、ビギナーには一番理解しにくいものです。\n\n私自身もかつてそうでしたが、ビギナーのうちから「バグがないように」や「エラーがないように」といったことを意識している人はほとんどいません。\n\n初心者のうちは、「とにかくアプリが完成すればOK」「とにかくアプリが動けばOK」というのがゴール地点になります。\n\nそして実際それでいいのです。\n\nビギナーにとって優先事項は、まずはとにかく完成させること、教材をひとつ終えること、そして「ここまで自力でできた！」という達成感と「もっと知りたい！」という向上心を得ることです。\n\nしかしTypeScriptを使っても、アプリの見た目が良くなるわけでもなければ、アプリの動くスピードが速くなるわけでもありません。\n\nその一方で、書くコードの量は増え、エラーの発生も多くなるため、開発に時間がかかるようになります。\n\nメリットがわかりづらいのです。\n\n## 【理由２】「型」の概念に馴染みがない\n\nTypeScriptは、「静的型付け言語」や「型（かた）のついたJavaScript」とよく紹介されます。\n\nしかし、これだけを聞いて意味をつかめる初心者はいません。\n\n「型」という概念が、HTML／CSS開発ではまったく出てこないものだからです。\n\nまた、JavaScript（or jQuery）を使ったことが多少はあっても、「型」を意識する機会がほとんどないことも理由としてあります。\n\n実はこの「型」は、JavaやC言語など他の言語でも出てくるプログラミングの基本コンセプトです。\n\nそして、これを意識して開発していくことが今のReact開発の潮流になっています。\n\nJavaScriptの「型」をより意識してコードを書けるTypeScriptの導入が進んでいるのは、そのためなのです。\n\n---\n\n型について少し見てみましょう（*ここは読み飛ばしても大丈夫です）。\n\nたとえばJavaScriptでは、次のように書けます。\n\n```js\n// JavaScriptの場合\n\nlet data = \"Hello\";\ndata = 123;        // エラーなし\n```\n\nTypeScriptで同じように書くと、エラーが出ます。\n\n```js\n// TypeScriptの場合\n\nlet data: string = \"Hello\";\ndata = 123;        // エラー: string型にnumber型は代入不可\n```\n\nTypeScriptでは型を合致させる必要があります。\n\nこの例でエラーを消すには、たとえば次のように書けます。\n\n```js\nlet data: string = \"Hello\";\ndata = \"123\";       // \"\"を使うことでnumber型をstring型に変換\n```\n\n## TypeScriptを楽に学ぶコツ\n\nTypeScriptは、コード量が多い中〜大規模アプリ、さらに複数の開発者が関わるチーム開発時にメリットを発揮します。\n\n商業用のアプリはチームで開発するのが一般的なので、TypeScriptが実際のReact開発現場ではデフォルトとなっているのです。\n\nでは、大規模アプリやチーム開発で真価を発揮するTypeScriptを、ビギナーがひとりで勉強するにはどうすればいいのでしょうか？\n\n「慣れること」です。\n\nコード量が少ない簡単なアプリでTypeScriptを使って、慣れていくことです。\n\nそして徐々に、より規模の大きなアプリでもTypeScriptを使っていきましょう。\n","coediting":false,"comments_count":0,"created_at":"2026-05-19T08:28:07+09:00","group":null,"id":"6514b32f7316c828e0da","likes_count":2,"private":false,"reactions_count":0,"stocks_count":1,"tags":[{"name":"ウェブ開発","versions":[]},{"name":"TypeScript","versions":[]},{"name":"Vue.js","versions":[]},{"name":"フロントエンド","versions":[]},{"name":"React","versions":[]}],"title":" ビギナーがTypeScriptを難しく感じる理由と、その解決法","updated_at":"2026-05-22T20:07:48+09:00","url":"https://qiita.com/monotein/items/6514b32f7316c828e0da","user":{"description":"非IT出身。専門用語の壁でプログラミングに挫折した経験から「専門用語なし」メソッドを確立。1200人以上の初心者をフロントエンド開発へ導く。『はじめてつくるReactアプリ with TypeScript』、『動かして学ぶ！Next.js/React開発入門（翔泳社／*韓国でも発売）』など著書多数。","facebook_id":"","followees_count":1,"followers_count":1,"github_login_name":null,"id":"monotein","items_count":17,"linkedin_id":"","location":"","name":"三好 アキ｜専門用語なしでプログラミング","organization":"","permanent_id":4429330,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/4429330/6d0af74f901b2b635b18a7d5878a099040933d53/x_large.png?1778461148","team_only":false,"twitter_screen_name":null,"website_url":"https://monotein.com/business/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false},{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:55\"\u003e\n\u003cspan id=\"番外編reactとvueの覇権spa時代へ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%95%AA%E5%A4%96%E7%B7%A8react%E3%81%A8vue%E3%81%AE%E8%A6%87%E6%A8%A9spa%E6%99%82%E4%BB%A3%E3%81%B8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e番外編⑤：ReactとVueの覇権（SPA時代へ）\u003c/h1\u003e\n\u003cp data-sourcepos=\"2:1-2:85\"\u003e\u003cstrong\u003e〜jQueryの終焉と「仮想DOM」がもたらした現代Webの最終形態〜\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"4:1-4:48\"\u003e\n\u003cspan id=\"導入つぎはぎだらけの限界\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%B0%8E%E5%85%A5%E3%81%A4%E3%81%8E%E3%81%AF%E3%81%8E%E3%81%A0%E3%82%89%E3%81%91%E3%81%AE%E9%99%90%E7%95%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e導入：「つぎはぎだらけ」の限界\u003c/h2\u003e\n\u003cp data-sourcepos=\"6:1-7:322\"\u003eこれまでの連載で、フロントエンド（jQuery）、バックエンド（Rails）、デザイン（Bootstrap）、そして開発環境（Node.js）の進化を見てきました。\u003cbr\u003e\n2010年代中盤、ブラウザやスマートフォンの性能が上がり、Webサイトはついに \u003cstrong\u003e「ページを移動せず、ひとつの画面のままアプリのように動く（SPA：シングルページアプリケーション）」\u003c/strong\u003e のが当たり前の時代に突入しようとしていました。\u003c/p\u003e\n\u003cp data-sourcepos=\"9:1-10:128\"\u003eしかし、ここで開発者たちは「ある絶望的な壁」にぶち当たります。\u003cbr\u003e\n\u003cstrong\u003e「jQueryで複雑なアプリを作ろうとすると、コードが地獄のように絡まる」\u003c/strong\u003e という問題です。\u003c/p\u003e\n\u003cp data-sourcepos=\"12:1-13:309\"\u003e例えば、「いいねボタンを押したら、ハートの色が赤くなり、全体のいいね数が+1され、マイページのリストにもその記事が追加される」という処理。\u003cbr\u003e\njQuery（DOMを直接書き換える手法）でこれを書こうとすると、「あっちの要素を探して色を変え、こっちの数字を書き換え……」と、プログラムが迷路のように入り組んでしまいます（これを \u003cstrong\u003eスパゲッティコード\u003c/strong\u003e と呼びます）。\u003c/p\u003e\n\u003cp data-sourcepos=\"15:1-16:166\"\u003e「もう、手作業で画面（DOM）をいじるのは限界だ……」\u003cbr\u003e\nそんな悲鳴が上がっていた2013年、巨大SNS企業・Facebook（現Meta）から、Webの常識を根底から覆す「魔法」が公開されました。\u003c/p\u003e\n\u003chr data-sourcepos=\"18:1-19:0\"\u003e\n\u003ch2 data-sourcepos=\"20:1-20:50\"\u003e\n\u003cspan id=\"1-2013年facebookからの刺客react\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-2013%E5%B9%B4facebook%E3%81%8B%E3%82%89%E3%81%AE%E5%88%BA%E5%AE%A2react\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 2013年、Facebookからの刺客「React」\u003c/h2\u003e\n\u003cp data-sourcepos=\"22:1-23:221\"\u003eFacebookがオープンソースとして発表したJavaScriptライブラリ、それが \u003cstrong\u003e「React（リアクト）」\u003c/strong\u003e です。\u003cbr\u003e\nFacebookやInstagramのような「超複雑で、画面のあちこちがリアルタイムに更新される」システムを破綻させずに作るため、彼ら自身が生み出した社内用ツールでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"25:1-25:112\"\u003eReactがもたらした最大の革命。それは \u003cstrong\u003e「仮想DOM（Virtual DOM）」\u003c/strong\u003e という概念です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"27:1-27:34\"\u003e\n\u003cspan id=\"仮想domという魔法\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BB%AE%E6%83%B3dom%E3%81%A8%E3%81%84%E3%81%86%E9%AD%94%E6%B3%95\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e「仮想DOM」という魔法\u003c/h3\u003e\n\u003cp data-sourcepos=\"28:1-28:329\"\u003eこれまでのjQueryは、ブラウザに表示されている実際の画面（リアルDOM）を直接いじっていました。これは例えるなら、\u003cstrong\u003e「完成したキャンバスの絵に、直接筆を入れて描き直す」\u003c/strong\u003e ようなもので、非常に神経を使い、動作も遅くなりがちでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"30:1-31:150\"\u003eReactは違います。\u003cbr\u003e\nブラウザの裏側（メモリ上）に、見えない \u003cstrong\u003e「もうひとつの画面（仮想DOM）」\u003c/strong\u003e のコピーを作っておくのです。\u003c/p\u003e\n\u003col data-sourcepos=\"35:1-39:0\"\u003e\n\u003cli data-sourcepos=\"35:1-35:51\"\u003eデータ（いいね数など）が変化する\u003c/li\u003e\n\u003cli data-sourcepos=\"36:1-36:146\"\u003eReactは、まず裏側の「仮想DOM」を丸ごと新しく作り直す（これはメモリ上の計算なので一瞬で終わります）\u003c/li\u003e\n\u003cli data-sourcepos=\"37:1-37:105\"\u003eそして、新しい仮想DOMと、古い画面を \u003cstrong\u003e「間違い探し」のように比較\u003c/strong\u003e する\u003c/li\u003e\n\u003cli data-sourcepos=\"38:1-39:0\"\u003e変更があった部分（ハートの色と数字） \u003cstrong\u003eだけ\u003c/strong\u003e を、実際の画面にパッチを当てるように一瞬で反映させる\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"40:1-41:224\"\u003e開発者は「いいねボタンが押されたら、こう画面を書き換えろ」という面倒な手順（DOM操作）を書く必要がなくなりました。\u003cbr\u003e\nただ \u003cstrong\u003e「今の状態（データ）はこうです」と宣言するだけ\u003c/strong\u003e で、あとはReactが勝手に「間違い探し」をして、最も効率的に画面を更新してくれるようになったのです。\u003c/p\u003e\n\u003chr data-sourcepos=\"43:1-44:0\"\u003e\n\u003ch2 data-sourcepos=\"45:1-45:75\"\u003e\n\u003cspan id=\"2-コンポーネント指向レゴブロックでwebを作る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E6%8C%87%E5%90%91%E3%83%AC%E3%82%B4%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E3%81%A7web%E3%82%92%E4%BD%9C%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. コンポーネント指向：「レゴブロック」でWebを作る\u003c/h2\u003e\n\u003cp data-sourcepos=\"47:1-47:98\"\u003eReactが持ち込んだもう一つの革命が、 \u003cstrong\u003e「コンポーネント指向」\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"49:1-50:107\"\u003eそれまで、Webサイトは「1つの巨大なHTMLファイル」で作るのが普通でした。\u003cbr\u003e\nしかしReactでは、画面を細かな \u003cstrong\u003e「部品（コンポーネント）」\u003c/strong\u003e に分割します。\u003c/p\u003e\n\u003cul data-sourcepos=\"52:1-56:0\"\u003e\n\u003cli data-sourcepos=\"52:1-52:23\"\u003eヘッダーの部品\u003c/li\u003e\n\u003cli data-sourcepos=\"53:1-53:26\"\u003eサイドバーの部品\u003c/li\u003e\n\u003cli data-sourcepos=\"54:1-54:35\"\u003e「いいねボタン」の部品\u003c/li\u003e\n\u003cli data-sourcepos=\"55:1-56:0\"\u003e「ユーザーアイコン」の部品\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"57:1-58:199\"\u003eこれらはすべて独立したプログラム（JavaScript）として作られます。開発者は、この \u003cstrong\u003e「レゴブロック」を組み合わせるだけ\u003c/strong\u003e で、複雑なWeb画面を組み上げていくのです。\u003cbr\u003e\n「いいねボタン」のプログラムを1つ作っておけば、記事一覧にも、詳細ページにも、マイページにも、ブロックをポンと置くだけで使い回せます。\u003c/p\u003e\n\u003cp data-sourcepos=\"60:1-60:255\"\u003eこの「仮想DOM」と「コンポーネント指向」の組み合わせにより、コードのスパゲッティ化は完全に解消され、複数人のチームで巨大なWebアプリを開発することが飛躍的に簡単になりました。\u003c/p\u003e\n\u003chr data-sourcepos=\"62:1-63:0\"\u003e\n\u003ch2 data-sourcepos=\"64:1-64:39\"\u003e\n\u003cspan id=\"3-vuejsの台頭と優しさ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-vuejs%E3%81%AE%E5%8F%B0%E9%A0%AD%E3%81%A8%E5%84%AA%E3%81%97%E3%81%95\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 「Vue.js」の台頭と優しさ\u003c/h2\u003e\n\u003cp data-sourcepos=\"66:1-67:69\"\u003eしかし、Reactには一つの弱点がありました。\u003cbr\u003e\n「学習コストが高すぎる（難しすぎる）」のです。\u003c/p\u003e\n\u003cp data-sourcepos=\"69:1-71:110\"\u003e「HTMLの中にJavaScriptを書く（JSX）なんて気持ち悪い！」\u003cbr\u003e\n「Node.jsの黒い画面（ビルド環境）が必須なのはハードルが高い！」\u003cbr\u003e\nReactの独自すぎる概念に、多くのWebデザイナーや初心者は戸惑い、挫折しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"73:1-73:204\"\u003eそこに2014年、元Googleのエンジニアであるエヴァン・ユー（Evan You）が、 \u003cstrong\u003e「Vue.js（ビュージェイエス）」\u003c/strong\u003e という新たなフレームワークを発表します。\u003c/p\u003e\n\u003cp data-sourcepos=\"75:1-75:291\"\u003eVue.jsは、Reactの「仮想DOM」や「コンポーネント」といった強力な武器を取り入れつつ、 \u003cstrong\u003e「昔ながらのHTML/CSS/JavaScriptの書き方で、直感的に始められる」\u003c/strong\u003e という圧倒的な優しさ（学習のしやすさ）を持っていました。\u003c/p\u003e\n\u003cp data-sourcepos=\"77:1-77:331\"\u003ejQueryに慣れ親しんだエンジニアやデザイナーたちは、「Reactは難しかったけど、Vueなら分かる！」と歓喜し、Vue.jsは瞬く間にReactと人気を二分する覇者へと成長しました。（※日本市場では特にVue.jsの人気が高く、多くの企業で採用されました）。\u003c/p\u003e\n\u003chr data-sourcepos=\"79:1-80:0\"\u003e\n\u003ch2 data-sourcepos=\"81:1-81:48\"\u003e\n\u003cspan id=\"4-spaの完成とjqueryの静かな引退\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-spa%E3%81%AE%E5%AE%8C%E6%88%90%E3%81%A8jquery%E3%81%AE%E9%9D%99%E3%81%8B%E3%81%AA%E5%BC%95%E9%80%80\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. SPAの完成と、jQueryの静かな引退\u003c/h2\u003e\n\u003cp data-sourcepos=\"83:1-83:141\"\u003eReactとVue（そしてAngularなどのフレームワーク）の普及により、Web開発の常識は完全に切り替わりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"85:1-86:149\"\u003eかつては「サーバー（PHPやRuby）がHTMLを組み立ててブラウザに送る」のが普通でした。\u003cbr\u003e\nしかし現在では、 \u003cstrong\u003e「フロントエンド（React/Vue）」と「バックエンド（API）」は完全に切り離されています。\u003c/strong\u003e\u003c/p\u003e\n\u003col data-sourcepos=\"88:1-91:0\"\u003e\n\u003cli data-sourcepos=\"88:1-88:114\"\u003eユーザーがサイトにアクセスすると、React/Vueの「空っぽのアプリ」が読み込まれる\u003c/li\u003e\n\u003cli data-sourcepos=\"89:1-89:106\"\u003eアプリが裏側でバックエンドに「データをちょうだい（Ajax）」とお願いする\u003c/li\u003e\n\u003cli data-sourcepos=\"90:1-91:0\"\u003eもらったデータを使って、仮想DOMが一瞬で画面を構築する\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"92:1-93:99\"\u003eページが切り替わる時の「画面が白くなる時間」は消え去り、私たちは今、スマホアプリと全く同じように滑らかに動くX（旧Twitter）やNetflix、Spotifyをブラウザ上で楽しんでいます。\u003cbr\u003e\nこれが、現在における \u003cstrong\u003eWebの最終形態（モダンフロントエンド）\u003c/strong\u003e です。\u003c/p\u003e\n\u003cp data-sourcepos=\"95:1-95:255\"\u003eそして、この複雑なエコシステムが完成したことで、かつて魔法の杖として世界を救った「jQuery」は、その歴史的使命を終え、新規の開発プロジェクトからは静かに姿を消していきました。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"97:1-97:46\"\u003e\n\u003cspan id=\"おわりに歴史は螺旋のように\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB%E6%AD%B4%E5%8F%B2%E3%81%AF%E8%9E%BA%E6%97%8B%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eおわりに：歴史は螺旋のように\u003c/h3\u003e\n\u003cp data-sourcepos=\"99:1-100:81\"\u003e「暗黒時代」のIE専用コードから始まり、jQueryの天下統一、iPhoneの黒船、Node.jsの黒い画面、そしてReact/VueによるSPA時代へ。\u003cbr\u003e\n番外編を含め、長く熱いWeb技術の歴史をたどってきました。\u003c/p\u003e\n\u003cp data-sourcepos=\"102:1-103:303\"\u003e現在のWeb開発は、「覚えることが多すぎる」「ツールが複雑すぎる」と言われることもあります。\u003cbr\u003e\nしかし、その複雑なパズルの一つ一つは、かつてのエンジニアたちが \u003cstrong\u003e「もっと速く」「もっと自由に」「もっとユーザーを楽しませたい」と願い、血のにじむような努力で限界を突破してきた「解決策の結晶」\u003c/strong\u003e なのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"105:1-106:146\"\u003e技術は常に進化し、時に過去の常識を否定しながら、螺旋階段のように上へと登っていきます。\u003cbr\u003e\n次にWebの歴史を塗り替えるのは、AIによる自動生成か、それともまだ誰も見たことのない新しい魔法の杖か。\u003c/p\u003e\n\u003cp data-sourcepos=\"108:1-108:66\"\u003eWebの進化の歴史に、決して終わりはありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"110:1-110:27\"\u003e（番外編 全5回 完）\u003c/p\u003e\n\u003chr data-sourcepos=\"112:1-113:0\"\u003e\n\u003ch3 data-sourcepos=\"114:1-114:30\"\u003e\n\u003cspan id=\"-参考文献出典\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE%E5%87%BA%E5%85%B8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e📚 参考文献・出典\u003c/h3\u003e\n\u003cp data-sourcepos=\"116:1-116:138\"\u003e本記事で紹介した技術概念やエピソードは、以下の公式ドキュメントや歴史的資料に基づいています。\u003c/p\u003e\n\u003col data-sourcepos=\"118:1-130:0\"\u003e\n\u003cli data-sourcepos=\"118:1-121:181\"\u003e\n\u003cstrong\u003eReactの公開（2013年）\u003c/strong\u003e\n\u003cul data-sourcepos=\"119:5-121:181\"\u003e\n\u003cli data-sourcepos=\"119:5-121:181\"\u003e\n\u003cstrong\u003eReact Official Blog:\u003c/strong\u003e\n\u003cul data-sourcepos=\"120:9-121:181\"\u003e\n\u003cli data-sourcepos=\"120:9-120:199\"\u003e2013年5月、JSConf USにてFacebookがReactをオープンソースとして発表した当時の公式ブログ記事。仮想DOMとコンポーネントの概念が提唱されました\u003c/li\u003e\n\u003cli data-sourcepos=\"121:9-121:181\"\u003e\n\u003ca href=\"https://reactjs.org/blog/2013/06/05/why-react.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eReact v0.3.0 - React Blog\u003c/a\u003e (※現在は react.dev に移行していますが、当時の思想を確認できます)\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"122:1-125:90\"\u003e\n\u003cstrong\u003eVue.jsの誕生（2014年）\u003c/strong\u003e\n\u003cul data-sourcepos=\"123:5-125:90\"\u003e\n\u003cli data-sourcepos=\"123:5-125:90\"\u003e\n\u003cstrong\u003eVue.js: The Documentary - Honeypot:\u003c/strong\u003e\n\u003cul data-sourcepos=\"124:9-125:90\"\u003e\n\u003cli data-sourcepos=\"124:9-124:226\"\u003e開発者Evan Youが「Angularの本当に好きだった部分だけを抽出して、もっと軽いものを作りたかった」と語る、Vue.js誕生の背景を描いたドキュメンタリー映像です。\u003c/li\u003e\n\u003cli data-sourcepos=\"125:9-125:90\"\u003e\u003ca href=\"https://www.youtube.com/watch?v=OrxmtDw4pVI\" rel=\"nofollow noopener\" target=\"_blank\"\u003eVue.js: The Documentary - YouTube\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"126:1-130:0\"\u003e\n\u003cstrong\u003e仮想DOM（Virtual DOM）の概念\u003c/strong\u003e\n\u003cul data-sourcepos=\"127:5-130:0\"\u003e\n\u003cli data-sourcepos=\"127:5-130:0\"\u003e\n\u003cstrong\u003eReact Official Documentation:\u003c/strong\u003e\n\u003cul data-sourcepos=\"128:9-130:0\"\u003e\n\u003cli data-sourcepos=\"128:9-128:184\"\u003eReactがどのようにUIをメモリ上に保持し、変更のあった部分（差分）だけをリアルDOMと同期（Reconciliation）させるかの公式解説です\u003c/li\u003e\n\u003cli data-sourcepos=\"129:9-130:0\"\u003e\u003ca href=\"https://react.dev/learn/preserving-and-resetting-state\" rel=\"nofollow noopener\" target=\"_blank\"\u003ePreserving and Resetting State – React\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr data-sourcepos=\"131:1-131:3\"\u003e\n\u003cp data-sourcepos=\"132:1-133:101\"\u003e\u003cstrong\u003e🤖 執筆協力\u003c/strong\u003e\u003cbr\u003e\n本記事の構成案作成および推敲には、生成AIのアシストを活用しています。\u003c/p\u003e\n","body":"# 番外編⑤：ReactとVueの覇権（SPA時代へ）\n**〜jQueryの終焉と「仮想DOM」がもたらした現代Webの最終形態〜**\n\n## 導入：「つぎはぎだらけ」の限界\n\nこれまでの連載で、フロントエンド（jQuery）、バックエンド（Rails）、デザイン（Bootstrap）、そして開発環境（Node.js）の進化を見てきました。\n2010年代中盤、ブラウザやスマートフォンの性能が上がり、Webサイトはついに **「ページを移動せず、ひとつの画面のままアプリのように動く（SPA：シングルページアプリケーション）」** のが当たり前の時代に突入しようとしていました。\n\nしかし、ここで開発者たちは「ある絶望的な壁」にぶち当たります。\n**「jQueryで複雑なアプリを作ろうとすると、コードが地獄のように絡まる」** という問題です。\n\n例えば、「いいねボタンを押したら、ハートの色が赤くなり、全体のいいね数が+1され、マイページのリストにもその記事が追加される」という処理。\njQuery（DOMを直接書き換える手法）でこれを書こうとすると、「あっちの要素を探して色を変え、こっちの数字を書き換え……」と、プログラムが迷路のように入り組んでしまいます（これを **スパゲッティコード** と呼びます）。\n\n「もう、手作業で画面（DOM）をいじるのは限界だ……」\nそんな悲鳴が上がっていた2013年、巨大SNS企業・Facebook（現Meta）から、Webの常識を根底から覆す「魔法」が公開されました。\n\n---\n\n## 1. 2013年、Facebookからの刺客「React」\n\nFacebookがオープンソースとして発表したJavaScriptライブラリ、それが **「React（リアクト）」** です。\nFacebookやInstagramのような「超複雑で、画面のあちこちがリアルタイムに更新される」システムを破綻させずに作るため、彼ら自身が生み出した社内用ツールでした。\n\nReactがもたらした最大の革命。それは **「仮想DOM（Virtual DOM）」** という概念です。\n\n### 「仮想DOM」という魔法\nこれまでのjQueryは、ブラウザに表示されている実際の画面（リアルDOM）を直接いじっていました。これは例えるなら、**「完成したキャンバスの絵に、直接筆を入れて描き直す」** ようなもので、非常に神経を使い、動作も遅くなりがちでした。\n\nReactは違います。\nブラウザの裏側（メモリ上）に、見えない **「もうひとつの画面（仮想DOM）」** のコピーを作っておくのです。\n\n\n\n1. データ（いいね数など）が変化する\n2. Reactは、まず裏側の「仮想DOM」を丸ごと新しく作り直す（これはメモリ上の計算なので一瞬で終わります）\n3. そして、新しい仮想DOMと、古い画面を **「間違い探し」のように比較** する\n4. 変更があった部分（ハートの色と数字） **だけ** を、実際の画面にパッチを当てるように一瞬で反映させる\n\n開発者は「いいねボタンが押されたら、こう画面を書き換えろ」という面倒な手順（DOM操作）を書く必要がなくなりました。\nただ **「今の状態（データ）はこうです」と宣言するだけ** で、あとはReactが勝手に「間違い探し」をして、最も効率的に画面を更新してくれるようになったのです。\n\n---\n\n## 2. コンポーネント指向：「レゴブロック」でWebを作る\n\nReactが持ち込んだもう一つの革命が、 **「コンポーネント指向」** です。\n\nそれまで、Webサイトは「1つの巨大なHTMLファイル」で作るのが普通でした。\nしかしReactでは、画面を細かな **「部品（コンポーネント）」** に分割します。\n\n* ヘッダーの部品\n* サイドバーの部品\n* 「いいねボタン」の部品\n* 「ユーザーアイコン」の部品\n\nこれらはすべて独立したプログラム（JavaScript）として作られます。開発者は、この **「レゴブロック」を組み合わせるだけ** で、複雑なWeb画面を組み上げていくのです。\n「いいねボタン」のプログラムを1つ作っておけば、記事一覧にも、詳細ページにも、マイページにも、ブロックをポンと置くだけで使い回せます。\n\nこの「仮想DOM」と「コンポーネント指向」の組み合わせにより、コードのスパゲッティ化は完全に解消され、複数人のチームで巨大なWebアプリを開発することが飛躍的に簡単になりました。\n\n---\n\n## 3. 「Vue.js」の台頭と優しさ\n\nしかし、Reactには一つの弱点がありました。\n「学習コストが高すぎる（難しすぎる）」のです。\n\n「HTMLの中にJavaScriptを書く（JSX）なんて気持ち悪い！」\n「Node.jsの黒い画面（ビルド環境）が必須なのはハードルが高い！」\nReactの独自すぎる概念に、多くのWebデザイナーや初心者は戸惑い、挫折しました。\n\nそこに2014年、元Googleのエンジニアであるエヴァン・ユー（Evan You）が、 **「Vue.js（ビュージェイエス）」** という新たなフレームワークを発表します。\n\nVue.jsは、Reactの「仮想DOM」や「コンポーネント」といった強力な武器を取り入れつつ、 **「昔ながらのHTML/CSS/JavaScriptの書き方で、直感的に始められる」** という圧倒的な優しさ（学習のしやすさ）を持っていました。\n\njQueryに慣れ親しんだエンジニアやデザイナーたちは、「Reactは難しかったけど、Vueなら分かる！」と歓喜し、Vue.jsは瞬く間にReactと人気を二分する覇者へと成長しました。（※日本市場では特にVue.jsの人気が高く、多くの企業で採用されました）。\n\n---\n\n## 4. SPAの完成と、jQueryの静かな引退\n\nReactとVue（そしてAngularなどのフレームワーク）の普及により、Web開発の常識は完全に切り替わりました。\n\nかつては「サーバー（PHPやRuby）がHTMLを組み立ててブラウザに送る」のが普通でした。\nしかし現在では、 **「フロントエンド（React/Vue）」と「バックエンド（API）」は完全に切り離されています。**\n\n1. ユーザーがサイトにアクセスすると、React/Vueの「空っぽのアプリ」が読み込まれる\n2. アプリが裏側でバックエンドに「データをちょうだい（Ajax）」とお願いする\n3. もらったデータを使って、仮想DOMが一瞬で画面を構築する\n\nページが切り替わる時の「画面が白くなる時間」は消え去り、私たちは今、スマホアプリと全く同じように滑らかに動くX（旧Twitter）やNetflix、Spotifyをブラウザ上で楽しんでいます。\nこれが、現在における **Webの最終形態（モダンフロントエンド）** です。\n\nそして、この複雑なエコシステムが完成したことで、かつて魔法の杖として世界を救った「jQuery」は、その歴史的使命を終え、新規の開発プロジェクトからは静かに姿を消していきました。\n\n### おわりに：歴史は螺旋のように\n\n「暗黒時代」のIE専用コードから始まり、jQueryの天下統一、iPhoneの黒船、Node.jsの黒い画面、そしてReact/VueによるSPA時代へ。\n番外編を含め、長く熱いWeb技術の歴史をたどってきました。\n\n現在のWeb開発は、「覚えることが多すぎる」「ツールが複雑すぎる」と言われることもあります。\nしかし、その複雑なパズルの一つ一つは、かつてのエンジニアたちが **「もっと速く」「もっと自由に」「もっとユーザーを楽しませたい」と願い、血のにじむような努力で限界を突破してきた「解決策の結晶」** なのです。\n\n技術は常に進化し、時に過去の常識を否定しながら、螺旋階段のように上へと登っていきます。\n次にWebの歴史を塗り替えるのは、AIによる自動生成か、それともまだ誰も見たことのない新しい魔法の杖か。\n\nWebの進化の歴史に、決して終わりはありません。\n\n（番外編 全5回 完）\n\n---\n\n### 📚 参考文献・出典\n\n本記事で紹介した技術概念やエピソードは、以下の公式ドキュメントや歴史的資料に基づいています。\n\n1.  **Reactの公開（2013年）**\n    * **React Official Blog:**\n        * 2013年5月、JSConf USにてFacebookがReactをオープンソースとして発表した当時の公式ブログ記事。仮想DOMとコンポーネントの概念が提唱されました\n        * [React v0.3.0 - React Blog](https://reactjs.org/blog/2013/06/05/why-react.html) (※現在は react.dev に移行していますが、当時の思想を確認できます)\n2.  **Vue.jsの誕生（2014年）**\n    * **Vue.js: The Documentary - Honeypot:**\n        * 開発者Evan Youが「Angularの本当に好きだった部分だけを抽出して、もっと軽いものを作りたかった」と語る、Vue.js誕生の背景を描いたドキュメンタリー映像です。\n        * [Vue.js: The Documentary - YouTube](https://www.youtube.com/watch?v=OrxmtDw4pVI)\n3.  **仮想DOM（Virtual DOM）の概念**\n    * **React Official Documentation:**\n        * ReactがどのようにUIをメモリ上に保持し、変更のあった部分（差分）だけをリアルDOMと同期（Reconciliation）させるかの公式解説です\n        * [Preserving and Resetting State – React](https://react.dev/learn/preserving-and-resetting-state)\n\n---\n**🤖 執筆協力**\n本記事の構成案作成および推敲には、生成AIのアシストを活用しています。\n","coediting":false,"comments_count":0,"created_at":"2026-05-15T19:10:27+09:00","group":null,"id":"1d402ce59b8d9153288e","likes_count":10,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vue.js","versions":[]},{"name":"歴史","versions":[]},{"name":"React","versions":[]}],"title":"番外編：開発技術のパラダイムシフト⑤","updated_at":"2026-05-22T19:00:06+09:00","url":"https://qiita.com/Shankou/items/1d402ce59b8d9153288e","user":{"description":"","facebook_id":"","followees_count":1,"followers_count":5,"github_login_name":null,"id":"Shankou","items_count":28,"linkedin_id":"","location":"","name":"","organization":"株式会社 ONE WEDGE","permanent_id":3639915,"profile_image_url":"https://secure.gravatar.com/avatar/a9026dfba65eef52b2e98960c13a2b99","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":"onewedge","slide":false},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:58\"\u003e\n\u003cspan id=\"nodejsがフロントエンド開発で必要な理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#nodejs%E3%81%8C%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E9%96%8B%E7%99%BA%E3%81%A7%E5%BF%85%E8%A6%81%E3%81%AA%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eNode.jsがフロントエンド開発で必要な理由\u003c/h2\u003e\n\u003cp data-sourcepos=\"2:1-2:66\"\u003e「\u003cem\u003eNode.jsはバックエンドで使うものでしょう？\u003c/em\u003e」\u003c/p\u003e\n\u003cp data-sourcepos=\"4:1-4:74\"\u003e「\u003cem\u003eなぜフロントエンドのReact開発でNode.jsがいるの？\u003c/em\u003e」\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:72\"\u003eこういった疑問を抱えているビギナーは多くいます。\u003c/p\u003e\n\u003cp data-sourcepos=\"8:1-8:59\"\u003eNode.jsが必要な理由を1分で説明しましょう。\u003c/p\u003e\n\u003chr data-sourcepos=\"10:1-11:0\"\u003e\n\u003cp data-sourcepos=\"12:1-12:78\"\u003eパソコン内には様々なアプリケーションが入っています。\u003c/p\u003e\n\u003cp data-sourcepos=\"14:1-14:123\"\u003e「パソコン」という「土台」の上でアプリケーションが動いているイメージです（次図）。\u003c/p\u003e\n\u003cp data-sourcepos=\"16:1-16:45\"\u003eブラウザもその内のひとつです。\u003c/p\u003e\n\u003cp data-sourcepos=\"18:1-18:125\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Fd2b4df18-e5df-4fc0-abd7-8697b8887495.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=65a1d15126010088793fed305e533e60\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Fd2b4df18-e5df-4fc0-abd7-8697b8887495.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=65a1d15126010088793fed305e533e60\" alt=\"nodejs-1.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Fd2b4df18-e5df-4fc0-abd7-8697b8887495.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=6571882e2094711be43e0527dcf438a9 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/d2b4df18-e5df-4fc0-abd7-8697b8887495.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cbr\u003e\n\u003cp data-sourcepos=\"22:1-22:79\"\u003eJavaScriptはウェブサイトやウェブアプリで使われています。\u003c/p\u003e\n\u003cp data-sourcepos=\"24:1-24:82\"\u003eなので、ブラウザ上でJavaScriptが動くのは分かると思います。\u003c/p\u003e\n\u003cbr\u003e\n\u003cp data-sourcepos=\"28:1-28:125\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Ffbf1df3f-3ffe-463c-874b-dbbdb53fad92.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=305260055701c337568c4e207c834198\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Ffbf1df3f-3ffe-463c-874b-dbbdb53fad92.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=305260055701c337568c4e207c834198\" alt=\"nodejs-2.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Ffbf1df3f-3ffe-463c-874b-dbbdb53fad92.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=f2a7ec38dfb9c7a32241384dd1e2434f 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/fbf1df3f-3ffe-463c-874b-dbbdb53fad92.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cbr\u003e\n\u003cp data-sourcepos=\"32:1-32:121\"\u003eしかしもしここで、ブラウザ以外のアプリケーション上でもJavaScriptが使えたら便利です。\u003c/p\u003e\n\u003cp data-sourcepos=\"34:1-34:49\"\u003eNode.jsはそのために開発されました。\u003c/p\u003e\n\u003cp data-sourcepos=\"36:1-36:76\"\u003eパソコン上にNode.jsをインストールしたものが次図です。\u003c/p\u003e\n\u003cbr\u003e\n\u003cp data-sourcepos=\"40:1-40:125\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F998daa4e-bf33-4a8d-b872-32941ce6fb49.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=625fdbd76272dfbae79728052fd9f575\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F998daa4e-bf33-4a8d-b872-32941ce6fb49.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=625fdbd76272dfbae79728052fd9f575\" alt=\"nodejs-3.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2F998daa4e-bf33-4a8d-b872-32941ce6fb49.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=9de0130fe61a929b1d6a8b5123a9325d 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/998daa4e-bf33-4a8d-b872-32941ce6fb49.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cbr\u003e\n\u003cp data-sourcepos=\"44:1-44:154\"\u003eこの結果、JavaScriptをブラウザ上だけでなく、ブラウザ以外の場所、つまりパソコン全体で使えるようになります。\u003c/p\u003e\n\u003cbr\u003e\n\u003cp data-sourcepos=\"48:1-48:125\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Faf4165ce-a2e0-47df-bd1c-ce7590957ca2.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=c28ff03960cb93ce52f08319ca52a9e9\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Faf4165ce-a2e0-47df-bd1c-ce7590957ca2.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=c28ff03960cb93ce52f08319ca52a9e9\" alt=\"nodejs-4.png\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F4429330%2Faf4165ce-a2e0-47df-bd1c-ce7590957ca2.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=cf09305dd797575f45f3159c08ceb1a7 1x\" data-canonical-src=\"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/af4165ce-a2e0-47df-bd1c-ce7590957ca2.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cbr\u003e\n\u003cp data-sourcepos=\"52:1-52:72\"\u003eブラウザ内で留まっていてはできない操作、例えば；\u003c/p\u003e\n\u003cp data-sourcepos=\"54:1-56:43\"\u003e• パソコン内のファイルの読み・書き\u003cbr\u003e\n• データベースとやり取り\u003cbr\u003e\n• コマンドラインツールの開発\u003c/p\u003e\n\u003cp data-sourcepos=\"58:1-58:73\"\u003eこういったことが、JavaScriptで行えるようになります。\u003c/p\u003e\n\u003cp data-sourcepos=\"60:1-60:90\"\u003e（*Node.jsが「JavaScriptの実行環境」と呼ばれるのはこのためです）。\u003c/p\u003e\n\u003chr data-sourcepos=\"62:1-63:0\"\u003e\n\u003cp data-sourcepos=\"64:1-64:65\"\u003eTypeScript開発でNode.jsが必要なのも同じ理由です。\u003c/p\u003e\n\u003cp data-sourcepos=\"66:1-66:52\"\u003eブラウザはTypeScriptを処理できません。\u003c/p\u003e\n\u003cp data-sourcepos=\"68:1-68:91\"\u003eブラウザ以外の場所でJavaScriptへと変換してあげる必要があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"70:1-70:43\"\u003eそのためNode.jsが必要なのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"72:1-72:106\"\u003eNode.jsの役割が分かったら、次は実際にインストールして動かしてみましょう。\u003c/p\u003e\n","body":"## Node.jsがフロントエンド開発で必要な理由\n「*Node.jsはバックエンドで使うものでしょう？*」\n\n「*なぜフロントエンドのReact開発でNode.jsがいるの？*」\n\nこういった疑問を抱えているビギナーは多くいます。\n\nNode.jsが必要な理由を1分で説明しましょう。\n\n---\n\nパソコン内には様々なアプリケーションが入っています。\n\n「パソコン」という「土台」の上でアプリケーションが動いているイメージです（次図）。\n\nブラウザもその内のひとつです。\n\n![nodejs-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/d2b4df18-e5df-4fc0-abd7-8697b8887495.png)\n\n\u003cbr/\u003e\n\nJavaScriptはウェブサイトやウェブアプリで使われています。\n\nなので、ブラウザ上でJavaScriptが動くのは分かると思います。\n\n\u003cbr/\u003e\n\n![nodejs-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/fbf1df3f-3ffe-463c-874b-dbbdb53fad92.png)\n\n\u003cbr/\u003e\n\nしかしもしここで、ブラウザ以外のアプリケーション上でもJavaScriptが使えたら便利です。\n\nNode.jsはそのために開発されました。\n\nパソコン上にNode.jsをインストールしたものが次図です。\n\n\u003cbr/\u003e\n\n![nodejs-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/998daa4e-bf33-4a8d-b872-32941ce6fb49.png)\n\n\u003cbr/\u003e\n\nこの結果、JavaScriptをブラウザ上だけでなく、ブラウザ以外の場所、つまりパソコン全体で使えるようになります。\n\n\u003cbr/\u003e\n\n![nodejs-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4429330/af4165ce-a2e0-47df-bd1c-ce7590957ca2.png)\n\n\u003cbr/\u003e\n\nブラウザ内で留まっていてはできない操作、例えば；\n\n• パソコン内のファイルの読み・書き\\\n• データベースとやり取り\\\n• コマンドラインツールの開発\n\nこういったことが、JavaScriptで行えるようになります。\n\n（*Node.jsが「JavaScriptの実行環境」と呼ばれるのはこのためです）。\n\n---\n\nTypeScript開発でNode.jsが必要なのも同じ理由です。\n\nブラウザはTypeScriptを処理できません。\n\nブラウザ以外の場所でJavaScriptへと変換してあげる必要があります。\n\nそのためNode.jsが必要なのです。\n\nNode.jsの役割が分かったら、次は実際にインストールして動かしてみましょう。\n","coediting":false,"comments_count":0,"created_at":"2026-05-14T15:55:57+09:00","group":null,"id":"5c0e1fbcd40aee2713ab","likes_count":2,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"JavaScript","versions":[]},{"name":"Node.js","versions":[]},{"name":"Vue.js","versions":[]},{"name":"フロントエンド","versions":[]},{"name":"React","versions":[]}],"title":"【図解】なぜフロントエンドのReact開発でNode.jsが必要なの？","updated_at":"2026-05-22T20:07:04+09:00","url":"https://qiita.com/monotein/items/5c0e1fbcd40aee2713ab","user":{"description":"非IT出身。専門用語の壁でプログラミングに挫折した経験から「専門用語なし」メソッドを確立。1200人以上の初心者をフロントエンド開発へ導く。『はじめてつくるReactアプリ with TypeScript』、『動かして学ぶ！Next.js/React開発入門（翔泳社／*韓国でも発売）』など著書多数。","facebook_id":"","followees_count":1,"followers_count":1,"github_login_name":null,"id":"monotein","items_count":17,"linkedin_id":"","location":"","name":"三好 アキ｜専門用語なしでプログラミング","organization":"","permanent_id":4429330,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/4429330/6d0af74f901b2b635b18a7d5878a099040933d53/x_large.png?1778461148","team_only":false,"twitter_screen_name":null,"website_url":"https://monotein.com/business/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false}]