Firebase Auth Context 설계: 로그인 권한과 라우팅 관리하기

··설명 약 6분·작성: 해비·블로그 소개

Firebase Auth Context에서 인증 확인 상태와 계정 전환의 늦은 응답은 어떻게 다루나요?

Auth Context는 인증 상태를 화면에 공유하는 장치이며 서버 권한을 대신하지 않습니다. 인증 확인 중·로그인·비로그인 상태를 구분하고 계정이 바뀔 때 이전 사용자의 늦은 응답이 새 상태를 덮지 못하게 하세요.

적용 조건·범위: 사용자가 바꿀 수 있는 프로필 role을 근거로 서버 권한을 허용하면 안 됩니다. isAdmin은 claim 기반 UI 안내일 뿐이며 관리자 API는 ID token 검증 뒤 claim을 다시 검사하고 Firestore·Storage는 Rules를 적용합니다. banned 상태를 화면에서만 체크하는 것도 실제 차단이 아닙니다. 서버·Rules의 정책과 연결해야 합니다.

본문 근거: firebase.google.com/docs/auth/admin/custom-claims · firebase.google.com/docs/auth/web/manage-users · react.dev/reference/react/useEffect

학습 목표와 사전 지식

목표: Auth Context의 로딩·계정 변경·권한 표시 상태를 설계하고 라우팅과 권한 집행을 구분합니다.
사전 지식: React Context/useEffect, Next.js App Router, Firebase 초기화

AuthProvider는 인증을 구독해 화면에 공유합니다. 서버 접근 권한까지 보장하는 장치는 아닙니다. 인증과 claim 읽기가 끝나기 전에 보호 화면을 렌더링하지 않고, 이전 계정의 비동기 응답이 새 계정 상태를 덮어쓰지 않게 해야 합니다.

상단의 계정 전환 사고 실습은 Firebase 계정 없이 읽을 수 있습니다. 전체 Context를 실행할 때 필요한 auth 인스턴스는 Firebase 초기화 구조: Next.js firebase.ts 설계 기준에서, 로그인·로그아웃 호출은 Firebase Authentication 사용 기준: 로그인 유지와 비밀번호 재설정에서 준비합니다. 이전 TaskFlow 화면을 완성한 상태를 요구하지 않습니다.

먼저 풀어 볼 문제: 계정 전환 중 늦게 도착한 응답

독자 질문: A 계정의 관리자 claim 조회가 늦는 동안 로그아웃하고 B 계정으로 바뀌면 어떤 상태를 보여야 할까요?

  1. 초기: user=null, loading=true, isAdmin=false. 첫 인증 결과가 아직 없으므로 보호 화면을 보여주지 않습니다.
  2. A 로그인: user=A, loading=true, isAdmin=false. A의 claim을 조회합니다.
  3. A 로그아웃: user=null, loading=false, isAdmin=false. 앞선 A 요청은 무효가 됩니다.
  4. 뒤늦게 A의 관리자 claim이 도착: A의 결과를 버립니다. 화면의 isAdmin이 true로 되돌아가면 오류입니다.
  5. B 로그인: B의 token 결과만 현재 상태에 반영합니다.

확인 질문: A 응답이 4단계에서 도착해도 왜 상태를 바꾸면 안 될까요? 아래 코드에서는 요청마다 generation을 증가시키고 응답 시 현재 번호와 비교합니다. 이 연습은 Firebase 연결 없이 상태 전이 원리를 이해하기 위한 것입니다. 실제 권한 집행은 서버의 ID token 검사와 Rules가 맡습니다.

전체 Context 모듈

Next.js 기존 프로젝트의 src/context/AuthProvider.tsx에 둡니다. src/shared/libs/firebase/firebase.ts는 초기화 글의 모듈을 사용합니다. 로그인·가입·로그아웃 동작은 Authentication 글의 유틸을 호출하고, Context는 현재 인증 상태와 권한 표시만 맡깁니다. 별도 프로필 쿼리 라이브러리는 이 최소 예제에 필요하지 않습니다.

src/context/AuthProvider.tsx · 전체 모듈; 기존 React/Next.js 프로젝트에 적용

"use client";
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
import { onIdTokenChanged, type User } from 'firebase/auth';
import { auth } from '../shared/libs/firebase/firebase';

type State = { user: User | null; loading: boolean; isAdmin: boolean; error: string | null };
const AuthContext = createContext<State | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [state, setState] = useState<State>({ user: null, loading: true, isAdmin: false, error: null });
  useEffect(() => {
    let generation = 0;
    const unsubscribe = onIdTokenChanged(auth, async (user) => {
      const current = ++generation;
      setState({ user, loading: !!user, isAdmin: false, error: null });
      if (!user) return;
      try {
        const token = await user.getIdTokenResult();
        if (current !== generation) return;
        setState({ user, loading: false, isAdmin: token.claims.admin === true, error: null });
      } catch {
        if (current !== generation) return;
        setState({ user, loading: false, isAdmin: false, error: '권한 정보를 확인하지 못했습니다.' });
      }
    });
    return () => { generation += 1; unsubscribe(); };
  }, []);
  return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const value = useContext(AuthContext);
  if (!value) throw new Error('AuthProvider 내부에서 사용하세요.');
  return value;
}
Firebase Auth 상태와 React Context 연결 흐름

왜 요청 세대 번호를 비교하는가

A 계정의 claim 조회가 느린 동안 로그아웃하거나 B로 로그인할 수 있습니다. generation을 바꾸고 완료 시 동일한 번호일 때만 상태를 반영하면 이전 요청 결과를 버립니다. 로그아웃 시 isAdmin을 즉시 false로 만들고 구독 해제 때도 진행 중 요청을 무효화합니다. 이 처리는 네트워크 요청을 취소하는 것과 다릅니다.

라우팅은 보호 화면 안에서

루트 layout의 children을 AuthProvider로 감싸고 보호 화면에서 loading이면 로딩 UI를, !user면 로그인 안내 또는 router.replace("/auth/login")를 실행합니다. 리다이렉트는 effect에서 처리하고 결과가 정해지기 전에는 보호 데이터를 표시하지 않습니다. pathname.includes("/mypage")처럼 우연히 포함된 문자열까지 보호하지 말고 경로 세그먼트를 명시합니다. Context 전체에 라우팅 부작용을 넣으면 공개 페이지까지 얽히기 쉽습니다.

로그인 유지와 보호 라우팅 점검 기준

role 필드와 claim을 혼동하지 않기

사용자가 바꿀 수 있는 프로필 role을 근거로 서버 권한을 허용하면 안 됩니다. isAdmin은 claim 기반 UI 안내일 뿐이며 관리자 API는 ID token 검증 뒤 claim을 다시 검사하고 Firestore·Storage는 Rules를 적용합니다. banned 상태를 화면에서만 체크하는 것도 실제 차단이 아닙니다. 서버·Rules의 정책과 연결해야 합니다.

확인 순서

새로고침 시 첫 인증 결과 전 loading이 유지되는지 확인합니다. A→로그아웃→B 전환 중 네트워크를 느리게 만들어 A의 권한이 되살아나지 않는지 봅니다. claim 조회 실패 시 관리자 화면을 숨기고 에러를 표시해야 합니다. UI isAdmin을 임의 변경해도 실제 관리자 요청이 거부되는지 개발 서버에서 따로 검사합니다.

직접 확인할 결과

타입/비동기 세대 처리 정적 검토
Next.js 빌드·브라우저 계정 전환 미실행

검증 범위와 기준일

공식 문서 확인일: 2026-09-13. 웹 SDK 예제는 모듈형 API 기준입니다. 이 글의 코드·구조 정적 검토와 실제 클라우드 인증·저장·배포 검증을 구분합니다. 본인 개발 Firebase 프로젝트의 실제 인증·권한·배포 요청은 이 편집 환경에서 실행하지 않았습니다.

이어서 학습하기

공식 문서

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 20개 · 필수 14개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기