🏫 Ddingsroom의 μ„Έμ…˜ μœ μ§€ ꡬ쑰 뢄석 ( + Local Storage, Session Storage, Cookie에 λŒ€ν•΄)

Yujin JungΒ·2025λ…„ 10μ›” 30일
post-thumbnail

μ›Ή κ°œλ°œμ„ ν•˜λ‹€ 보면 β€œλ‘œκ·ΈμΈ μƒνƒœλ₯Ό μ–΄λ–»κ²Œ μœ μ§€ν• κΉŒ?” λΌλŠ” 문제λ₯Ό λ°˜λ“œμ‹œ λ§ˆμ£Όν•˜κ²Œ λ©λ‹ˆλ‹€. μ΄λ•Œ μ‚¬μš©ν•˜λŠ” 것이 λ°”λ‘œ λΈŒλΌμš°μ € μ €μž₯μ†Œ (Web Storage)μž…λ‹ˆλ‹€.

이번 κΈ€μ—μ„œλŠ”
1️⃣ Local Storage, Session Storage, Cookie 의 차이점
2️⃣ 그리고 μ‹€μ œλ‘œ μ œκ°€ 운영 쀑인 Ddingsroom ν”„λ‘œμ νŠΈμ—μ„œ μ–΄λ–»κ²Œ μ„Έμ…˜μ„ κ΄€λ¦¬ν•˜κ³  μžˆλŠ”μ§€
λ₯Ό ν•¨κ»˜ μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€.


πŸ“¦ Local Storage

Local StorageλŠ” λΈŒλΌμš°μ €μ— 반영ꡬ적으둜 데이터λ₯Ό μ €μž₯ν•˜λŠ” μ €μž₯μ†Œμž…λ‹ˆλ‹€.

  • 유효 κΈ°κ°„: λ¬΄κΈ°ν•œ (μ‚¬μš©μžκ°€ 직접 μ‚­μ œν•˜μ§€ μ•ŠλŠ” 이상 λ‚¨μŒ)
  • μš©λŸ‰: μ•½ 5MB (λΈŒλΌμš°μ €λ³„λ‘œ 닀름)
  • νŠΉμ§•: μ„œλ²„λ‘œ μ „μ†‘λ˜μ§€ μ•ŠμŒ, ν΄λΌμ΄μ–ΈνŠΈμ—μ„œλ§Œ μ ‘κ·Ό κ°€λŠ₯

μ˜ˆμ‹œ

localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark"
localStorage.removeItem("theme");

μ£Όμš” μ‚¬μš© μ˜ˆμ‹œ

  • 닀크 λͺ¨λ“œ / 라이트 λͺ¨λ“œ μ„€μ •
  • 졜근 λ³Έ μƒν’ˆ, μ„ ν˜Έ μΉ΄ν…Œκ³ λ¦¬ μ €μž₯

πŸ” Session Storage

Session StorageλŠ” νƒ­ λ‹¨μœ„λ‘œ μœ μ§€λ˜λŠ” μž„μ‹œ μ €μž₯μ†Œμž…λ‹ˆλ‹€.
λΈŒλΌμš°μ € 탭을 λ‹«μœΌλ©΄ 데이터가 μ‚­μ œλ©λ‹ˆλ‹€.

  • 유효 κΈ°κ°„: λΈŒλΌμš°μ € 탭을 닫을 λ•ŒκΉŒμ§€
  • μ„œλ²„ 전솑: μžλ™ 전솑 μ•ˆ 됨
  • μš©λŸ‰: μ•½ 5MB

μ˜ˆμ‹œ

sessionStorage.setItem("accessToken", "abc123");
sessionStorage.getItem("accessToken"); // "abc123"
sessionStorage.clear();

μ£Όμš” μ‚¬μš© μ˜ˆμ‹œ

  • 둜그인 μ„Έμ…˜ μœ μ§€ (JWT 토큰 μ €μž₯)
  • μž„μ‹œ μž…λ ₯ 폼 데이터

CookieλŠ” μ„œλ²„μ™€ ν΄λΌμ΄μ–ΈνŠΈκ°€ μžλ™μœΌλ‘œ 주고받을 수 μžˆλŠ” 데이터 μ‘°κ°μž…λ‹ˆλ‹€.

  • 유효 κΈ°κ°„: μ„€μ • κ°€λŠ₯ (max-age / expires)
  • μš©λŸ‰: μ•½ 4KB
  • μ„œλ²„ 전솑: μžλ™ 전솑
  • λ³΄μ•ˆ μ˜΅μ…˜: Secure, HttpOnly, SameSite λ“±μœΌλ‘œ κ°•ν™” κ°€λŠ₯

μ˜ˆμ‹œ

document.cookie = "user=Yujin; max-age=3600; path=/";

μ£Όμš” μ‚¬μš© μ˜ˆμ‹œ

  • μžλ™ 둜그인, μ„Έμ…˜ μ‹λ³„μž (session_id)
  • μ›Ή 뢄석, κ΄‘κ³  νŠΈλž˜ν‚Ή

μ„Έ μ €μž₯μ†Œ 비ꡐ


🏫 Ddingsroom ν”„λ‘œμ νŠΈμ˜ μ„Έμ…˜ μœ μ§€ ꡬ쑰

κ°„λ‹¨ν•˜κ²Œ Local Storage, Session Storage, Cookie에 λŒ€ν•΄ μ•Œμ•„λ΄€μœΌλ‹ˆ 이제 μ‹€μ œ μ œκ°€ μš΄μ˜μ€‘μΈ Ddingsroom ν”„λ‘œμ νŠΈμ—μ„œ μ–΄λ–»κ²Œ μ„Έμ…˜μ„ μœ μ§€ν•˜λŠ”μ§€ μ‚΄νŽ΄λ³΄λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€.
Ddingsroom은 JWT 기반 ν΄λΌμ΄μ–ΈνŠΈ μ„Έμ…˜ 관리λ₯Ό μ‚¬μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 즉, μ„œλ²„ μ„Έμ…˜μ΄ μ•„λ‹Œ 토큰 기반 인증 λ°©μ‹μž…λ‹ˆλ‹€.


둜그인 μ΄ν›„μ˜ 흐름

1️⃣ 둜그인 성곡 μ‹œ μ„œλ²„μ—μ„œ accessToken, refreshToken, userIdλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
2️⃣ ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ 이λ₯Ό sessionStorage에 μ €μž₯ν•©λ‹ˆλ‹€.
3️⃣ Axios μΈμŠ€ν„΄μŠ€κ°€ λͺ¨λ“  API μš”μ²­μ— Authorization 헀더λ₯Ό μžλ™ μΆ”κ°€ν•©λ‹ˆλ‹€.


πŸ“ κ΄€λ ¨ 파일 ꡬ쑰

libs/
 └── api/
      β”œβ”€β”€ instance.js     ← axios μΈμŠ€ν„΄μŠ€ + 인터셉터
      β”œβ”€β”€ getUserId.js    ← 토큰 기반 μœ μ € 식별
      └── admin.js        ← κ΄€λ¦¬μžμš© API
stores/
 β”œβ”€β”€ useTokenStore.js     ← Zustand μ „μ—­ μƒνƒœλ‘œ 토큰 관리
 β”œβ”€β”€ useReservationStore.js
 └── useCommunityStore.js

1. axios μΈμŠ€ν„΄μŠ€ μ„€μ • (libs/api/instance.js)

import axios from 'axios';

const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
  timeout: 10000,
});

axiosInstance.interceptors.request.use((config) => {
  const accessToken = sessionStorage.getItem('accessToken');
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

-> 인증이 ν•„μš”ν•œ λͺ¨λ“  μš”μ²­μ— μžλ™μœΌλ‘œ 토큰이 λΆ™μŠ΅λ‹ˆλ‹€.


2. 응닡 μΈν„°μ…‰ν„°μ—μ„œ 만료 토큰 처리

axiosInstance.interceptors.response.use(
  (response) => response,
  (error) => {
    const { response } = error;
    if (response && (response.status === 401 || response.status === 403)) {
      sessionStorage.removeItem('accessToken');
    }
    return Promise.reject(error);
  }
);

-> 토큰이 만료되면 μžλ™μœΌλ‘œ μ„Έμ…˜μ΄ μ’…λ£Œλ˜μ–΄ μž¬λ‘œκ·ΈμΈμ„ μœ λ„ν•©λ‹ˆλ‹€.


3. Zustandλ₯Ό ν†΅ν•œ μ „μ—­ μ„Έμ…˜ 관리 (useTokenStore.js)

import { create } from 'zustand';

const useTokenStore = create((set) => ({
  accessToken: sessionStorage.getItem('accessToken') || '',
  refreshToken: sessionStorage.getItem('refreshToken') || '',
  userId: parseInt(sessionStorage.getItem('userId')) || null,

  setAccessToken: (token) => {
    set({ accessToken: token });
    sessionStorage.setItem('accessToken', token);
  },
  clearTokens: () => {
    set({ accessToken: '', refreshToken: '', userId: null });
    sessionStorage.clear();
  },
}));

-> Zustand와 sessionStorageλ₯Ό μ–‘λ°©ν–₯ λ™κΈ°ν™”ν•˜μ—¬, μƒˆλ‘œκ³ μΉ¨ν•΄λ„ μ„Έμ…˜μ΄ μœ μ§€λ©λ‹ˆλ‹€.


μ™œ Session Storageλ₯Ό μ„ νƒν–ˆμ„κΉŒ?

  • λ³΄μ•ˆμ„±: 토큰이 HTTP μš”μ²­λ§ˆλ‹€ μžλ™ μ „μ†‘λ˜μ§€ μ•Šμ•„ XSSμ—λ§Œ μ£Όμ˜ν•˜λ©΄ μ•ˆμ „ν•˜λ‹€ νŒλ‹¨ν–ˆμŠ΅λ‹ˆλ‹€.
  • μœ μ§€ κΈ°κ°„: λΈŒλΌμš°μ € νƒ­ λ‹¨μœ„λ‘œ 둜그인 μ‹œκ°„μ΄ μœ μ§€λ˜κΈ°μ— 곡용 PC λ“±μ—μ„œ μžλ™ λ‘œκ·Έμ•„μ›ƒμ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€.
  • UX ν–₯상: μƒˆλ‘œκ³ μΉ¨ μ‹œμ—λ„ Zustand의 rehydrate()둜 μ„Έμ…˜ μœ μ§€κ°€ κ°€λŠ₯ν•©λ‹ˆλ‹€.
  • 간결함: λ³΅μž‘ν•œ μ„œλ²„ μ„Έμ…˜ 관리 λŒ€μ‹  ν΄λΌμ΄μ–ΈνŠΈ λ‹¨μ—μ„œ κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€.

μ„Έμ…˜ ꡬ쑰 λ‹€μ΄μ–΄κ·Έλž¨

[μ„œλ²„] 
  ↓ (둜그인 응닡)
  accessToken, refreshToken, userId
  ↓
[ν΄λΌμ΄μ–ΈνŠΈ]
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚ sessionStorage            β”‚
 β”‚  β”œ accessToken            β”‚
 β”‚  β”œ refreshToken           β”‚
 β”‚  β”” userId                 β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
 [useTokenStore.js] ← Zustand μ „μ—­ μƒνƒœ
         β”‚
         β–Ό
 [axiosInstance] β†’ Authorization 헀더 μžλ™ λΆ€μ°©

κ²°λ‘  β€” μ‹€μ„œλΉ„μŠ€μ—μ„œ λ“œλŸ¬λ‚œ λ¬Έμ œμ™€ κ°œμ„  λ°©ν–₯

Ddingsroom은 JWT + Session Storage 기반 μ„Έμ…˜ μœ μ§€ ꡬ쑰λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

  • μ„œλ²„ μ„Έμ…˜ λŒ€μ‹  ν΄λΌμ΄μ–ΈνŠΈ μ€‘μ‹¬μ˜ 토큰 인증
  • Axios μΈν„°μ…‰ν„°λ‘œ 인증 헀더 μžλ™ λΆ€μ°©
  • Zustand둜 μ „μ—­ μƒνƒœ 동기화
    = νƒ­ λ‹¨μœ„μ˜ λ³΄μ•ˆ 쀑심 UX

κ·ΈλŸ¬λ‚˜ 졜근 μ‹€μ œ μ„œλΉ„μŠ€ 운영 쀑 λ‹€μŒκ³Ό 같은 μ‚¬μš©μž ν”Όλ“œλ°±μ΄ μ ‘μˆ˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€.


πŸ—£οΈ μ‚¬μš©μž ν”Όλ“œλ°± 사둀

β€œμžκΎΈ μ˜ˆμ•½ 내역이 μ‚¬λΌμ§‘λ‹ˆλ‹€.
μ „λ‚  μ˜ˆμ•½ν–ˆλŠ”λ° λ‹€μŒλ‚  듀어와 λ³΄λ‹ˆ μ˜ˆμ•½μ΄ μ•ˆ λ˜μ–΄ μžˆμ–΄μ„œ λ„ˆλ¬΄ κ³€λž€ν–ˆμŠ΅λ‹ˆλ‹€.
μ˜€λŠ˜λ„ 또 κ·ΈλŸ¬λ„€μš”. μ™œ 그런 κ±΄κ°€μš”?”

_μ‹€μ œ μ„œλΉ„μŠ€μ— μ ‘μˆ˜λœ 건의 λ‚΄μ—­ ν™”λ©΄ 캑쳐본_

이 ν”Όλ“œλ°±μ€ β€œμ˜ˆμ•½μ΄ μ •μƒμ μœΌλ‘œ μ™„λ£Œλ˜μ§€ μ•Šμ€ κ²ƒμ²˜λŸΌ λ³΄μΈλ‹€β€λŠ” λ‚΄μš©μ΄μ—ˆμ§€λ§Œ,
μ„œλ²„ 둜그λ₯Ό λΆ„μ„ν•΄λ³΄λ‹ˆ μ˜ˆμ•½ API μžμ²΄κ°€ λ°±μ—”λ“œλ‘œ μ „μ†‘λ˜μ§€ μ•Šμ•˜κ±°λ‚˜, 인증 μ‹€νŒ¨(401) 둜 처리된 κ²½μš°μ˜€μŠ΅λ‹ˆλ‹€.


Access Token이 만료된 μƒνƒœμ—μ„œ μ‚¬μš©μžκ°€ μ˜ˆμ•½ λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ μš”μ²­μ΄ μ„œλ²„λ‘œ μ „λ‹¬λ˜μ§€ μ•Šκ±°λ‚˜ 401 Unauthorized둜 κ±°μ ˆλ˜μ§€λ§Œ, UIμ—λŠ” 별닀λ₯Έ 였λ₯˜ μ•ˆλ‚΄κ°€ ν‘œμ‹œλ˜μ§€ μ•Šμ•„ μ‚¬μš©μžλŠ” μ˜ˆμ•½μ΄ 된 쀄 μ°©κ°ν•˜κ²Œ λ©λ‹ˆλ‹€.
κ²°κ΅­ μ˜ˆμ•½ 데이터가 DB에 μ €μž₯λ˜μ§€ μ•Šμ€ 채 μ‚¬λΌμ§€λŠ” ν˜„μƒμ΄ λ°œμƒν•œ κ²ƒμž…λ‹ˆλ‹€.


이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ ν˜„μž¬

  • Access Token 사전 κ°±μ‹  (Preemptive Refresh)
  • 401 응닡 μ‹œ μžλ™ μž¬λ°œκΈ‰ 및 μž¬μ‹œλ„ 둜직
  • HttpOnly μΏ ν‚€ 기반 Refresh Token 보관
  • λ©€ν‹°νƒ­ 동기화 ꡬ쑰 κ°œμ„ 
    λ“±μ˜ κ°œμ„ μ„ μ§„ν–‰ 쀑에 μžˆμŠ΅λ‹ˆλ‹€.

이 κ°œμ„ μ΄ 적용되면, μ‚¬μš©μžλŠ” 토큰 만료λ₯Ό μΈμ‹ν•˜μ§€ μ•Šκ³ λ„ μ„œλΉ„μŠ€ λ‚΄μ—μ„œ 둜그인 μƒνƒœκ°€ λŠκΉ€ 없이 μœ μ§€λ  것 μž…λ‹ˆλ‹€.

πŸ’‘ λ‹€μŒ ν¬μŠ€νŠΈμ—μ„œλŠ” 이 Refresh Token λ‘œμ§μ„ μ‹€μ œ μ½”λ“œ λ ˆλ²¨μ—μ„œ μ–΄λ–»κ²Œ κ΅¬ν˜„ν–ˆλŠ”μ§€, Axios 인터셉터와 Network Interceptor 기반으둜 μžμ„Ένžˆ λ‹€λ€„λ³΄κ² μŠ΅λ‹ˆλ‹€.

profile
맀일맀일 μ‘°κΈˆμ”© μ„±μž₯ν•˜λ € λ…Έλ ₯ν•˜λŠ” ν”„λ‘ νŠΈμ—”λ“œ κ°œλ°œμžμž…λ‹ˆλ‹€!

1개의 λŒ“κΈ€

comment-user-thumbnail
2025λ…„ 10μ›” 31일

둜컬 μŠ€ν† λ¦¬μ§€, μ„Έμ…˜, μΏ ν‚€ 각 κ°œλ…μ— λŒ€ν•΄ ν—·κ°ˆλ¦΄ λ•Œκ°€ μ’…μ’… μžˆμ—ˆλŠ”λ°, 잘 μ •λ¦¬ν•΄μ£Όμ…”μ„œ μ΄λ²ˆμ—μ•Όλ§λ‘œ ν™•μ‹€νžˆ μ΄ν•΄ν•˜κ²Œ 된 κ±° κ°™μ•„μš”~ λ˜ν•œ μ‹€μ œλ‘œ ν”„λ‘œμ νŠΈμ—μ„œ μ–΄λ–€ λ°©μ‹μœΌλ‘œ μ„Έμ…˜ 관리λ₯Ό ν•˜κ³  μžˆλŠ”μ§€μ— λŒ€ν•œ μ„€λͺ…도 ν₯λ―Έλ‘œμ› μŠ΅λ‹ˆλ‹€! μ–΄λ–€ λ°©μ‹μœΌλ‘œ 문제λ₯Ό ν•΄κ²°ν•˜μ‹€μ§€ κΆκΈˆν•˜λ„€μš” γ…Ž.γ…Ž 잘 μ½μ—ˆμŠ΅λ‹ˆλ‹€!

λ‹΅κΈ€ 달기