다음과 같은 접근 정책이 있다고 하자.

제휴 랜딩: 로그인 필요 → 로그인 화면
로그인 화면: 세션 존재 → 원래 랜딩
제휴 랜딩: 소속 미확정 → 다시 로그인 화면

세션은 생겼지만 소속은 아직 대기 중이다. 로그인 완료가 랜딩 조건을 충족하지 못해 두 화면이 반복된다. 로그인 화면이 진입할 때마다 외부 인증을 호출하면 화면 이동이 요청을 증폭시킨다.

이 글의 질문은 외부 429를 어떻게 없앨지가 아니라 어떤 상태 전이가 인증 작업을 다시 시작하는가다. 429와 루프의 실제 인과는 호출 기록을 연결해 확인해야 한다.

로그인과 소속 준비를 다른 상태로 둔다

ANONYMOUS, AUTHENTICATED, AFFILIATION_PENDING, READY, ERROR를 구분하고 각 상태의 목적지를 정한다. 소속이 대기 중이면 이미 성공한 로그인으로 보내는 대신 현재 인증 시도의 대기 화면을 유지할 수 있다.

클라이언트와 서버가 다른 쿠키·세션 상태를 읽는지도 확인한다. 여러 화면에 비슷한 guard를 복사하는 방식은 정책 차이를 만들기 쉽다. 무엇을 기준으로 접근을 허용하는지 공유한다.

랜딩과 로그인의 조건이 달라 화면 복귀마다 외부 인증 요청을 반복하는 순서

그림은 호출 증폭이 생길 수 있는 경로다. 실제 조사에서는 각 이동이 외부 요청을 시작했는지 요청 ID와 시각으로 확인한다.

복귀 URL이 인증 경로를 다시 가리키지 않게 한다

복귀 경로를 문자열로 계속 감싸면 로그인 URL 자체가 목적지로 남을 수 있다. 파싱 후 허용된 origin과 경로를 검사한다.

function safeReturnPath(raw: string | null): string {
  const base = new URL("https://service.example");
  if (!raw) return "/";
  try {
    const target = new URL(raw, base);
    if (target.origin !== base.origin) return "/";
    if (target.pathname === "/auth" || target.pathname.startsWith("/auth/"))
      return "/";
    return target.pathname + target.search;
  } catch {
    return "/";
  }
}

외부 origin과 인증 경로 자신을 배제하는 최소 예시다. 실제 서비스의 허용 경로와 query 계약을 더 제한한다. 동일 origin이라고 해서 목적지 권한까지 허용되는 것은 아니므로 도착한 서버에서 인가를 다시 검사한다. OWASP 리다이렉트 가이드를 함께 확인한다.

오류 재시도와 화면 이동을 연결하지 않는다

외부 오류를 잡을 때 무조건 로그인으로 이동하면 그 화면이 같은 호출을 다시 만들 수 있다. 진행 중인 요청 중복을 줄이고 재진입 시 서버의 시도 상태를 확인한다. 탭 하나의 boolean은 새로고침·다른 탭을 넘어 중복을 막지 못한다.

429에 Retry-After가 있으면 재시도 정책에 반영한다. RFC 6585는 요청 제한의 응답과 재시도 안내를 설명한다. 상한·대기·지터를 적용해도 인증을 성공으로 간주해 통과시키지는 않는다. timeout 후 재호출의 안전성은 제공자의 멱등성 계약에 달려 있다.

루프 중단 장치는 접근 정책을 대체하지 않는다. 이동 횟수가 많으면 대기·오류 상태로 멈추되 원래 목적을 무조건 잃게 하는 홈 이동만으로 끝내지 않는다.

한 인증 흐름의 이동과 호출을 연결한다

흐름 ID로 랜딩→로그인→랜딩의 반복, 시도 상태, 외부 호출을 나란히 놓는다. 토큰이나 세션 원문을 기록할 필요는 없다. 전체 호출 수뿐 아니라 시도 하나에서 몇 번 작업이 시작됐는지 본다.

검증에는 세션은 있으나 소속이 없는 사용자, 오래된 쿠키, 복귀 URL, 외부 429·timeout을 포함한다. 허용 이동·호출 횟수는 제품 흐름별로 정하고 상한을 넘어 테스트가 끝없이 기다리지 않게 한다. 다음 판단은 제한값을 늘리기 전에 이동할 때 재시작되는 작업과 충족되지 않은 접근 조건을 찾는 데 둔다.