dev.syw

에뮬레이터 스위트, 통합 테스트, 로깅과 트레이싱으로 Firebase 앱을 안정적으로 검증하고 디버깅한다.

테스트, 디버깅, 로컬 개발 환경

Firebase 앱을 실제 프로젝트에 적용하다 보면 "코드가 맞는 것 같은데 왜 프로덕션에서 오류가 날까?" 하는 상황을 자주 마주칩니다. 실제 Firebase 리소스에 직접 접근해 개발하면 데이터 오염, 과금 발생, 재현 불가능한 버그라는 세 가지 함정이 기다립니다. Firebase Local Emulator Suite는 이 문제를 해결하기 위한 공식 도구로, Firestore·Auth·Cloud Functions·Storage 등 주요 서비스를 로컬 머신에서 완전히 재현합니다.

입문편에서 보안 규칙과 Cloud Functions 작성 방법을 익혔다면, 이번 레슨에서는 그 규칙과 함수들을 실제로 어떻게 검증하고, 팀 전체가 일관된 상태에서 개발하며, CI 파이프라인에서 자동으로 테스트하는지 살펴봅니다. 개발 생산성과 품질을 동시에 높이는 워크플로 전체를 다룹니다.

학습 목표

  • Firebase Local Emulator Suite로 Firestore, Auth, Functions, Storage를 통합 구성하는 방법을 이해한다.
  • 시드 데이터와 import/export를 활용해 재현 가능한 테스트 상태를 관리한다.
  • Jest + @firebase/rules-unit-testing 대신 통합 테스트 관점에서 에뮬레이터를 활용한다.
  • GitHub Actions 등 CI 환경에서 에뮬레이터 기반 자동 테스트를 실행한다.
  • 구조화 로그, 에러 리포팅, 느린 쿼리·규칙 거부·트리거 실패 등의 디버깅 기법을 실무에 적용한다.

Emulator Suite 통합 환경 구성

설치와 초기화

Firebase CLI가 설치되어 있다면 firebase init emulators 명령 한 번으로 에뮬레이터를 설정할 수 있습니다. 프로젝트 루트의 firebase.json에 에뮬레이터 구성이 추가됩니다.

# Firebase CLI 최신 버전 확인
firebase --version

# 에뮬레이터 초기화 (대화형 선택)
firebase init emulators

# 개별 에뮬레이터 직접 지정
firebase init emulators --only firestore,auth,functions,storage

firebase.json 예시:

{
  "emulators": {
    "auth": { "port": 9099 },
    "firestore": { "port": 8080 },
    "functions": { "port": 5001 },
    "storage": { "port": 9199 },
    "ui": { "enabled": true, "port": 4000 },
    "singleProjectMode": true
  }
}

💡 TIP singleProjectMode: true로 설정하면 실제 프로젝트 ID가 없어도 에뮬레이터 간 통신이 가능합니다. 팀원 모두가 동일한 가상 프로젝트 ID(demo-project)로 개발할 수 있습니다.

앱에서 에뮬레이터 연결

클라이언트 SDK와 Admin SDK 모두 에뮬레이터 연결을 지원합니다. 환경 변수로 분기하면 프로덕션 코드를 건드리지 않고 안전하게 전환할 수 있습니다. 단, 클라이언트 SDK는 브라우저 번들로 빌드되므로 process.env 대신 번들러가 정적으로 치환하는 빌드 타임 변수(Vite의 import.meta.env)로 분기해야 합니다.

// src/firebase.ts
import { initializeApp } from "firebase/app";
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore";
import { getAuth, connectAuthEmulator } from "firebase/auth";
import { getFunctions, connectFunctionsEmulator } from "firebase/functions";
import { getStorage, connectStorageEmulator } from "firebase/storage";

const app = initializeApp({
  projectId: "demo-project",
  apiKey: "fake-api-key",
  authDomain: "demo-project.firebaseapp.com",
});

const db = getFirestore(app);
const auth = getAuth(app);
const functions = getFunctions(app);
const storage = getStorage(app);

// ✅ 환경 변수로 에뮬레이터 여부 분기 (클라이언트 SDK이므로 import.meta.env 사용)
if (import.meta.env.VITE_USE_EMULATOR === "true") {
  connectFirestoreEmulator(db, "localhost", 8080);
  connectAuthEmulator(auth, "http://localhost:9099");
  connectFunctionsEmulator(functions, "localhost", 5001);
  connectStorageEmulator(storage, "localhost", 9199);
}

export { db, auth, functions, storage };

Admin SDK(Functions 내부 또는 테스트 코드)에서는 환경 변수만 설정하면 됩니다.

// Admin SDK 에뮬레이터 연결 (테스트 파일 또는 functions/src/admin.ts)
process.env.FIRESTORE_EMULATOR_HOST = "localhost:8080";
process.env.FIREBASE_AUTH_EMULATOR_HOST = "localhost:9099";
process.env.FIREBASE_STORAGE_EMULATOR_HOST = "localhost:9199";

import * as admin from "firebase-admin";

admin.initializeApp({ projectId: "demo-project" });
const adminDb = admin.firestore();

⚠️ 주의 connectAuthEmulator에는 프로토콜(http://)을 포함한 전체 URL을 전달해야 합니다. 다른 에뮬레이터 연결 함수와 형식이 다르므로 주의하세요.

시드 데이터와 import/export로 재현 가능한 상태 만들기

에뮬레이터의 가장 강력한 기능 중 하나는 데이터 상태를 파일로 저장하고 언제든 복원할 수 있다는 점입니다. 팀 전체가 동일한 초기 상태에서 테스트를 시작할 수 있어 "내 환경에서는 됐는데" 문제를 방지합니다.

데이터 export와 import

# 에뮬레이터 실행 중에 현재 상태를 파일로 저장
firebase emulators:export ./emulator-seed

# 저장된 시드 데이터를 불러와 에뮬레이터 시작
firebase emulators:start --import=./emulator-seed

# 종료 시 자동 저장 (개발 중 상태 유지)
firebase emulators:start --import=./emulator-seed --export-on-exit=./emulator-seed

emulator-seed/ 디렉터리는 Git에 커밋하여 팀 전체가 공유합니다. .gitignore에 추가하지 않도록 주의하세요.

프로그래밍 방식 시드 스크립트

복잡한 초기 데이터나 테스트 직전에 동적으로 상태를 구성해야 할 때는 스크립트로 처리합니다.

// scripts/seed.ts
import * as admin from "firebase-admin";

process.env.FIRESTORE_EMULATOR_HOST = "localhost:8080";
process.env.FIREBASE_AUTH_EMULATOR_HOST = "localhost:9099";

admin.initializeApp({ projectId: "demo-project" });

async function seed() {
  const db = admin.firestore();
  const auth = admin.auth();

  // 테스트용 유저 생성
  const user = await auth.createUser({
    uid: "test-user-001",
    email: "tester@example.com",
    displayName: "테스트 유저",
  });

  // 배치 쓰기로 초기 데이터 삽입
  const batch = db.batch();

  const productsData = [
    { name: "상품 A", price: 10000, stock: 50 },
    { name: "상품 B", price: 25000, stock: 20 },
    { name: "상품 C", price: 5000, stock: 100 },
  ];

  for (const product of productsData) {
    const ref = db.collection("products").doc();
    batch.set(ref, { ...product, createdAt: admin.firestore.FieldValue.serverTimestamp() });
  }

  // 특정 uid로 사용자 프로필 생성
  batch.set(db.collection("users").doc(user.uid), {
    displayName: user.displayName,
    role: "tester",
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
  });

  await batch.commit();
  console.log("시드 완료:", user.uid);
}

seed().catch(console.error);
# 에뮬레이터 실행 후 시드 적용
npx ts-node scripts/seed.ts

에뮬레이터 기반 통합 테스트

테스트 환경 설계 원칙

보안 규칙 단위 테스트는 3강에서 다뤘습니다. 여기서는 클라이언트 SDK와 Functions가 함께 동작하는 통합 테스트에 집중합니다. 통합 테스트는 실제 사용 시나리오를 처음부터 끝까지(end-to-end) 검증합니다.

// tests/integration/order.test.ts

// ⚠️ initializeTestEnvironment는 클라이언트 SDK 기반 RulesTestContext만 제공할 뿐
//    firebase-admin SDK를 초기화하지 않습니다. Admin SDK를 사전 데이터 세팅·검증에
//    쓰려면 아래처럼 환경 변수를 import/initializeApp 이전에 설정하고 직접 초기화해야 합니다.
process.env.FIRESTORE_EMULATOR_HOST = "localhost:8080";

import { initializeTestEnvironment, RulesTestEnvironment } from "@firebase/rules-unit-testing";
import * as admin from "firebase-admin";
import { readFileSync } from "fs";

let testEnv: RulesTestEnvironment;

beforeAll(async () => {
  // ✅ Admin SDK를 에뮬레이터에 연결되도록 명시적으로 초기화
  //    (FIRESTORE_EMULATOR_HOST가 설정돼 있어야 프로덕션이 아닌 에뮬레이터로 연결됨)
  if (admin.apps.length === 0) {
    admin.initializeApp({ projectId: "demo-project" });
  }

  // ✅ 에뮬레이터에 연결된 테스트 환경 생성
  testEnv = await initializeTestEnvironment({
    projectId: "demo-project",
    firestore: {
      host: "localhost",
      port: 8080,
      rules: readFileSync("firestore.rules", "utf8"),
    },
  });
});

afterEach(async () => {
  // 각 테스트 후 데이터 초기화 (테스트 격리)
  await testEnv.clearFirestore();
});

afterAll(async () => {
  await testEnv.cleanup();
});

describe("주문 생성 시나리오", () => {
  it("인증된 사용자는 자신의 주문을 생성할 수 있다", async () => {
    const uid = "user-abc";
    const userCtx = testEnv.authenticatedContext(uid, { email: "user@test.com" });
    const db = userCtx.firestore();

    // Admin SDK로 사전 조건 데이터 세팅
    // (beforeAll에서 initializeApp + FIRESTORE_EMULATOR_HOST를 설정했으므로 에뮬레이터에 연결됨)
    const adminDb = admin.firestore();
    await adminDb.collection("products").doc("prod-1").set({ name: "상품 A", price: 10000, stock: 10 });

    // 클라이언트로 주문 생성 시도
    const orderRef = db.collection("orders").doc();
    await expect(
      orderRef.set({ userId: uid, productId: "prod-1", quantity: 2, status: "pending" })
    ).resolves.toBeUndefined();

    // Admin SDK로 결과 검증
    const snap = await adminDb.collection("orders").doc(orderRef.id).get();
    expect(snap.data()?.status).toBe("pending");
  });

  it("비인증 사용자는 주문을 생성할 수 없다", async () => {
    const anonCtx = testEnv.unauthenticatedContext();
    const db = anonCtx.firestore();

    await expect(
      db.collection("orders").doc().set({ userId: "hacker", productId: "prod-1", quantity: 1, status: "pending" })
    ).rejects.toThrow();
  });
});

Functions 트리거 통합 테스트

Functions가 Firestore 이벤트에 반응하는 시나리오를 테스트할 때는 트리거가 완료될 때까지 잠시 대기해야 합니다.

// tests/integration/functions.test.ts
import * as admin from "firebase-admin";

// 에뮬레이터 환경 변수 설정 (jest.setup.ts에서 처리하는 것이 더 깔끔함)
process.env.FIRESTORE_EMULATOR_HOST = "localhost:8080";

admin.initializeApp({ projectId: "demo-project" });
const db = admin.firestore();

function waitForTrigger(ms = 2000): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

it("주문 생성 시 Functions가 재고를 차감해야 한다", async () => {
  // 초기 재고 설정
  await db.collection("products").doc("prod-2").set({ name: "상품 B", stock: 10 });

  // 주문 생성 (onWrite 트리거 발동)
  await db.collection("orders").doc("order-001").set({
    productId: "prod-2",
    quantity: 3,
    status: "confirmed",
  });

  // ✅ 트리거 처리 대기
  await waitForTrigger();

  // 재고 차감 검증
  const productSnap = await db.collection("products").doc("prod-2").get();
  expect(productSnap.data()?.stock).toBe(7);
});

⚠️ 주의 setTimeout으로 Functions 완료를 기다리는 방법은 CI 환경에서 불안정할 수 있습니다. 결과 문서를 폴링하거나, Pub/Sub 또는 커스텀 이벤트로 완료 신호를 명시적으로 전달하는 방식이 더 안정적입니다.

CI 파이프라인에서 에뮬레이터 자동 테스트

GitHub Actions 워크플로 구성

에뮬레이터는 헤드리스 환경에서도 완전히 동작하므로 CI에서 실제 Firebase 리소스 없이 모든 테스트를 실행할 수 있습니다.

# .github/workflows/test.yml
name: Firebase Emulator Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Node.js 설정
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Java 설치 (에뮬레이터 필수 의존성)
        uses: actions/setup-java@v4
        with:
          distribution: "temurin"
          java-version: "17"

      - name: 의존성 설치
        run: npm ci

      - name: Firebase CLI 설치
        run: npm install -g firebase-tools

      - name: 에뮬레이터 캐시 복원
        uses: actions/cache@v4
        with:
          path: ~/.cache/firebase/emulators
          key: firebase-emulators-${{ hashFiles('firebase.json') }}

      - name: 에뮬레이터와 함께 테스트 실행
        run: firebase emulators:exec --import=./emulator-seed "npm test"
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
          # ❌ GOOGLE_APPLICATION_CREDENTIALS 불필요 — 에뮬레이터는 인증 없이 동작

firebase emulators:exec 명령은 에뮬레이터를 시작하고, 지정한 명령을 실행한 후, 에뮬레이터를 자동으로 종료합니다. CI에서 가장 권장되는 패턴입니다.

package.json 테스트 스크립트 구성

{
  "scripts": {
    "test": "jest --runInBand --forceExit",
    "test:emulator": "firebase emulators:exec --import=./emulator-seed \"npm test\"",
    "test:watch": "firebase emulators:start --import=./emulator-seed & jest --watch"
  },
  "jest": {
    "testEnvironment": "node",
    "globalSetup": "./tests/setup/globalSetup.ts",
    "globalTeardown": "./tests/setup/globalTeardown.ts"
  }
}
// tests/setup/globalSetup.ts
export default async function globalSetup() {
  // 에뮬레이터가 이미 실행 중인지 확인 (exec 모드에서는 불필요)
  process.env.FIRESTORE_EMULATOR_HOST = "localhost:8080";
  process.env.FIREBASE_AUTH_EMULATOR_HOST = "localhost:9099";
  process.env.FIREBASE_STORAGE_EMULATOR_HOST = "localhost:9199";
}

Functions 로깅, 구조화 로그, 에러 리포팅

구조화 로그 작성

Cloud Functions에서 console.log로도 로그를 남길 수 있지만, 구조화 로그를 사용하면 Cloud Logging에서 필터링, 집계, 알림 설정이 훨씬 쉬워집니다.

// functions/src/logger.ts
import { logger } from "firebase-functions/v2";

export async function processOrder(orderId: string, userId: string) {
  // ✅ 구조화 로그 — 필드를 두 번째 인수 객체로 전달
  logger.info("주문 처리 시작", {
    orderId,
    userId,
    timestamp: new Date().toISOString(),
  });

  try {
    // 처리 로직...
    logger.info("주문 처리 완료", { orderId, status: "success" });
  } catch (error) {
    // ✅ 에러도 구조화 로그로 기록
    logger.error("주문 처리 실패", {
      orderId,
      userId,
      error: error instanceof Error ? error.message : String(error),
      stack: error instanceof Error ? error.stack : undefined,
    });
    throw error; // 상위로 재던짐
  }
}
// functions/src/index.ts
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { logger } from "firebase-functions/v2";
import { HttpsError, onCall } from "firebase-functions/v2/https";

// ❌ 나쁜 예 — 구조화되지 않은 문자열 로그
export const badOrderHandler = onDocumentCreated("orders/{orderId}", (event) => {
  console.log("주문 생성됨: " + event.params.orderId); // 검색 어려움
});

// ✅ 좋은 예 — 구조화 로그
export const goodOrderHandler = onDocumentCreated("orders/{orderId}", async (event) => {
  const { orderId } = event.params;
  const data = event.data?.data();

  logger.info("주문 생성 이벤트", {
    orderId,
    userId: data?.userId,
    productId: data?.productId,
  });
});

Firebase Crashlytics와 에러 리포팅

클라이언트 앱의 미처리 예외는 Crashlytics로 수집합니다. Functions 레벨의 에러는 Cloud Error Reporting과 연동됩니다.

// functions/src/errorReporting.ts
import { onCall, HttpsError } from "firebase-functions/v2/https";
import { logger } from "firebase-functions/v2";

export const riskyOperation = onCall(async (request) => {
  const { uid } = request.auth ?? {};

  if (!uid) {
    // ✅ HttpsError를 사용해야 클라이언트에 안전한 메시지만 전달됨
    throw new HttpsError("unauthenticated", "로그인이 필요합니다.");
  }

  try {
    // 위험한 외부 API 호출 등
    const result = await externalApiCall();
    return { success: true, data: result };
  } catch (error) {
    // ✅ 내부 에러를 구조화 로그로 상세히 기록
    logger.error("외부 API 호출 실패", {
      uid,
      error: error instanceof Error ? error.message : "알 수 없는 오류",
      retryable: true,
    });

    // ✅ 클라이언트에는 범용 메시지만 노출
    throw new HttpsError("internal", "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
  }
});

async function externalApiCall(): Promise<unknown> {
  // 구현...
  return {};
}

에뮬레이터에서 로그 확인

에뮬레이터 UI(http://localhost:4000)의 Logs 탭에서 구조화 로그를 실시간으로 확인할 수 있습니다. 터미널에서 직접 확인하려면 아래처럼 활용합니다.

# 에뮬레이터 로그를 파일로 저장
firebase emulators:start --import=./emulator-seed 2>&1 | tee emulator.log

# 특정 함수 로그만 필터링
grep "주문 처리" emulator.log

느린 쿼리·규칙 거부·트리거 실패 디버깅

Firestore 느린 쿼리 디버깅

에뮬레이터는 누락된 인덱스를 즉시 감지하고 터미널에 경고를 출력합니다. 프로덕션에서 인덱스 없이 쿼리를 날리면 에러가 발생하지만, 에뮬레이터에서는 경고와 함께 처리되므로 배포 전에 반드시 확인해야 합니다.

// 에뮬레이터 실행 중 이런 경고가 출력됩니다:
// ⚠ cloud.firestore: Unindexed query. Consider adding an index for 'status' + 'createdAt'

// ❌ 복합 인덱스 없이 다중 필드 정렬
const slowQuery = db
  .collection("orders")
  .where("status", "==", "pending")
  .orderBy("createdAt", "desc"); // 인덱스 필요

// ✅ firestore.indexes.json에 인덱스 추가
// firestore.indexes.json
{
  "indexes": [
    {
      "collectionGroup": "orders",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "status", "order": "ASCENDING" },
        { "fieldPath": "createdAt", "order": "DESCENDING" }
      ]
    }
  ]
}

보안 규칙 거부 디버깅

규칙이 거부했을 때 클라이언트는 막연히 "Permission denied"만 받습니다. 에뮬레이터 UI의 Firestore 탭 → Request 항목에서 어느 규칙 조건이 false가 됐는지 단계별로 확인할 수 있습니다.

// firestore.rules 디버깅에 유용한 패턴
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /orders/{orderId} {
      allow read: if isOwner(orderId);
      // ✅ 에뮬레이터 UI에서 isOwner 함수 평가 결과를 단계별로 확인 가능
    }

    function isOwner(orderId) {
      return request.auth != null
        && get(/databases/$(database)/documents/orders/$(orderId)).data.userId
           == request.auth.uid;
    }
  }
}

터미널에서 규칙 거부 로그를 직접 보는 방법:

# 에뮬레이터 실행 후 규칙 거부 로그만 필터링
firebase emulators:start --import=./emulator-seed 2>&1 | grep -i "denied\|permission\|rule"

Functions 트리거 실패 디버깅

트리거가 실행됐는데 기대한 부작용이 없을 때의 디버깅 체크리스트:

// functions/src/index.ts — 트리거 실패 시 확인 포인트

export const onOrderCreated = onDocumentCreated("orders/{orderId}", async (event) => {
  // 1. 이벤트 수신 확인
  logger.info("트리거 수신", { orderId: event.params.orderId });

  const data = event.data?.data();

  // ✅ 2. data가 null일 수 있음 (문서 삭제 이벤트와 구별)
  if (!data) {
    logger.warn("이벤트 데이터 없음 — 건너뜀", { orderId: event.params.orderId });
    return;
  }

  // ✅ 3. 멱등성 보장 — 트리거가 중복 실행될 수 있음
  const processed = data.processed === true;
  if (processed) {
    logger.info("이미 처리된 주문 — 건너뜀", { orderId: event.params.orderId });
    return;
  }

  // 실제 처리 로직
  const db = admin.firestore();
  await db.runTransaction(async (tx) => {
    const orderRef = db.collection("orders").doc(event.params.orderId);
    const orderSnap = await tx.get(orderRef);

    if (!orderSnap.exists || orderSnap.data()?.processed) return;

    // ✅ 4. 트랜잭션 내 처리 완료 마킹
    tx.update(orderRef, { processed: true, processedAt: admin.firestore.FieldValue.serverTimestamp() });
  });
});
증상원인 가능성확인 방법
트리거가 아예 실행 안 됨함수 배포 안 됨, 경로 패턴 불일치에뮬레이터 UI Functions 탭 확인
트리거 실행 후 DB 변경 없음조건 분기로 조기 반환, 트랜잭션 충돌구조화 로그에 처리 단계 추적
중복 실행Firestore 트리거 at-least-once 보장멱등성 처리(processed 플래그)
외부 API 타임아웃Functions 기본 제한 60초timeoutSeconds 설정 증가

프로덕션과 분리된 안전한 로컬 개발 워크플로

환경 분리 전략

# .env.local (클라이언트)
VITE_USE_EMULATOR=true
VITE_FIREBASE_PROJECT_ID=demo-project

# .env.production (클라이언트)
VITE_USE_EMULATOR=false
VITE_FIREBASE_PROJECT_ID=my-real-project-id
// src/firebase.ts — 환경 변수 기반 분기
const useEmulator = import.meta.env.VITE_USE_EMULATOR === "true";

if (useEmulator) {
  connectFirestoreEmulator(db, "localhost", 8080);
  connectAuthEmulator(auth, "http://localhost:9099");
  // ...
  console.info("[DEV] Firebase 에뮬레이터에 연결되었습니다.");
}

에뮬레이터 사용 시 주의사항

항목에뮬레이터프로덕션
데이터 지속성재시작 시 초기화 (import 없을 때)영구 저장
보안 규칙 적용동일하게 적용됨동일
Cloud Messaging (FCM)미지원지원
Extensions일부 미지원전체 지원
Remote Config미지원지원
과금없음적용됨

💡 TIP 에뮬레이터 UI(http://localhost:4000)에서 Firestore 데이터를 직접 편집하고, Auth 사용자를 추가하거나 삭제할 수 있습니다. 개발 중 빠른 상태 조작에 매우 유용합니다.

팀 개발 워크플로 체크리스트

# 신규 팀원 온보딩 절차
git clone <repo>
npm install
npm install -g firebase-tools

# 에뮬레이터 바이너리 다운로드 (최초 1회)
firebase setup:emulators:firestore
firebase setup:emulators:ui

# 시드 데이터와 함께 에뮬레이터 시작
firebase emulators:start --import=./emulator-seed

# 새 터미널에서 앱 실행
npm run dev

요약

  • Firebase Local Emulator Suite는 Firestore, Auth, Functions, Storage를 로컬에서 완전히 재현하며, singleProjectMode로 실제 프로젝트 없이도 통합 환경을 구성할 수 있다.
  • **emulators:export / --import**로 시드 데이터를 버전 관리하면 팀 전체가 동일한 초기 상태를 공유하는 재현 가능한 테스트 환경을 만들 수 있다.
  • **firebase emulators:exec**를 CI 파이프라인에 통합하면 실제 Firebase 리소스와 과금 없이 모든 테스트를 자동화할 수 있다.
  • logger.info/error구조화 로그는 Cloud Logging에서 검색·필터링·알림 설정을 쉽게 하고, HttpsError는 내부 에러를 안전하게 클라이언트에 전달한다.
  • 에뮬레이터 UI와 터미널 로그를 통해 규칙 거부 단계, 누락 인덱스 경고, 트리거 실행 흐름을 직접 추적할 수 있다.
  • Functions 트리거는 at-least-once 실행을 보장하므로 멱등성 처리가 필수이며, 환경 변수 기반 분기로 에뮬레이터와 프로덕션을 안전하게 격리해야 한다.

연습문제

  1. 로컬 에뮬레이터 환경에서 Firestore, Auth, Functions, Storage가 모두 동작하는 firebase.json 설정과 클라이언트 SDK 연결 코드를 작성하세요. demo-project를 프로젝트 ID로 사용하고, 환경 변수로 에뮬레이터 연결 여부를 분기하세요.

힌트 connectFirestoreEmulator, connectAuthEmulator 등 각 SDK의 connect 함수 시그니처에서 Auth만 URL 형식이 다름을 기억하세요.

  1. 다음 요구사항을 만족하는 시드 스크립트를 작성하세요. (1) uid: seed-admin인 관리자 사용자를 Auth에 생성한다. (2) users/seed-admin 문서에 { role: "admin" }을 설정한다. (3) products 컬렉션에 3개의 상품 문서를 배치 쓰기로 삽입한다.

힌트 Admin SDK의 auth.createUserfirestore.batch()를 조합하세요. 에뮬레이터 환경 변수를 admin.initializeApp() 이전에 설정해야 합니다.

  1. GitHub Actions 워크플로에서 firebase emulators:exec를 사용해 npm test를 실행하는 YAML을 작성하세요. Java 설치, 에뮬레이터 바이너리 캐시, 시드 데이터 ./emulator-seed 로드를 포함해야 합니다.

힌트 actions/setup-java@v4actions/cache@v4를 활용하세요. 에뮬레이터는 JVM 기반이므로 Java가 반드시 필요합니다.

  1. onDocumentCreated 트리거가 중복 실행되는 문제를 방지하기 위해 멱등성을 보장하는 코드 패턴을 작성하세요. processed 플래그를 Firestore 트랜잭션으로 원자적으로 확인하고 설정하는 방식으로 구현하세요.

힌트 db.runTransaction 내부에서 문서를 읽고(tx.get), processed 필드가 이미 true이면 즉시 반환하는 guard 조건을 추가하세요.

💡 연습문제 풀이

불러오는 중…

함께 보면 좋은 자료

댓글 0

Firebase 심화” 강좌에 대한 댓글입니다.

댓글을 작성하려면 로그인이 필요합니다.