Chrome拡張機能のストレージAPI完全ガイド

Chrome拡張機能でデータを保存・取得するための`chrome.storage` APIについて詳しく解説します。

## 目次

1. [ストレージの種類](#ストレージの種類)
2. [基本的な使い方](#基本的な使い方)
3. [非同期処理について](#非同期処理について)
4. [Promise化する方法](#promise化する方法)
5. [変更の監視](#変更の監視)
6. [エラー処理](#エラー処理)
7. [実用的な例](#実用的な例)
8. [よくある質問](#よくある質問)

## ストレージの種類

Chrome拡張機能では主に2種類のストレージが提供されています：

### 1. `chrome.storage.sync`

- データがGoogleアカウントと同期される
- ユーザーが異なるデバイスで拡張機能を使用する場合も同じデータが利用可能
- 容量制限: 1アイテムあたり8KB、合計100KBまで
- ユーザー設定や小さなデータの保存に最適

### 2. `chrome.storage.local`

- ローカルマシンにのみデータを保存
- 同期されない
- 容量制限: 合計5MBまで
- 大きなデータや同期する必要のないデータに最適

## 基本的な使い方

### データの保存 (set)
`chrome.storage.sync/local.set` は、キーと値のペアを保存するためのメソッドです。オブジェクト形式でデータを渡します。
```javascript
// 同期ストレージにデータを保存
chrome.storage.sync.set(
    { 
        // キー: 値 の形式でデータを指定
        'userData': { 
            name: 'John', 
            email: 'john@example.com' 
        },
        // 複数のキーを一度に保存可能
        'lastLogin': new Date().toISOString(),
        'counter': 42
    }, 
    // 完了時のコールバック関数（省略可能）
    () => { 
        // 保存が完了したときに実行される
        console.log('User data is stored in sync storage');
        // エラーチェックも可能
        if (chrome.runtime.lastError) {
            console.error('Error:', chrome.runtime.lastError);
        }
    }
);

// ローカルストレージにデータを保存（構造は同じ）
chrome.storage.local.set({ 'cachedImages': imageDataArray }, () => {
    console.log('Images are stored locally');
});
```

**データ形式:**

- JSONシリアライズ可能なデータのみ保存できます（オブジェクト、配列、文字列、数値、ブール値）
- 関数、DOM要素、循環参照を含むオブジェクトは保存できません
- `chrome.storage.sync` の場合、1アイテムあたり8KB、合計100KBの制限があります

### データの取得 (get)
`chrome.storage.sync/local.get` は、保存したデータを取得するためのメソッドです。キー名または配列で取得したいデータを指定します。
```javascript 
// 単一のキーからデータを取得
chrome.storage.sync.get('userData', (result) => {
    // result は { userData: { name: 'John', email: 'john@example.com' } } のようなオブジェクト
    console.log('Retrieved user data:', result.userData);
    
    // キーが存在しない場合は undefined になる
    if (result.userData === undefined) {
        console.log('User data not found');
    }
});

// 複数のキーを一度に取得
chrome.storage.sync.get(['settings', 'userData'], (result) => {
    // result は { settings: {...}, userData: {...} } のようなオブジェクト
    console.log('Settings:', result.settings);  // 存在しない場合は undefined
    console.log('User data:', result.userData);
    
    // 安全に取得するには
    const settings = result.settings || { theme: 'default' }; // デフォルト値を設定
});

// すべてのデータを取得
chrome.storage.sync.get(null, (items) => {
    // items はストレージ内のすべてのキーと値を含むオブジェクト
    console.log('All sync data:', items);
    
    // オブジェクトの列挙
    for (const [key, value] of Object.entries(items)) {
        console.log(`Key: ${key}, Value:`, value);
    }
});
```

**戻り値の形式:**

- コールバック関数の `result` 引数は、リクエストしたキーをプロパティとして持つオブジェクト
- 存在しないキーを要求した場合、そのキーは結果オブジェクトに含まれない
- `get(null)` はすべてのデータを含むオブジェクトを返す

### データの削除 (remove)
`chrome.storage.sync/local.remove` は、指定したキーのデータを削除するメソッドです。
```javascript 
// 特定のキーを削除
chrome.storage.sync.remove('temporaryData', () => {
    console.log('Temporary data removed');
});

// 複数のキーを削除
chrome.storage.sync.remove(['cache', 'tempSettings'], () => {
    console.log('Multiple keys removed');
});
```
<details><summary>storage.sync/local.remove() のより実践的な例</summary>

```javascript
// 特定のキーを削除
chrome.storage.sync.remove('temporaryData', () => {
    // 削除完了時に実行されるコールバック
    console.log('Temporary data removed');
    
    // 削除に失敗した場合のエラーチェック
    if (chrome.runtime.lastError) {
        console.error('Error removing data:', chrome.runtime.lastError);
    }
});

// 複数のキーを削除
chrome.storage.sync.remove(['cache', 'tempSettings'], () => {
    console.log('Multiple keys removed');
    
    // 削除後に確認したい場合は、get を使用
    chrome.storage.sync.get(['cache', 'tempSettings'], (result) => {
        // 削除されていれば、これらのキーは result に存在しない
        console.log('Deleted keys still exist?', 'cache' in result, 'tempSettings' in result);
    });
});
```
</details>

### すべてのデータを削除 (clear)
`chrome.storage.sync.clear` は、ストレージ内のすべてのデータを削除するメソッドです。
```javascript 
chrome.storage.sync.clear(() => {
    console.log('All sync data cleared');
});
```
<details><summary>storage.sync/local.clear() のより実践的な例</summary>

```javascript
chrome.storage.sync.clear(() => {
    // すべてのデータが削除された後に実行されるコールバック
    console.log('All sync data cleared');
    
    // エラーチェック
    if (chrome.runtime.lastError) {
        console.error('Error clearing storage:', chrome.runtime.lastError);
    }
    
    // 確認
    chrome.storage.sync.get(null, (items) => {
        console.log('Storage after clearing:', items); // 空のオブジェクト {} になるはず
        console.log('Number of items:', Object.keys(items).length); // 0 になるはず
    });
});
```
</details>


## 非同期処理について
**重要:** `chrome.storage` のすべてのメソッドは非同期で動作しますが、Promise ベースではなくコールバックベースです。

### 正しい非同期処理の扱い方
```javascript 
// 間違った使用法 (非同期処理を考慮していない)
let myData;
chrome.storage.sync.get('userData', (result) => {
    myData = result.userData;  // この代入は非同期で行われる
});
console.log(myData);  // undefined になる可能性が高い

// 正しい使用法
chrome.storage.sync.get('userData', (result) => {
    const myData = result.userData;
    console.log(myData);  // ここでデータを使用する
    // データを使った処理をここに書く
});
```

### Promise化する方法
Chrome拡張機能APIは基本的にコールバックベースですが、Promiseを使って扱いやすくすることができます。

```javascript
// 単一キーのデータを取得するPromiseラッパー
function getStorageData(key) {
    return new Promise((resolve, reject) => {
        chrome.storage.sync.get(key, (result) => {
            if (chrome.runtime.lastError) {
                reject(chrome.runtime.lastError);
            } else {
                resolve(result[key]);
            }
        });
    });
}
```

**なぜPromise化が必要か？**
`chrome.storage.sync.get` は非同期処理ですが、Promise ベースではなくコールバックベースの API です。そのため、直接 .then() でつなげることはできません。Promise化する主な理由は：

1. モダンな非同期パターンを使えるようにする：

    - async/await 構文が使える
    - Promise チェーンが使える
    - より読みやすく、メンテナンスしやすいコードになる

2. エラーハンドリングの統一：
    - try/catch でエラーをキャッチできる
    - Promise の reject でエラーを伝播できる

3. 複数の非同期処理の連携：
    - Promise.all などの Promise ユーティリティが使える

<details><summary>Promise ラッパー関数の実装例</summary>

```javascript 
// 単一キーのデータを取得するPromiseラッパー
function getStorageData(key) {
    return new Promise((resolve, reject) => {
        chrome.storage.sync.get(key, (result) => {
            if (chrome.runtime.lastError) {
                reject(chrome.runtime.lastError);
            } else {
                resolve(result[key]);
            }
        });
    });
}

// 複数キーまたはすべてのデータを取得するPromiseラッパー
function getAllStorageData(keys = null) {
    return new Promise((resolve, reject) => {
        chrome.storage.sync.get(keys, (result) => {
            if (chrome.runtime.lastError) {
                reject(chrome.runtime.lastError);
            } else {
                resolve(result);
            }
        });
    });
}

// データを保存するPromiseラッパー
function setStorageData(data) {
    return new Promise((resolve, reject) => {
        chrome.storage.sync.set(data, () => {
            if (chrome.runtime.lastError) {
                reject(chrome.runtime.lastError);
            } else {
                resolve();
            }
        });
    });
}

// データを削除するPromiseラッパー
function removeStorageData(keys) {
    return new Promise((resolve, reject) => {
        chrome.storage.sync.remove(keys, () => {
            if (chrome.runtime.lastError) {
                reject(chrome.runtime.lastError);
            } else {
                resolve();
            }
        });
    });
}
```
</details>

### Promiseラッパーの使用例
上記で定義した、`chrome.storage.sync.get()` の処理をPromiseでラップする `getStorageData()` を用いて例を示すと：
```javascript 
// .then() を使った例
getStorageData('userData')
    .then(userData => {
        console.log('User data:', userData);
        return getStorageData('settings');
    })
    .then(settings => {
        console.log('Settings:', settings);
        // 両方のデータを使った処理
    })
    .catch(error => {
        console.error('Error:', error);
    });

// async/await を使った例
async function loadUserProfile() {
    try {
        const userData = await getStorageData('userData');
        const settings = await getStorageData('settings');
        
        // 両方のデータが揃ってから処理を続行
        console.log('User:', userData.name);
        console.log('Theme:', settings.theme);
        
        // UIを更新
        updateUserInterface(userData, settings);
    } catch (error) {
        console.error('Failed to load profile:', error);
        showErrorMessage('プロフィールの読み込みに失敗しました');
    }
}
```

## 変更の監視
ストレージの変更を監視することで、データが更新されたときに自動的に処理を実行できます：

```javascript 
chrome.storage.onChanged.addListener((changes, namespace) => {
    for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
        console.log(
            `Storage key "${key}" in ${namespace} changed:`,
            `Old value:`, oldValue,
            `New value:`, newValue
        );
        
        // 特定のキーの変更に対応
        if (key === 'settings' && namespace === 'sync') {
            updateUIWithNewSettings(newValue);
        }
    }
});
```

## エラー処理
Chrome拡張機能のストレージ操作では、エラーハンドリングが重要です：

```javascript
// コールバックでのエラー処理
chrome.storage.sync.get('userData', (result) => {
    if (chrome.runtime.lastError) {
        console.error('Error retrieving data:', chrome.runtime.lastError);
        showErrorToUser('データの取得に失敗しました');
        return;
    }
    
    // 正常系の処理
    displayUserData(result.userData);
});

// Promiseでのエラー処理
getStorageData('userData')
    .then(userData => {
        displayUserData(userData);
    })
    .catch(error => {
        console.error('Error retrieving data:', error);
        showErrorToUser('データの取得に失敗しました');
    });
```

## 実用的な例
### 例1: ユーザー設定の管理
```javascript 

// デフォルト設定
const DEFAULT_SETTINGS = {
    theme: 'light',
    fontSize: 'medium',
    notifications: true
};

// 設定を読み込む
async function loadSettings() {
    try {
        const settings = await getStorageData('settings');
        return settings || DEFAULT_SETTINGS;
    } catch (error) {
        console.error('Failed to load settings:', error);
        return DEFAULT_SETTINGS;
    }
}

// 設定を保存する
async function saveSettings(newSettings) {
    try {
        await setStorageData({ 'settings': newSettings });
        console.log('Settings saved successfully');
        return true;
    } catch (error) {
        console.error('Failed to save settings:', error);
        return false;
    }
}

// 設定画面の初期化
async function initSettingsPage() {
    const settings = await loadSettings();
    
    // UIに設定を反映
    document.getElementById('theme').value = settings.theme;
    document.getElementById('fontSize').value = settings.fontSize;
    document.getElementById('notifications').checked = settings.notifications;
    
    // 保存ボタンのイベントリスナー
    document.getElementById('saveButton').addEventListener('click', async () => {
        const newSettings = {
            theme: document.getElementById('theme').value,
            fontSize: document.getElementById('fontSize').value,
            notifications: document.getElementById('notifications').checked
        };
        
        const success = await saveSettings(newSettings);
        if (success) {
            showMessage('設定を保存しました');
        } else {
            showError('設定の保存に失敗しました');
        }
    });
}
```

### 例2: 認証トークンの管理
```javascript 
// トークンを保存
async function saveAuthToken(token) {
    try {
        await setStorageData({ 'authToken': token });
        console.log('Auth token saved');
        return true;
    } catch (error) {
        console.error('Failed to save auth token:', error);
        return false;
    }
}

// トークンを取得
async function getAuthToken() {
    try {
        return await getStorageData('authToken');
    } catch (error) {
        console.error('Failed to get auth token:', error);
        return null;
    }
}

// ログイン処理
async function login(username, password) {
    try {
        // APIからトークンを取得する処理（例）
        const response = await fetch('https://api.example.com/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ username, password })
        });
        
        if (!response.ok) throw new Error('Login failed');
        
        const data = await response.json();
        
        // トークンを保存
        await saveAuthToken(data.token);
        
        return true;
    } catch (error) {
        console.error('Login error:', error);
        return false;
    }
}

// 認証が必要なAPIリクエスト
async function fetchProtectedData() {
    const token = await getAuthToken();
    
    if (!token) {
        console.error('No auth token available');
        return null;
    }
    
    try {
        const response = await fetch('https://api.example.com/data', {
            headers: {
                'Authorization': `Bearer ${token}`
            }
        });
        
        if (!response.ok) throw new Error('API request failed');
        
        return await response.json();
    } catch (error) {
        console.error('API request error:', error);
        return null;
    }
}
```

### よくある質問
#### Q: chrome.storage.syncとchrome.storage.localのどちらを使うべきですか？
A: 以下の基準で選択するとよいでしょう：

ユーザー設定や小さなデータで、デバイス間で同期したい場合はchrome.storage.sync
大きなデータ（画像、キャッシュなど）や、デバイス固有のデータはchrome.storage.local

#### Q: chrome.storage.sync.getを直接Promise化せずに.then()でつなげられますか？
A: できません。chrome.storage.sync.getはコールバックベースのAPIであり、Promiseを返さないため、直接.then()でつなげることはできません。Promiseラッパー関数を作成する必要があります。

#### Q: ストレージの容量制限を超えた場合はどうなりますか？
A: エラーが発生します。chrome.runtime.lastErrorでキャッチできるので、大きなデータを保存する前に容量チェックを行うか、適切なエラーハンドリングを実装しましょう。

#### Q: 保存できるデータの型に制限はありますか？
A: JSONシリアライズ可能なデータのみ保存できます。これには、文字列、数値、ブール値、配列、オブジェクトが含まれます。関数、Symbol、Mapなどは直接保存できません。

#### Q: バックグラウンドスクリプトとコンテンツスクリプトの両方からストレージにアクセスできますか？
A: はい、どちらからもアクセス可能です。ただし、コンテンツスクリプトからはchrome.storageを直接使用するためにmanifest.jsonで適切な権限を設定する必要があります。

```json 
{
  "permissions": [
    "storage"
  ]
}
```


## まとめ
Chrome拡張機能のストレージAPIは、データの永続化に非常に便利なツールです。コールバックベースのAPIですが、Promiseでラップすることで、モダンな非同期パターンを活用できます。適切なエラーハンドリングと非同期処理の理解を持って使用することで、堅牢な拡張機能を開発することができます。