| 항목 | localStorage | sessionStorage |
|---|---|---|
| 유지 기간 | 브라우저를 닫아도 남아 있음 (영구) | 브라우저 탭을 닫으면 삭제됨 (세션 단위) |
| 저장 위치 | 클라이언트 브라우저 내 저장소 | 클라이언트 브라우저 내 저장소 |
| 접근 방식 | 자바스크립트로 직접 접근 (window.localStorage) | 자바스크립트로 직접 접근 (window.sessionStorage) |
| 용도 | 장기 저장 (예: 자동 로그인 유지) | 임시 저장 (예: 한 세션 내 상태 유지) |
| 보안 | XSS 취약 | XSS 여전히 취약 |
쿠키는 클라이언트가 저장하고 서버로 자동 전송되는 작은 데이터 조각이다.
HTTP 요청 시 자동으로 서버에 함께 전송된다.
주로 로그인 유지, 세션 정보, 사용자 설정 등을 저장하는 데 사용된다.
HttpOnly, Secure, SameSite 옵션을 통해 보안 설정이 가능하다.
용량 제한이 있고 너무 많은 데이터 저장에는 적합하지 않다.
| 저장소 종류 | 보안 등급 | 권장 여부 | 설명 |
|---|---|---|---|
localStorage | 낮음 | ❌ 사용 권장 X | 자바스크립트로 접근 가능하여 XSS에 매우 취약함. 특히 access token을 저장하면 위험함. |
sessionStorage | 중간 | ⚠️ 상황에 따라 | localStorage보다 범위가 좁고 탭을 닫으면 초기화되지만 여전히 자바스크립트로 접근 가능하므로 XSS에 노출됨. |
cookie (HttpOnly) | 높음 | ✅ 권장 | HttpOnly 옵션을 사용하면 자바스크립트 접근 불가 → XSS 방지. 단 CSRF 공격에는 취약할 수 있어 SameSite 옵션 설정 필요. |
Memory only | 높음 | ✅ (SPA에서) | 리렌더링 또는 새로고침 시 초기화됨. 가장 안전하지만 상태 관리 어려움. access token은 매번 재요청해야 함. 보안은 높지만 UX엔 제약이 생김. |
인터셉터(interceptor)는 요청(Request) 또는 응답(Response)이 실제로 서버와 오고 가기 전에 axios가 가로채서 공통 로직을 삽입할 수 있는 전처리기이다.
모든 요청에 공통적인 작업(예: 요청 헤더에 access token 자동으로 넣기)을 추가할 수 있고 모든 응답에 대한 공통 처리(예 : 응답이 401(토큰 만료)이면 자동으로 refresh 요청 보내기)도 가능하기 때문에 사용한다.
| 구분 | 역할 | 예시 |
|---|---|---|
| 요청 인터셉터 | 서버에 요청 보내기 전에 조작 | 토큰 넣기, 언어 설정 등 |
| 응답 인터셉터 | 서버 응답 받은 후 가공 | 에러 처리, 토큰 갱신, 공통 응답 포맷 가공 등 |
: 서버에 요청이 보내지기 전에 동작
: 용도는 access token 자동 추가, 언어 설정, 공통 헤더 설정
: 응답이 도착한 뒤 처리
: 용도는 401 에러 시 자동으로 refresh 요청, 공통 에러 처리, 응답 포맷 가공
401 Unauthorized 에러
일반적으로 인증이 실패했을 때 발생하는데 대표적인 경우가 바로 Access Token의 유효 기간이 만료되었을 때이다.
실무에선 401 에러 발생 시 보통 refresh 토큰으로 새로운 access 토큰을 요청하고 요청 실패 시 로그아웃 처리한다.
// 요청 인터셉터: access token 자동 첨부
axios.interceptors.request.use((config) => {
const access = localStorage.getItem("access");
if (access) {
config.headers.Authorization = `Bearer ${access}`;
}
return config;
});
// 응답 인터셉터: access token 만료 시 refresh로 재발급 요청
axios.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response.status === 401) {
const originalRequest = error.config;
// access token이 만료되었고 아직 재시도 안 했을 경우
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refresh = localStorage.getItem("refresh");
// refresh token으로 새로운 access token 요청
const res = await axios.post("http://localhost:8000/api/auth/jwt/refresh/", {
refresh: refresh,
});
const newAccess = res.data.access;
localStorage.setItem("access", newAccess);
// 새 access token으로 Authorization 헤더 갱신
originalRequest.headers.Authorization = `Bearer ${newAccess}`;
// 요청 재시도
return axios(originalRequest);
} catch (refreshError) {
console.error("❌ 토큰 재발급 실패", refreshError);
// refresh token도 만료되었을 경우: 로그아웃 처리
localStorage.removeItem("access");
localStorage.removeItem("refresh");
window.location.href = "/login"; // 로그인 페이지로 이동
}
}
return Promise.reject(error);
}
);