이번 이슈는 로그인 유지가 가끔 끊기는 문제였다. 증상은 명확했다. 잠시 후 페이지를 이동하면 /api/bff/auth/me 401이 발생하고, 브라우저 쿠키가 사라지면서 로그아웃 상태가 되어버렸다. 그런데 DB를 보면 revoked_at은 비어 있어 세션이 살아있는 경우가 있었다. 즉, 백엔드 세션은 유효한데 프론트/BFF 쪽에서 먼저 세션을 끊는 흐름이 존재했다.처음 점검한 핵심 포인트는 BFF의 401 - refresh - retry 체인이었다. 여기서 실제로 문제가 될 수 있는 지점이 여러 개 있었다.
1) refresh 성공 응답에서 access token 추출 경로가 좁았다.
기존에는 refresh body의 accessToken/access_token만 봤다. 하지만 환경에 따라 body가 아니라 Set-Cookie로만 내려오는 경우가 있다. 이때 retry 토큰을 못 잡으면 원 요청 재시도가 실패하고, 결과적으로 사용자는 로그아웃처럼 보인다.그래서 토큰 추출을 body, body.data, Set-Cookie까지 확장했다.
2) refresh 호출 시 헤더 전달이 불안정했다.
refresh는 쿠키 기반이어야 가장 안전한데, 기존 Authorization이 따라가면 예외 케이스를 만들 수 있다.refresh 호출에서 Content-Type: application/json을 명시하고, 불필요한 incoming Authorization 전달을 차단했다.
3) refresh 실패 시 쿠키 삭제 조건이 과했다.
가장 큰 문제였다.일시적 장애(타임아웃/5xx)도 401처럼 처리되면 쿠키가 바로 삭제돼버릴 수 있다.그래서 진짜 토큰 무효일 때만 삭제하도록 바꿨다.예: refresh_missing, refresh_invalid, refresh_expired, invalid_token 같은 명확한 코드일 때만 삭제.
4) refresh 실패 상태코드를 401로 뭉개지 않도록 수정했다.
이전에는 실패를 거의 401로 반환해 프론트가 즉시 로그아웃 분기로 빠지기 쉬웠다.이제는 upstream 상태(예: 500/504)를 그대로 전달해 일시 장애 vs 인증 만료를 구분할 수 있게 했다.추가로 권한 반영 지연 이슈도 같이 정리했다.getUserRole()에서 access 쿠키 선검사를 제거하고 항상 /auth/me를 호출하게 바꿨다. 이로써 access가 비어 있어도 refresh가 트리거될 수 있다.헤더 인증 UI도 route 변경/포커스 복귀/visibility 변경 시 재동기화하게 수정했다. 로그인/로그아웃 직후엔 router.refresh()로 서버 상태를 즉시 반영하도록 했다.이번 정리의 결론은 단순했다.로그인 유지는 토큰 저장 위치 문제가 아니라, 실패 케이스 분기와 상태코드 전달, 쿠키 삭제 타이밍 같은 계약(Contract) 문제다.특히 BFF 구조에서는 refresh 실패를 섣불리 로그아웃으로 단정하면 정상 세션까지 끊어버릴 수 있다.앞으로도 원칙은 동일하다:
- refresh 성공 경로는 넓게,
- 쿠키 삭제는 엄격하게,
- 일시 장애와 인증 만료를 분리해서 처리하기.