D-5: 브라우저 저장소 & 개발자 도구 — 로그인을 기억하고, 들여다보고, 멈춰서 고치기
안녕하세요, 홍순구 튜터입니다. 지난 시간(D-4)에 우리는 댓글을 서버에 저장(POST)하고, 무한 스크롤로 게시물을 더 불러오고, 로딩·에러 표시까지 얹었어요. 받기만 하던 화면이 진짜 앱처럼 저장하고, 더 불러오고, 기다림을 알려주는 화면으로 자랐죠.
그런데 딱 하나, 찜찜한 게 남아 있었어요. 댓글을 저장할 때 누가 썼는지를 username: "soongu_hong"으로 고정해 뒀거든요. 누가 댓글을 달아도 전부 "soongu_hong"이 단 걸로 저장됐어요. 진짜 앱이라면, 댓글을 단 사람은 지금 로그인한 나여야 하죠.
그러려면 한 가지가 필요해요. 브라우저가 "지금 로그인한 사람이 누구인지"를 기억하고 있어야 해요. 그런데 우리가 지금까지 쓰던 변수(let, const)는 새로고침하면 깡그리 사라져요. 로그인하고 새로고침 한 번 했더니 "당신 누구세요?"가 되면 안 되잖아요. 그래서 오늘은 새로고침해도, 탭을 닫았다 열어도 살아남는 브라우저 저장소(localStorage) 를 배워요.
지난 시간 (D-4) 오늘 (D-5)
┌──────────────────────────┐ ┌──────────────────────────┐
│ 댓글 작성자 = "soongu_hong" │ ──▶ │ 댓글 작성자 = 로그인한 나 │
│ (누가 달아도 고정) │ │ 로그인 정보를 브라우저가 기억 │
│ 변수는 새로고침하면 사라짐 │ │ localStorage는 남아 있음 │
└──────────────────────────┘ └──────────────────────────┘
그리고 하나 더. 오늘부터 여러분은 개발자의 가장 강력한 무기를 손에 넣어요. 바로 크롬 개발자 도구(DevTools) 예요. 저장한 데이터를 직접 들여다보고, 서버로 오가는 요청을 엿보고, 코드를 한 줄씩 멈춰가며 "지금 이 변수에 뭐가 들었나"를 확인하는 도구죠. 앞으로 버그를 만나면 가장 먼저 여는 창이 될 거예요.
💡 오늘 수업의 핵심 — "변수는 새로고침하면 사라지지만, localStorage에 저장하면 남는다. 로그인 정보를 localStorage에 기억해 뒀다가, 서버 요청마다 토큰을 실어 보낸다. 그리고 DevTools로 저장소·네트워크·코드를 직접 들여다본다." 🎯
🎯 학습 목표
- 브라우저 저장소 3종(쿠키·localStorage·sessionStorage)의 차이와 영속성 범위를 구분합니다.
localStorage의setItem·getItem·removeItem으로 데이터를 저장하고 꺼냅니다.- 로그인 폼 제출을 가로채(
submit이벤트), 입력값을 저장소에 기억시킵니다. - 저장한 토큰을 API 요청의
Authorization헤더에 자동으로 실어 보냅니다. JSON.stringify·JSON.parse로 객체와 문자열을 오가며, 여러 정보를 하나로 묶어 저장합니다.- DevTools의 Application 탭에서 저장된 값을, Network 탭에서 오가는 요청을 직접 확인합니다.
- DevTools의 중단점(breakpoint)으로 코드를 멈춰 세우고, 변수 값을 들여다보며 디버깅합니다.
Step 1: "새로고침하면 왜 사라질까?" — 브라우저 저장소 3종
본격적으로 코드를 짜기 전에, "저장"이라는 말부터 정리할게요. 지금까지 우리가 만든 변수는 전부 메모리에 잠깐 머물다 사라졌어요. let count = 0으로 좋아요 수를 세도, 새로고침(F5) 한 번이면 0으로 돌아갔죠. 페이지를 새로 그리면서 변수가 통째로 날아가거든요.
쪽지에 비유하면 이래요. 변수는 손바닥에 적은 메모예요. 잠깐은 보이지만 손을 씻으면(새로고침) 지워지죠. 우리가 원하는 건 책상 서랍에 넣어 두는 거예요. 서랍에 넣어 두면, 자리를 떠났다 돌아와도(새로고침·재방문) 그대로 있잖아요. 브라우저에도 그런 서랍이 있어요.
브라우저가 주는 서랍은 크게 세 종류예요. 각각 성격이 달라요.
┌─ 쿠키(Cookie) ──────────────────────────────────────┐
│ · 크기 작음 (약 4KB) │
│ · 서버에 요청할 때마다 "자동으로" 딸려 감 │
│ · 만료 날짜를 정할 수 있음 │
└──────────────────────────────────────────────────────┘
┌─ localStorage ──────────────────────────────────────┐
│ · 크기 큼 (약 5~10MB) │
│ · 내가 코드로 "직접" 꺼내야 함 (자동 전송 안 됨) │
│ · 지우기 전까지 영구 보존 (새로고침·탭 닫기·재방문 OK) │
└──────────────────────────────────────────────────────┘
┌─ sessionStorage ────────────────────────────────────┐
│ · localStorage와 사용법 똑같음 │
│ · 단, 탭(브라우저 창)을 닫으면 사라짐 │
└──────────────────────────────────────────────────────┘
표로 한 번 더 정리할게요.
| 항목 | 쿠키 | localStorage | sessionStorage |
|---|---|---|---|
| 크기 | 약 4KB | 약 5~10MB | 약 5~10MB |
| 수명 | 만료일까지 | 지울 때까지 영구 | 탭 닫으면 끝 |
| 서버 자동 전송 | O (요청마다) | X (직접 꺼냄) | X (직접 꺼냄) |
| 주 용도 | 서버 세션 | 로그인 상태·설정 | 일회성 임시 데이터 |
오늘 우리가 쓸 건 localStorage예요. 로그인 정보는 "지울 때까지 남아야" 하니까요(탭을 닫아도 다음에 또 로그인돼 있어야 편하죠). sessionStorage는 사용법이 완전히 똑같고 수명만 짧으니, 오늘 localStorage를 익히면 그대로 따라와요. 쿠키는 "서버가 자동으로 받아 가는" 특수한 저장소라, 진짜 인증을 다룰 때(다음 과목 백엔드) 본격적으로 만나게 돼요.
💡 변수는 새로고침하면 사라지는 메모, 저장소는 남는 서랍이에요. 서랍은 세 종류 — 쿠키(서버에 자동 전송, 작음), localStorage(직접 꺼냄, 영구), sessionStorage(탭 닫으면 끝). 오늘은 로그인 상태를 영구 보존하려고 localStorage를 써요.
Step 2: "서랍에 넣고 빼기" — localStorage 기본 3동작
localStorage는 외울 게 딱 세 개예요. 넣기(setItem) · 빼기(getItem) · 버리기(removeItem). 직접 손으로 해보는 게 제일 빨라요. 아무 페이지나 Live Server로 열고, F12를 눌러 콘솔(Console)을 켜세요. 거기에 한 줄씩 쳐볼게요.
// 넣기 — setItem(서랍이름, 값)
localStorage.setItem("nickname", "재훈");
// 빼기 — getItem(서랍이름) → 값
localStorage.getItem("nickname"); // "재훈"
// 버리기 — removeItem(서랍이름)
localStorage.removeItem("nickname");
localStorage.getItem("nickname"); // null (이제 없으니까)
setItem은 "서랍 이름(key)"과 "넣을 값(value)" 두 개를 줘요. getItem은 서랍 이름만 주면 그 값을 꺼내 주고요. 넣은 적 없는 서랍을 꺼내면 null이 나와요. 이 null이 아주 중요해요. "로그인한 적 있나?"를 물을 때, getItem이 null이면 "아직 로그인 안 함"이라는 뜻이거든요.
여기서 딱 하나만 기억하세요. localStorage에는 문자열(글자)만 넣을 수 있어요. 숫자를 넣어도 문자열로 변해요. 한번 확인해볼까요?
localStorage.setItem("age", 25); // 숫자 25를 넣어도
localStorage.getItem("age"); // "25" — 문자열로 나와요!
typeof localStorage.getItem("age"); // "string"
숫자 25를 넣었는데 꺼내면 문자열 "25"예요. 그럼 객체({ name: "재훈", age: 25 })처럼 여러 정보를 묶은 건 어떻게 저장할까요? 그게 바로 Step 5에서 배울 JSON.stringify의 역할이에요. 지금은 "localStorage는 문자열만 담는다"만 손에 쥐고 넘어갈게요.
💡 localStorage는 세 동작이 전부예요 —
setItem(key, value)로 넣고,getItem(key)로 빼고,removeItem(key)로 버려요. 없는 걸 꺼내면null. 그리고 담기는 건 오직 문자열이에요.
Step 3: "로그인하면 기억해" — 폼 제출 가로채기
이제 진짜로 로그인을 만들어볼게요. 우리 index.html엔 이미 A-4에서 만든 로그인 폼이 있어요. 사용자 이름과 비밀번호를 받는 폼이죠. 지금은 "로그인" 버튼을 눌러도 아무 일도 안 일어나요(정확히는 폼이 서버로 전송되며 페이지가 새로고침돼요). 이걸 JavaScript로 가로채서, 입력한 이름을 localStorage에 기억시킬 거예요.
새 파일 js/auth.js를 만들어요. 인증(authentication)을 담당하는 파일이라 auth라고 이름 지었어요. 먼저 저장·조회 함수부터 만들게요. 지금은 이해를 돕기 위해 단순하게 — 이름과 토큰을 따로따로 저장해요. (Step 5에서 이 둘을 하나로 깔끔하게 묶을 거예요.)
// instagram-clone-frontend/js/auth.js
// (중간 단계 — Step 5에서 객체 하나로 묶어요)
// 로그인 정보를 저장해요. 토큰은 원래 서버가 발급하지만, 지금은 흉내만 내요.
export function saveAuth(username) {
localStorage.setItem("username", username);
localStorage.setItem("token", `fake-token-${username}`);
}
// 로그인한 사용자 이름을 꺼내요. 없으면 null.
export function getCurrentUser() {
return localStorage.getItem("username");
}
// 요청에 실을 토큰을 꺼내요. 없으면 null.
export function getToken() {
return localStorage.getItem("token");
}
// 로그아웃 — 저장된 로그인 정보를 지워요.
export function clearAuth() {
localStorage.removeItem("username");
localStorage.removeItem("token");
}
여기서 토큰(token) 이라는 새 단어가 나왔어요. 토큰은 "나 로그인한 사람 맞아요"를 증명하는 출입증 같은 문자열이에요. 진짜 앱에서는 로그인에 성공하면 서버가 이 출입증을 발급해 줘요. 지금은 서버 인증을 아직 안 배웠으니, fake-token-재훈처럼 가짜 문자열로 흉내만 낼게요. 중요한 건 "출입증을 저장해 뒀다가, 요청할 때마다 보여준다"는 흐름이에요.
이제 로그인 폼이 제출되는 순간을 가로챌 차례예요. 같은 auth.js 파일 맨 아래에 폼 처리 코드를 붙여요.
// instagram-clone-frontend/js/auth.js
// ===== 로그인 폼 처리 (index.html 에서만 동작) =====
const loginForm = document.querySelector(".login-form");
if (loginForm) {
loginForm.addEventListener("submit", (event) => {
event.preventDefault(); // 폼 기본 동작(서버로 전송 + 새로고침)을 멈춰요
const username = loginForm.querySelector("#username").value.trim();
if (!username) return; // 빈 이름이면 아무것도 안 해요
saveAuth(username); // 로그인 정보를 브라우저에 저장
location.href = "feed.html"; // 저장했으니 피드 화면으로 이동
});
}
D-2에서 배운 submit 이벤트와 preventDefault가 그대로 쓰였죠? 폼은 기본적으로 제출되면 서버로 전송하며 새로고침해요. 그걸 preventDefault로 막고, 대신 우리가 직접 처리하는 거예요. 입력칸(#username)의 값을 꺼내 saveAuth로 저장하고, location.href로 피드 페이지로 보내요.
if (loginForm)이라는 가드가 보이죠? auth.js는 곧 피드 화면에서도 불러 쓸 거예요(토큰을 꺼내려고요). 그런데 피드 화면엔 로그인 폼이 없어요. 그럼 querySelector(".login-form")이 null을 돌려주는데, if가 그 경우를 막아서 폼 코드를 그냥 건너뛰어요. 이렇게 해두면 한 파일을 여러 페이지에서 안전하게 공유할 수 있어요.
마지막으로 이 파일을 index.html에 연결해요. 페이지 맨 아래, </body> 직전에 한 줄 추가하면 끝이에요.
<!-- instagram-clone-frontend/index.html -->
<script type="module" src="js/auth.js"></script>
</body>
type="module"을 붙인 건 C-5에서 배운 import/export를 쓰기 때문이에요. 이제 로그인 폼에 이름을 넣고 버튼을 누르면, 그 이름이 localStorage에 저장되고 피드로 넘어가요.
💡 폼 제출을
preventDefault로 가로채, 입력한 이름을saveAuth로 localStorage에 저장하고 피드로 이동해요.auth.js는 피드 화면에서도 쓰이니, 로그인 폼이 없을 때를if (loginForm)가드로 막아 한 파일을 안전하게 공유해요.
Step 4: "이건 누구의 요청이다" — 토큰을 헤더에 실어 보내기
저장한 토큰을 이제 써먹을 차례예요. 지난 시간 댓글을 저장할 때 작성자를 "soongu_hong"으로 고정했던 그 자리에, 진짜 로그인한 사용자가 들어가요. 그리고 요청을 보낼 때마다 토큰을 함께 실어, "이 요청은 이 사람이 보낸 겁니다"를 서버에 알려요.
먼저 api.js 맨 위에서 방금 만든 auth.js의 함수들을 가져와요.
// instagram-clone-frontend/js/api.js
import { getToken, getCurrentUser } from "./auth.js";
const BASE_URL = "http://localhost:3001";
// 모든 요청에 붙일 헤더를 만들어요.
// localStorage 에 토큰이 있으면 Authorization 헤더로 실어 보내요(없으면 안 붙여요).
function authHeaders(extra = {}) {
const headers = { ...extra };
const token = getToken();
if (token) headers["Authorization"] = `Bearer ${token}`;
return headers;
}
authHeaders는 요청에 붙일 헤더 묶음을 만들어 주는 작은 도우미예요. getToken()으로 저장된 토큰을 꺼내고, 토큰이 있으면 Authorization이라는 이름의 헤더에 Bearer 토큰값 형식으로 담아요. 헤더(header) 는 요청에 붙이는 쪽지 같은 거예요. "이건 JSON이에요"(Content-Type), "이건 이 사람 요청이에요"(Authorization)처럼, 본문과 별개로 딸려 가는 정보죠.
Bearer는 "이 토큰을 가진 사람(bearer = 소지자)"이라는 뜻의 약속된 단어예요. 토큰 인증의 표준 표기라, 거의 모든 서버가 Authorization: Bearer xxx 형식을 알아들어요. ...extra는 C-4에서 배운 스프레드예요. 이미 있던 헤더(Content-Type 같은)에 Authorization을 더 얹는 거죠.
이제 게시물을 받아 오는 fetchPosts에 이 헤더를 달아요.
// instagram-clone-frontend/js/api.js
export async function fetchPosts(page = 1, perPage = 3) {
const url = `${BASE_URL}/posts?_page=${page}&_per_page=${perPage}`;
// headers: 토큰이 있으면 Authorization 을 자동으로 실어 보내요.
const response = await fetch(url, {
headers: authHeaders(),
signal: AbortSignal.timeout(8000),
});
if (!response.ok) {
throw new Error(describeStatus(response.status));
}
return await response.json();
}
그리고 핵심, 댓글을 저장하는 createComment예요. 고정돼 있던 작성자가 드디어 진짜 사용자로 바뀌어요.
// instagram-clone-frontend/js/api.js
export async function createComment(postId, text) {
// 지난 시간엔 "soongu_hong" 으로 고정했던 자리에, 진짜 로그인한 사용자가 들어가요.
const username = getCurrentUser() ?? "guest"; // 로그인 안 했으면 guest
const response = await fetch(`${BASE_URL}/comments`, {
method: "POST",
headers: authHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ postId, username, text }),
});
if (!response.ok) {
throw new Error(describeStatus(response.status));
}
return await response.json();
}
getCurrentUser() ?? "guest"를 보세요. C-5에서 배운 ??(널 병합)예요. 로그인했으면 그 사람 이름을, 안 했으면 "guest"를 작성자로 써요. 그리고 헤더엔 authHeaders({ "Content-Type": "application/json" })를 줬어요. "JSON 보낼게요"라는 기존 헤더에, 토큰까지 함께 실리는 거죠.
지난 시간의 가장 큰 찜찜함이 여기서 풀려요. 재훈으로 로그인하고 댓글을 달면 작성자가 jaehoon으로, 민지로 로그인하면 minji로 저장돼요. 그리고 모든 요청에 그 사람의 토큰이 자동으로 따라붙어요. 이게 실제로 동작하는지는 Step 7에서 DevTools Network 탭으로 두 눈으로 확인할 거예요.
💡
authHeaders로 토큰을Authorization: Bearer xxx헤더에 자동으로 실어요.createComment의 고정 작성자"soongu_hong"은getCurrentUser() ?? "guest"로 바뀌어, 로그인한 사람이 댓글 작성자가 돼요.
Step 5: "여러 정보를 하나로" — JSON.stringify / JSON.parse
Step 3에서 우리는 이름과 토큰을 따로따로 저장했어요(username 서랍, token 서랍). 동작은 하지만, 정보가 늘어날수록 서랍이 늘어나요. 로그인한 시각도 기억하고 싶다? 또 서랍 하나. 흩어진 서랍을 하나씩 챙기는 건 번거롭죠. 관련 정보는 객체 하나로 묶는 게 깔끔해요.
// 이렇게 묶고 싶은데...
const auth = {
username: "jaehoon",
token: "fake-token-jaehoon-...",
loginAt: "2026-06-08T...",
};
문제는 Step 2에서 봤듯이 localStorage엔 문자열만 들어간다는 거예요. 객체를 그냥 넣으면 어떻게 될까요? 콘솔에서 직접 해보면 알아요.
localStorage.setItem("auth", { username: "재훈" });
localStorage.getItem("auth"); // "[object Object]" — 내용이 통째로 사라졌어요!
객체가 "[object Object]"라는 쓸모없는 글자로 변해 버려요. 그래서 필요한 게 JSON.stringify예요. 객체를 "JSON 문자열"이라는, 내용을 고스란히 담은 글자로 바꿔 주거든요. 꺼낼 땐 반대로 JSON.parse로 다시 객체로 풀고요.
객체 ──JSON.stringify──▶ 문자열 (저장할 때)
{ username:"재훈" } '{"username":"재훈"}'
문자열 ──JSON.parse──▶ 객체 (꺼낼 때)
'{"username":"재훈"}' { username:"재훈" }
D-4에서 댓글을 POST로 보낼 때 JSON.stringify를 이미 썼던 거 기억나요? 객체를 서버로 보내려고 문자열로 바꿨었죠. 똑같은 도구를, 이번엔 localStorage에 저장하려고 쓰는 거예요. 이제 auth.js를 객체 방식으로 다시 정리하면, 우리 파일의 최종 모습이 나와요.
// instagram-clone-frontend/js/auth.js
// localStorage 에 넣을 때 쓸 "서랍 이름"(key). 하나로 정해 두고 재사용해요.
const AUTH_KEY = "auth";
// 로그인 정보를 저장해요.
// localStorage 는 "문자열"만 담을 수 있어서, 객체를 JSON 문자열로 바꿔(stringify) 넣어요.
export function saveAuth(username) {
const auth = {
username,
// 진짜 토큰은 로그인에 성공하면 서버가 발급해 줘요. 지금은 그 흉내만 내요.
token: `fake-token-${username}-${Date.now()}`,
loginAt: new Date().toISOString(),
};
localStorage.setItem(AUTH_KEY, JSON.stringify(auth)); // 객체 → 문자열 → 저장
return auth;
}
// 저장된 로그인 정보를 꺼내 "객체"로 돌려줘요. 저장된 게 없으면 null.
// 꺼낸 값은 문자열이라, JSON.parse 로 다시 객체로 풀어요.
export function getAuth() {
const raw = localStorage.getItem(AUTH_KEY); // 문자열 또는 null
if (!raw) return null; // 로그인한 적 없으면 null
return JSON.parse(raw); // 문자열 → 객체
}
saveAuth는 이름·토큰·로그인 시각을 객체 하나로 묶고, JSON.stringify로 문자열로 바꿔 "auth"라는 서랍 하나에 저장해요. 서랍이 셋에서 하나로 줄었죠. getAuth는 그 문자열을 꺼내(getItem), 저장된 게 없으면 null을, 있으면 JSON.parse로 객체로 풀어 돌려줘요.
그리고 Step 3에서 만든 getCurrentUser · getToken은 이제 이 getAuth를 거쳐요.
// instagram-clone-frontend/js/auth.js
// 로그인한 사용자 이름만 꺼내요. 없으면 null.
export function getCurrentUser() {
const auth = getAuth();
return auth ? auth.username : null;
}
// 서버 요청에 실어 보낼 토큰을 꺼내요. 없으면 null.
export function getToken() {
const auth = getAuth();
return auth ? auth.token : null;
}
// 로그아웃 — 저장된 로그인 정보를 지워요.
export function clearAuth() {
localStorage.removeItem(AUTH_KEY);
}
여기서 한 가지 짚어둘 게 있어요. getCurrentUser와 getToken은 함수 이름과 사용법이 Step 3과 똑같아요. 안에서 저장 방식만 "서랍 셋"에서 "객체 하나"로 바뀌었을 뿐이죠. 그래서 이 둘을 가져다 쓰는 api.js는 한 글자도 안 고쳐도 그대로 동작해요. 겉(함수 이름)은 그대로 두고 속(구현)만 바꾸는 것 — 이게 함수로 감싸 두면 좋은 이유예요. 쓰는 쪽은 내부가 어떻게 바뀌든 신경 안 써도 되니까요.
💡 localStorage엔 문자열만 들어가니, 객체는
JSON.stringify로 문자열로 바꿔 저장하고JSON.parse로 풀어 꺼내요. 흩어진 서랍 셋을 객체 하나로 묶어"auth"한 서랍에 담아요.getToken·getCurrentUser의 사용법은 그대로라api.js는 안 고쳐도 돼요.
Step 6: "저장된 게 진짜 있나?" — DevTools Application 탭
코드를 다 짰으니, 이제 진짜로 저장이 됐는지 두 눈으로 확인할 차례예요. 여기서 개발자의 무기 DevTools가 등장해요. 브라우저에서 F12(또는 마우스 우클릭 → "검사")를 누르면 화면 한쪽에 개발자 도구가 열려요. 위쪽에 여러 탭이 보일 거예요.
┌─────────────────────────────────────────────────────────┐
│ Elements │ Console │ Sources │ Network │ Application │ ... │ ← 탭 줄
└─────────────────────────────────────────────────────────┘
HTML 콘솔 코드 통신 저장소
구조 메시지 디버깅 관찰 들여다보기
각 탭의 역할을 한 줄씩 보면 — Elements는 지금 화면의 HTML 구조를, Console은 console.log와 에러 메시지를, Sources는 코드 파일과 디버깅을, Network는 서버와 오가는 요청을, Application은 localStorage 같은 저장소를 보여줘요. 오늘은 Application · Network · Sources 셋을 차례로 써볼 거예요.
먼저 Application 탭을 눌러요. 왼쪽 목록에서 "Local Storage"를 펼치고 우리 사이트 주소(http://127.0.0.1:5500 같은)를 클릭하면, 저장된 서랍들이 표로 보여요.
Application 탭
┌── 왼쪽 목록 ──┬───────── 오른쪽 표 ───────────────────────────┐
│ ▾ Local Storage│ Key │ Value │
│ 127.0.0.1 │ ───── │ ──────────────────────────────────── │
│ Session Stor.│ auth │ {"username":"jaehoon", │
│ Cookies │ │ "token":"fake-token-jaehoon-17...", │
│ │ │ "loginAt":"2026-06-08T..."} │
└────────────────┴───────────────────────────────────────────────┘
로그인을 한 번 했다면 auth라는 서랍에, 우리가 저장한 JSON 문자열이 그대로 들어 있을 거예요. JSON.stringify가 객체를 어떻게 문자열로 바꿨는지 눈으로 확인되죠. 만약 로그인을 안 했다면 이 표는 비어 있어요.
콘솔에서도 직접 확인할 수 있어요. Console 탭으로 가서 쳐보세요.
localStorage.getItem("auth"); // 저장된 JSON 문자열이 나와요
JSON.parse(localStorage.getItem("auth")); // 객체로 풀어서 보기
저장이 안 보이면? 로그인 폼에서 이름을 넣고 버튼을 눌렀는지, auth.js가 index.html에 연결됐는지부터 확인하세요. Application 탭은 "내 코드가 정말 저장을 했나"를 확인하는 첫 번째 자리예요.
💡 F12로 DevTools를 열고 Application 탭 → Local Storage에서 저장된 서랍을 표로 볼 수 있어요.
auth서랍에 우리가JSON.stringify로 만든 문자열이 그대로 들어 있죠. Console에서localStorage.getItem을 직접 쳐서 확인할 수도 있어요.
Step 7: "요청에 토큰이 진짜 실렸나?" — DevTools Network 탭
Step 4에서 모든 요청에 토큰을 실어 보내도록 만들었죠. 정말 실렸을까요? 이건 Network 탭에서 확인해요. 서버와 오가는 모든 요청이 여기 줄줄이 기록되거든요.
json-server를 켜고(npx json-server mock/db.json --port 3001), 피드 페이지를 Live Server로 연 다음, DevTools에서 Network 탭을 열어요. 그 상태로 댓글을 하나 달아 보세요. 그러면 요청 목록에 comments라는 줄이 새로 생겨요. 그걸 클릭하면 오른쪽에 자세한 정보가 떠요.
Network 탭
┌── 요청 목록 ──┬──── 클릭한 요청 상세 (Headers) ──────────────┐
│ posts?_page=1 │ ▾ General │
│ posts?_page=2 │ Request URL: .../comments │
│ comments ◀───│ Request Method: POST │
│ │ Status Code: 201 Created ← 저장 성공! │
│ │ ▾ Request Headers │
│ │ Content-Type: application/json │
│ │ Authorization: Bearer fake-token-jae... │
│ │ ▾ Request Payload (보낸 본문) │
│ │ { postId: 1, username: "jaehoon", ... } │
└───────────────┴───────────────────────────────────────────────┘
세 가지를 확인하세요. 첫째, Status Code가 201 Created예요. D-4에서 배운 "새로 만들었어"라는 성공 신호죠. 둘째, Request Headers에 Authorization: Bearer fake-token-...이 보여요. Step 4에서 만든 authHeaders가 토큰을 제대로 실은 거예요. 셋째, Request Payload(보낸 본문)의 username이 "soongu_hong"이 아니라 로그인한 사람(jaehoon)으로 바뀐 게 보이죠.
코드만 봐선 "실리겠지" 싶지만, Network 탭은 그걸 증거로 보여줘요. 헤더가 빠졌거나, 작성자가 엉뚱하거나, 상태 코드가 400·500이면 여기서 바로 드러나요. 서버와 관련된 버그는 거의 다 이 탭에서 잡혀요. "내 코드가 보낸 게 정확히 뭔가"를 보는 자리거든요.
💡 Network 탭은 서버와 오가는 요청의 증거예요. 댓글을 달고
comments요청을 클릭하면, Status201, Request Headers의Authorization: Bearer ..., Payload의 바뀐username을 직접 확인할 수 있어요. 서버 관련 버그는 대부분 여기서 잡혀요.
Step 8: "멈춰서 들여다보기" — Breakpoints 디버깅
마지막으로 개발자의 진짜 무기를 하나 익혀요. 바로 중단점(breakpoint) 이에요. 지금까지 우리는 값이 궁금하면 console.log를 코드에 끼워 넣었죠. 그것도 좋지만, 매번 코드를 고쳐 넣고 지우는 게 번거로워요. 중단점은 코드를 건드리지 않고, 원하는 줄에서 실행을 잠깐 멈춘 다음, 그 순간의 변수들을 통째로 들여다보게 해줘요.
Sources 탭으로 가서 왼쪽 파일 목록에서 auth.js를 열어요. 그리고 멈추고 싶은 줄의 줄 번호를 클릭하면, 거기 파란 표시가 생겨요. 이게 중단점이에요. 예를 들어 saveAuth 안의 localStorage.setItem(...) 줄에 걸어볼게요.
Sources 탭 — auth.js
┌─────────────────────────────────────────────────────────┐
│ 11 export function saveAuth(username) { │
│ 12 const auth = { │
│ 13 username, │
│ 14 token: `fake-token-${username}-${Date.now()}`, │
│ 15 loginAt: new Date().toISOString(), │
│ 16 }; │
│🔵17 localStorage.setItem(AUTH_KEY, JSON.stringify(au.. │ ← 여기서 멈춤
│ 18 return auth; │
└─────────────────────────────────────────────────────────┘
┌── 멈췄을 때 오른쪽 패널 ──────────────────┐
│ ▾ Scope (지금 이 순간의 변수들) │
│ username: "jaehoon" │
│ auth: { username: "jaehoon", │
│ token: "fake-token-jaehoon-..", │
│ loginAt: "2026-06-08T.." } │
└────────────────────────────────────────────┘
이제 로그인 폼에 이름을 넣고 버튼을 눌러 보세요. saveAuth가 호출되다가, 17번 줄에서 실행이 딱 멈춰요. 화면도 그 순간에 정지해요. 오른쪽 Scope 패널을 보면, 그 순간 username엔 뭐가 들었는지, auth 객체가 어떻게 만들어졌는지가 전부 보여요. console.log를 안 써도, 멈춘 그 자리의 모든 변수를 들여다볼 수 있는 거죠.
위쪽 버튼으로 흐름을 제어해요. Resume(▶) 은 다시 끝까지 실행, Step over(⤵) 는 다음 한 줄만 실행하고 또 멈춰요. 한 줄씩 넘기면서 "여기서 이 변수가 이렇게 바뀌는구나"를 따라가면, 버그가 어느 줄에서 생기는지 정확히 짚여요.
처음엔 어색해도 괜찮아요. 앞으로 "분명 코드는 맞는데 왜 안 되지?" 싶을 때, 중단점을 걸고 한 줄씩 따라가는 습관이 여러분을 구해줄 거예요. 에러는 친구이고, DevTools는 그 친구와 대화하는 통로예요.
💡 중단점은 코드를 안 고치고도 원하는 줄에서 실행을 멈춰, 그 순간의 변수를 Scope 패널로 들여다보게 해줘요. Sources 탭에서 줄 번호를 클릭해 걸고, Step over로 한 줄씩 넘기며 값의 변화를 추적해요.
console.log보다 강력한 디버깅 도구예요.
마무리
오늘 우리는 인스타 클론에 "누가 로그인했는지 기억하는" 마지막 조각을 끼웠어요. 새로고침해도 사라지지 않는 저장소를 손에 넣었고, 개발자의 핵심 도구인 DevTools를 열어 봤죠. 되짚어볼게요.
- 브라우저 저장소 3종 — 쿠키(서버 자동 전송)·localStorage(영구·직접 꺼냄)·sessionStorage(탭 닫으면 끝)를 구분했어요.
- localStorage 3동작 —
setItem으로 넣고,getItem으로 빼고,removeItem으로 버려요. 없으면null, 담기는 건 문자열뿐. - 폼 가로채기 —
submit이벤트를preventDefault로 막고, 입력값을 저장한 뒤 피드로 이동했어요. - Authorization 헤더 —
authHeaders로 토큰을Bearer형식으로 모든 요청에 자동으로 실었어요. - JSON.stringify / parse — 객체를 문자열로 바꿔 저장하고, 꺼낼 때 다시 객체로 풀었어요.
- DevTools — Application(저장소)·Network(요청)·Sources(중단점 디버깅) 세 탭을 직접 써봤어요.
한 줄로 외워두세요. 저장은 localStorage에(setItem) → 묶을 땐 JSON으로(stringify/parse) → 요청엔 토큰을 헤더로(Authorization) → 확인은 DevTools로. 이 흐름이 로그인을 다루는 거의 모든 화면의 기본기예요.
한 가지만 분명히 해둘게요. 오늘 만든 localStorage 로그인은 어디까지나 "클라이언트(브라우저)가 잠깐 기억하는" 수준이에요. 진짜 인증 — "이 토큰이 정말 유효한가"를 검사하고 발급하는 일 — 은 서버(백엔드)가 해요. 우리가 만든 fake-token은 흉내만 낸 거고요. 그래서 비밀번호 같은 민감한 정보는 localStorage에 절대 넣으면 안 돼요. 누구나 DevTools로 들여다볼 수 있으니까요. 이 이야기는 생각해볼 주제에서 더 다뤄볼게요.
다음 시간 예고
오늘로 Category D — DOM과 브라우저 API — 가 마무리됐어요. 좋아요·댓글·무한 스크롤·fetch·저장소까지, 화면이 사용자와 진짜로 대화하게 만들었죠. 그런데 지금 우리 화면은 모든 게 "툭툭" 끊겨서 나타나요. 좋아요를 누르면 하트가 갑자기 색만 바뀌고, 댓글이 휙 생기고, 화면 전환도 딱딱하죠.
다음 시간(B-7)엔 이 움직임에 부드러움을 입혀요. 좋아요 하트가 통! 하고 튀어오르고, 메뉴가 스르륵 펼쳐지고, 화면이 매끄럽게 전환되는 — CSS의 transition·transform·@keyframes, 그리고 화면 전환을 자연스럽게 잇는 View Transitions까지요. 오늘 만든 "로그인 성공 → 피드로 이동"에도, 딱딱한 점프 대신 부드러운 전환을 입힐 수 있어요. 정적이던 클론에 생기가 도는 시간이 될 거예요. 다음 시간에 만나요!
과제
오늘 배운 흐름(저장소 · 토큰 · JSON · DevTools)을 직접 익혀볼 차례예요. 모든 과제는 json-server를 켠 채로(npx json-server mock/db.json --port 3001), Live Server로 페이지를 열고, F12로 DevTools를 함께 띄워 놓고 진행하세요.
[구현] 로그아웃 버튼 만들기
로그인은 만들었으니, 이제 로그아웃을 만들어볼 차례예요. 우리는 이미 clearAuth라는 함수를 만들어 뒀죠.
feed.html의 적당한 곳(예: 상단 네비게이션)에 "로그아웃" 버튼을 하나 추가하세요.- 그 버튼을 클릭하면
clearAuth()를 호출하고,location.href = "index.html"로 로그인 페이지로 보내세요. (feed.js또는 새 작은 스크립트에서auth.js의clearAuth를import하면 돼요.) - 로그아웃한 뒤 Application 탭을 열어,
auth서랍이 정말 사라졌는지 확인하세요. 그 상태로 댓글을 달면 작성자가 다시guest가 되는 것도 관찰해 보세요.
[탐구] localStorage vs sessionStorage, 뭐가 다를까?
오늘 localStorage를 썼지만, sessionStorage도 사용법이 똑같아요. 수명만 다르죠. 그 차이를 직접 체험해 보는 탐구예요.
- 콘솔에서
localStorage.setItem("test", "영구")와sessionStorage.setItem("test", "임시")를 둘 다 실행하세요. - 탭을 완전히 닫았다가 같은 페이지를 다시 여세요. 콘솔에서 두 값을 각각 꺼내(
getItem) 보세요. 어느 쪽이 살아남고 어느 쪽이null이 됐나요? - 그렇다면 "로그인 유지"에는 왜 localStorage가, "이번 방문에만 쓸 임시 데이터"에는 왜 sessionStorage가 어울리는지 한 문장으로 정리해 보세요.
[구현] DevTools로 값을 직접 바꿔 보기
DevTools는 보기만 하는 도구가 아니에요. 값을 직접 고쳐서 실험할 수도 있어요.
- 로그인한 상태에서 Application 탭 → Local Storage →
auth값을 더블클릭하면 수정할 수 있어요.username을 다른 이름으로 바꿔 보세요(JSON 형식이 깨지지 않게 따옴표 주의). - 그 상태로 페이지에서 댓글을 달아 보세요. Network 탭에서 보낸
username이 방금 고친 이름으로 나가나요? - 이 실험으로 알 수 있는 사실이 있어요. 클라이언트(브라우저)의 저장값은 사용자가 마음대로 고칠 수 있다는 거죠. 이게 왜 "진짜 인증은 서버가 해야 한다"의 이유가 되는지 생각해 보세요.
생각해볼 주제
정답을 적는 문제가 아니에요. 오늘 배운 것의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.
1. 비밀번호를 localStorage에 저장하면 왜 안 될까?
오늘 우리는 로그인 정보를 localStorage에 저장했어요. 그런데 만약 편하다는 이유로, 사용자의 비밀번호까지 localStorage에 저장해 두면 어떨까요?
localStorage는 누구나 F12 → Application 탭에서 평문 그대로 들여다볼 수 있어요. 심지어 악성 스크립트가 페이지에 끼어들면 저장된 값을 통째로 훔쳐 갈 수도 있죠(이런 공격을 XSS라고 불러요). 그래서 비밀번호처럼 치명적인 정보는 localStorage에 두면 안 돼요. 그렇다면 토큰은 왜 그나마 괜찮을까요? "비밀번호"와 "토큰"의 위험도가 어떻게 다른지, 그리고 토큰이 새어 나가도 피해를 줄이는 방법(만료 시간 등)은 무엇일지 생각해 보세요.
2. 토큰을 왜 굳이 헤더에 직접 실을까?
오늘 토큰을 Authorization 헤더에 우리 손으로 실어 보냈어요. 그런데 Step 1에서 봤듯이, 쿠키는 요청할 때마다 자동으로 서버에 딸려 가요. 그럼 토큰도 그냥 쿠키에 넣어서 자동으로 보내면 편하지 않을까요?
자동 전송은 편하지만, "자동"이라서 생기는 문제도 있어요(원치 않는 요청에도 딸려 가는 CSRF 같은 공격). 반대로 헤더에 직접 싣는 방식은 번거로워도, 어떤 요청에 토큰을 보낼지 우리가 통제할 수 있죠. "편한 자동"과 "통제되는 수동" 사이의 맞바꿈이에요. 각 방식이 어떤 상황에 어울릴지, 둘을 섞어 쓰는 경우는 없을지 곱씹어 보세요.
3. DevTools는 왜 개발자의 "첫 번째 무기"일까?
오늘 우리는 버그를 만나기도 전에 DevTools부터 열었어요. Application으로 저장을 확인하고, Network로 요청을 들여다보고, Sources에서 코드를 멈춰 세웠죠.
console.log만으로도 디버깅은 돼요. 그런데 왜 현업 개발자들은 DevTools를 손에서 놓지 않을까요? "코드를 안 고치고도 실행 중인 화면의 속을 들여다본다"는 게 어떤 힘인지 생각해 보세요. 또 화면(Elements)·동작(Console)·통신(Network)·코드(Sources)를 한 창에서 동시에 본다는 게, 문제의 원인을 좁혀 가는 데 왜 결정적인지도요. 추측으로 코드를 이리저리 고치는 것과, 증거를 보고 정확히 한 줄을 고치는 것의 차이예요.
✅ 예시 답안정답 보기
과제는 정답이 하나가 아니에요. 아래는 "이렇게 풀면 좋다"는 예시와 채점 기준이에요. 직접 만든 코드와 비교하며, 왜 이렇게 했는지를 곱씹어보세요.
과제 1: 로그아웃 버튼 만들기
핵심 접근
로그인의 반대는 "저장된 로그인 정보를 지우는 것"이에요. 우리는 이미 auth.js에 clearAuth()를 만들어 뒀으니, 새 로직을 짤 필요가 거의 없어요. 버튼 하나를 두고, 클릭하면 clearAuth()를 부른 뒤 로그인 페이지로 보내면 끝이에요. "기능은 이미 있고, 연결만 하면 된다"는 걸 체감하는 과제예요.
예시 구현
먼저 feed.html의 상단 네비게이션에 버튼을 하나 넣어요.
<!-- instagram-clone-frontend/feed.html — 상단 nav 안 적당한 위치 -->
<button type="button" id="logoutBtn" class="icon-btn">로그아웃</button>
그리고 feed.js 맨 아래(또는 별도 스크립트)에서 clearAuth를 가져와 연결해요.
// instagram-clone-frontend/js/feed.js
import { clearAuth } from "./auth.js";
const logoutBtn = document.querySelector("#logoutBtn");
if (logoutBtn) {
logoutBtn.addEventListener("click", () => {
clearAuth(); // 1) 저장된 로그인 정보 삭제
location.href = "index.html"; // 2) 로그인 페이지로 이동
});
}
clearAuth()는 내부에서 localStorage.removeItem("auth") 한 줄을 실행해요. 이걸 부른 뒤 location.href로 페이지를 옮기면, 로그아웃이 완성돼요.
확인은 DevTools로 해요. 로그아웃 버튼을 누른 뒤 Application 탭 → Local Storage를 보면, auth 서랍이 사라져 있어요. 그 상태로 댓글을 달면, getCurrentUser()가 null을 돌려주니 작성자가 다시 guest로 저장되죠.
채점 포인트
| 항목 | 배점 | 확인 |
|---|---|---|
clearAuth()를 직접 만들지 않고 기존 함수를 재사용했는가 |
30% | 새 removeItem을 또 짜지 않음 |
| 클릭 후 로그인 페이지로 이동하는가 | 25% | location.href = "index.html" |
Application 탭에서 auth 삭제를 확인했는가 |
25% | 저장소가 비워짐 |
로그아웃 후 작성자가 guest로 돌아가는가 |
20% | getCurrentUser() → null 흐름 이해 |
흔한 실수
clearAuth()를 호출하고location.href를 안 줘서, 저장은 지워졌는데 화면은 그대로 피드에 머무는 경우. 사용자는 로그아웃됐는지 알 수가 없어요.localStorage.clear()로 전부 지우는 경우. 그러면 다른 설정값까지 날아가요. 우리가 만든clearAuth처럼removeItem("auth")로 필요한 서랍만 지우는 게 안전해요.import { clearAuth }의 이름을export한 이름과 다르게 적어undefined가 되는 경우. 콘솔 에러를 꼭 확인하세요.
실무 개선 포인트 (심화)
진짜 앱에서는 로그아웃이 "브라우저 저장만 지우는" 것으로 끝나지 않아요. 서버에도 "이 토큰 이제 무효예요"라고 알려야(서버 측 토큰 폐기) 완전한 로그아웃이 돼요. 안 그러면 누군가 그 토큰을 미리 베껴 뒀다가 계속 쓸 수 있거든요. 클라이언트 로그아웃(저장 삭제)과 서버 로그아웃(토큰 무효화)이 짝을 이뤄야 한다는 점을 기억해 두세요.
과제 2: localStorage vs sessionStorage, 뭐가 다를까?
핵심 접근
두 저장소는 메서드(setItem/getItem/removeItem)가 완전히 같아요. 차이는 오직 수명이에요. localStorage는 지울 때까지 영구, sessionStorage는 탭을 닫으면 사라지죠. 글로 외우는 것보다 직접 닫았다 열어 보면 한 번에 와닿아요.
예시 관찰
콘솔에서 두 저장소에 값을 하나씩 넣어요.
localStorage.setItem("test", "영구");
sessionStorage.setItem("test", "임시");
지금 둘 다 꺼내 보면 정상적으로 값이 나와요.
localStorage.getItem("test"); // "영구"
sessionStorage.getItem("test"); // "임시"
이제 탭을 완전히 닫았다가 같은 페이지를 다시 열어요. 그리고 다시 꺼내 보면 결과가 갈려요.
localStorage.getItem("test"); // "영구" ← 살아남음
sessionStorage.getItem("test"); // null ← 사라짐
localStorage는 탭을 닫아도 남아 있고, sessionStorage는 탭을 닫는 순간 비워져요. 이게 둘의 전부예요.
한 문장 정리 예시 — "로그인 유지처럼 '다음에 와도 기억해야 하는' 정보는 localStorage에, 이번 방문 동안만 쓰는 '한 번 쓰고 버릴' 임시 데이터(예: 작성하다 만 글 임시 보관)는 sessionStorage에 어울려요."
채점 포인트
| 항목 | 배점 | 확인 |
|---|---|---|
| 두 저장소에 값을 넣고 꺼내 봤는가 | 25% | setItem/getItem 실행 |
| 탭을 닫았다 열어 수명 차이를 관찰했는가 | 35% | sessionStorage만 null |
| 메서드는 같고 수명만 다름을 이해했는가 | 20% | 사용법 동일 |
| 각각 어울리는 용도를 한 문장으로 정리했는가 | 20% | 영구 vs 일회성 구분 |
흔한 실수
- 새로고침(F5) 만 하고 "sessionStorage도 남네?"라고 결론짓는 경우. sessionStorage는 새로고침으로는 안 사라져요. 탭(창)을 완전히 닫아야 사라집니다.
- 새 탭에서 같은 사이트를 열어 보고 헷갈리는 경우. sessionStorage는 탭마다 따로예요. 다른 탭에선 처음부터 비어 있는 게 정상이에요.
실무 개선 포인트 (심화)
"수명"이라는 한 축만으로 저장소를 고르는 게 아니에요. 민감도(누가 봐도 되는 값인가), 크기, 서버 전송 필요 여부까지 함께 따져요. 예를 들어 결제 과정처럼 "이 탭을 벗어나면 초기화돼야 안전한" 흐름엔 일부러 sessionStorage를 골라, 탭이 닫히면 흔적이 남지 않게 설계하기도 해요. 저장소 선택도 작은 설계 결정이라는 감각을 가져가세요.
과제 3: DevTools로 값을 직접 바꿔 보기
핵심 접근
DevTools는 "읽기 전용"이 아니에요. 저장값을 직접 고쳐서, 코드를 안 바꾸고도 다양한 상황을 흉내 낼 수 있어요. 이 과제의 진짜 목표는 "클라이언트 저장값은 사용자가 마음대로 바꿀 수 있다"를 몸으로 깨닫는 거예요. 이게 보안의 출발점이거든요.
예시 관찰
로그인한 상태에서 Application 탭 → Local Storage → auth 값을 더블클릭하면 편집할 수 있어요. JSON 형식을 깨지 않게 username만 바꿔요.
바꾸기 전: {"username":"jaehoon","token":"fake-token-jaehoon-..","loginAt":".."}
바꾼 후 : {"username":"hacker","token":"fake-token-jaehoon-..","loginAt":".."}
이 상태로 댓글을 달고 Network 탭에서 보낸 요청을 확인하면, Payload의 username이 방금 고친 "hacker"로 나가요. 우리 코드는 한 줄도 안 바꿨는데, 저장값을 손댄 것만으로 동작이 달라진 거예요.
여기서 핵심을 짚어요. 브라우저에 저장된 값은 그 컴퓨터 주인이 얼마든지 고칠 수 있어요. 그래서 "이 사람이 정말 jaehoon인가"를 브라우저 저장값만 믿고 판단하면 안 돼요. 진짜 검증은 서버가, 위조할 수 없는 방식(서명된 토큰 등)으로 해야 하죠.
채점 포인트
| 항목 | 배점 | 확인 |
|---|---|---|
| Application 탭에서 값을 직접 수정했는가 | 30% | auth 더블클릭 편집 |
| 수정이 실제 요청에 반영됨을 Network 탭에서 확인했는가 | 35% | Payload username 변경 |
| JSON 형식을 깨뜨리지 않고 편집했는가 | 15% | 따옴표·중괄호 유지 |
| "클라이언트 값은 신뢰할 수 없다"를 설명했는가 | 20% | 서버 검증 필요성 |
흔한 실수
- 값을 편집할 때 따옴표를 빠뜨려 JSON이 깨지는 경우. 그러면
getAuth()의JSON.parse가 에러를 던져요. 콘솔에 빨간 에러가 뜨면 형식부터 확인하세요. - "내 코드가 안전하니 괜찮다"고 넘기는 경우. 프론트엔드 코드는 사용자에게 전부 공개돼 있고, 저장값도 다 보여요. "프론트는 못 믿는다"가 보안의 기본 전제예요.
실무 개선 포인트 (심화)
이 실험이 바로 "프론트엔드 검증은 편의, 서버 검증은 보안" 이라는 원칙의 근거예요. 입력값 길이 체크나 로그인 UI는 사용자 편의를 위해 프론트에서 하지만, "정말 이 사람이 맞는가 / 이 값이 유효한가"의 최종 판단은 반드시 서버가 해요. 프론트의 검증은 사용자가 우회할 수 있다는 걸 늘 전제하세요. 이 감각은 다음 백엔드 과목에서 인증·인가를 배울 때 그대로 이어져요.
생각해볼 주제 1: 비밀번호를 localStorage에 저장하면 왜 안 될까?
[문제 상황 요약]
로그인 정보를 localStorage에 저장하는 김에, 편하다고 비밀번호까지 저장하면 어떨까? localStorage는 F12로 누구나 평문으로 볼 수 있고, 악성 스크립트(XSS)에 통째로 털릴 수도 있다. 그렇다면 토큰은 왜 그나마 저장해도 되는가?
[튜터의 가이드 및 해설]
핵심은 "새어 나갔을 때의 피해 크기"예요. 비밀번호는 사용자의 마스터 키예요. 한 번 털리면 그 계정은 물론이고, 같은 비밀번호를 쓰는 다른 서비스까지 다 뚫려요. 게다가 사용자가 직접 바꾸기 전까지 영원히 유효하죠.
토큰은 달라요. 토큰은 "한시적 출입증"이에요. 보통 만료 시간이 있어서, 새어 나가도 일정 시간이 지나면 저절로 무효가 돼요. 서버가 "이 토큰 폐기"라고 강제로 끊을 수도 있고요. 비밀번호 자체는 노출되지 않으니, 토큰이 털려도 비밀번호를 바꿀 필요는 없어요. 즉 피해 범위가 좁고, 통제 가능해요.
그래서 설계 원칙은 이래요. 비밀번호는 브라우저에 절대 저장하지 않고, 로그인 순간에만 서버로 보내고 잊어버려요. 서버는 비밀번호를 그대로 저장하지 않고 해시(되돌릴 수 없게 변환)해서 보관하고요. 클라이언트가 들고 다니는 건 만료되는 토큰뿐이에요. 위험한 건 아예 안 두고, 두더라도 "털려도 덜 아픈 것"만 두는 거죠.
🎯 면접관을 홀리는 핵심 멘트
"비밀번호는 마스터 키라 한 번 새면 피해가 영구적이고 광범위하지만, 토큰은 만료·폐기가 가능한 한시적 출입증이라 피해를 통제할 수 있습니다. 그래서 클라이언트에는 비밀번호를 절대 두지 않고, 새어 나가도 덜 치명적인 토큰만 두는 게 보안 설계의 기본입니다."
생각해볼 주제 2: 토큰을 왜 굳이 헤더에 직접 실을까?
[문제 상황 요약]
쿠키는 요청마다 자동으로 서버에 딸려 간다. 그럼 토큰도 쿠키에 넣어 자동 전송하면 편하지 않나? 그런데 우리는 일부러 Authorization 헤더에 손으로 실었다. 자동과 수동, 무엇이 더 나은가?
[튜터의 가이드 및 해설]
"편함"과 "통제" 사이의 맞바꿈이에요. 쿠키의 자동 전송은 분명 편해요. 코드로 일일이 토큰을 챙길 필요가 없으니까요. 그런데 "자동"이라서 생기는 함정이 있어요. 사용자가 의도하지 않은 요청에도 쿠키가 딸려 가거든요. 악성 사이트가 사용자를 속여 우리 서버로 요청을 보내게 하면, 쿠키가 자동으로 따라가 마치 본인이 한 것처럼 처리될 수 있어요. 이걸 CSRF 공격이라고 불러요.
헤더에 직접 싣는 방식은 번거로운 대신, 우리가 통제해요. 어떤 요청에 토큰을 붙일지 코드로 정하니까, 엉뚱한 곳으로 토큰이 새지 않아요. 오늘 만든 authHeaders가 정확히 그 역할이었죠. 자바스크립트가 명시적으로 붙이지 않는 한 토큰은 안 나가요.
현실에서는 둘 다 써요. 토큰 기반 인증(헤더 방식)은 모바일 앱·외부 API처럼 쿠키를 쓰기 어려운 환경에 강하고, 쿠키 기반 세션은 전통적인 웹에서 잘 통해요. 쿠키를 쓰더라도 CSRF를 막는 장치(SameSite 속성, CSRF 토큰)를 함께 거는 식으로 약점을 보완하죠. "자동이라 편한 쿠키 vs 통제되는 헤더"라는 맞바꿈을 이해하고, 상황에 맞게 고르는 게 핵심이에요.
🎯 면접관을 홀리는 핵심 멘트
"쿠키의 자동 전송은 편하지만 CSRF처럼 의도치 않은 요청에도 딸려 가는 위험이 있고, Authorization 헤더 방식은 번거로워도 어떤 요청에 토큰을 실을지 개발자가 통제할 수 있습니다. 모바일·외부 API엔 헤더 토큰이, 전통 웹엔 보완 장치를 건 쿠키 세션이 어울려, 환경에 맞춰 고르는 문제라고 봅니다."
생각해볼 주제 3: DevTools는 왜 개발자의 "첫 번째 무기"일까?
[문제 상황 요약]
console.log만으로도 디버깅은 된다. 그런데 왜 현업 개발자는 버그를 만나기도 전에 DevTools부터 열까? "코드를 안 고치고 실행 중인 화면의 속을 들여다본다"는 게 어떤 힘인가?
[튜터의 가이드 및 해설]
디버깅의 본질은 "추측을 증거로 바꾸는 일"이에요. console.log는 "여기 값이 이상할 것 같은데?"라고 미리 찍어 둔 자리만 보여줘요. 짐작이 빗나가면 다시 코드를 고쳐 넣고, 새로고침하고, 또 찍고 — 추측에 기댄 반복이죠.
DevTools는 다르게 접근해요. Application으로 "저장이 진짜 됐나", Network로 "요청이 정확히 뭘 보냈나", Sources의 중단점으로 "이 줄에서 변수가 정말 뭐였나"를 실행 중인 그 순간 들여다봐요. 코드를 안 고치고도, 화면 뒤에서 무슨 일이 벌어지는지를 직접 보는 거예요. 추측으로 코드를 이리저리 고치는 것과, 증거를 보고 정확히 한 줄을 짚는 것의 차이예요.
특히 강력한 건 한 창에서 여러 층위를 동시에 본다는 점이에요. 버그는 보통 한 층에만 있지 않아요. "화면(Elements)은 멀쩡한데 동작(Console)이 안 되네? 알고 보니 요청(Network)이 400을 받았고, 그 원인은 코드(Sources) 한 줄이었다" — 이렇게 층을 넘나들며 원인을 좁혀 가요. console.log만으론 이 연결을 보기 어렵죠. 그래서 숙련된 개발자일수록 "일단 찍어 보자"보다 "일단 열어서 보자"가 먼저예요.
🎯 면접관을 홀리는 핵심 멘트
"console.log는 미리 찍어 둔 자리만, 그것도 추측에 기대 보여주지만, DevTools는 실행 중인 화면의 저장소·네트워크·코드를 증거로 직접 보여줍니다. 화면·동작·통신·코드를 한 창에서 넘나들며 원인을 좁힐 수 있어서, 추측으로 고치는 대신 증거를 보고 정확히 한 줄을 짚게 해주는 게 DevTools의 힘이라고 생각합니다."