🔫

会話形式で綴る「fetch()のキャッシュ」

2023/08/06に公開

登場人物

僕・・・フロントエンドエンジニア
教えたがりおじさん・・・フロントエンドエンジニア

とあるシステム開発会社にて

僕「あれ?」
僕「ブラウザを何度もリロードしても、1分に1回くらいしかデータが更新されないなぁ」

教えたがりおじさん(以下、おじさん)「ん?どうしたんや」

僕「あ、教えたがりおじさん」
僕「実はですね」

社員名簿的なWebシステムを作っている

僕「今、社員名簿的なWebシステムを作ってまして」
僕「社員一覧っていうページを作ろうとしてるんですけど」

おじさん「ほー、弊社の社員一覧を画面に表示する感じやね」

僕「そうです」
僕「APIの/membersっていうエンドポイントを叩いて、社員一覧のデータを取得して」
僕「それを画面に表示するところまではできたんです」

おじさん「なるほどな」
おじさん「SPAのフロントエンド部分を作ってる感じやね」
おじさん「それで、何が問題なん?」

僕「えっとですね」
僕「データベースに社員を追加してから、ブラウザをリロードしても」
僕「追加した社員が表示されないんですよ」
僕「1分くらい経ってからブラウザをリロードすると、やっと表示されるんです」

おじさん「ほ〜、コードはどんな感じ?」

僕「え〜と」

const getUsers = async () => {
  const response = await fetch("https://example.com/api/members");
  const body = await response.json();
  return body;
};

僕「↑こんな感じで普通にfetch()してます」

おじさん「なるほどな」
おじさん「それやと、ブラウザをリロードしてもHTTPリクエストが飛んでない可能性あるわ」

僕「え、そうなんですか」
僕「APIと通信してないってことですか?」

おじさん「せやで」
おじさん「Chromeのデベロッパーツール開いて、ネットワークタブを見てみ?」
おじさん「/membersと通信してるはずの行をな」

僕「はい」

ネットワークタブを見てみた

僕「/membersの行の、サイズっていう項目が」
僕「〇〇kBとかじゃなくて(ディスクキャッシュ)って表示されてますね」

おじさん「やっぱりな」
おじさん「そう表示されとる場合は、APIへのリクエストは飛んでへんわ」
おじさん「ブラウザのキャッシュに保存されてた、前回のデータが使われとるわ」

僕「え、そうなんですね」

おじさん「ああ、せやで」
おじさん「fetch()はな、取得したデータをブラウザのキャッシュに保存してくれて」
おじさん「次回以降も使い回してくれるんや」

僕「うーん、それじゃ困ります」
僕「データベースに社員を追加しても、社員一覧ページに反映されないじゃないですか」

おじさん「せやな」
おじさん「でも、fetch()はデフォルトでそういう挙動になってんねん」

僕「へぇ・・・」
僕「ただ、1分間くらい間隔を空けてからブラウザをリロードすると」
僕「追加した社員も画面に表示されましたよ?」
僕「あれは何故なんでしょうか?」

おじさん「データが古くなった後で、再度fetch()をした場合」
おじさん「ブラウザ君はキャッシュのデータを使わずに、もう一度APIにリクエストを送って」
おじさん「新しいデータを取得してくれるんや」

僕「へぇ、1分間経ったキャッシュデータは、ブラウザ君にとって古いってことになるんですか?」

おじさん「今回の場合は、そうなっとるみたいやな」

僕「それは、どこを見れば分かるんですか?」

おじさん「それも、デベロッパーツールのネットワークタブで分かるで」
おじさん「/membersの行をクリックして」
おじさん「ヘッダーというタブを見てみ?」

僕「はい」

おじさん「レスポンスヘッダーの部分にCache-Control:って書いてないか?」

Cache-Control: max-age=60

僕「↑こう書いてありますね」

おじさん「せやろ?」
おじさん「それはな」

このレスポンスは、60秒経過するまでは新鮮だという扱いとする

おじさん「↑こんな意味なんや」

僕「なるほど、社員一覧のデータと一緒に」
僕「こんな情報もAPIからブラウザに来てたんですね」

おじさん「せやで」
おじさん「だからブラウザ君もそれを受けて、以下のように動くんや」

ブラウザ君「お、ブラウザがリロードされた」
ブラウザ君「ほなまたfetch()を実行して/membersからデータを取得しよか」
ブラウザ君「あれ、でもさっき取得したばっかりや」
ブラウザ君「さっき取得した時、レスポンスヘッダにCache-Control: max-age=60って書いてあったな」
ブラウザ君「じゃあ、今回はAPIにリクエストを投げへんでええわ」
ブラウザ君「前回からまだ60秒も経過してへんからな」
ブラウザ君「ワイがキャッシュに保存しておいた、さっきの社員一覧を返しとこか」

僕「なるほど」

キャッシュされたデータを使いたくない場合はどうすればいいか

僕「じゃあ、60秒待たずに、すぐに最新の社員一覧を取得したい場合はどうすればいいんですか?」

おじさん「fetch()に第二引数を渡してやればええんや」

    const getUsers = async () => {
-     const response = await fetch("https://example.com/api/members");
+     const response = await fetch("https://example.com/api/members", { cache: "no-store" });
      const body = await response.json();
      return body;
    };

僕「へぇ、第二引数として{ cache: "no-store" }を指定するんですね」

おじさん「せや、こうしてやると」
おじさん「キャッシュを使わずに、毎回APIと通信してくれるんや」

僕「なるほど〜」

おじさん「でも、社員って毎日増えるわけやないから」
おじさん「キャッシュの値を使っとけばええんちゃう?」
おじさん「ブラウザをリロードするたびにAPIと通信するの、無駄やん」

僕「そう言われてみるとそうですね」
僕「むしろ、弊社なんて1年に1人くらいしか社員が増えませんからね」
僕「ずっとキャッシュの値のままでもいいくらいですね」

おじさん「悲しいけども、そうやな」

僕「じゃあ、ずっとキャッシュの値を使い続けることもできるんですか?」

おじさん「できるで」

キャッシュされたデータを使い続けたい場合はどうすればいいか

    const getUsers = async () => {
-     const response = await fetch("https://example.com/api/members", { cache: "no-store" });
+     const response = await fetch("https://example.com/api/members", { cache: "force-cache" });
      const body = await response.json();
      return body;
    };

おじさん「こうや」

僕「なるほど」
僕「fetch()の第二引数の{ cache: "no-store" }を」
僕「{ cache: "force-cache" }に変えたんですね」

おじさん「せや」
おじさん「こうしておくと、ずっとキャッシュを使ってくれるんや」
おじさん「初めてfetch()する時とか、キャッシュがない場合だけ」
おじさん「APIとの通信が発生する感じや」

僕「なるほど〜」
僕「ネットワークタブって、こんなに重要な情報がいっぱい書いてあったんですね」
僕「これで僕も、ブラウザのキャッシュを上手く使えるようになりそうです!」
僕「ブラウザのキャッシュ、完全に理解しました!」

おじさん「せやな!」
おじさん「でも、サーバが返すレスポンスヘッダに」

Cache-Control: no-store

おじさん「↑こう書いてあったらブラウザ君はキャッシュに保存してくれへんし」
おじさん「他にも色々な条件があるから、まだまだ勉強せんとやで」

僕「Oh・・・」

おじさん「Next.jsを使う場合なんかは、fecth()が独自拡張されてたりして」
おじさん「挙動も違うみたいやし」

僕「ファーーーー」
僕「勉強すること多すぎですね・・・」

おじさん「せやな」
おじさん「もうダルいから、一緒に退職しよか」

僕「そうですね」
僕「じゃあ、この社員名簿システムにメンバー削除機能を実装して」
僕「二人で退職しましょう!」

〜完〜

参考資料

株式会社ゆめみ

Discussion