✍️

Claude Codeで学ぶ -キャッシュについて-

に公開

エンジニアの新卒・中途採用をやっています三浦陸です。
自分は情報工学科で4年間学んできたのですが、最近は理学系や文系からエンジニアを目指す方も多いです。
その中には1から体系的に学ぼうとする方がいます。

突然ですが、イラストレーターは「とにかく描け」とよく言われています。
デッサンの基本を学ぶことも大事ですが、描いて学んだ方が身につきやすく、続きやすいからこのように指摘されるそうです。

エンジニアも同じで、こんなもの作りたいと思ったものを作ってみる事が成長につながります。
とはいえ、何を作ったらいいか分からんとなるのも理解できます。
そんな人のために、生きている中で湧いた疑問・違和感や体系的な学習の中で知った言葉・概念からClaude Codeに何かを作ってもらうことを提案します。

キャッシュは1つじゃない

キャッシュはエンジニアじゃなくても聞く言葉だと思います。

「その不具合、キャッシュをクリアしたら直るよ」

と言われた時、あなたはどのような行動をしますか?

自分はブラウザの設定から履歴削除とかと同じ項目にあるキャッシュクリアを実行する事だけしか選択肢がないと思ってました。

過去に開発したシステムをレビューしてもらってる時、上司からこんなことを言われました。

上司「画面をリセットするにはどうすればいいの?」

三浦「まだリセットボタンを実装できてないので、キャッシュをクリアしてもらえます?」

上司「どのキャッシュ?」

どの?

「どの」ってキャッシュに付く言葉なの?

ChatGPTに聞いてみた

Webサイトに関して言うと、キャッシュにはいくつかの種類があります:

  • ブラウザキャッシュ
    ユーザーの端末(PCやスマホ)のブラウザが、画像・CSS・JavaScript・HTMLなどを一時保存しておく。
    → もう一度同じページを開いたときにサーバーに取りに行かず、保存しておいたものを使うから表示が速い。
  • サーバーキャッシュ
    WebサーバーやCDN(Cloudflareなど)が、動的に生成したページや画像を一時保存する。
    → 同じリクエストが来たら毎回生成せず、キャッシュを返すのでサーバー負荷が減る。
  • プロキシキャッシュ
    ネットワークの中継地点(プロキシサーバー)がデータを保存し、複数のユーザーが同じデータを利用できる。

なんかいっぱいあった!?

この時、意識的にキャッシュにデータを入れてユーザーごとの違いやどのタイミングで消えるか確かめてみたいと思いました。
なので、こんなアプリを作ってみました!

ふせんアプリ「Cache Sticky」

まずは、実際に使ってみてください!

https://rikumiura.com/projects/cache_sticky/

本当はブラウザのキャッシュに入れたかったのですが、ChatGPTいわくJavaScriptから直接操作する事が難しいそうなので、Cache APIで実現しました。
そのため、ディベロッパーツールからアプリケーションのキャッシュを消しに行く作りになってます。
こいつの存在を知らなかったので、学びでした!

最小構成として依頼した事

今回はClaude CodeとSpec-Kitで作成しました。
下記をChatGPTに伝えて、Spec-Kitのコマンドは生成してもらいました。

最小構成考えてみたから、実現可能かだけ判断して

# 目的
キャッシュの動きを可視化する
キャッシュの限界を知る

# 要件
ブレスト用として、ふせんをたくさん残せるアプリ
cache apiを利用して、ふせんをキャッシュに残す
ふせんの中に書ける文字数は200文字まで
ふせんの数は制限を設けない
 →増えすぎるとどういう挙動になるか知りたいから
新しいふせんを作成し始めたら、それ以外のふせんは編集できないようにする
ふせんは削除できないようにする

# 技術
claude codeで全て生成する

実装内容の解説

生成が終わったらClaude Codeに実装内容を解説してもらいました。
自分で書くのとは視点が違うので、解説と質問を繰り返すだけでいい勉強になります。

https://github.com/riku-miura/cache_sticky

Cache APIの基本構造

  Cache Stickyでは、CacheService.tsでCache APIを使用しています。主要な実装箇所を見ていきましょう。

  1. キャッシュの初期化

  // src/services/CacheService.ts:20-30
  private async initCache(): Promise<Cache | null> {
    try {
      if ('caches' in window) {
        return await caches.open(this.CACHE_NAME);
      }
      console.warn('Cache API is not available in this browser');
      return null;
    } catch (error) {
      console.warn('Cache API is not available or encountered an error, using memory-only mode:', error);
      return null;
    }
  }

  ポイント:
  - 'caches' in windowでブラウザがCache APIをサポートしているか確認
  - caches.open()で指定した名前のキャッシュストレージを開く(なければ作成)
  - エラー時はnullを返し、メモリモードにフォールバック

  2. ノートをキャッシュに書き込む

  // src/services/CacheService.ts:32-47
  async saveNote(note: Note): Promise<void> {
    this.notes.set(note.id, note);

    if (!this.cache) return;

    try {
      const response = new Response(JSON.stringify(note), {
        headers: {
          'Content-Type': 'application/json',
          'X-Timestamp': Date.now().toString()
        }
      });

      await this.cache.put(`/notes/${note.id}`, response);
    } catch (error) {
      console.warn('Failed to save note to cache:', error);
    }
  }

  重要な実装ポイント:

  3. 二重保存: this.notes.set()でメモリに保存し、Cache APIにも保存
    - Cache API失敗時でもメモリには残る(フォールバック戦略)
  4. Responseオブジェクトの作成: Cache APIはRequest/Responseペアで動作
    - new Response(JSON.stringify(note))でデータをJSON文字列化してResponseに変換
    - Content-Type: application/jsonでJSON形式を明示
    - X-Timestampカスタムヘッダーで保存時刻を記録
  5. URL形式のキー: /notes/${note.id}という擬似URL形式
    - Cache APIはHTTPキャッシュを模倣しているため、URL形式が必要
    - 実際のHTTPリクエストではなく、ローカルストレージとして使用

  6. キャッシュから読み込む

  // src/services/CacheService.ts:49-62
  async getNote(id: string): Promise<Note | undefined> {
    const memoryNote = this.notes.get(id);
    if (memoryNote) return memoryNote;

    if (!this.cache) return undefined;

    try {
      const response = await this.cache.match(`/notes/${id}`);
      if (response) {
        return await response.json();
      }
    } catch (error) {
      console.warn('Failed to get note from cache:', error);
    }
    return undefined;
  }

  優先順位:
  7. まずメモリ(this.notes)を確認
  8. メモリになければCache APIから取得
  9. cache.match()でキーに対応するResponseを取得
  10. response.json()でJSONをパース

  11. 全ノート取得時のキャッシュ処理

  // src/services/CacheService.ts:64-85
  async getAllNotes(): Promise<Note[]> {
    if (this.notes.size > 0) {
      return Array.from(this.notes.values());
    }

    if (!this.cache) return [];

    try {
      const requests = await this.cache.keys();
      const noteRequests = requests.filter(req => req.url.includes('/notes/'));

      const notes: Note[] = [];
      for (const request of noteRequests) {
        const response = await this.cache.match(request);
        if (response) {
          const note = await response.json();
          notes.push(note);
          this.notes.set(note.id, note);
        }
      }
      return notes;
    } catch (error) {
      console.warn('Failed to get notes from cache:', error);
      return [];
    }
  }

  ポイント:
  - cache.keys()で保存されているすべてのリクエストキーを取得
  - /notes/を含むものだけフィルタリング
  - 取得したデータをメモリにも展開(次回アクセスの高速化)

まとめ

100日チャレンジを皮切りにAIとの学習に関するコンテンツも増えてきています。
ただ、現場の肌感としてはまだまだ資格や有料の学習コンテンツをやってる人、それをアピールしている人が多いです。
今回のように作って、何を学んだか残していく事もいい学習だと考えているので、これからも実践して広めていければと思います。

Discussion