[트러블슈팅] 자리 비운 뒤 관리자 권한이 사라지는 문제, BFF 인증 흐름으로 해결한 방법

#401#AccessToken#BFF#REFRESH#서버권한조회로직

로그인 상태로 잠시 자리를 비운 뒤 돌아와 페이지를 이동하면,헤더에서 My 버튼이 사라지거나 관리자 화면이 보기 전용으로 보이고, 새로고침해야 정상으로 돌아오는 이슈가 있었다.이번 글은 이 문제를 어떻게 진단했고, 어떤 코드로 해결했는지 정리한 내용이다.

문제 증상

  • 로그인 상태였는데 페이지 이동 시 권한 UI가 비로그인처럼 보임

  • 관리자 페이지가 수정 불가(보기 전용)로 렌더링됨

  • 새로고침 후에야 관리자 권한이 다시 반영됨

원인

핵심은 서버 권한 조회 로직이었다.

  • 기존에는 accessToken 쿠키가 없으면 /auth/me 호출 자체를 생략

  • 그래서 BFF의 401 - refresh - retry가 트리거되지 않음

  • 결과적으로 권한이 null로 판정되어 UI가 비정상 상태로 렌더링됨

또한 헤더 인증 상태는 최초 마운트 시 1회만 확인하고 있어서,탭 복귀/라우트 이동 시 최신 세션 상태와 동기화가 늦었다.

해결 전략

  1. 서버 권한 조회는 항상 /auth/me 호출

(accessToken 선검사 제거)

  1. 헤더 인증 상태 재동기화

  • 라우트 변경 시

  • 탭 포커스 복귀 시

  • visibilitychange가 visible이 될 때

  1. 로그인/로그아웃 직후 router.refresh()로 서버 상태 즉시 반영


핵심 코드 1) 서버 권한 조회 선검사 제거

export async function getUserRole(): Promisestring  null {

const res = await GET{ role?: string; user?: { role?: string }; data?: { role?: string } }("/auth/me", {

cache: "no-store",

});

if (!res.ok) return null;

return res.data.role ?? res.data.user?.role ?? res.data.data?.role ?? null;

}


export async function isAdmin(): Promiseboolean {

return (await getUserRole()) === "admin";

}

accessToken이 없더라도 /auth/me를 호출해 BFF가 refresh를 시도할 수 있게 만든 것이 포인트다.


핵심 코드 2) 헤더 인증 상태 재동기화

  useEffect(() = {
    let cancelled = false;

    const syncAuthStatus = async () = {
      try {
        const res = await getCurrentUser();
        if (!cancelled) setAuthStatus(!!res?.data);
      } catch {
        if (!cancelled) setAuthStatus(false);
      }
    };

    const onVisibilityChange = () = {
      if (document.visibilityState === "visible") {
        void syncAuthStatus();
      }
    };

    void syncAuthStatus();
    window.addEventListener("focus", syncAuthStatus);
    document.addEventListener("visibilitychange", onVisibilityChange);

    return () = {
      cancelled = true;
      window.removeEventListener("focus", syncAuthStatus);
      document.removeEventListener("visibilitychange", onVisibilityChange);
    };
  }, [pathname]);



경로 변경 + 포커스 복귀 + visible 전환 시점마다 인증 상태를 다시 맞춘다.


핵심 코드 3) 로그인 직후 서버 상태 강제 반영

        showToast({
          message: response.message ?? '로그인에 성공했습니다.',
          variant: 'success',
          duration: 2500,
        });
        router.replace('/');
        router.refresh();
        return;



로그인 후 바로 새 서버 상태(쿠키/권한)를 반영한다.


핵심 코드 4) 로그아웃 직후 서버 상태 강제 반영

    try {
      await logout();
      showToast({ message: '로그아웃 되었습니다.', variant: 'success' });
      router.replace('/login');
      router.refresh();
    } catch {
      router.replace('/login');
      router.refresh();
    } finally {

로그아웃 이후에도 헤더/권한 UI가 즉시 일관되도록 처리했다.


결과

  • 자리 비운 뒤 복귀해도 권한 반영 지연이 크게 줄어듦

  • 관리자 버튼/보기 전용 오판 빈도 감소

  • 새로고침해야 정상화되는 UX 제거

정리

이번 이슈는 토큰 저장 위치보다 권한 판별 흐름 문제였다.

  • 권한 체크 전에 refresh 경로를 막지 말 것

  • 헤더 같은 전역 UI는 이벤트 기반 재동기화할 것

  • 로그인/로그아웃 직후에는 router.refresh()로 서버 상태를 즉시 동기화할 것

BFF 인증에서는 결국 토큰보다 흐름 계약(Contract)이 더 중요했다.






21

댓글