BE
[Firebase] Firebase Functions 도입기 (feat. 실패했지만 배운 것들)
| 서론
안녕하세요 팡일입니다.
오늘은 Firebase의 색다른 서비스 중 하나인 'Firebase Functions'를 도입했다가, 실패했지만 배운 점들에 대해서 공유해보려고 합니다.
| Functions란?
Firebase Functions(정식명: Cloud Functions for Firebase)는 서버 없이도 백엔드 로직을 작성할 수 있는 기능입니다.
쉽게 말해, Firebase가 제공하는 서버리스(Serverless) 환경이에요.
코드를 직접 서버에 배포하지 않아도,
Firebase 내부 이벤트(예: 회원가입, 회원탈퇴, Firestore 데이터 변경 등)에 자동으로 반응하는 코드를 작성할 수 있습니다.
예를 들어, 아래와 같은 일이 가능합니다:
사용자가 회원가입하면 users 컬렉션에 자동으로 문서 생성
회원이 탈퇴하면 Auth + Firestore 데이터 동시 삭제
Firestore에 데이터가 추가되면, 자동으로 통계 데이터 갱신
이런 “백엔드 자동화”를 가능하게 하는 게 바로 Firebase Functions입니다.
| 왜 Functions를 사용하게 되었는가
Re:Log 프로젝트에서는 사용자가 회원 탈퇴를 했을 때,
users 컬렉션뿐 아니라 그 사용자가 작성한 retrospectives 문서들도 함께 삭제되어야 했습니다.
그런데 클라이언트(Svelte) 단에서 이걸 처리하려면,
Firestore를 순회하면서 모든 문서를 찾아 삭제해야 하고
만약 중간에 네트워크 오류가 발생하면 데이터가 일부만 지워질 수도 있습니다.
그래서 보다 안전하고 일관된 서버 측 로직이 필요했죠.
그게 바로 Firebase Functions를 도입한 이유입니다.
| 사용 방법 (기본 구조)
Firebase 프로젝트 안에서 Functions를 사용하려면 루트 디렉토리에서 아래 명령어를 실행합니다:
firebase init functions이렇게 하면 functions/ 폴더가 자동 생성되고 TypeScript 환경이 세팅됩니다.
(기본 구조는 아래와 같습니다 ?)
functions/
├── src/
│ └── index.ts
├── package.json
└── tsconfig.json이후 functions/src/index.ts 파일에서 Cloud Function을 정의합니다.
import { onAuthUserDelete } from 'firebase-functions/v2/auth';
import { getFirestore } from 'firebase-admin/firestore';
import { initializeApp } from 'firebase-admin/app';
initializeApp();
const db = getFirestore();
// 회원 탈퇴 시 해당 사용자의 데이터 자동 삭제
export const onUserDelete = onAuthUserDelete(async (user) => {
const userId = user.uid;
const retros = await db.collection('retrospectives').where('userId', '==', userId).get();
const batch = db.batch();
retros.forEach((doc) => batch.delete(doc.ref));
await batch.commit();
await db.collection('users').doc(userId).delete();
});| 마주했던 트러블 슈팅 모음
1) ESLint 충돌 문제
처음 Functions를 배포하려고 했을 때,
Firebase의 기본 lint 명령어(eslint --ext .js,.ts .)가 최신 ESLint 버전과 맞지 않아 오류가 발생했습니다.
(1) 에러 메시지
Invalid option '--ext' - perhaps you meant '-c'?(2) 해결 방법
: functions/package.json에서 lint 스크립트를 수정하거나,
"lint": "eslint ."혹은 아예 firebase.json의 predeploy 항목에서 lint 단계를 제거했습니다.
2) @typescript-eslint 규칙 충돌
그 다음엔 이런 오류가 떴습니다:
Error while loading rule '@typescript-eslint/no-unused-expressions'(1) 해결 방법
Functions 전용 ESLint 구성을 완전히 제거했습니다.
즉, functions/.eslintrc.cjs 파일을 삭제하고
firebase.json에서 "predeploy": ["npm run lint"] 부분도 삭제했죠.
3) 배포 시 Cloud Build 권한 오류
Lint 문제를 모두 해결하고 나서, 드디어 firebase deploy --only functions 명령을 실행했지만... ?
Error: Your project must be on the Blaze (pay-as-you-go) plan.즉, Functions를 사용하려면 Blaze 플랜으로 업그레이드해야 한다는 뜻이었어요.
무료(Spark) 플랜에서는 Cloud Functions와 Cloud Build API를 사용할 수 없습니다.
| 결국 포기하게 된 이유

결국 Functions 배포는 포기했습니다. 이유는 단 하나!
Cloud Functions를 사용하려면 “Blaze 플랜(유료)”으로 전환해야 하기 때문이에요.
Blaze 플랜은 종량제라서 실제 사용량이 적으면 과금은 0원이지만, 카드 등록이 필수입니다.
이번 프로젝트는 개인 학습이 목적이라,
결제 정보를 등록하지 않고 클라이언트 기반 로직으로 처리하기로 했습니다.
| 해결 방법
Functions 배포는 불가능했지만, 회원 탈퇴 시 데이터 동기화 문제 자체는 다른 방식으로 해결했습니다.
Firebase Functions 대신, 클라이언트 단에서 직접 Auth + Firestore를 모두 삭제하는 로직을 구현했습니다.
단순히 deleteUser()만 호출하면 오류가 발생하기 때문에, 재인증 과정을 거친 뒤에 삭제하도록 수정했습니다.
결국 최종 코드는 다음과 같습니다:
async function handleDelete() {
const user = $currentUser;
if (!user) return;
const confirmDelete = confirm('정말 탈퇴하시겠습니까? 모든 데이터가 삭제됩니다.');
if (!confirmDelete) return;
let password: string | undefined;
// 이메일 로그인인 경우 비밀번호 요청
if (user.providerData[0]?.providerId === 'password') {
password = prompt('계정 삭제를 위해 비밀번호를 입력해주세요.') ?? undefined;
if (!password) {
alert('비밀번호가 입력되지 않았습니다.');
return;
}
}
const { success, error } = await deleteUserAccount(user, password);
if (success) {
goto('/login');
} else {
console.error(error);
alert('회원 탈퇴 중 문제가 발생했습니다.');
}
}export async function deleteUserAccount(user: any, password?: string) {
const userId = user.uid;
const providerId = user.providerData[0]?.providerId;
// 1️⃣ 재인증
if (providerId === 'password') {
const credential = EmailAuthProvider.credential(user.email, password);
await reauthenticateWithCredential(user, credential);
} else if (providerId === 'google.com') {
const googleProvider = new GoogleAuthProvider();
await reauthenticateWithPopup(user, googleProvider);
}
// 2️⃣ Auth 삭제
await deleteUser(user);
// 3️⃣ Firestore 데이터 삭제
const retrosRef = collection(db, 'retrospectives');
const q = query(retrosRef, where('userId', '==', userId));
const snapshot = await getDocs(q);
const deletePromises = snapshot.docs.map((docSnap) => deleteDoc(docSnap.ref));
await Promise.all(deletePromises);
await deleteDoc(doc(db, 'users', userId));
}이 로직은
이메일 로그인 사용자는 비밀번호를 입력받아 재인증 후 삭제,
구글 로그인 사용자는 팝업을 통한 재인증 후 삭제,
이후 users, retrospectives 컬렉션의 문서까지 모두 정리하는 구조로 작동합니다.
결과적으로 Functions 없이도,
“회원 탈퇴 시 Auth와 Firestore가 모두 깨끗하게 삭제되는 흐름”을 완성할 수 있었습니다
| 마무리하며
Firebase Functions를 실제 배포까지 해보지는 못했지만, 이번 과정에서 다음을 배웠습니다:
서버리스 구조의 개념
Firebase Auth, Firestore, Functions의 관계
Firebase CLI 배포 흐름
Lint 및 TypeScript 환경 충돌 해결 경험
결국은 실패로 끝났지만,
“프론트엔드 개발자가 백엔드 로직을 자동화할 수 있다”는 걸 체감할 수 있었던 유익한 시도였습니다.
다음엔 Blaze 플랜을 적용해서 회원 탈퇴 시 데이터 자동 삭제 기능을 완성해보고 싶네요.