<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>LocalStorage on 怠惰技術ブログ</title>
    <link>/tags/localstorage/</link>
    <description>Recent content in LocalStorage on 怠惰技術ブログ</description>
    <generator>Hugo -- 0.147.7</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 28 Mar 2026 15:00:00 +0900</lastBuildDate>
    <atom:link href="/tags/localstorage/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>React で作る堅牢なゲームセーブシステム：localStorage と Reducer を疎結合に保つ設計</title>
      <link>/posts/2026-03-28-save-load-system-reducer/</link>
      <pubDate>Sat, 28 Mar 2026 15:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-save-load-system-reducer/</guid>
      <description>&lt;h1 id=&#34;react-で作る堅牢なゲームセーブシステムlocalstorage-と-reducer-を疎結合に保つ設計&#34;&gt;React で作る堅牢なゲームセーブシステム：localStorage と Reducer を疎結合に保つ設計&lt;/h1&gt;
&lt;p&gt;ゲーム開発において、プレイヤーの進行状況を保存する「セーブ / ロード」は最も重要な機能の一つです。Web ブラウザで動作する React アプリケーションの場合、最も手軽な保存先は &lt;code&gt;localStorage&lt;/code&gt; です。&lt;/p&gt;
&lt;p&gt;しかし、単純に &lt;code&gt;localStorage.setItem&lt;/code&gt; をコードのあちこちに散りばめてしまうと、テスタビリティが低下し、データ構造の変更（スキーマ変更）に弱いシステムになってしまいます。&lt;/p&gt;
&lt;p&gt;本記事では、架空の RPG 『React Odyssey』を例に、&lt;code&gt;SavePort&lt;/code&gt; インターフェースと &lt;code&gt;Reducer&lt;/code&gt; の初期化関数を組み合わせた、クリーンで堅牢なセーブ / ロードの実装方法を解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-概要なぜ直接-localstorageを避けるのか&#34;&gt;1. 概要：なぜ「直接 localStorage」を避けるのか&lt;/h2&gt;
&lt;p&gt;React の &lt;code&gt;useReducer&lt;/code&gt; を使ったゲーム開発では、ゲームの状態（State）は一つの大きなオブジェクトとして管理されます。これを &lt;code&gt;JSON.stringify&lt;/code&gt; して &lt;code&gt;localStorage&lt;/code&gt; に保存するのは簡単です。&lt;/p&gt;
&lt;p&gt;しかし、以下の理由から、ロジックの中に直接 &lt;code&gt;localStorage&lt;/code&gt; を書くべきではありません。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;副作用の分離&lt;/strong&gt;: Reducer は純粋関数であるべきです。セーブ処理（副作用）を Reducer の中に入れることはできません。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;環境非依存&lt;/strong&gt;: 将来的に保存先を &lt;code&gt;IndexedDB&lt;/code&gt; やクラウド（Firebase 等）に変更したくなったとき、コードを大幅に書き換える必要が出てきます。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;テストのしやすさ&lt;/strong&gt;: &lt;code&gt;localStorage&lt;/code&gt; が存在しない Node.js 環境（Vitest 等）でロジックのテストを行う際、モック化が容易である必要があります。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;これらを解決するために、&lt;strong&gt;Dependency Inversion Principle（依存性逆転の原則）&lt;/strong&gt; に基づいた設計を採用します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-saveport-設計抽象化の定義&#34;&gt;2. SavePort 設計：抽象化の定義&lt;/h2&gt;
&lt;p&gt;まずは、ゲームエンジン側が必要とする「セーブ機能」のインターフェースを定義します。これを &lt;code&gt;SavePort&lt;/code&gt; と呼びます。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// domain/save/save-port.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/** 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * 保存されるデータの構造（スキーマ）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * ゲームの現在の状態に加え、メタ情報を付与する
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SaveData&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;version&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;;        &lt;span style=&#34;color:#75715e&#34;&gt;// セーブデータのバージョン
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;timestamp&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;      &lt;span style=&#34;color:#75715e&#34;&gt;// 保存日時
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;GameState&lt;/span&gt;;       &lt;span style=&#34;color:#75715e&#34;&gt;// 実際のゲーム状態
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * セーブ/ロードに関する抽象インターフェース
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SavePort&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** データを保存する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;SaveData&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;void&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** データを読み込む。存在しない場合は null を返す */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;load&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;SaveData&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;null&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** セーブデータが存在するか確認する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;exists&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;boolean&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** セーブデータを削除する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;clear&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;void&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;なぜインターフェースにするのか？&lt;/strong&gt;
ゲームのメインロジック（UseCase）は、この &lt;code&gt;SavePort&lt;/code&gt; を通じて読み書きを行います。この時点では「どうやって保存するか」は知らなくてよく、「保存できる」という事実だけに依存させます。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
