Wails 実践講座 #1 実践プロジェクトの設計 — 何を作り、どう分けるか

読了 6分

Wails でデスクトップアプリを作る 入門シリーズで、バインディング、ランタイム統合、ビルドまで概念を習得しました。ただしそのシリーズの ToDo アプリはデータがメモリだけにあり、アプリを閉じると消える学習用の骨組みでした。この実践シリーズはその骨組みに肉を付け、実際にインストールして使えるローカルノートアプリを 6 編にわたって完成させます。最初の記事は、コードを書く前に何を作るかを決め、構造の境界を引く設計です。

全 10 編、2 部構成です。第 1 部で配信可能なアプリを作り、第 2 部でリピーターが生まれる完成度を扱います。

第 1 部 — アプリを作って配信する

  • #1 実践プロジェクトの設計 — 何を作り、どう分けるか ← この記事
  • #2 SQLite ローカルデータベース — CGO なしの純粋な Go で
  • #3 全文検索とデータフロー — FTS5 とイベント駆動の更新
  • #4 トレイ常駐とグローバルショートカット — バックグラウンドから素早くキャプチャ
  • #5 署名と公証 — デプロイしたアプリが信頼される仕組み
  • #6 CI/CD 自動リリース — GitHub Actions で 3 プラットフォームへ配信

第 2 部 — 完成度とリピーター

  • #7 ノートエディタの実践 — マークダウンプレビューと一覧の UX
  • #8 テスト戦略 — サービスとリポジトリを検証する
  • #9 自動更新の実装 — 新しいバージョンを安全に届ける
  • #10 仕上げと完成度 — 設定・ダークモード・データバックアップ

何を作るか — ローカルノートアプリ #

作るのはマークダウンのノートをローカルに保存して素早く検索するデスクトップアプリです。クラウドなしで自分の端末の中で完結するオフラインファーストのツールで、次の機能を目標にします。

  • ノートの作成・編集・削除、ローカルデータベースへの永続保存(#2)
  • タイトル・本文の全文検索(#3)
  • トレイに常駐し、グローバルショートカットでどこからでも新規ノートをキャプチャ(#4)
  • 署名・公証されたインストーラーとして配布(#5)
  • タグをプッシュすると 3 プラットフォームのリリースが自動生成(#6)

小さくても実際のアプリが備えるべき要素(永続性、検索、バックグラウンド常駐、配布の信頼、自動化)をすべて通過するように選んだ範囲です。画面は単純です。左にノート一覧と検索ボックス、右にエディタが一つあれば足ります。

境界を引く — 三つの層 #

実践アプリと学習用の骨組みの最初の違いは、すべてのロジックをアプリ構造体の一つに詰め込まないことです。入門シリーズでは App 構造体にメソッドを直接付けましたが、機能が増えるとその構造体がデータ保存、検索、ウィンドウ制御をすべて知る巨大な塊になります。最初から三つの層に分けます。

役割本シリーズの例
バインディング層(App)フロントエンドに公開される薄い入口App.CreateNote, App.Search
サービス層実際のドメインロジックNoteService — 検証、検索、ルール
リポジトリ層データの永続化NoteRepository — SQLite アクセス

バインディング層はフロントエンドのリクエストを受け取ってサービスに渡し、結果を返すだけです。ドメインロジック(タイトルが空なら拒否、検索語の正規化といったルール)はサービスに、DB を実際に読み書きするコードはリポジトリに置きます。こう分けると、リポジトリを SQLite から別のものに替えてもサービスはそのままで、サービスのロジックを Wails なしの純粋な Go テストで検証できます。

構造の方向 — 依存は上から下へのみ
// App(バインディング) → NoteService(ドメイン) → NoteRepository(保存)
type App struct {
	ctx     context.Context
	notes   *NoteService
}

func (a *App) CreateNote(title, body string) (Note, error) {
	// バインディング層はサービスに委譲するだけ
	return a.notes.Create(title, body)
}

依存は一方向です。バインディングがサービスを知り、サービスがリポジトリを知りますが、逆にリポジトリがバインディングを知ることはありません。このルール一つが、アプリが大きくなっても構造が崩れないように守ってくれます。

ドメインモデル — 最小限のノート #

ノート 1 件を表す型をまず決めます。実践では識別子と時刻の情報が必ず必要です。一覧のソート、編集の追跡、後の同期まで、これらのフィールドが基盤になります。

note.go — ドメインモデル
type Note struct {
	ID        int64     `json:"id"`
	Title     string    `json:"title"`
	Body      string    `json:"body"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

構造体タグの json:"..."入門 #3 で扱ったとおり、フロントエンドに渡るときのフィールド名です。Go の CreatedAt が JavaScript で createdAt になるようにキャメルケースで合わせておきます。

フロントエンドの状態 — 何を選ぶか #

フロントエンドは入門シリーズと同じバニラの組み合わせでも動きますが、実践アプリは状態が増えます。一覧、選択中のノート、検索語、編集中の内容、保存の状態が同時に動きます。ここで選択肢が分かれます。

  • バニラ JS: 依存がなくバンドルが最も小さいです。状態が単純なときに適しますが、上の状態が絡み合うと手動の DOM 更新がすぐに複雑になります。
  • Svelte・Vue・React: 状態と画面を自動で結び付けます。Wails はフロントエンドを選ばないので何でも動き、このシリーズは状態管理の負担を減らすために軽量なコンポーネントフレームワークを前提に説明します(コードはフレームワークに大きく依存しないように保ちます)。

核心の原則は一つです。フロントエンドは画面と入力だけを担い、データの真実は常に Go 側に置きます。 検索結果もノート一覧もフロントエンドが自前で管理せず、Go に問い合わせます。こうすればデータの整合性を一箇所だけで守れば済みます。

注記
このシリーズは安定版の Wails v2 を基準にします。v3 はこの記事の時点でアルファで、マルチウィンドウと改善されたシステムトレイを予告しています。ここで扱うサービス層の分離、リポジトリの設計、署名・CI の概念は v3 でもそのまま有効なので、v2 で習得した構造は移行コストなしで引き継がれます。

まとめ #

  • 実践シリーズはオフラインファーストのローカルノートアプリを 6 編にわたって、永続性・検索・トレイ・署名・CI まで完成させます。
  • 学習用の骨組みとの最初の違いは層の分離です。バインディング(App)・サービス・リポジトリの三層に分け、依存は一方向にのみ流します。
  • ドメインモデルには ID と作成・更新の時刻を必ず入れ、JSON タグでフロントエンドのフィールド名をキャメルケースに合わせます。
  • フロントエンドは画面と入力だけを担い、データの真実は Go に置きます。状態が絡み合うなら、軽量なコンポーネントフレームワークが手動の DOM 更新より優れます。
  • 次の記事ではリポジトリ層を SQLite で実装しますが、CGO なしでクロスコンパイルできる純粋な Go のドライバーを使います。
X