블로그에 Firebase Google 로그인을 붙였을 때, 데스크톱에서는 멀쩡한데 모바일 사파리에서만 로그인 버튼이 먹통이 되는 현상을 만났습니다. 처음에는 모바일 브라우저 호환성 문제로 보였는데, 파고들어 보니 popup과 redirect 두 흐름의 트레이드오프와 Firebase authDomain 제약이 얽혀 있었습니다. 같은 함정을 다시 밟지 않으려고 정리해 둡니다.
popup이 먼저, redirect는 fallback#
Firebase Auth의 Google 로그인 흐름은 두 가지입니다.
signInWithPopup: 새 창을 띄워서 Google OAuth 화면을 보여주고, 사용자가 동의하면 부모 창으로 결과를 전달합니다.signInWithRedirect: 현재 페이지를 통째로 Google 도메인으로 보내고, 인증이 끝나면 원래 사이트로 돌아옵니다.
데스크톱은 popup이 압도적으로 편합니다. 사용자가 글 읽던 화면을 잃지 않고, 인증 후 바로 그 자리에서 댓글 달기를 이어갈 수 있습니다. 모바일은 다릅니다. iOS Safari나 인앱 브라우저는 popup을 막거나 다른 탭으로 보내 버려서 결과가 부모 창으로 돌아오지 않는 경우가 있습니다. 이때 Firebase는 auth/popup-blocked 또는 auth/operation-not-supported-in-this-environment 에러를 던집니다.
가장 깔끔한 처리는 popup을 먼저 시도하고, 위 두 에러일 때만 redirect로 넘어가는 패턴입니다.
try {
await signInWithPopup(auth, provider);
} catch (error) {
if (shouldUseRedirectFallback(error)) {
await signInWithRedirect(auth, provider);
return;
}
throw new Error(getFirebaseErrorMessage(error));
}
function shouldUseRedirectFallback(error: unknown) {
const code = getFirebaseErrorCode(error);
return (
code === "auth/popup-blocked" ||
code === "auth/operation-not-supported-in-this-environment"
);
}
핵심은 fallback 조건을 좁게 잡는 것입니다. 사용자가 popup을 직접 닫은 auth/popup-closed-by-user, 네트워크 실패, 권한 문제까지 redirect로 넘기면 의도하지 않은 페이지 이동이 발생합니다. popup이 환경적으로 불가능한 경우에만 redirect를 사용한다는 규칙을 코드로 굳혔습니다.
redirect fallback이 위험할 때가 있다#
여기까지는 흔한 패턴입니다. 함정은 다음 줄에 있습니다.
signInWithRedirect가 동작하려면 현재 호스트가 Firebase 콘솔의 authDomain과 같은 도메인이거나, 로컬 개발 환경(localhost, 127.x)이어야 안전합니다. custom domain을 쓰는 정적 사이트라면 보통 lifeisonecoin.com에서 운영되는데 authDomain은 your-project.firebaseapp.com처럼 다른 도메인일 가능성이 높습니다. 이 상태에서 redirect를 쓰면 OAuth 흐름이 다른 도메인으로 갔다가 돌아오면서 세션이 끊기거나 unauthorized domain 에러로 떨어집니다.
그래서 fallback 조건에 가드를 한 단계 더 두었습니다.
export function canUseFirebaseRedirectFallback(options: {
authDomain?: string | null;
currentHost?: string | null;
currentHostname?: string | null;
}) {
const authDomainHost = normalizeHost(options.authDomain);
const currentHost = normalizeHost(options.currentHost);
const currentHostname = normalizeHostname(options.currentHostname);
if (!currentHost) return false;
if (isLoopbackHostname(currentHostname)) return true;
return Boolean(authDomainHost) && authDomainHost === currentHost;
}
브라우저에서는 window.location.host를 넘겨서 같은 함수를 호출합니다. 이 가드가 false면 redirect로 넘어가는 대신, 사용자에게 popup 차단을 풀어달라는 명시적인 메시지를 띄웁니다. 무리해서 redirect를 시도하기보다, "이 환경에서는 popup이 필요합니다"라고 알려주는 편이 사용자 경험이 낫다고 판단했습니다.
if (shouldUseRedirectFallback(error)) {
if (!canUseFirebaseRedirectFallbackInBrowser()) {
throw new Error(getRedirectFallbackUnavailableMessage());
}
await signInWithRedirect(auth, provider);
return;
}
authorized domains와 popup의 cross-origin 동작#
Firebase Authentication 콘솔에는 authorized domains 목록이 있습니다. 이 목록에 운영 도메인과 Cloudflare Pages preview 도메인까지 모두 들어가 있어야 popup도 redirect도 정상 동작합니다. 한 줄이 누락되면 로그인 자체는 되는데 getRedirectResult()에서 빈 결과가 돌아오거나, popup 창에서 동의를 마쳤는데 부모 창으로 결과가 전파되지 않는 식의 미묘한 실패가 생깁니다.
저는 다음 도메인을 모두 등록했습니다.
- 운영 custom domain (
lifeisonecoin.com) - Firebase 기본
authDomain(*.firebaseapp.com) - Cloudflare Pages preview 도메인 (
*.pages.dev) - 로컬 개발용
localhost
Cloudflare Pages 정적 배포 함정 글에서 환경변수가 빌드 시점에 박힌다는 점을 다뤘는데, authorized domains도 같은 결의 운영 작업입니다. 코드 쪽이 아무리 정확해도 콘솔 한 줄이 빠지면 결과물이 망가집니다.
사용자 피드백 메시지를 sessionStorage로 넘기기#
Redirect 흐름에서 까다로운 점은 페이지가 한 번 통째로 떠난다는 사실입니다. popup 흐름에서는 try/catch로 잡은 에러를 그 자리에서 토스트로 보여주면 끝나지만, redirect는 다른 도메인을 거쳐 돌아오기 때문에 "방금 일어난 일"의 컨텍스트가 사라집니다.
이 문제는 sessionStorage로 우회했습니다.
const authFeedbackStorageKey = "lifeisonecoin.firebaseAuthFeedback";
export function persistFirebaseAuthFeedback(message: string) {
window.sessionStorage.setItem(authFeedbackStorageKey, message);
}
export function consumeFirebaseAuthFeedback() {
const message = window.sessionStorage.getItem(authFeedbackStorageKey);
if (!message) return null;
window.sessionStorage.removeItem(authFeedbackStorageKey);
return message;
}
Redirect 직전에 의도한 메시지를 저장해 두고, 돌아온 후 첫 렌더에서 한 번만 읽고 비웁니다. 이 한 줄짜리 패턴이 redirect 후의 사용자 컨텍스트 단절을 메워 줍니다.
정적 export 사이트라는 제약#
이 블로그는 Next.js 정적 export로 빌드된 사이트입니다. 서버 세션, middleware auth, route handler auth 같은 도구를 쓸 수 없습니다. 모든 인증 상태는 클라이언트 Firebase SDK가 들고 있고, 관리자 권한은 NEXT_PUBLIC_OWNER_EMAIL과 로그인 계정 이메일 비교로 결정합니다.
이 제약이 popup-first 결정에 더 무게를 실어 줬습니다. 정적 사이트는 redirect 후 돌아왔을 때 서버에서 세션을 다시 만들어 줄 수 없고, 모든 복원이 클라이언트 SDK의 onAuthStateChanged 콜백에 달려 있습니다. popup이 가능한 환경에서는 popup이 단순히 더 빠르고 안정적입니다.
정리하면#
Firebase Google 로그인을 모바일에서 안정화하려면 결국 세 줄짜리 규칙으로 압축됩니다.
- popup을 먼저 시도하고, 환경적 실패에 한해서만 redirect로 넘어간다
- redirect는 현재 host가
authDomain과 같거나 localhost일 때만 허용한다 - redirect를 쓰지 못하는 상황에서는 명시적인 메시지를 사용자에게 보여준다
코드 쪽 분기보다 더 중요한 건 Firebase 콘솔의 authorized domains 목록입니다. 이게 어긋나면 위 코드가 아무리 정확해도 모바일 인증이 무너집니다. 블로그를 처음 만들 때 운영 인프라가 코드만큼 중요하다는 걸 배웠는데, Firebase Auth는 그 교훈이 가장 또렷하게 드러나는 영역이었습니다.