"헤더로 추측하지 말고, 사용자가 직접 신호를 보내게 하라."
Admin 페이지에 로그인하면 세션이 무기한 유지됐습니다. Supabase Auth의 리프레시 토큰이 유효한 한, 미들웨어가 요청마다 세션을 자동으로 연장해줬기 때문입니다. 자리를 비우고 브라우저 탭을 켜둔 채로 몇 시간이 지나도 로그인 상태가 그대로 유지되는 구조였습니다.
관리자가 30분간 아무 조작도 하지 않으면 자동으로 로그아웃되고, /admin/login?reason=idle로 리다이렉트되어 안내 문구가 표시됩니다. 반대로 실제로 마우스나 키보드를 조작하며 작업 중이라면, 페이지 이동 없이 같은 화면에 머물러 있어도 세션이 정확하게 연장됩니다. Vercel Speed Insights, Next.js의 백그라운드 프리페치 등 사람이 인지하지 못하는 사이 발생하는 어떤 요청도 이제 이 판정에 영향을 주지 않습니다.
Admin 페이지에 유휴 30분 자동 로그아웃 기능을 추가하는 단순한 작업이었습니다. 하지만 배포 후 실사용 테스트 과정에서 세 가지 서로 다른 원인이 연쇄적으로 발견됐고, 결국 "서버가 요청을 관찰해서 판단하는 방식"에서 "클라이언트가 명시적으로 활동을 알리는 방식"으로 아키텍처 자체를 재설계하게 됐습니다. 이 글은 그 판단 과정을 그대로 기록합니다.
처음 접근은 "미들웨어가 요청을 감지해서 활동 여부를 판단"하는 방식이었습니다. 매 요청마다 마지막 활동 시각을 쿠키에 갱신하고, 30분간 갱신이 없으면 로그아웃시키는 구조였죠. 배포 직후 실사용 테스트에서 30분을 훌쩍 넘겨도 로그아웃이 안 되는 문제가 발견됐고, 원인을 좁혀가는 과정에서 세 가지가 차례로 드러났습니다.
첫 번째는 Vercel Speed Insights였습니다. 페이지 성능을 측정하려고 브라우저가 몇 분마다 자동으로 보내는 백그라운드 요청이 admin 경로를 그대로 통과하면서 활동 쿠키를 계속 갱신시키고 있었습니다. 이 요청 경로를 식별해 미들웨어 초입에서 걸러내는 방식으로 해결했습니다.
두 번째는 Next.js의 링크 프리페치였습니다. 사이드바에 떠 있는 메뉴 링크들을 Next.js가 자동으로 미리 불러오면서, next-router-prefetch 헤더가 실린 요청을 계속 보내고 있었습니다. 이번엔 헤더 값으로 프리페치 요청을 구분해서 활동 판정에서 제외하는 방식으로 대응했습니다.
그런데 세 번째 재현이 결정적이었습니다. 같은 증상이 또 발생했는데, 이번엔 next-router-prefetch 헤더가 없는 전혀 다른 형태의 백그라운드 요청이었습니다. Next.js 내부적으로 프리페치 캐시를 조용히 재검증하는 동작으로 추정되는데, 이 요청은 우리가 알던 헤더 패턴을 따르지 않았습니다.
여기서 문제를 다시 정의했습니다. "헤더로 백그라운드 요청을 구분한다"는 접근 자체가 근본적으로 불안정하다는 결론이었습니다. Next.js가 내부적으로 어떤 방식으로 요청을 보낼지는 프레임워크 버전이나 내부 최적화 정책에 따라 계속 바뀔 수 있고, 우리는 그걸 일일이 예측하고 쫓아다닐 수 없었습니다.
그래서 판단 주체를 완전히 바꿨습니다. 서버가 요청을 관찰해서 "이게 사람 행동 같다"고 추측하는 대신, 브라우저에서 실제 마우스 움직임·키보드 입력·클릭을 직접 감지하고, 그때만 클라이언트가 명시적으로 "지금 활동 중입니다"라는 신호(하트비트)를 서버에 보내도록 했습니다. 미들웨어는 더 이상 쿠키를 갱신하지 않고, 오직 "마지막 신호로부터 30분이 지났는지"만 읽어서 판단합니다. 이러면 백그라운드에서 무슨 요청이 오가든 이 판정에 영향을 줄 수 없습니다.
재설계 이후에도 한 가지 함정이 더 있었습니다. 하트비트 쿠키의 자체 만료 시간(maxAge)을 유휴 타임아웃과 똑같이 30분으로 설정했는데, 그러면 정확히 30분 시점에 브라우저가 쿠키를 스스로 삭제해버려서 미들웨어가 "쿠키 없음"과 "유휴 만료"를 구분하지 못하고 통과시켜버리는 문제가 생겼습니다. 쿠키의 물리적 수명과 애플리케이션 로직의 타임아웃 값은 반드시 분리해야 한다는 걸 다시 확인했습니다.
이번에 정리된 원칙은 이후 비슷한 "유휴 감지"나 "세션 관리" 기능에도 그대로 적용할 수 있습니다.
서버가 요청 헤더나 경로로 "사람의 의도"를 추측하려는 방식은, 프레임워크나 인프라의 내부 동작이 바뀔 때마다 다시 뚫릴 수 있는 구조적으로 불안정한 접근입니다. 판단이 중요한 로직일수록, 추측이 아니라 명시적 신호(하트비트, 이벤트 기반 콜백 등)로 전환하는 게 근본적입니다.
쿠키나 캐시에 만료 시간을 둘 때는, "저장소 자체의 물리적 수명"과 "애플리케이션이 판단하는 로직상의 타임아웃"을 분리해야 합니다. 둘을 같은 값으로 맞추면 경계 시점에 판정 불가 상태가 생깁니다.
"빌드 성공 ≠ 실제 동작"이라는 원칙이 이번에 특히 잘 들어맞았습니다. 코드 리뷰와 타입 체크를 다 통과해도, 실사용 시나리오(30분 방치)를 직접 재현해보지 않았다면 세 가지 버그 중 어느 것도 발견하지 못했을 겁니다.
관련 프로젝트
프로젝트 개요 기획자(PM)로서 AI를 활용한 업무 자동화(AX) 사례를 문서화하는 개인 포트폴리오 사이트입니다. 국내 기업의 AX 기획자/AI PM 직군으로의 커리어 전환을 목표로, 사이트 자체가 채용 지원용 스토리텔링 자료로 실사용되고 있습니다. 핵심 패턴: 업무 병목 발견 → AI로 워크플로우 재설계 → Before/After·근거·재현 가능성 문서화 전체 서사는 '실행하지 않은 아이디어 나열'이 아니라, 하나의 검증된 자동화 패턴과 도메인별 확장 블루프린트를 갖춘 재사용 가능한 엔진을 보유하는 방향으로 수렴하고 있습니다. 기술 스택 Next.js 16, Tailwind CSS v4, TypeScript Supabase (개발/운영 환경 분리), Vercel, Resend Claude API 기반 기술 자문 및 프롬프트 설계 보안 아키텍처 관리자 CRUD는 세션 기반 인증과 RLS(Row Level Security)로 보호 공개 읽기 전용 경로는 별도의 익명 클라이언트로 분리 서비스 롤 키는 애플리케이션 코드에서 완전히 배제 권한 검증은 마이그레이션 파일 및 대시보드 검토를 통해서만 진행 작업 방식 (에이전틱 IDE 협업 워크플로우) AI 에이전트 기반 IDE가 파일/버전관리 작업을 담당하고, LLM은 기술 자문 및 프롬프트 작성 역할을 수행하는 협업 구조를 운영합니다. AI 자문 → 에이전트용 프롬프트 작성 → 실행자가 붙여넣기 → 계획 확인 → 자문 검토 → 실행 승인 → 로그 검토 → 직접 클릭 테스트 → 명시적 지시 시에만 커밋 통제 규칙 실행 전 계획 검토 필수 요약 보고가 아닌 직접 클릭 테스트로 검증 개발 환경 우선 적용 후 명시적 승인 하에 운영 환경 반영 커밋은 명시적 지시가 있을 때만 수행 DB 자격증명 직접 연결 및 범위 외 삭제 금지 브랜치 전략: 개발 브랜치에서 작업 후 운영 브랜치는 배포 전용으로 직접 커밋 금지, 병합 전 변경 범위 검증 현재 진행 상황 고객사 미팅 브리핑 자동화 사례: 1단계 완료, 개발/운영 환경 배포 및 실데이터 테스트 완료 범위 변경 감지 자동화 사례: 상태 확인 진행 중 리스크 조기 경보 자동화 사례: 운영 환경 배포 완료 보안 사고 관련 포스트: 'AI 리스크 거버넌스 사례'로 재구성 예정 AI 활용 관련 콘텐츠 시리즈 초안 작성 완료, 게시 미확정 핵심 학습 원칙 보고가 아닌 검증: 버그·보안 이슈는 항상 직접 로그/브라우저 검토를 통해서만 발견되며, 에이전트의 자체 결과 요약을 그대로 신뢰하지 않음 보안 사고의 콘텐츠화: 실제 발생한 권한 관련 이슈 사례를 케이스 스터디로 재구성하여 콘텐츠화 커밋 의미 분리: 기능 단위/변경 유형별로 항상 분리 커밋 자동화하지 않는 것도 스토리: 저사용 빈도 기능을 의도적으로 고도화하지 않은 결정 자체를 AX 스토리텔링 소재로 활용