Mert Özen Her Satırda Daha İleri
Frontend JavaScript Orta — #15

JavaScript Web Storage ve Cookie: localStorage, sessionStorage ve document.cookie

Mert Özen 3 Kas 2026 9 dk 3 görüntülenme
JavaScript Web Storage ve Cookie: localStorage, sessionStorage ve document.cookie

localStorage ile sessionStorage farkını, JSON ile object saklamayı, sekmeler arası storage event'ini, kota ve güvenlik sınırlarını, cookie attribute'larını ve HttpOnly'nin neden görünmediğini anlatıyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Herhangi bir sayfada konsolu aç ve şu satırlara bak. Bir sayı ve bir object saklayıp geri okuyoruz:

localStorage.setItem("count", 5);
const count = localStorage.getItem("count");
console.log(count + 1);

localStorage.setItem("user", { name: "Ada" });
console.log(localStorage.getItem("user"));

localStorage.removeItem("count");
localStorage.removeItem("user");

İki console.log ne yazdırır? Okumaya devam etmeden önce bir tahmin yap. İpucu: 6 ve { name: "Ada" } diyorsan, bu yazının üçüncü bölümü tam sana göre.

Tarayıcının Küçük Dolapları

Sayfayı yenilediğinde bütün değişkenlerin sıfırlanır. Kullanıcının seçtiği tema, yarım kalan bir yazı, sepetteki ürünler, hepsi gider. Web Storage bu yüzden var: tarayıcının her site için ayırdığı, anahtar ve değerlerden oluşan küçük bir depo. İki çeşidi var ve ikisinin API'si birebir aynı.

localStorage'ı evindeki bir dolap gibi düşün. İçine koyduğun şey, sen ya da kullanıcı çıkarana kadar orada kalır; tarayıcıyı kapatıp bir hafta sonra açsan bile. sessionStorage ise otel odasındaki kasaya benzer. Odada kaldığın sürece işini görür, sayfayı yenilesen de içindekiler durur, ama sekmeyi kapattığında boşalır. Üstelik her sekmenin kendi kasası vardır; aynı siteyi iki sekmede açarsan iki ayrı sessionStorage olur.

localStorage.setItem("theme", "dark");   // yaz
localStorage.getItem("theme");           // "dark"
localStorage.getItem("missing");         // null
localStorage.length;                     // kayıt sayısı
localStorage.key(0);                     // 0. sıradaki anahtarın adı
localStorage.removeItem("theme");        // tek kaydı sil
localStorage.clear();                    // bu sitenin bütün kayıtlarını sil

Dolabın sahibi "origin"dir, yani protokol, alan adı ve port üçlüsü. https://example.com ile http://example.com farklı dolaplara sahiptir, https://shop.example.com de öyle. Bir site başka bir sitenin dolabını açamaz. Bir de motorun nasıl çalıştığına dair bir not: Web Storage senkrondur. setItem satırı dönene kadar başka hiçbir kod çalışmaz; küçük veride bunu hissetmezsin ama büyük veriyle ana thread'i tıkayabilir.

Her Şey String Olur

Baştaki sorunun cevabı burada. Web Storage yalnızca string saklar. setItem("count", 5) dediğinde 5 sayısı sessizce "5"'e çevrilir. Geri okuduğunda elinde bir string vardır ve "5" + 1 daha önce gördüğümüz coercion kuralıyla "51" olur. Object'te durum daha da kötü: object toString() ile string'e çevrilir ve geriye yalnızca "[object Object]" kalır. Veri gitmiş, yerinde bir etiket duruyor.

Çözüm daha önce gördüğümüz JSON. Yazarken JSON.stringify, okurken JSON.parse. Okuma tarafını bir yardımcı fonksiyona sarmak iyi bir alışkanlıktır, çünkü kayıt hiç olmayabilir ya da biri onu DevTools'tan elle bozmuş olabilir:

function saveJSON(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
}

function loadJSON(key, fallback) {
  const raw = localStorage.getItem(key);
  if (raw === null) return fallback;
  try {
    return JSON.parse(raw);
  } catch {
    return fallback;
  }
}

saveJSON("cart", [{ id: 7, qty: 2 }]);
console.log(loadJSON("cart", []));        // [{ id: 7, qty: 2 }]
console.log(loadJSON("nothing", []));     // []

Bunu konsola yapıştırıp dene. JSON'un sınırlarını da hatırla: Date object'i string olarak döner, Map ve Set boş object'e dönüşür, function'lar kaybolur. Tarihi saklarken genelde ISO string ya da timestamp saklayıp okurken yeniden Date'e çevirirsin.

Örnek: Kaybolmayan Taslak

Önceki yazının sonunda sorduğumuz soruya dönelim: kullanıcı formu yarıda bırakıp sekmeyi kapatırsa ne olur? Aşağıdaki dosyayı kaydedip aç, bir şeyler yaz, sonra sayfayı yenile ya da sekmeyi kapatıp yeniden aç.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Draft</title>
</head>
<body>
  <form id="note">
    <p><input name="title" placeholder="Title"></p>
    <p><textarea name="body" rows="6" cols="40" placeholder="Your note"></textarea></p>
    <button>Send</button>
    <span id="saved"></span>
  </form>
  <script>
    const KEY = "draft:note";
    const form = document.querySelector("#note");
    const savedLabel = document.querySelector("#saved");

    // Sayfa açılınca taslağı geri yükle
    try {
      const draft = JSON.parse(localStorage.getItem(KEY)) ?? {};
      for (const [name, value] of Object.entries(draft)) {
        if (form.elements[name]) form.elements[name].value = value;
      }
    } catch {
      localStorage.removeItem(KEY);
    }

    // Her değişiklikte kaydet
    form.addEventListener("input", () => {
      const data = Object.fromEntries(new FormData(form));
      localStorage.setItem(KEY, JSON.stringify(data));
      savedLabel.textContent = `Draft saved (${new Date().toLocaleTimeString("en-US")})`;
    });

    // Gönderilince taslağı temizle
    form.addEventListener("submit", (event) => {
      event.preventDefault();
      console.log("Sent:", Object.fromEntries(new FormData(form)));
      localStorage.removeItem(KEY);
      form.reset();
      savedLabel.textContent = "";
    });
  </script>
</body>
</html>

Anahtar adında draft: gibi bir ön ek kullanmak, aynı origin'de çalışan başka kodların kayıtlarıyla çakışmayı önler. JSON.parse(null)'ın hata vermeyip null döndüğüne de dikkat et; ?? {} bu yüzden orada. Burada localStorage yerine sessionStorage yazarsan taslak yenilemeye dayanır ama sekme kapanınca gider. Hangisinin doğru olduğu ürün kararıdır, teknik değil.

storage Event'i: Sekmeler Arası Haber

Aynı siteyi iki sekmede açtığını düşün. Birinde temayı koyu yaptın; diğeri bunu nasıl öğrenir? localStorage değiştiğinde tarayıcı, aynı origin'deki diğer sekme ve pencerelere bir storage event'i gönderir. Kritik ayrıntı şu: değişikliği yapan sekmenin kendisinde bu event tetiklenmez. Haber yalnızca ötekilere gider.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Theme sync</title>
  <style>
    body.dark { background: #222; color: #eee; }
  </style>
</head>
<body>
  <button id="toggle">Toggle theme</button>
  <script>
    function applyTheme(theme) {
      document.body.classList.toggle("dark", theme === "dark");
    }

    applyTheme(localStorage.getItem("theme"));

    document.querySelector("#toggle").addEventListener("click", () => {
      const next = localStorage.getItem("theme") === "dark" ? "light" : "dark";
      localStorage.setItem("theme", next);
      applyTheme(next); // bu sekmede storage event'i gelmez, kendimiz uyguluyoruz
    });

    window.addEventListener("storage", (event) => {
      console.log(event.key, event.oldValue, "→", event.newValue, event.url);
      if (event.key === "theme" || event.key === null) {
        applyTheme(localStorage.getItem("theme"));
      }
    });
  </script>
</body>
</html>

Bu sayfayı iki sekmede aç ve birinde butona bas; öteki sekme kendiliğinden renk değiştirir. Event object'i sana hangi anahtarın değiştiğini (key), eski ve yeni değeri (oldValue, newValue) ve değişikliğin hangi sayfadan geldiğini (url) söyler. Biri clear() çağırırsa key null gelir; kodda bu durumu ayrıca ele almamız bu yüzden. sessionStorage sekmeye özel olduğu için onun değişiklikleri başka sekmelere hiç ulaşmaz.

Kota, Hatalar ve Güvenlik

Dolap küçüktür. Tarayıcılar her origin için localStorage ve sessionStorage'a yaklaşık 5 MiB'lık yer ayırır. Dolduğunda setItem bir QuotaExceededError fırlatır. Kullanıcı verisi yazdığın her yerde bunu try/catch ile yakalamak, uygulamanın bir anda çökmesini önler:

try {
  localStorage.setItem("cache:products", JSON.stringify(bigList));
} catch (error) {
  if (error.name === "QuotaExceededError") {
    console.warn("Storage is full, skipping the cache");
  } else {
    throw error;
  }
}

Gizli pencerede açılan sayfalarda depolama, pencere kapanınca silinir. Kullanıcı da istediği an site verilerini temizleyebilir. Yani Web Storage'a "kaybolsa da dünyanın sonu olmayan" şeyleri koy: tercihler, taslaklar, önbellek. Büyük ve yapılandırılmış veri için tarayıcının IndexedDB adında ayrı, asenkron bir veritabanı olduğunu bil.

Güvenlik tarafı daha da önemli. Sayfada çalışan her JavaScript, senin yazmadığın bir reklam ya da analiz betiği de dahil, localStorage'ın tamamını okuyabilir. Sayfana bir XSS açığıyla sızan kod da öyle. Bu yüzden şifre, kimlik doğrulama token'ı, kredi kartı ya da kişisel veri buraya konmaz. DevTools'ta Application (Firefox'ta Storage) sekmesini açıp herhangi bir sitenin dolabına bakarsan, bu verinin ne kadar açıkta durduğunu kendi gözünle görürsün.

Cookie'ler Web Storage'dan çok daha eskidir ve temel bir farkla çalışır: tarayıcı, bir alan adına ait cookie'leri o alan adına yapılan her HTTP isteğine kendiliğinden ekler. Bir kulüp bilekliği gibi düşün. Girişte takarlar, sonra her kapıdan geçerken görevli bileğine bakar. Oturumun açık kalması, dil tercihinin sunucuya ulaşması bu sayede olur. Bedeli de var: her istekle gidip geldikleri için küçük tutulmaları gerekir, cookie başına yaklaşık 4 KB.

JavaScript'ten cookie'lere document.cookie ile ulaşırsın ve bu property alışılmadık davranır. Okuduğunda sayfanın görebildiği bütün cookie'leri tek bir string olarak verir. Yazdığında ise o string'i değiştirmez; yalnızca adını verdiğin tek bir cookie'yi ekler ya da günceller.

document.cookie = "lang=en; path=/; max-age=2592000; SameSite=Lax; Secure";
document.cookie = "banner=closed; path=/; max-age=86400; SameSite=Lax; Secure";

console.log(document.cookie); // "lang=en; banner=closed" (attribute'lar görünmez)

function getCookie(name) {
  const pair = document.cookie
    .split("; ")
    .find((item) => item.startsWith(`${name}=`));
  return pair ? decodeURIComponent(pair.slice(name.length + 1)) : null;
}

console.log(getCookie("lang")); // "en"

// Silmek: aynı path ile ömrünü sıfırla
document.cookie = "banner=; path=/; max-age=0";

Okurken attribute'ların hiçbirini göremezsin; yalnızca ad ve değer çiftleri gelir. Değerde boşluk, noktalı virgül ya da Türkçe karakter olabilecekse yazarken encodeURIComponent, okurken decodeURIComponent kullan. Silmek için ayrı bir method yok; cookie'yi aynı ad, aynı path ve aynı domain ile max-age=0 vererek yeniden yazarsın. Path farklıysa tarayıcı onu başka bir cookie sayar ve asıl cookie yerinde kalır. Örnekte Secure yazdığımız için onu HTTPS ile açılmış bir sayfanın konsolunda dene; düz HTTP'de tarayıcı Secure cookie'yi kaydetmez (çoğu tarayıcı localhost için istisna tanır).

Cookie'nin davranışını yanındaki attribute'lar belirler. Tek tek bakalım:

path: cookie'nin hangi yollara gönderileceği. Yazmazsan cookie'yi oluşturan sayfanın dizini kullanılır; genelde path=/ ile bütün siteye açarsın. domain: yazmazsan cookie yalnızca mevcut alan adına aittir; domain=example.com dersen alt alan adları da dahil olur. max-age saniye cinsinden ömür, expires ise UTC biçiminde bir bitiş tarihidir; ikisi birden varsa max-age kazanır, hiçbiri yoksa cookie tarayıcı oturumu bitince silinir.

Secure: cookie yalnızca HTTPS bağlantılarında gönderilir. SameSite: başka bir siteden başlayan isteklerde cookie'nin gidip gitmeyeceğini belirler. Strict hiç göndermez, Lax yalnızca kullanıcının bir linke tıklayıp siteye gelmesi gibi üst düzey gezinmelerde gönderir, None her durumda gönderir ve mutlaka Secure ile birlikte yazılmalıdır. SameSite'ı yazmadığında bazı tarayıcılar varsayılan olarak Lax uygular ama bu her tarayıcıda aynı değil; o yüzden açıkça yaz.

Bir attribute daha var ve onu JavaScript'ten koyamazsın: HttpOnly. Bu işaret yalnızca sunucunun gönderdiği Set-Cookie header'ıyla konur:

Set-Cookie: session=a8f3c2; Path=/; Secure; HttpOnly; SameSite=Lax

HttpOnly bir cookie tarayıcı tarafından her istekle sunucuya gönderilir ama document.cookie'de hiç görünmez. Konsolda document.cookie yazdığında oturum cookie'ni göremiyorsan, bu bir hata değil, tam olarak istenen şeydir. Sayfana XSS ile kod sızsa bile o kod oturum cookie'sini okuyup çalamaz. Önceki bölümdeki "token'ı localStorage'a koyma" uyarısının arkasındaki sebep de bu: localStorage'ın HttpOnly gibi bir koruması yok.

Peki hangisi ne zaman? Sunucunun her istekte bilmesi gereken küçük bir bilgi varsa cookie. Sadece tarayıcıda kalacak tercih, taslak ve önbellek için localStorage. Yalnızca o sekmenin ömrü kadar yaşayacak bir durum için sessionStorage. Oturum gibi hassas bir şey için ise sunucunun koyduğu HttpOnly cookie.

Sık Düşülen Tuzaklar

Sayıyı ya da boolean'ı olduğu gibi okumak. localStorage.getItem("isAdmin") sana "false" string'ini verir ve boş olmayan her string gibi bu da truthy'dir. if içinde beklediğinin tam tersi çalışır. Ya JSON kullan ya da === "true" ile karşılaştır.

Aynı sekmede storage event'i beklemek. Değişikliği yapan sekme event'i almaz; arayüzü orada kendin güncellemelisin.

document.cookie = "" ile bütün cookie'leri silmeye çalışmak. Bu hiçbir şeyi silmez. Her cookie'yi kendi adı ve path'iyle tek tek sona erdirmen gerekir.

Web Storage'ı güvenli bir kasa sanmak. İçeriği kullanıcı da, sayfadaki her betik de okuyup değiştirebilir. Oradan okuduğun değere, dışarıdan gelen herhangi bir veriye davrandığın gibi şüpheyle davran.

Küçük Bir Deneme

★☆☆ Bir sayfa ziyaret sayacı yaz: her yenilemede localStorage'daki sayıyı bir artırıp ekranda göster. Sonra aynı kodu sessionStorage ile dene ve sekmeyi kapatıp açınca ne olduğuna bak.

★★☆ Çıktıyı tahmin et, sonra dene: localStorage.setItem("done", false) çalıştırdıktan sonra if (localStorage.getItem("done")) console.log("done") konsola bir şey yazar mı? Neden?

★★★ Tema senkronu örneğine sessionStorage'da tutulan bir "bu sekmeye özel tema" seçeneği ekle. Sekmeye özel tema seçilmişse, diğer sekmeden gelen storage event'i o sekmenin rengini değiştirmesin.

Bir şeyi hatırlamanın bir yolu daha var ve o, adres çubuğunda duruyor. Kullanıcının seçtiği filtre, açık olan sekme ya da sayfa numarası URL'de durursa link olarak paylaşılabilir ve geri tuşuyla geri gelir. Bir sonraki yazıda URL, URLSearchParams ve History API ile sayfayı yenilemeden adresi yönetmeyi göreceğiz.