내가 만든 쿠키 Cookie... LocalStorage, SessionStorage
안녕하세요! 포테코입니다.
웹 개발에서 클라이언트 사이드 데이터 저장은 필수적인 기능이에요. 브라우저는 데이터를 저장하는 세 가지 주요 방법을 제공합니다: Cookie, LocalStorage, SessionStorage. 이번 글에서는 각각의 특징, 사용 시나리오, 차이점을 실무 관점에서 다뤄볼게요.
개요
Cookie (쿠키)
쿠키는 서버와 클라이언트 간 데이터를 주고받기 위해 사용되는 작은 텍스트 파일이에요. HTTP 요청과 응답 헤더에 자동으로 포함돼요.
LocalStorage (로컬 스토리지)
LocalStorage는 브라우저에 영구적으로 데이터를 저장하는 Web Storage API예요. 명시적으로 삭제하지 않는 한 데이터가 유지돼요.
SessionStorage (세션 스토리지)
SessionStorage는 브라우저 탭이 열려있는 동안만 데이터를 저장하는 Web Storage API예요. 탭을 닫으면 데이터가 자동으로 삭제돼요.
Cookie는 서버와 클라이언트 간 통신에 주로 사용되고, LocalStorage와 SessionStorage는 클라이언트 사이드에서만 사용되는 Web Storage API예요. 각각의 목적과 특성이 다르므로 상황에 맞게 선택하는 것이 중요해요.
Cookie (쿠키)
특징
- 크기 제한: 약 4KB (도메인당)
- 자동 전송: HTTP 요청 시 자동으로 서버에 전송
- 만료 시간: 설정 가능 (만료 시간 또는 세션 종료 시)
- 도메인/경로 제한: 특정 도메인과 경로에만 접근 가능
- 보안 옵션:
HttpOnly,Secure,SameSite플래그 지원
사용 시나리오
- 인증 토큰 저장: 로그인 상태 유지
- 서버 세션 관리: 서버에서 세션 ID 추적
- 사용자 설정: 언어, 테마 등 사용자 선호도
- 트래킹: 사용자 행동 분석 (주의 필요)
기본 사용법
// Cookie 설정
document.cookie =
"username=john; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/";
// Cookie 읽기
function getCookie(name: string): string | null {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(";").shift() || null;
return null;
}
// Cookie 삭제
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";고급 사용법 (HttpOnly, Secure)
// 서버에서 설정 (Node.js 예시)
res.cookie("token", "abc123", {
httpOnly: true, // JavaScript에서 접근 불가 (XSS 방지)
secure: true, // HTTPS에서만 전송
sameSite: "strict", // CSRF 공격 방지
maxAge: 3600000, // 1시간
});Cookie의 장단점
장점:
- 서버에서 자동으로 접근 가능
- 만료 시간 설정 가능
- 도메인/경로 제한으로 보안 강화 가능
단점:
- 크기 제한이 작음 (4KB)
- 모든 HTTP 요청에 포함되어 성능 오버헤드
- JavaScript에서 직접 접근이 불편함
LocalStorage (로컬 스토리지)
특징
- 크기 제한: 약 5-10MB (브라우저마다 다름)
- 영구 저장: 명시적으로 삭제하지 않는 한 유지
- 도메인별 격리: 같은 도메인에서만 접근 가능
- 동기 API: 블로킹 작업 (메인 스레드 차단)
- 문자열만 저장: 객체는 JSON으로 변환 필요
사용 시나리오
- 사용자 설정 저장: 테마, 언어, 선호도
- 캐시 데이터: API 응답 캐싱
- 오프라인 데이터: 오프라인 상태에서 사용할 데이터
- 장바구니/위시리스트: 사용자 쇼핑 데이터
기본 사용법
// 데이터 저장
localStorage.setItem("username", "john");
localStorage.setItem("user", JSON.stringify({ id: 1, name: "John" }));
// 데이터 읽기
const username = localStorage.getItem("username");
const user = JSON.parse(localStorage.getItem("user") || "{}");
// 데이터 삭제
localStorage.removeItem("username");
// 전체 삭제
localStorage.clear();
// 키 목록 가져오기
const keys = Object.keys(localStorage);React Hook 예제
"use client";
import { useState, useEffect } from "react";
function useLocalStorage<T>(key: string, initialValue: T) {
// 초기값 가져오기
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// 값 설정 함수
const setValue = (value: T | ((val: T) => T)) => {
try {
// 함수인 경우 이전 값 사용
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// 사용 예시
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Current theme: {theme}
</button>
);
}LocalStorage의 장단점
장점:
- 큰 용량 (5-10MB)
- 간단한 API
- 영구 저장
- 서버 요청 없이 클라이언트에서만 처리
단점:
- 동기 작업으로 성능 영향
- 문자열만 저장 가능
- XSS 공격에 취약
- 서버에서 직접 접근 불가
SessionStorage (세션 스토리지)
특징
- 크기 제한: 약 5-10MB (브라우저마다 다름)
- 세션 기반: 탭이 닫히면 자동 삭제
- 탭별 격리: 각 탭마다 독립적인 저장소
- 동기 API: LocalStorage와 동일
- 문자열만 저장: 객체는 JSON으로 변환 필요
사용 시나리오
- 임시 데이터: 폼 입력 임시 저장
- 탭별 상태: 각 탭의 독립적인 상태 관리
- 일회성 데이터: 페이지 새로고침 시 유지하되 탭 닫으면 삭제
- 멀티탭 격리: 같은 사이트의 다른 탭과 데이터 공유 불필요
기본 사용법
// 데이터 저장
sessionStorage.setItem(
"formData",
JSON.stringify({ name: "John", email: "john@example.com" })
);
// 데이터 읽기
const formData = JSON.parse(sessionStorage.getItem("formData") || "{}");
// 데이터 삭제
sessionStorage.removeItem("formData");
// 전체 삭제
sessionStorage.clear();React Hook 예제
"use client";
import { useState, useEffect } from "react";
function useSessionStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.sessionStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// 사용 예시: 폼 임시 저장
function ContactForm() {
const [formData, setFormData] = useSessionStorage("contactForm", {
name: "",
email: "",
message: "",
});
return (
<form>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Name"
/>
{/* ... */}
</form>
);
}SessionStorage의 장단점
장점:
- 탭별 독립성
- 자동 정리 (탭 닫으면 삭제)
- 큰 용량 (5-10MB)
- 간단한 API
단점:
- 탭이 닫히면 데이터 손실
- 동기 작업으로 성능 영향
- 문자열만 저장 가능
- 서버에서 직접 접근 불가
비교표
| 특징 | Cookie | LocalStorage | SessionStorage |
|---|---|---|---|
| 크기 제한 | 약 4KB | 5-10MB | 5-10MB |
| 저장 기간 | 만료 시간 설정 가능 | 영구 저장 | 탭 닫을 때까지 |
| 서버 접근 | ✅ 자동 전송 | ❌ 불가능 | ❌ 불가능 |
| 도메인 접근 | 도메인/경로 제한 | 도메인별 격리 | 도메인별 격리 |
| 탭 간 공유 | ✅ 공유 | ✅ 공유 | ❌ 탭별 독립 |
| API 방식 | 문자열 조작 | 동기 API | 동기 API |
| 성능 영향 | 모든 요청에 포함 | 동기 블로킹 | 동기 블로킹 |
| 보안 옵션 | HttpOnly, Secure, SameSite | ❌ | ❌ |
| 사용 목적 | 서버 통신, 인증 | 영구 데이터 저장 | 임시 데이터 저장 |
Cookie는 서버와의 통신이 필요할 때, LocalStorage는 영구적으로 저장해야 할 때, SessionStorage는 탭이 열려있는 동안만 필요할 때 사용하는 것이 적절해요.
실전 사용 예제
예제 1: 인증 토큰 관리
// Cookie 사용 (서버에서 접근 필요)
// 서버 사이드
res.cookie("authToken", token, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7일
});
// LocalStorage 사용 (클라이언트에서만 접근)
// 클라이언트 사이드
localStorage.setItem("authToken", token);
// 토큰 읽기
function getAuthToken(): string | null {
// Cookie에서 읽기 (서버에서 설정한 경우)
// 또는 LocalStorage에서 읽기
return localStorage.getItem("authToken");
}예제 2: 사용자 설정 저장
// LocalStorage 사용 (영구 저장)
interface UserSettings {
theme: "light" | "dark";
language: "ko" | "en";
notifications: boolean;
}
function saveUserSettings(settings: UserSettings) {
localStorage.setItem("userSettings", JSON.stringify(settings));
}
function getUserSettings(): UserSettings {
const stored = localStorage.getItem("userSettings");
return stored
? JSON.parse(stored)
: { theme: "light", language: "ko", notifications: true };
}예제 3: 폼 데이터 임시 저장
// SessionStorage 사용 (탭 닫으면 삭제)
function saveFormDraft(formData: FormData) {
sessionStorage.setItem("formDraft", JSON.stringify(formData));
}
function loadFormDraft(): FormData | null {
const draft = sessionStorage.getItem("formDraft");
return draft ? JSON.parse(draft) : null;
}
// 페이지 언로드 시 자동 저장
window.addEventListener("beforeunload", () => {
const formData = getFormData();
saveFormDraft(formData);
});예제 4: Next.js에서 사용하기
// hooks/useStorage.ts
"use client";
import { useState, useEffect } from "react";
type StorageType = "localStorage" | "sessionStorage";
function useStorage<T>(
key: string,
initialValue: T,
storageType: StorageType = "localStorage"
) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const storage =
storageType === "localStorage"
? window.localStorage
: window.sessionStorage;
const item = storage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
const storage =
storageType === "localStorage"
? window.localStorage
: window.sessionStorage;
storage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
export default useStorage;// components/ThemeProvider.tsx
"use client";
import useStorage from "@/hooks/useStorage";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useStorage("theme", "light", "localStorage");
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}보안 고려사항
1. 민감한 데이터 저장 금지
// ❌ 나쁜 예: 민감한 정보를 LocalStorage에 저장
localStorage.setItem("password", userPassword);
localStorage.setItem("creditCard", cardNumber);
// ✅ 좋은 예: 서버에서만 관리
// 인증 토큰은 HttpOnly Cookie 사용
res.cookie("token", token, { httpOnly: true, secure: true });2. XSS 공격 방지
// LocalStorage/SessionStorage는 XSS 공격에 취약
// 사용자 입력을 저장하기 전에 검증 및 이스케이프 처리
function sanitizeInput(input: string): string {
// HTML 태그 제거, 특수 문자 이스케이프
return input.replace(/<[^>]*>/g, "").trim();
}
const userInput = sanitizeInput(userInput);
localStorage.setItem("userData", JSON.stringify({ input: userInput }));3. Cookie 보안 설정
// ✅ 안전한 Cookie 설정
res.cookie("sessionId", sessionId, {
httpOnly: true, // JavaScript 접근 차단
secure: true, // HTTPS에서만 전송
sameSite: "strict", // CSRF 공격 방지
maxAge: 3600000, // 만료 시간 설정
domain: ".example.com", // 도메인 제한
path: "/", // 경로 제한
});성능 최적화
1. 비동기 처리
// LocalStorage/SessionStorage는 동기 작업
// 큰 데이터를 저장할 때는 비동기로 처리
async function saveLargeData(key: string, data: any) {
return new Promise<void>((resolve) => {
// Web Worker나 requestIdleCallback 사용
requestIdleCallback(() => {
try {
localStorage.setItem(key, JSON.stringify(data));
resolve();
} catch (error) {
console.error("Storage quota exceeded", error);
resolve();
}
});
});
}2. 용량 관리
// LocalStorage 용량 체크
function getStorageSize(storage: Storage): number {
let total = 0;
for (let key in storage) {
if (storage.hasOwnProperty(key)) {
total += storage[key].length + key.length;
}
}
return total;
}
// 용량이 부족하면 오래된 데이터 삭제
function manageStorage() {
const maxSize = 5 * 1024 * 1024; // 5MB
const currentSize = getStorageSize(localStorage);
if (currentSize > maxSize) {
// 오래된 데이터 삭제 로직
const keys = Object.keys(localStorage).sort(
(a, b) =>
parseInt(localStorage.getItem(a + "_timestamp") || "0") -
parseInt(localStorage.getItem(b + "_timestamp") || "0")
);
// 가장 오래된 데이터 삭제
keys.slice(0, Math.floor(keys.length / 4)).forEach((key) => {
localStorage.removeItem(key);
});
}
}베스트 프랙티스
✅ DO
1. 적절한 저장소 선택
- 서버 통신 필요 → Cookie
- 영구 저장 필요 → LocalStorage
- 임시 데이터 → SessionStorage
2. 에러 처리
try {
localStorage.setItem("key", value);
} catch (error) {
if (error.name === "QuotaExceededError") {
console.error("Storage quota exceeded");
// 오래된 데이터 삭제 또는 사용자에게 알림
}
}3. 타입 안정성
interface StoredData {
theme: "light" | "dark";
userId: number;
}
function getStoredData(): StoredData {
const stored = localStorage.getItem("data");
if (!stored) {
return { theme: "light", userId: 0 };
}
return JSON.parse(stored) as StoredData;
}4. SSR 고려
// Next.js에서 사용 시
if (typeof window !== "undefined") {
localStorage.setItem("key", value);
}❌ DON'T
1. 민감한 정보 저장 금지
// ❌ 절대 하지 말 것
localStorage.setItem("password", password);
localStorage.setItem("creditCard", cardNumber);2. 과도한 데이터 저장
// ❌ 큰 데이터를 저장하지 말 것
localStorage.setItem("largeData", JSON.stringify(hugeArray));3. 동기 작업 남용
// ❌ 반복적인 동기 작업
for (let i = 0; i < 1000; i++) {
localStorage.setItem(`key${i}`, data[i]);
}마무리
Cookie, LocalStorage, SessionStorage는 각각 다른 목적과 특성을 가지고 있어요:
- Cookie: 서버와의 통신, 인증 토큰, 작은 데이터
- LocalStorage: 영구 저장, 사용자 설정, 캐시 데이터
- SessionStorage: 임시 데이터, 탭별 독립 상태
각 저장소의 특징을 이해하고 상황에 맞게 선택하면, 보안과 성능을 모두 고려한 최적의 데이터 저장 전략을 수립할 수 있어요. 민감한 정보는 절대 클라이언트에 저장하지 않고, 적절한 보안 설정을 적용하는 것이 중요해요.
적절한 저장소를 선택하면 더 안전하고 효율적인 웹 애플리케이션을 만들 수 있어요. 앞으로도 브라우저 저장소를 활용한 실전 사례와 보안 팁을 계속 공유할 예정이니 기대해 주세요!
