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주 내) | 비타협 런타임 layer | apps/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 에 묶음 | 비타협 audit | ops 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). 디자이너 의제이지 인프라 의제 아님 |
배경
사고 전말
- ops 콘솔 dashboard 에서 모든 KPI 가 0 으로 표시. 사용자는 "테넌트 0건" 으로 인지.
- 다회차 디버깅 — PR #1132 (orderBy createdAt 누락 오진), PR #1136 (stats 서브문서 mismatch 부분 수정), 결국 PR #1157 에서 v9 호환 가드 진짜 원인 발견. 약 40분 + 3 PR 소요.
- 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 사용 중이었음)
isFirebaseReady()로 일괄 교체.app !== null && "app" in adminDb && "app" in adminAuth— v9 인스턴스는 자신을 만든 FirebaseApp 을.appstable 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.tsfallback 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-syntaxPOC — 폐기 결론. 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곳 적용) [근본 원인 해결]