빌드노트
쿠키 Expires가 미래인데도 바로 사라지는 이유
미래 Expires와 Max-Age=0이 함께 오면 쿠키는 즉시 만료된다. 새 브라우저에서 반대 조건과 세션 쿠키까지 비교한 저장·전송 결과로, 실제 응답에서 확인할 속성과 수명 설정을 구별한다.
Set-Cookie의 Expires가 미래인데 쿠키가 저장 직후 사라진다면, 같은 헤더의 Max-Age부터 확인한다. 두 속성이 함께 있으면 유효한 Max-Age가 우선한다. Max-Age=0은 즉시 만료다. Expires 날짜만 더 뒤로 바꿔도 0이 그대로면 쿠키를 유지할 수 없다.
Expires는 만료할 날짜·시각이고, Max-Age는 쿠키가 만료되기까지의 초 단위 수명이다. 이 우선순위는 RFC 6265의 Max-Age 설명 (새 탭에서 열림)에 명시돼 있다. 브라우저의 처리 절차 (새 탭에서 열림)에서도 0 이하의 값을 즉시 만료되는 시각으로 처리한다. 아래는 실제 로그인 장애 기록이 아니라, 가짜 쿠키로 이 차이를 확인한 별도 실험이다.
미래 날짜가 있어도 기존 쿠키가 삭제됐다
기존 브라우저와 분리한 새 프로필을 만들고 127.0.0.1의 임시 HTTP 서버만 사용했다. 먼저 lab_zero=seed를 Max-Age=120; Path=/; SameSite=Lax로 저장했다. 브라우저 저장소에 행이 생겼고, 서버도 다음 요청에서 Cookie: lab_zero=seed를 받았다. 이후 삭제 대상과 이름·호스트·Path가 같은 쿠키를 포함해 네 가지 조건을 한 응답에서 보냈다.
브라우저 쪽에서 수집한 실제 응답 헤더다. 각 쿠키는 별개의 Set-Cookie 헤더이며, 이 날짜들은 당시 실험값이다.
Date: Wed, 21 Oct 2026 06:06:48 GMT
Set-Cookie: lab_zero=zero; Expires=Wed, 21 Oct 2026 06:08:48 GMT; Max-Age=0; Path=/; SameSite=Lax
Set-Cookie: lab_short=short; Max-Age=12; Expires=Tue, 20 Oct 2026 06:06:48 GMT; Path=/; SameSite=Lax
Set-Cookie: lab_expires=date; Expires=Wed, 21 Oct 2026 06:08:48 GMT; Path=/; SameSite=Lax
Set-Cookie: lab_session=session; Path=/; SameSite=Lax
첫 쿠키는 2분 뒤의 Expires와 Max-Age=0을 함께 받았다. 두 번째는 하루 전의 Expires와 Max-Age=12를 받았다. 세 번째는 미래 Expires만, 네 번째는 두 만료 속성을 모두 생략했다. 네 쿠키의 다른 전송 조건은 같게 맞췄다.
설정 직후 저장소를 조회하고, 같은 출처의 /echo로 요청을 보내 서버가 받은 Cookie 헤더를 돌려받았다. 실제로 13초를 기다린 뒤 같은 조회와 요청을 반복했다. 기다리는 동안 쿠키를 다시 설정하거나 시스템 시계를 바꾸지 않았다.
설정 직후
저장소: lab_short, lab_expires, lab_session
서버 수신 Cookie: lab_short=short; lab_expires=date; lab_session=session
13초 대기 뒤
저장소: lab_expires, lab_session
서버 수신 Cookie: lab_expires=date; lab_session=session
lab_zero는 처음에는 저장되고 전송됐지만, 미래 날짜와 0을 함께 받은 뒤 저장소와 요청에서 모두 사라졌다. 빈 값을 넣어 둔 쿠키가 남은 것도 아니었다. 이 응답에서는 Max-Age=0이 기존 쿠키의 삭제로 이어졌다.
반대로 lab_short는 Expires가 과거인데도 직후 저장되고 전송됐다. 13초 뒤에는 저장소와 요청에서 빠졌다. 이 쿠키는 Expires를 헤더의 뒤쪽에 배치했는데도 12초의 수명이 적용됐다. 두 속성 중 뒤에 적힌 것을 고르는 규칙으로 해석하면 이 결과를 설명할 수 없다.
lab_expires는 같은 비교 구간 동안 남았다. 이 대조는 만료 속성의 우선순위를 확인한 것이며, 미래 날짜가 있으면 그때까지 어떤 상황에서도 보존된다는 보장은 아니다. RFC도 브라우저가 지정 수명보다 먼저 쿠키를 제거할 수 있음을 설명한다.
만료 속성 생략은 Max-Age=0과 다르다
두 속성을 모두 생략한 lab_session도 설정 직후와 13초 뒤에 저장돼 있었고 서버로 전송됐다. 자동화 API에서 읽은 만료 표시는 expires: -1이었다. 이 값은 이 API의 세션 쿠키 표시다. 서버가 Max-Age=-1을 보냈다는 뜻이 아니다.
유효한 Max-Age와 Expires가 모두 없으면 브라우저가 정하는 현재 세션의 수명을 따른다. 반면 0은 만료 속성을 생략하는 값이 아니다. MDN의 만료·세션 쿠키 설명 (새 탭에서 열림)은 세션 복원 기능이 쿠키도 복원할 수 있다고 덧붙인다. 창을 닫았다가 다시 열어 값이 남았다는 사실만으로 영속 쿠키라고 판단하기도 어렵다. 이번 실험은 같은 세션 안에서만 관찰했으며 종료·재시작 뒤의 동작은 확인하지 않았다.
내 응답에서는 두 속성과 다음 요청을 함께 본다
Chrome 개발자 도구를 연 상태에서 문제가 나는 동작을 수행한다. 서버 코드에 적힌 날짜보다 브라우저가 실제로 받은 응답을 기준으로 확인한다.
- Network에서 쿠키를 설정한 응답의 Headers를 열고 해당
Set-Cookie한 줄의Expires와Max-Age를 함께 읽는다. 리다이렉트가 있다면 중간 응답도 포함한다. 뒤의 응답이 같은 쿠키를 다시 만료시키는지 확인한다. 이름만 같고 Domain·Path가 다른 쿠키를 섞어 읽지 않는다. - Application → Storage → Cookies에서 해당 사이트를 선택하고 이름과 Expires / Max-Age를 본다. 이 열은 브라우저가 처리한 만료 결과를 보는 곳이다. 응답의 두 속성을 확인하는 일은 1번에서 따로 해야 한다. 세션 쿠키는
Session으로 표시된다. Chrome의 쿠키 조회 안내 (새 탭에서 열림) - 뒤이어 보낸 요청의 Cookie 헤더와 같은 요청의 서버 수신을 대조한다. 가짜 쿠키 실험에서는 위 출력처럼 두 쪽의 값이 일치했다. 실제 인증 쿠키를 조사할 때는 값 전체를 로그로 남기지 않고 이름의 존재 여부를 확인해도 된다.
요청의 Cookie 헤더에는 Expires와 Max-Age가 다시 실리지 않는다. 따라서 서버가 받은 Cookie 헤더만으로 어떤 만료 설정을 적용했는지 알아낼 수 없다. 설정 응답과 다음 요청을 연결해 봐야 한다. RFC의 Cookie 헤더 설명 (새 탭에서 열림)
실제 응답에 Max-Age=0이 도착했고 쿠키를 유지하려던 상황이라면, 쿠키를 만드는 코드와 공통 응답 처리에서 0이 들어가는 지점을 찾는다. 일정 시간 유지하려면 의도한 초 단위 양수 수명을 설정하고, Expires도 함께 보낼 때는 같은 만료 의도에 맞춘다. 세션 쿠키가 목적이라면 두 만료 속성을 모두 생략한다. 단순히 Max-Age만 지우고 미래 Expires를 남기면 세션 쿠키가 되지는 않는다.
수정 후에는 새 응답 헤더를 다시 읽고, 저장 직후와 의도한 수명 뒤의 전송을 비교한다. 0이 없는데도 사라지거나 저장소에는 남았는데 요청에만 없다면 이번 원인으로 단정할 수 없다. 브라우저의 차단 사유와 실제 요청 URL에 맞는 전송 조건을 이어 확인한다.
직접 실행한 환경은 Windows 11, Python 3.12.10, Playwright 1.59.0, Chromium 147.0.7727.15다. 미래 Expires 단독 쿠키는 13초 비교 구간에서 남은 것까지 확인했으며 120초 전체 수명은 기다리지 않았다. 다른 브라우저, HTTPS·교차 사이트 요청, 시계 차이 보정, 실제 로그인과 서버 세션의 유효성은 이번 관찰에 포함되지 않는다.