- 발행일
로그아웃이 세 번 만에 됐다 — 지운 뒤에 보내던 토큰, 암호문을 그대로 보내던 헤더
로그아웃이 세 번 만에 됐다
이 글은 2026년 9월에 당시 커밋 이력을 다시 보며 정리한 것입니다. 날짜는 작업한 날 기준입니다.
2024년 2월, 스위그 3기에서 직짱건강이라는 건강 설문 서비스를 만들고 있었습니다. 프런트는 둘이었고 저는 온보딩·로그인·마이페이지·설문·찜을 맡았어요. React는 3년째 쓰고 있었지만 로그인부터 로그아웃까지 인증 흐름을 처음부터 끝까지 혼자 맡은 건 이때가 처음이었습니다.
로그아웃 버튼은 2월 28일에 붙였고, "로그아웃 고치기 성공"이라는 커밋은 3월 14일 새벽 2시 51분입니다. 보름 동안 세 판을 짰어요.
첫 판 — localStorage에서 꺼내 Bearer로
2월 28일 91147d3. 마이페이지의 로그아웃 모달에 console.log('Performing logout...')만 있던 자리를 실제 함수로 바꿨습니다.
const refreshToken = localStorage.getItem('refreshToken');
// HTTP 요청 헤더에 'Authorization' 대신 'RefreshToken' 사용 (서버 구현에 따라 다를 수 있음)
await axios.post('/api/members/logout', {}, {
headers: {
'Authorization': `Bearer ${refreshToken}`
}
});
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
주석은 RefreshToken 헤더를 쓰라고 적어 놓고 코드는 Authorization으로 보냅니다. 서버가 어느 헤더를 읽는지 확인하기 전에 쓴 코드라는 게 주석에 그대로 남아 있어요. 이때 토큰은 localStorage에 있었습니다.
토큰이 쿠키로 옮겨 갔다
이틀 뒤 3월 1일, 팀원이 cf6b6a1 "axios 토큰 및 리프래쉬 설정"으로 인증 기반을 통째로 바꿨습니다. 토큰은 zzgg_at·zzgg_rt라는 쿠키에 7일짜리로 들어가고, 넣을 때 crypto-js로 AES 암호화를 합니다. 요청마다 쿠키를 읽어 Authorization에 얹는 인터셉터도 이 커밋이에요.
const SECERET_KEY = 'temp';
const setEncryptedCookie = (name: string, value: string, days: number) => {
const encryptedValue = encryptData(value); // 데이터를 암호화
document.cookie = `${name}=${encryptedValue}${expires}; path=/`;
};
키가 'temp'로 코드에 박혀 있어서 번들을 열면 누구나 풀 수 있습니다. 그러니 보안이라기보다 쿠키 값을 눈으로 못 읽게 하는 정도예요. 다만 이건 지금 보니 그렇다는 것이고, 그때 저는 이 훅을 그냥 가져다 썼습니다.
둘째 판 — 지운 다음에 보냈다
3월 4일 b5acb95, 마이페이지를 새로 짜면서 로그아웃도 이 기반 위로 옮겼습니다.
const onLogout = async () => {
localStorage.clear();
logoutDeleteToken();
// todo : 토큰이 안보내지고 있는거 같다.
await logout();
};
logoutDeleteToken()이 쿠키 두 개를 지우고, 그다음에 logout()을 부릅니다. 그런데 logout()은 인터셉터가 붙은 axiosInstance.patch('/api/members/logout')이고, 인터셉터는 요청 직전에 쿠키에서 액세스 토큰을 읽어 헤더에 넣어요. 지운 뒤에 읽으니 넣을 게 없습니다. "토큰이 안보내지고 있는거 같다"는 todo는 정확한 관찰이었는데, 순서가 원인이라는 데까지는 그때 못 갔어요. 이 인과는 이 글을 쓰면서 인터셉터 코드를 읽고 맞춘 겁니다.
localStorage.clear()도 남아 있습니다. 토큰이 이미 쿠키로 간 뒤라 지울 게 없는데, 첫 판의 잔재가 그대로 따라왔어요.
셋째 판 — 순서를 뒤집고, 응답으로 판정하고
3월 12일 9f4a409(PR #89). 커밋 제목이 "쿠키 초기화로 로그아웃이 아닌 실제 로그아웃 진행 하도록"입니다. 순서를 뒤집었어요.
const refreshToken = getTokenValue('zzgg_rt');
const onLogout = async () => {
if (refreshToken) {
const response = await logout(refreshToken);
if (response?.data?.blacklist?.length !== 0) {
logoutDeleteToken();
await router.push('/Home');
} else {
alert(response?.data?.message);
}
}
};
먼저 서버에 보내고, 응답의 blacklist 길이로 성공을 판정한 뒤에 쿠키를 지웁니다. 헤더 이름은 RefreshToken, 값은 Bearer 뒤에 리프레시 토큰을 붙인 형태로, 2월 주석이 말하던 그 헤더 이름을 이제야 씁니다. 인터셉터에도 로그아웃 주소일 때만 리프레시 토큰을 Authorization에 넣는 분기를 넣었어요.
그런데 이 커밋에도 함정이 하나 있었습니다. getTokenValue는 js-cookie의 Cookies.get이라 쿠키에 든 암호문을 그대로 돌려줍니다. 복호화 함수는 getDecryptedCookie였는데 다른 걸 불렀어요. 서버는 AES 암호문을 리프레시 토큰이라고 받은 셈입니다.
고치기 성공 — 셋을 한꺼번에
3월 14일 새벽 8e84c98 "로그아웃 고치기 성공". 그 사이 6a3ea4a가 로그아웃 호출을 axiosInstance에서 인터셉터 없는 맨 axios로 빼 두었고, 6f751b6이 인터셉터의 로그아웃 분기를 다시 걷어냈습니다. 마지막 커밋은 세 가지를 바꿉니다.
-const refreshToken = getTokenValue('zzgg_rt');
+const refreshToken = getDecryptedCookie('zzgg_rt');
-const response = await axios.patch(logoutUrl, {}, {
+const response = await axios.patch(`${process.env.NEXT_PUBLIC_API_URL}${logoutUrl}`, {}, {
headers: {
- 'RefreshToken': `Bearer ${refreshToken}`,
+ 'RefreshToken': `${refreshToken}`,
},
});
복호화한 값을 보내고, Bearer 접두어를 떼고, 상대 경로를 절대 URL로 바꿨습니다. 그리고 됐어요. 어느 것이 고친 건지는 커밋에 없고, 셋을 한 번에 바꿔서 지금 diff를 봐도 가릴 수 없습니다. 암호문을 보내던 건 확실히 문제였을 테고, 서버가 Bearer 없는 값을 기대했는지는 서버 코드가 없어서 모릅니다.
메서드는 3월 4일부터 줄곧 PATCH였습니다. 왜 PATCH인지 이때는 묻지 않았고, 한 달 뒤 로그아웃은 PATCH인가 DELETE인가2024-04-09로그아웃은 PATCH인가 DELETE인가 — 그리고 이 비교가 놓쳤던 것로그아웃 API를 설계하며 PATCH(상태 전환)와 DELETE(세션 삭제)를 비교했던 글입니다. 실제 프로젝트에서는 PATCH를 채택했는데, 지금 다시 보면 이 비교에는 빠진 선택지(실무 사실상 표준인 POST)와 더 중요한 질문("무엇의 상태인가")이 있었어요. 그리고 예시 코드의 결함이 실제 코드에도 그대로 들어갔다는 것까지 확인했습니다.에서 그 비교를 따로 했어요.
검증
- 그날 확인한 것: 로그아웃 뒤 홈으로 이동하고 마이페이지에 다시 못 들어가는 것. 기록은 커밋 제목 "고치기 성공"뿐이고 네트워크 탭 캡처는 없습니다.
- 이 글을 쓰며 확인한 것:
git show b5acb95로logoutDeleteToken()이logout()보다 앞에 있는 것,cf6b6a1의 인터셉터가 요청 시점에getDecryptedCookie('zzgg_at')를 읽는 것.9f4a409:src/hooks/useToken.ts의getTokenValue가Cookies.get한 줄인 것과8e84c98에서getDecryptedCookie로 바뀐 것. git log -S'localStorage.clear()'로 그 줄이 3월 4일에 들어와 3월 16일까지 남아 있던 것을 확인했습니다.- 서버가 어느 헤더·어느 형식을 읽었는지는 이 저장소로는 알 수 없습니다.
남은 것 · 한계
- 셋을 한꺼번에 바꿔 원인을 모릅니다. 하나씩 바꿨으면 "암호문을 보내고 있었다" 한 줄로 끝났을 문제예요. 같은 버릇이 그해 11월 배포 첫날 커밋 열 개2024-11-28배포 첫날 커밋 열 개 — www 누락, 리라이트 삭제, 그리고 읽히지도 않던 middlewareTripTune을 처음 Netlify에 올린 날, 커밋 열 개가 전부 next.config.js와 middleware.ts를 만지는 것이었습니다. 백엔드 주소에 www가 빠졌고, 리라이트를 지웠다가 되살렸고, 다음날에는 middleware의 matcher를 통째로 뒤집었어요. 지금 커밋을 다시 보니 그 middleware는 위치 때문에 애초에 실행된 적이 없었을 가능성이 큽니다.에서 다시 나옵니다.
- 암호화 키가
'temp'로 번들에 있습니다. 팀원이 만든 훅이라 제가 정한 건 아니지만, 가져다 쓰면서 한 번도 묻지 않았어요. 넉 달 뒤 TripTune에서 같은 이름의useSaveLocalContent·setEncryptedCookie를 다시 만들었는데, 그쪽은 암호화 자체를 안 했습니다2024-08-01액세스 토큰을 7일 쿠키에 넣고 있었다 — 이름만 암호화였던 setEncryptedCookie로그인하면 액세스 토큰, 리프레시 토큰, userId를 전부 7일짜리 쿠키에 넣었습니다. 넣는 함수 이름은 setEncryptedCookie였는데 안에서 하는 일은 js-cookie의 set 한 줄이었고, 같이 깔아 둔 crypto-js는 한 번도 import되지 않았어요. 액세스 토큰이 5분으로 줄어든 건 한 달 뒤였고, 그 이름을 버린 건 여덟 달 뒤였습니다.. 이름은 가져가고 하는 일은 뺀 셈이에요. - 성공 판정을
blacklist배열 길이로 합니다. 서버가 응답 모양을 바꾸면 조용히 로그아웃이 안 되는데, 그 계약은 코드 어디에도 적혀 있지 않습니다. - 로그아웃 요청만 인터셉터 밖의 맨
axios로 나갑니다. 왜 인터셉터를 피했는지는 커밋에 없어요. 지금 보면 로그아웃 주소 분기를 인터셉터에 넣었다 뺐다 하다가 포기한 흔적으로 읽힙니다.
관련 글: 직짱건강 — 백엔드를 기다리지 않고 설문 인터페이스를 먼저 완성한 방법2024-03-31직짱건강 — 백엔드를 기다리지 않고 설문 인터페이스를 먼저 완성한 방법직장인 대상 건강 설문과 위치 기반 병원 추천을 묶은 헬스케어 서비스입니다. 6인 협업 프로젝트에서 프런트엔드로 참여했고, 백엔드 API가 없는 상태에서 JSON 목업으로 설문 플로우를 먼저 완성해 병렬 작업이 가능하게 만든 게 핵심 기여였어요. · 로그아웃은 PATCH인가 DELETE인가2024-04-09로그아웃은 PATCH인가 DELETE인가 — 그리고 이 비교가 놓쳤던 것로그아웃 API를 설계하며 PATCH(상태 전환)와 DELETE(세션 삭제)를 비교했던 글입니다. 실제 프로젝트에서는 PATCH를 채택했는데, 지금 다시 보면 이 비교에는 빠진 선택지(실무 사실상 표준인 POST)와 더 중요한 질문("무엇의 상태인가")이 있었어요. 그리고 예시 코드의 결함이 실제 코드에도 그대로 들어갔다는 것까지 확인했습니다. · 액세스 토큰을 7일 쿠키에 넣고 있었다2024-08-01액세스 토큰을 7일 쿠키에 넣고 있었다 — 이름만 암호화였던 setEncryptedCookie로그인하면 액세스 토큰, 리프레시 토큰, userId를 전부 7일짜리 쿠키에 넣었습니다. 넣는 함수 이름은 setEncryptedCookie였는데 안에서 하는 일은 js-cookie의 set 한 줄이었고, 같이 깔아 둔 crypto-js는 한 번도 import되지 않았어요. 액세스 토큰이 5분으로 줄어든 건 한 달 뒤였고, 그 이름을 버린 건 여덟 달 뒤였습니다. · 리프레시 토큰을 프런트가 지울 수 있나요2025-04-30리프레시 토큰을 프런트가 지울 수 있나요 — 쿠키를 서버로 넘기던 열흘로그인 응답의 리프레시 토큰을 프런트가 쿠키에 넣고 갱신 요청 body에 실어 보내던 구조를, 서버가 쿠키를 심고 프런트는 credentials만 붙이는 구조로 바꿨습니다. 열흘 동안 쿠키 이름이 두 번 바뀌고, 만료 시간이 사라졌다가 돌아오고, "프론트가 지울 수 있나요?"라는 TODO가 세 곳에 붙었다가 하루 뒤 지워졌어요. 토큰 보관 주체가 옮겨 간 기록입니다.