×

子供の受験記録アプリを作って、家の Mac から配信している話

子供の受験記録アプリを作って、家の Mac から配信している話

受験記録は特定の子どもの得点推移そのものなので、クラウドには出しませんでした。React + TypeScript + Vite で作った Web アプリを、家の常時起動 Mac 上で静的配信し、同じ Wi-Fi の中だけで使っています。配信は Node 標準の http と launchd だけで、外部パッケージも証明書も使っていません。

1. 発端

子供の受験を控えています。成績は良い方ですが、本人の希望で塾には行かず受験することになりました。塾に通う子どもとの最大の差は情報だと思ったので、模試だけは受け、それ以外の情報収集は親が担うことにしました。方針は過去問派です。過去問を解いて弱いところを補強します。勉強の計画そのものは別の話になるので、ここでは扱いません。

記録の手段はスプレッドシートでも足ります。ただ、点数を並べたエクセルの表は親の管理には向きますが、子どもが自分から見に行く画面にはなりにくいものです。ゲーム画面のようなインターフェースなら、記録そのものが進捗の見える化になるのではないか。そう考えて自作しました。

作った画面の名前は NIGHT BAND です。この記事の主題はアプリの見た目ではなく、そのアプリをどこに置いたか、そしてなぜ外に出さなかったか、です。

2. 回数を経験値にして、苦手が見えるようにした

実装で悩んだのは、科目ごとの実施件数だけを残しても情報として足りない、という点でした。弱いところ・苦手なところを後から取り出せなければ、記録は日記で終わります。

そのため、過去5年分の高校入試の過去問を積み、大まかな傾向を見ました。大問構成が大きく変わっていなかったので、それを軸にしました。1回解いたらレベル1、2回解いたらレベル2、というように、回数を経験値としてレベルが上がる形にしています。目標は300点満点中270点以上です。学校の偏差値に合わせて、いまどこにいるのかが自分で分かるようにしたいと思いました。

 

点数は、画面の中だけで完結させず、後から書き出せるようにしています。AI に読み込ませて分析させるためです。分析基盤をアプリに内蔵することは、この時点ではやりませんでした。記録が先、分析は後です。

/

「意志で続けさせる」より、「開いたときに見返りがある」方が続く、という仮説で画面を組んでいます。レベルと得点の推移が、その見返りになる想定です。入力と閲覧は1台の端末に寄せています。複数端末で同じ数字を見たくなった時点で、保存の仕組みを変えます。先に作りません。

3. 置き場所をどうするか — クラウドを選ばなかった理由

作ったのは React + TypeScript + Vite の Web アプリです。普通ならここで Vercel や GitHub Pages に置いて終わりですが、外部には一切出しませんでした。

理由は3つあります。

理由1:中身が個人情報の塊だから。
記録されるのは、特定の子どもの教科別の得点推移です。仮に URL を知られなければ大丈夫、という運用は成立しません。「知られなければ安全」は設計ではなく祈りです。

理由2:使う人が家の中に1人しかいないから。
外部に出す必要が最初からありません。要件が「同じ Wi-Fi の中で見られればいい」なら、インターネットに出す構成は、リスクだけを増やして利便性を増やさない選択になります。

理由3:家に常時稼働している Mac があったから。
24時間動いているマシンが既にあるなら、配信サーバーの調達コストはゼロです。

4. 構成 — vite build → 静的配信 → launchd で常駐

最終的な構成は、部品の数だけ見れば短いものです。

[Mac mini(常時稼働)]
  app/dist/  ← vite build の出力
      ↑
  scripts/serve.mjs  ← Node標準の http だけで静的配信(ポート8080 / 0.0.0.0 バインド)
      ↑
  launchd(LaunchAgent)  ← ログイン時に自動起動 + 異常終了時に自動再起動

        ↓ 同一Wi-Fi
[スマホ] http://<Mac名>.local:8080

プロセスは3つだけです。ビルド済みの静的ファイル、それを返す小さな HTTP サーバ、落ちたら起こす launchd。家族が使うものなので、機能の追加より「動いていない」状態を作らないことの方が先でした。

4-1. 配信スクリプトは外部パッケージを使わない

expressserve も入れていません。Node に最初から入っている http モジュールだけで書いています。

家庭内で何年も動かし続けるものに npm パッケージを足すと、そのぶん脆弱性通知とメンテナンスが発生します。静的ファイルを返すだけの処理に、その負債を負う理由がありません。

実装上の注意は1点だけあります。SPA なので、存在しないパスは index.html にフォールバックさせます。これをやらないと、ページをリロードした瞬間に 404 が出ます。

// scripts/serve.mjs(骨子)
// 1. リクエストされたパスのファイルが dist にあれば、それを返す
// 2. なければ index.html を返す(SPAのルーティングをクライアント側に任せる)
// 3. ポート8080、0.0.0.0 にバインド(localhost だと他端末から見えない)

0.0.0.0 へのバインドも忘れやすい箇所です。localhost にバインドすると Mac 自身からしかアクセスできず、スマホから見えません。逆に言えば、0.0.0.0 は同じ Wi-Fi にいる端末からは誰でも届きます。来客用のネットワークを分けていない家では、その前提で置くことになります。

4-2. launchd で常駐させる

macOS で「ログイン時に自動起動し、落ちたら勝手に再起動する」を実現する標準の仕組みが launchd です。~/Library/LaunchAgents/ に plist を置きます。

<!-- 骨子。KeepAlive が「落ちたら再起動」、RunAtLoad が「ログイン時に起動」 -->
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>(ログの出力先)</string>
<key>StandardErrorPath</key>
<string>(エラーログの出力先)</string>

KeepAlive があるおかげで、Mac を再起動しても、プロセスが死んでも、何もしなくても復帰します。ログの出力先は必ず指定します。指定しないと、動かないときに何も分かりません。

4-3. HTTPS は、この要件では不要にしました

LAN 内の HTTP で運用しています。いま使っている範囲では、localStorage もホーム画面への追加も HTTP で動作します。証明書の準備はしていません。将来ブラウザ側の制限が厳しくなったら、そのときに足します。先に足しません。

5. 詰まったところ

記事として残す価値があるのは、たぶんここです。

5-1. .local のホスト名が、Mac の「コンピュータ名」と違う

スマホからのアクセスは IP 直打ちではなく mDNS(http://<Mac名>.local:8080)を第一手段にしました。ルーターの DHCP で IP が変わってもブックマークが切れないからです。

ただし、システム設定に表示されている「コンピュータ名」と、mDNS で解決される名前は一致しません。mDNS 側は ASCII に変換され、記号やスペースが置換されます。日本語や記号を含む名前を付けていると、思っている名前では引けません。

確認は次の2つを並べて見ます。

scutil --get ComputerName    # 表示用の名前
scutil --get LocalHostName   # .local で使われる名前 ← こちらが正

5-2. Vite の allowedHosts が大文字小文字を区別する

開発サーバ(vite dev)にスマホからアクセスしたとき、ブラウザはホスト名を小文字にして送ります。一方 allowedHosts に大文字混じりで書いていると、文字列比較で一致せず弾かれます。

やっかいなのは、Mac から curl で叩くと 200 が返ることです。「サーバは動いているのにスマホからだけ見えない」という、切り分けにくい症状になります。

本番の serve.mjs は allowlist を持たない実装なので、この問題は起きません。開発サーバ特有の罠です。

5-3. localStorage は「端末ごと」に保存される

これは仕様どおりの挙動ですが、運用に直結します。

データは、アクセスした端末のブラウザに保存されます。スマホで入力した記録はスマホにしか残らず、Mac や他の端末から見ると空です。サーバは静的ファイルを配っているだけで、データを一切持っていません。

取りうる対策は2つありました。

  • A案(採用):入力・閲覧を1台のスマホに統一する。バックアップは設定画面からの JSON 書き出し
  • B案:Mac 側に保存 API を持たせてサーバ保存に変える。端末間で共有できますが、バックエンドが必要になります

A案を選びました。「親も自分の端末で進捗を見たい」という要求が実際に出てから B案を検討する、という順序です。使われるか分からない機能のために構成を複雑にしない判断が、今回いちばん効きました。

6. この構成の割り切り

この構成には、はっきりした欠点があります。

欠点 現状の扱い
端末をまたいでデータを共有できない 1台に統一する運用でカバー
Mac が落ちていると使えない 常時稼働前提。いまのところ実害は出ていない
外出先から使えない 家で解いて家で記録する運用なので問題なし
バックアップが手動 設定画面から JSON を書き出す。サーバ側には原本がない

すべて「家庭内で1人が使う」という前提とセットで成立しています。この前提が崩れた瞬間に、構成ごと作り直しになる設計です。それを分かったうえで、いま必要のない汎用性を捨てました。

7. まとめ

  • 個人開発では、クラウドに出さない選択肢を最初に検討する価値があります。利用者が家の中だけなら、外に出すのはリスクを増やすだけです
  • macOS の launchd は、常駐サービスを動かす手段として実用になります。KeepAliveRunAtLoad だけで、家族が使う常駐には足りました
  • localStorage の「端末ごと」という性質は、アーキテクチャの制約として先に扱います。後から気づくと運用ルールごと作り直しになります
  • 使われるか分からない機能は作りません。要求が実際に出てから作ります

NIGHT BAND は、いまは家の中の1台で動いている記録画面です。公開用に別系統を育てるとしても、この記事の範囲はそこまで入りません。家の中に閉じた判断が、まだ壊れていない、という記録として書いておきます。

その後の顛末として

最初にアプリを見せた時は喜んでくれましたが、テストの結果をスマホからぽちぽち入力するのがめんどくさかったようで、最近は放置気味です。(まだレベル5)

全部の端末で入力することができるようにするのと、解答用紙をスキャンしたら点数を読み取れるようにするか改良を検討中です。

ニーズがあれば、もうちょっと作り込んだのちにオープンソースとして公開するかもしれません。

コメントを送信

CAPTCHA