본문으로 건너뛰기

ADR 0029 — Firebase 초기화 fail-loud 정책 (v9 silent-failure 사고 회고)

  • Status: Accepted — 2026-04-29 5인 회의 (P1·P2·P3·P4·P5) tier B, R1 → R3.
  • Date: 2026-04-29
  • Decision Drivers: 2026-04-29 ops 콘솔 KPI 가 모두 0 으로 표시된 사고. 12 곳에서 typeof adminDb.collection !== "function" v8 호환 가드를 사용했으나, Firebase v9 modular SDK 의 Firestore 인스턴스에는 .collection 메서드가 존재하지 않아 정상 인스턴스조차 가드 통과 못 함 → 모든 Firestore 호출이 silent skip → 빈 화면. fallback cast ({} as Firestore) 가 환경변수 미설정 시 빈 객체를 노출해 가드 동기를 키웠다. 코드 분석 다회차에서 발견 못 함. PR #1157 에서 isFirebaseReady() ("app" in adminDb) 도입으로 12 곳 수정.
  • Anti-Goal Compliance: ADR 0001 Anti-Goal 4종 무관 — 인프라 안전선 정책.

결정 (요약)

Firebase 초기화 실패는 fail-loud (런타임 throw + visible 에러) 가 비타협 원칙이다. silent skip 패턴은 코드·런타임·테스트 3 layer 에서 차단한다. UX 측 두 번째 안전망(빈 상태 vs 에러 상태 구분) 은 별도 안건으로 분리하되 동일 origin 사고로 묶어 트래킹한다.

채택된 의제 (C-1·C-5·C-2·C-6 부분 + audit 범위 확장)

의제결정Tier비고
C-1: fallback cast {} as Firestore 폐기즉시 (1 PR, 1주 내)비타협 런타임 layerapps/ops/lib/firebase/client.ts · apps/web/lib/firebase/admin.ts 등 모든 init 진입점. 환경변수 미설정 시 throw new Error("Firebase 미초기화: NEXT_PUBLIC_FIREBASE_*"). fallback 빈 객체 export 금지. isFirebaseReady() 는 init 성공 여부만 확인하는 단일 SSoT 로 유지 ("app" in instance)
C-5: 초기화 경로 unit test 강제즉시 (C-1 PR 에 묶음)비타협 회귀 방지apps/ops·apps/web 양쪽 __tests__/firebase-init.test.ts. fallback 빈 객체 vs v9 정상 인스턴스 구분 회귀 검증. isFirebaseReady() truthy/falsy 분기 양쪽
C-2: v8 호환 API 의존 가드 ESLint 차단POC 후 폐기 (2026-04-29)정책 문서화로 대체grep audit 결과 apps/ops scenarios server actions (ADR 0028) 가 firebase-admin SDK 의 adminDb.collection(...).doc(...) 메서드 체이닝을 30+ 곳에서 정상 사용. admin SDK 는 client modular SDK 와 달리 namespaced API + 메서드 호출이 정식 패턴. AST 레벨에서 client v8 메서드 호출과 admin SDK 메서드 호출을 안정적으로 구분 불가 → 거짓 양성률 압도. 결론: ESLint 룰 도입 폐기. CLAUDE.md "Firebase 초기화 fail-loud" 정책 + isFirebaseReady() SSoT + 회귀 테스트 stack 으로 충분 (P5 R1 우려 적중)
C-6: LLM 코드 hedging 정책 문서화C-1 PR 에 묶음 (CLAUDE.md 한 단락)정책 layer"그럴듯한 방어 가드 / 환경변수 미설정 시 fallback / typeof 검사" 류 hedging 패턴은 silent failure 의 산실. 새 가드 추가 시 (1) 가드가 fallback 자체를 막지 않는다면 폐기, (2) 가드 통과 후 호출이 실제 동작하는지 emulator 로 1회 검증 — 둘 다 자동화 불가, 코드 작성자 의무
audit 범위 확장: web (변호사·사무장·의뢰인) 화면C-1 PR 에 묶음비타협 auditops 12곳은 PR #1157 에서 수정됐으나 web (apps/web) 의 동일 v8 호환 가드 패턴을 grep audit. 동일 origin 사고 가능성 — write 경로(mutation) 도 audit 범위에 포함 (P2 양보 불가)

보류된 의제

의제결정근거
C-3: 가드 추가 시 runtime 검증 의무화 (자동화)보류 — CLAUDE.md 정책 한 단락으로 대체1인 개발자 + 자율 모드 환경에서 휴먼 리뷰 게이트 자동화 불가. C-1 폐기 후 가드 패턴 자체가 거의 사라지면 over-spec. C-1+C-2+C-5 stack 으로 silent skip 의 물리적 차단이 우선. 단, P1·P2 Minority 영역으로 기록 — recurrence 발생 시 즉시 격상
C-4: 개발 시작 시 emulator 강제 진입반대사고 원인은 production credentials 미설정이지 emulator 미사용이 아님. emulator 강제는 production Firestore 와의 split-brain 위험 (memory project_adr0028_extraction_session 의 에뮬레이터 split-brain 사고 전례). isEmulatorEnabled() 는 옵션 유지
C-7~C-10 (UX 빈/실패 상태 구분 디자인 시스템)별도 후속 안건으로 분리P4 디자이너 + P1·P2 가 독립 제안. 코어 안건은 "초기화 fail-loud" 로 좁히고, UX 측 두 번째 안전망은 4-state · 카피 SSoT · SystemStatusBadge · 에러 토스트 hook 4종을 묶어 별도 회의 (/meeting 빈 상태 vs 실패 상태 UI 패턴 통일 tier=B). 디자이너 의제이지 인프라 의제 아님

배경

사고 전말

  1. ops 콘솔 dashboard 에서 모든 KPI 가 0 으로 표시. 사용자는 "테넌트 0건" 으로 인지.
  2. 다회차 디버깅 — PR #1132 (orderBy createdAt 누락 오진), PR #1136 (stats 서브문서 mismatch 부분 수정), 결국 PR #1157 에서 v9 호환 가드 진짜 원인 발견. 약 40분 + 3 PR 소요.
  3. 12 곳 동일 패턴 발견:
    • apps/ops/app/(ops)/dashboard/_lib/use-dashboard-data.ts (2곳)
    • apps/ops/app/(ops)/tenants/page.tsx (1곳)
    • apps/ops/app/(ops)/tenants/[tid]/page.tsx (2곳)
    • apps/ops/app/(ops)/public-rag/page.tsx (1곳)
    • apps/ops/lib/realtime/use-firestore-snapshot.ts (3곳)
    • apps/ops/lib/auth.ts (3곳)
    • apps/ops/components/providers/AuthProvider.tsx (1곳, AuthProvider 만 isFirebaseReady 사용 중이었음)
  4. isFirebaseReady() 로 일괄 교체. app !== null && "app" in adminDb && "app" in adminAuth — v9 인스턴스는 자신을 만든 FirebaseApp 을 .app stable API 로 노출.

왜 v9 에서 가드가 항상 true 였나

  • v8 호환 API: firebase.firestore().collection("x") — 인스턴스에 .collection 메서드 존재
  • v9 modular API: collection(db, "x") — 인스턴스는 단순 핸들, .collection 메서드 없음
  • 가드 의도: "fallback 빈 객체 vs 정상 인스턴스 구분"
  • 실제: v9 정상 인스턴스도 .collection 메서드 없음 → 모든 인스턴스 가드 통과 못 함

왜 다회차 코드 분석에서 못 찾았나 (메타 분석)

  • 그럴듯함 휴리스틱 — typeof !== "function" 가 권위적 코드처럼 보였음
  • 12곳 일관 적용 = 합의된 규약처럼 보였음
  • 정적 분석에만 의존, 실제 호출·로그 추적 안 함
  • fallback cast ({} as Firestore) 가 가드의 정당성을 추가 지지
  • LLM 생성 hedging 패턴 — 환경변수 미설정 시 안전한 척, 실제로는 silent fail 마스킹

대안 검토

대안채택 여부근거
6개 의제 모두 즉시 도입거부over-spec. 1인 개발자 환경에서 ROI 음수 (P3·P5 합의). C-3 휴먼 리뷰 자동화 불가
isFirebaseReady() 도입만 하고 정책 안 만듦거부새 Server Action 이 또 자체 가드 짜면 재발. 정책 + lint stack 필요 (P3 양보 불가)
C-2 lint 단독 (cast 유지)거부cast 가 살아있으면 lint 잡을 v8 패턴 자체가 안 생김 — silent fail 우회 (P5 양보 불가)
C-7~C-10 UX 의제 코어 ADR 에 포함거부인프라 안건과 디자인 시스템 안건 혼합 시 결정 강도 약화. 별도 회의로 분리해 디자이너 P4 주도하되 동일 origin 사고로 트래킹 (P3 PM 결정)
C-4 emulator 강제 진입거부사고 원인은 prod credentials 미설정. emulator split-brain 위험 (P5 양보 불가). 옵션 유지

후속 과제

즉시 (1주, 1 PR)

  • apps/ops/lib/firebase/client.ts fallback cast ({} as ...) 폐기 → init 실패 시 throw new Error("Firebase 미초기화"). 단 dev 환경 (env 미설정) 에서 메시지로 환경변수 가이드.
  • apps/web/lib/firebase/admin.ts · apps/web/lib/firebase/client.ts 동일 패턴 점검 + 동일 정책 적용.
  • apps/web 코드 grep audit — typeof adminDb.collection !== "function" 패턴 또는 변형. 발견 시 isFirebaseReady() 로 교체. write 경로(mutation) 우선.
  • apps/ops/__tests__/firebase-init.test.ts + apps/web/__tests__/firebase-init.test.ts 추가. fallback 빈 객체 vs v9 정상 인스턴스 회귀 검증.
  • CLAUDE.md "Firebase 초기화 fail-loud" 섹션 추가 (C-6 정책 문서화). hedging 패턴 금지 명시.

POC 결과 (2026-04-29 즉시 audit)

  • ESLint no-restricted-syntax POC — 폐기 결론. ops scenarios (ADR 0028) 의 firebase-admin SDK 메서드 체이닝 30+ 건과 client v8 호환 메서드를 AST 레벨에서 구분 불가. 거짓 양성률 압도. CLAUDE.md 정책 + isFirebaseReady() SSoT 로 대체 (PR #1158 follow-up).

별도 회의 (P4 주도)

  • /meeting 빈 상태 vs 실패 상태 UI 패턴 통일 tier=B — C-7 4-state · C-8 카피 SSoT · C-9 SystemStatusBadge · C-10 에러 토스트 hook 4종 묶음.

모니터링 (1주)

  • C-1 ship 후 production 에서 Firebase 초기화 throw 가 실제 발화하는지 Sentry/로그 1주 모니터링. ESLint 통과 ≠ runtime 안전 (P3 지적).

성공 지표

  • v9 호환 가드 패턴 grep 결과 0건 (apps/ops + apps/web).
  • isFirebaseReady() 가 단일 SSoT — 다른 변형 init 검사 0건.
  • Firebase 초기화 실패 시 콘솔 + UI 에 visible 에러 발화 (silent skip 0건).
  • Recurrence 0 — 6개월 내 동일 origin silent failure 사고 0건.

Minority Report

P1 변호사 + P2 사무장 — "C-3 fail-loud UX 즉시 시행"

"런타임 throw 와 ESLint 는 개발자 보호 layer 일 뿐이다. 변호사·사무장은 콘솔 에러를 안 본다. fail-loud 가 사용자 화면 (에러 토스트·배너) 까지 도달하지 않으면 silent failure 의 본질은 그대로 남는다. C-7~C-10 을 별도 회의로 분리한 결정은 실용적이지만, 그 회의가 미뤄지면 변호사 화면이 또 silent 0 을 띄울 수 있다. 별도 회의를 2주 데드라인으로 확약하고 ADR 에 명시할 것을 요구한다."

중재자 응답: 정당한 우려. 후속 과제 "별도 회의 (P4 주도)" 항목에 2026-05-13 까지 회의 개시 데드라인 추가. recurrence 발생 시 C-3 자동화 즉시 격상.

P4 디자이너 — "UX 의제 코어 격상"

"이 사고의 두 번째 안전망 = 화면이 사용자에게 '이상하다' 고 말하는 것. 그게 부재한 상태로 코드 layer 만 강화하면 다음 사고 양상은 바뀌지만 silent 0 은 또 일어난다. C-7~C-10 은 동일 origin 사고 대응이지 별개 안건이 아니다."

중재자 응답: 인정. ADR 0029 와 후속 회의 ADR (예정) 을 동일 origin 사고 대응 페어로 트래킹. 후속 회의에서 4-state 디자인 시스템 결정 시 ADR 0029 cross-link.

후속 안건

  • /meeting 빈 상태 vs 실패 상태 UI 패턴 통일 tier=B — 2026-05-13 데드라인. P4 주도, P1·P2 자문.
  • /meeting Firebase 초기화 stale 회복 정책 tier=C — 앱 재시작 없이 재초기화 가능한가, 토큰 만료 후 회복 흐름은? (recurrence 시).

관련 PR

  • PR #1132 — KPI=0 1차 오진 (orderBy createdAt 누락) [실패한 진단]
  • PR #1136 — KPI=0 2차 부분 수정 (stats 서브문서 mismatch) [부분 성공]
  • PR #1157 — KPI=0 진짜 원인 수정 (isFirebaseReady() 12곳 적용) [근본 원인 해결]