ツール論

ローカルストレージで作る『アカウント不要』のツール設計パターン

公開: 2026年5月4日

個人開発の Web ツールで「ユーザーの入力を保存したいが、 アカウント登録は要求したくない」という要件は頻繁に出てきます。 この場合、最も筋が良いのはブラウザのローカルストレージを使う設計です。

本記事では、ローカルストレージベースの「アカウント不要」設計パターンを、 頻出するパターンと落とし穴に分けて整理します。 想定するのは中小規模の Web ツール(計算系、エディタ系、簡易メモ系)です。

ローカルストレージの基本特性

まず特性を確認しておきます。

  • ブラウザごと・オリジン(プロトコル + ドメイン + ポート)ごとに分離されている
  • 容量制限は通常 5〜10MB 程度(ブラウザ依存)
  • 値は文字列のみ。JSON.stringify / JSON.parse を介して構造化データを扱う
  • ユーザーがブラウザを変えると消える、シークレットモードでは永続化しない
  • 同期 API(読み書きが即座にメインスレッドをブロック)。 大きなデータを毎回読み書きすると UI がカクつく

この特性のもとで、設計の選択肢が決まります。

パターン 1: 単一オブジェクトをまるごと保存

最もシンプルな構成。アプリの状態をひとつのオブジェクトに集約し、 そのまま JSON 化して 1 つのキーに保存します。

const KEY = "myapp:state:v1";

function loadState(): AppState {
  const raw = localStorage.getItem(KEY);
  if (!raw) return defaultState;
  try {
    return JSON.parse(raw);
  } catch {
    return defaultState;
  }
}

function saveState(state: AppState) {
  localStorage.setItem(KEY, JSON.stringify(state));
}

利点は実装の単純さ。欠点はオブジェクトが大きくなると 毎回の保存が重くなり、UI に影響が出始めること(数 MB を超えるあたりから)。

小規模なツール(フォーム入力の保存、計算結果の履歴 10〜20 件程度)には このパターンで十分です。

パターン 2: キーを分けて部分更新

状態が大きくなる場合、キーを分けて部分的に保存・読込みします。

localStorage.setItem("myapp:settings", JSON.stringify(settings));
localStorage.setItem("myapp:history", JSON.stringify(history));
localStorage.setItem("myapp:draft", JSON.stringify(draft));

頻繁に変わる部分(draft)と、変更が少ない部分(settings)を 別キーにすることで、保存コストを下げられます。

ただしキーが増えるほど管理が複雑になるため、3〜5 個程度が現実的な上限。 それを超える規模なら IndexedDB に切り替えます。

パターン 3: バージョニングを最初から入れる

ツールを更新するうちに、保存しているデータの構造を変えたくなることがあります。 その時に古いデータを読み込んで壊れる、というのが最頻出のバグです。

対策として、最初からキーにバージョン番号を付け、 起動時にマイグレーションするパターンが有効です。

const CURRENT_VERSION = 2;

function loadState(): AppState {
  const v1 = localStorage.getItem("myapp:state:v1");
  const v2 = localStorage.getItem("myapp:state:v2");

  if (v2) return JSON.parse(v2);

  if (v1) {
    // マイグレーション: v1 → v2
    const migrated = migrateV1ToV2(JSON.parse(v1));
    localStorage.setItem("myapp:state:v2", JSON.stringify(migrated));
    return migrated;
  }

  return defaultState;
}

最初の v1 を作るとき「これ後で変わるかな」と気にせず、 まずは v1 を切る習慣にしておくと、後から楽になります。

パターン 4: エクスポート / インポート機能

アカウント不要のトレードオフは、ブラウザを変えるとデータが消えること。 この弱点を補うために、JSON エクスポート / インポート機能を 最低限提供しておくと、ユーザーの満足度が上がります。

function exportState() {
  const data = JSON.stringify(loadState(), null, 2);
  const blob = new Blob([data], { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `myapp-export-${Date.now()}.json`;
  a.click();
  URL.revokeObjectURL(url);
}

function importState(file: File) {
  const reader = new FileReader();
  reader.onload = () => {
    const data = JSON.parse(reader.result as string);
    saveState(data);
  };
  reader.readAsText(file);
}

この機能があると、ユーザーは「データを大事にしたければ自分で書き出す」 という運用ができます。アカウントなしでも、データの所有権を ユーザー側に保てる構造です。

パターン 5: ストレージイベントで複数タブ同期

ユーザーが同じツールを複数タブで開いているとき、 片方の変更がもう片方に反映されない、というのは小さくないストレスです。

storage イベントを使うと、他のタブからの変更を検知できます。

window.addEventListener("storage", (e) => {
  if (e.key === "myapp:state:v1" && e.newValue) {
    setState(JSON.parse(e.newValue));
  }
});

注意: storage イベントは 他のタブ でだけ発火します。 同じタブで setItem しても、そのタブには通知されません。 この前提を忘れて「動かない」と数時間消費するのが定番のハマりポイントです。

落とし穴と回避策

シークレットモードでの空挙動

シークレットモードでは、ローカルストレージは使えるが セッション終了で消えます。「設定が保存できない」報告は ほぼこれが原因です。重要な変更時にトーストで 「シークレットモードでは保存されません」と通知するのが安全。

容量制限への対処

ローカルストレージの容量を超えると setItem が QuotaExceededError を投げます。try / catch で握り、 古いデータの削除を促す UI を出すか、IndexedDB へ移行します。

SSR との相性

Next.js 等で SSR している場合、サーバー側では window が存在せず、 ローカルストレージにアクセスするとエラーになります。 必ず typeof window !== "undefined" ガードを入れるか、 useEffect でマウント後に読み込むようにします。

JSON のシリアライズ漏れ

Date オブジェクト、Map、Set、関数は JSON.stringify で 正しくシリアライズされません。Date は ISO 文字列に変換、 Map / Set は配列に変換、というように事前処理が必要。 この漏れは「保存して読み込んだら型が変わっていた」というバグになります。

セキュリティ的に保存してはいけないもの

  • パスワード(ハッシュ化されていても保存しない)
  • API キー、認証トークン(XSS で漏れる)
  • クレジットカード情報
  • 個人情報のうち他人に紐づくもの

ローカルストレージは XSS で簡単に読み取られます。 重要情報は最初からサーバー側で扱うべきで、 「ローカル保存だから安全」は誤解です。

「アカウント不要」を長く続けるために

この設計パターンの利点は、ユーザーが「使い始めるまでの摩擦」が ほぼゼロになることです。実装側は次のことを意識すると、 後で困りにくくなります。

  • バージョニングを最初から入れる
  • エクスポート / インポート機能を初期リリースから付ける
  • シークレットモードへの説明文をアプリ内に持つ
  • 重要操作(削除、リセット)は確認ダイアログを挟む

まとめ

ローカルストレージベースの「アカウント不要」設計は、 個人開発の小規模ツールで強力に機能します。 サーバー運用の責任を負わずに状態保存が実現でき、 ユーザーは登録の摩擦なく使い始められる。

スケールしてサーバー保存に移行することはあっても、 最初の段階ではローカル完結で出すほうが、 プロダクトを世に出すまでの距離が確実に短くなります。