OAuth 로그인 버튼이 뒤로가기 후 동작하지 않는 문제

경험을 토대로 AI를 활용하여 작성한 글 입니다.

OAuth 로그인 버튼이 뒤로가기 후 동작하지 않는 문제 (dev 모드 한정)

Supabase OAuth 로그인 화면에서 로그인 버튼 → OAuth 페이지 이동 → 뒤로가기로 돌아오면 버튼이 동작하지 않던 문제. 결론부터 말하면 개발 모드(next dev)에서만 재현되는 현상이었고, 프로덕션에서는 bfcache가 정상 작동하여 아무 문제가 없었다. 즉 실제 사용자 환경의 버그가 아니라 dev 환경 특유의 착시였다.


1. 문제 현상

구글 / 카카오 OAuth 로그인 화면에서 다음 흐름을 거치면 로그인 버튼이 동작하지 않았다.

  1. 로그인 버튼 클릭
  2. 구글/카카오 OAuth 동의 페이지로 이동
  3. 뒤로가기로 로그인 화면 복귀
  4. 로그인 버튼을 다시 눌러도 아무 반응 없음 (onClick 자체가 호출되지 않음)

문제가 났던 원본 코드는 아래와 같이 loading 상태나 disabled 처리가 전혀 없는 단순한 형태였다.

'use client'
 
import { createClient } from '@/lib/supabase/client'
 
export function GoogleLoginButton() {
  const handleGoogleLogin = async () => {
    const supabase = createClient()
    await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: { redirectTo: `${window.location.origin}/auth/callback` },
    })
  }
 
  return (
    <button type="button" onClick={handleGoogleLogin} /* ... */>
      Google로 계속하기
    </button>
  )
}

버튼을 비활성화할 상태 자체가 없으므로, "상태가 잘못 박제되어 막혔다" 같은 설명은 처음부터 성립하지 않는다. 클릭하면 매번 signInWithOAuth를 호출할 뿐이다.


2. 핵심 단서 — 환경에 따라 결과가 갈린다

디버깅에서 가장 중요했던 관측은 개발 모드와 프로덕션의 동작이 정반대라는 점이었다.

환경bfcache 동작뒤로가기 후 버튼
dev 모드 (next dev)HMR websocket이 차단 → bfcache 진입 불가동작 안 함
프로덕션 (next build && start)정상 작동정상 동작

이 표가 인과의 방향을 결정한다. bfcache가 작동하는 프로덕션에서는 버튼이 멀쩡하고, bfcache가 막힌 dev에서만 버튼이 고장났다. 즉 bfcache는 문제의 원인이 아니라 오히려 정상 동작에 필요한 메커니즘이었다.


3. 원인 분석 과정

3-1. pageshow 이벤트가 잡히지 않는다

bfcache 복원을 감지하는 표준 방법은 pageshow 이벤트의 e.persisted 플래그다. 이를 확인하는 가드 훅을 넣어 디버깅했다.

function useBfcacheGuard() {
  console.log('useBfcacheGuard')
  useEffect(() => {
    const onPageShow = (e: PageTransitionEvent) => {
      console.log('onPageShow', e.persisted)
      if (e.persisted) window.location.reload()
    }
    window.addEventListener('pageshow', onPageShow)
    return () => window.removeEventListener('pageshow', onPageShow)
  }, [])
}

확인 결과 단서들이 서로 모순되어 보였다.

확인 항목결과
최초 접근 시 useBfcacheGuard 로그찍힘 (훅은 정상 호출)
'use client' 지시어있음
뒤로가기 시 onPageShow 로그안 찍힘
뒤로가기 시 useBfcacheGuard 재실행안 됨
cleanup 제거 후 재시도여전히 onPageShow 안 찍힘
콘솔에 RAW pageshow 리스너 직접 등록 후 뒤로가기안 찍힘

pageshow가 어떤 방식으로도 잡히지 않아 한동안 원인을 잘못 짚었다.

3-2. 결정적 단서 — DevTools Back/forward cache 진단

크롬 DevTools → Application 탭 → Back/forward cache → Test back/forward cache 를 실행하자 원인이 드러났다.

Pages with WebSocket cannot enter back/forward cache.

이 페이지는 처음부터 bfcache에 진입조차 못 하고 있었다. 그래서 pageshow(persisted: true)가 영원히 발생하지 않았고, 디버깅 단서가 전부 모순되어 보였던 것이다.


4. 진짜 원인

개발 모드(next dev)의 HMR(Hot Module Replacement) websocket 연결이 bfcache를 차단하고, 그 결과 dev 환경에서만 뒤로가기 복원이 정상적으로 이루어지지 않았다.

흐름을 정리하면:

  • 열려있는 websocket 연결이 있으면 브라우저는 해당 페이지를 bfcache에 저장하지 않는다. (네트워크 탭의 webpack-hmr ... websocket 101이 그 연결)
  • dev 모드에서는 bfcache를 못 쓰므로, 외부 OAuth 도메인에서 뒤로가기로 돌아올 때 페이지가 bfcache 스냅샷으로 복원되지 못한다.
  • 이때 dev 환경 특유의 복원 경로에서 버튼 DOM은 다시 그려지지만 React 이벤트 핸들러가 깨끗하게 재연결되지 않아 onClick이 죽은 상태가 된다.
  • 반면 프로덕션에서는 bfcache가 정상 작동하여 DOM + JS 상태 + 이벤트 핸들러를 통째로 얼렸다가 그대로 복원한다. 핸들러가 살아있는 채로 복원되므로 버튼이 멀쩡하다.

참고: dev 모드에서 이벤트 핸들러가 재연결되지 않는 정확한 내부 메커니즘은 Next.js 버전, Turbopack 사용 여부 등 환경에 따라 다를 수 있다. 여기서는 "dev에서만 재현되는 현상"으로 보는 것이 정확하며, 단일 코드 한 줄을 원인으로 단정하기는 어렵다.

그동안 본 모든 모순된 단서는 "dev 환경에서는 HMR websocket 때문에 bfcache가 비활성"이라는 한 가지 사실로 전부 설명된다.

관측된 현상실제 의미
onPageShow 안 찍힘dev 모드는 websocket 때문에 bfcache 진입 불가 → 복원 이벤트 자체가 없음
뒤로가기 시 가드 훅 재실행 안 됨full load도 bfcache 복원도 아닌 dev 환경 특유의 복원 경로
RAW 리스너도 안 잡힘pageshow 이벤트 자체가 발생하지 않음
프로덕션에서는 정상HMR이 없어 bfcache가 정상 작동, 핸들러까지 복원됨

5. 해결 방안

핵심: 프로덕션 환경에서 검증할 것

이 현상은 dev 모드 전용이다. next build && next start 또는 실제 배포 환경에서는 HMR websocket이 없어 bfcache가 정상 작동하고, 버튼도 정상 동작한다. 따라서 별도의 코드 수정 없이도 실제 사용자에게는 문제가 없다.

useBfcacheGuard는 사실상 불필요할 가능성

useBfcacheGuard(bfcache 복원 시 reload())는 dev 모드에서는 bfcache 자체가 안 되니 동작할 기회가 없고, 프로덕션에서는 bfcache가 이미 핸들러까지 정상 복원하므로 굳이 새로고침할 필요가 없다. 즉 양쪽 환경 모두에서 이 훅이 실질적인 역할을 하지 않을 가능성이 높다.

확인 방법: 프로덕션 빌드에서 useBfcacheGuard를 제거하고도 뒤로가기 후 버튼이 정상 동작하는지 테스트한다. 정상이라면 이 훅은 제거해도 무방하다.

방어가 꼭 필요하다면

프로덕션에서도 만일을 대비해 방어 코드를 두고 싶다면, bfcache 복원 시 페이지를 강제로 새로 그리는 가드를 유지할 수 있다. 단, 이 경우 사용자가 OAuth를 취소하고 돌아왔을 때 화면 깜빡임이 생긴다는 점을 감수해야 한다.

'use client'
 
import { useEffect } from 'react'
 
function useBfcacheGuard() {
  useEffect(() => {
    const onPageShow = (e: PageTransitionEvent) => {
      if (e.persisted) window.location.reload()
    }
    window.addEventListener('pageshow', onPageShow)
    return () => window.removeEventListener('pageshow', onPageShow)
  }, [])
}

6. 결과

  • 문제는 개발 환경(next dev)에서만 재현되던 현상이었고, 원본 코드 로직 자체는 멀쩡했다.
  • 프로덕션 빌드에서는 bfcache가 정상 작동하여 로그인 버튼이 의도대로 동작함을 확인했다.
  • 원인은 dev 모드의 HMR websocket이 bfcache 진입을 막아, 뒤로가기 복원 시 이벤트 핸들러가 재연결되지 않았던 것.
  • 실제 배포 환경에는 영향이 없으므로 필수 수정 사항은 아니다. useBfcacheGuard는 검증 후 제거를 검토할 수 있다.

7. 핵심 요약

  • 증상: OAuth 페이지에서 뒤로가기로 돌아오면 로그인 버튼의 onClick이 동작하지 않음. (원본 코드에는 loading/disabled 같은 상태가 전혀 없었음)
  • 환경 차이: dev 모드에서는 고장, 프로덕션에서는 정상. → bfcache가 있어야 정상 동작하는 구조.
  • 실제 원인: 개발 모드의 HMR websocket이 bfcache 진입을 차단 → dev에서만 뒤로가기 복원 시 React 이벤트 핸들러가 재연결되지 않아 버튼이 죽음. 프로덕션은 bfcache가 핸들러까지 통째로 복원하므로 정상.
  • 검증 방법: DevTools → Application → Back/forward cache → Test 로 차단 사유 확인 (Pages with WebSocket cannot enter back/forward cache).
  • 해결: 별도 수정 불필요. dev 전용 현상이며 프로덕션에서 정상. useBfcacheGuard는 실효성이 없을 가능성이 높아 제거 검토 가능.
  • 교훈:
    • bfcache 관련 동작은 반드시 프로덕션 빌드에서 검증할 것. dev 모드의 HMR websocket이 bfcache를 막아 디버깅을 오도한다.
    • disk cache(HTTP 캐시)와 bfcache(메모리 스냅샷)는 별개다. 네트워크 탭의 disk cache 표기는 bfcache 복원의 증거가 아니다.
    • 단서가 서로 모순될 땐 추측 대신 DevTools의 전용 진단 도구(Application → Back/forward cache)로 직접 측정하는 것이 가장 빠르다.
    • 증상을 일으킨 실제 코드를 먼저 확인하고 진단할 것. 코드를 보지 않고 세운 가설(loading 박제 등)은 틀리기 쉽다.