발행일

액세스 토큰을 7일 쿠키에 넣고 있었다 — 이름만 암호화였던 setEncryptedCookie

액세스 토큰을 7일 쿠키에 넣고 있었다

이 글은 2026년 9월에 당시 커밋 이력을 다시 보며 정리한 것입니다. 날짜는 작업한 날 기준입니다.

2024년 여름 TripTune의 로그인 코드는 이렇게 생겼었습니다.

const { setEncryptedCookie } = useSaveLocalContent();
// ...
setEncryptedCookie('trip-tune_at', accessToken, 7);
setEncryptedCookie('trip-tune_rt', refreshToken, 7);
setEncryptedCookie('userId', userId, 7);

액세스 토큰도 7일, 리프레시 토큰도 7일, userId도 7일. 그리고 함수 이름이 setEncryptedCookie입니다. 프런트 4년차였지만 인증 흐름을 처음부터 제가 잡아 본 건 이 프로젝트가 처음이었어요. 그때 무엇을 알고 무엇을 몰랐는지가 이 세 줄에 그대로 있습니다.

이름만 암호화였다

useSaveLocalContent는 프로젝트 첫 커밋(7186614, 6월 27일)부터 있던 훅입니다. 전문이 이렇습니다.

import Cookies from 'js-cookie';

const useSaveLocalContent = () => {
  const setEncryptedCookie = (name: string, value: string, days: number) => {
    Cookies.set(name, value, { expires: days });
  };

  const getDecryptedCookie = (name: string) => {
    return Cookies.get(name);
  };

  return { setEncryptedCookie, getDecryptedCookie };
};

EncryptedDecrypted도 이름에만 있습니다. 안에서는 js-cookiesetget을 그대로 부릅니다. 암호화 라이브러리인 crypto-js는 7월 8일(bcefe6d)에 package.json에 추가됐는데, git log -S'crypto-js' -- src를 돌리면 아무 커밋도 안 나와요. 한 번도 import된 적이 없습니다. 9월 7일(06121bd)에 패키지째 빠졌습니다.

이름을 먼저 짓고 구현은 나중에 하려던 건지, 처음부터 그냥 지은 이름인지는 커밋에 남아 있지 않습니다. 확실한 건 두 달 동안 이 이름이 코드 곳곳에서 불렸고, 읽는 사람은 뭔가 암호화가 되는 줄 알았을 거라는 점입니다. 저도 그렇게 읽었을 거예요.

7일이 5분이 된 날

8월 1일(7349a53) 토큰 갱신 인터셉터에도 같은 값이 있었습니다.

Cookies.set('trip-tune_at', newAccessToken, { expires: 7 });

갱신받은 새 액세스 토큰을 다시 7일로 저장합니다. 리프레시 토큰이 있는데 액세스 토큰이 7일이면 리프레시 토큰이 할 일이 없어요. 이 구조에서는 액세스 토큰 하나가 새어 나가면 일주일 동안 유효합니다.

이걸 바꾼 건 한 달 뒤 9월 2일(c17bfc6)입니다. 커밋 제목이 "access(5분) refresh(1주일) 토근 발급"이에요.

-        setEncryptedCookie('trip-tune_at', accessToken, 7);
+        setEncryptedCookie('trip-tune_at', accessToken, 5 / (24 * 60));
         setEncryptedCookie('trip-tune_rt', refreshToken, 7);
         setEncryptedCookie('userId', userId, 7);

js-cookieexpires는 일 단위라 5분은 5 / (24 * 60)으로 씁니다. 커밋 제목이 서버가 발급하는 토큰의 수명을 적고 있는데, 서버와 프런트 어느 쪽이 먼저 정했는지는 프런트 저장소만 봐서는 모릅니다. 이틀 뒤 67973cf에서 인터셉터 쪽도 같은 함수, 같은 5분으로 통일됐습니다.

액세스 토큰이 5분이 되자 갱신이 일상이 됐습니다. 이전에는 7일에 한 번 일어날까 말까 한 경로였던 갱신이 이제 몇 분마다 도는 경로가 됐고, 그 경로에 있던 버그가 사용자에게 드러날 조건이 생긴 거예요. 실제로 무엇이 언제 보였는지는 뒤의 글들이 다룹니다.

그 이름을 버린 날

setEncryptedCookie라는 이름은 그 뒤로도 오래 살았습니다. 2025년 4월 20일(88b8003)에 secure: truesameSite: 'strict'가 붙었고, 4월 27일(d912217)에는 만료일 인자 자체가 없어졌어요. 리프레시 토큰을 서버가 관리하는 쪽으로 옮기던 시기입니다.

그리고 4월 28일(6829f14), 커밋 제목이 "리프레시 토큰 암호화 해제, 이해 안되는 코드 정리"입니다.

-  const { getDecryptedCookie } = saveLocalContent();
-  const refreshToken = getDecryptedCookie('refreshToken');
+  const refreshToken = Cookies.get('refreshToken');

"암호화 해제"라고 적었지만 해제할 암호화는 처음부터 없었습니다. 열 달 전의 제가 지은 이름을 열 달 뒤의 제가 "이해 안되는 코드"라고 부르며 걷어낸 거예요. 같은 커밋에 이런 주석도 남겼습니다.

//TODO : 리프레시 토큰을 프론트가 지울 수 있나요?
Cookies.remove('refreshToken');

여기까지 오면 질문의 급이 달라져 있습니다. 2024년 8월에는 "며칠짜리로 저장하나"를 물었고, 2025년 4월에는 "이걸 프런트가 들고 있어야 하나"를 묻고 있어요.

검증

  • git show 7186614:src/hooks/useSaveLocalContent.ts로 첫 커밋의 함수 본문이 Cookies.set 한 줄인 것을 확인했습니다.
  • git log -S'crypto-js' -- srcgit log -S'CryptoJS' -- src가 둘 다 비어 있는 것으로 "한 번도 import되지 않았다"를 확인했습니다. package.json에서의 추가·삭제는 bcefe6d·06121bd입니다.
  • 7349a53useLogin.ts에서 세 쿠키 모두 7인 것, c17bfc6에서 액세스 토큰만 5 / (24 * 60)으로 바뀐 것을 diff로 확인했습니다.
  • 당시 브라우저에서 쿠키 만료를 직접 확인한 기록은 없습니다.

남은 것 · 한계

  • 왜 7일이었는지는 모릅니다. 리프레시 토큰과 같은 값을 준 것으로 보아 둘의 역할 차이를 코드에 반영하지 않은 상태였다는 것까지만 말할 수 있습니다.
  • userId도 쿠키에 그대로 넣었습니다. 이건 11월에 닉네임으로 바뀌고, 한글 닉네임이 쿠키에서 깨지는 문제로 2025년 4월에 encodeURIComponent를 거치게 되는데, 이 글의 범위 밖입니다.
  • securesameSite는 2025년 4월에야 붙었습니다. 2024년 내내 없었어요.
  • 액세스 토큰이 5분이 되면서 드러난 갱신 경로의 버그들은 2년 가까이 흩어져 있다가 2026년 7월에 한곳으로 모였습니다. 로그인이 자꾸 풀리던 서비스가 그 결말이고, 지금 구조가 access 5분 / refresh 1주인 것도 거기에 적혀 있습니다. 그 글에서 "초기에 겪은 버그"라고 부르는 것들의 출발점이 이 7일 쿠키입니다.

관련 글: 로그인이 자꾸 풀리던 서비스 — 401 처리를 한곳으로 모으기까지 · 인증이 확정되기 전에 쏜 요청 · 로그아웃은 PATCH인가 DELETE인가 · 리프레시 토큰을 프런트가 지울 수 있나요 · JWT exp를 프런트가 읽던 함수를 지웠다