Firebase 12 업그레이드 가이드

오준석·2025년 7월 26일
0

코딩삽질방지

목록 보기
58/61

본 문서는 AlimiPro Angular 프로젝트에서 Firebase JavaScript SDK를 버전 11에서 12로 업그레이드하는 과정과 주요 breaking change 해결 방법을 기록합니다.

📋 목차

업그레이드 개요

업그레이드 버전

  • Firebase JavaScript SDK: 11.7.112.0.0
  • @firebase/rules-unit-testing: 4.0.15.0.0
  • 업그레이드 일자: 2025년 7월 26일
  • 관련 PR: #18

업그레이드 이유

  1. 보안 개선: Firebase 12의 보안 패치 및 개선사항 적용
  2. 성능 향상: 최신 Firebase SDK의 성능 최적화 혜택
  3. 장기 지원: Firebase 11의 지원 종료에 대비
  4. 등원 기능 오류 해결: 기존 Timestamp 관련 오류 완전 해결

주요 Breaking Changes

🔥 Timestamp 객체 사용 제한

문제: Firebase 12에서 setDoc(), addDoc(), updateDoc() 작업 시 커스텀 Timestamp 객체 사용 금지

// ❌ Firebase 12에서 오류 발생
const timestamp = Timestamp.now();
const customTimestamp = Timestamp.fromDate(new Date());

await setDoc(docRef, {
  createdAt: timestamp,        // 오류: "Unsupported field value: a custom Timestamp object"
  updatedAt: customTimestamp   // 오류: "Unsupported field value: a custom Timestamp object"
});

오류 메시지:

FirebaseError: Unsupported field value: a custom Timestamp object

✅ 해결 방법

1. 서버 타임스탬프 사용

import { serverTimestamp } from '@angular/fire/firestore';

// ✅ 서버에서 타임스탬프를 생성하는 경우
await setDoc(docRef, {
  createdAt: serverTimestamp(),  // Firebase 서버에서 생성
  updatedAt: serverTimestamp()   // Firebase 서버에서 생성
});

2. JavaScript Date 객체 직접 사용

// ✅ 클라이언트에서 타임스탬프를 생성하는 경우
await setDoc(docRef, {
  createdAt: new Date(),    // JavaScript Date 객체 직접 사용
  updatedAt: new Date()     // JavaScript Date 객체 직접 사용
});

패키지 업데이트

package.json 변경사항

{
  "dependencies": {
    "firebase": "^12.0.0"  // 이전: "^11.7.1"
  },
  "devDependencies": {
    "@firebase/rules-unit-testing": "^5.0.0"  // 이전: "^4.0.1"
  }
}

업데이트 명령어

# Firebase 메인 패키지 업데이트
npm install firebase@^12.0.0

# Firebase 테스트 도구 업데이트
npm install --save-dev @firebase/rules-unit-testing@^5.0.0

# 의존성 일관성 확인
npm audit fix

코드 수정 가이드

1. Academy Service (src/app/core/services/academy.service.ts)

인터페이스 분리

// 저장용 인터페이스 (FieldValue 허용)
export interface NoticeRecordInput {
  title: string;
  contents: string;
  attachments: UploadFile[];
  sentAt: Timestamp | FieldValue;  // serverTimestamp() 허용
  sentBy: string;
  targetCount: number;
  studentName: string[];
}

// 읽기용 인터페이스 (Timestamp만 허용)
export interface NoticeRecord {
  title: string;
  contents: string;
  attachments: UploadFile[];
  sentAt: Timestamp;  // 읽기 시에는 항상 Timestamp
  sentBy: string;
  targetCount: number;
  studentName: string[];
}

서버 타임스탬프 사용

// ❌ 이전 코드
const noticeHistory = {
  title: notice.title,
  contents: notice.contents,
  sentAt: Timestamp.now(),  // 오류 발생
  sentBy: user.uid
};

// ✅ 수정된 코드
import { serverTimestamp } from '@angular/fire/firestore';

const noticeHistory: NoticeRecordInput = {
  title: notice.title,
  contents: notice.contents,
  sentAt: serverTimestamp(),  // 서버 타임스탬프 사용
  sentBy: user.uid
};

2. Auth Service (src/app/core/services/auth.service.ts)

// ❌ 이전 코드
await updateDoc(userRef, {
  lastLoginTime: Timestamp.now()  // 오류 발생
});

// ✅ 수정된 코드
import { serverTimestamp } from '@angular/fire/firestore';

await updateDoc(userRef, {
  lastLoginTime: serverTimestamp()  // 서버 타임스탬프 사용
});

3. Attendance Status Component (src/app/attendance-status/attendance-status.component.ts)

가장 중요한 수정사항 - 등원 기능

// ❌ 이전 코드 - 등원 시 오류 발생
async setInOutTime(student: Student, type: '등원' | '하원'): Promise<void> {
  const updatedStudent: Student = {
    ...student,
    punchInTimeToday: type === '등원' ? Timestamp.fromDate(new Date()) : student.punchInTimeToday,
    punchOutTimeToday: type === '하원' ? Timestamp.fromDate(new Date()) : null,
  };
  
  await this.repo.updateStudentPromise(updatedStudent);  // 오류 발생
}

// ✅ 수정된 코드 - Firebase 12 호환
async setInOutTime(student: Student, type: '등원' | '하원'): Promise<void> {
  const updatedStudent: StudentInput = {
    ...student,
    punchInTimeToday: type === '등원' ? new Date() : student.punchInTimeToday,
    punchOutTimeToday: type === '하원' ? new Date() : null,
  };
  
  await this.repo.updateStudentPromise(updatedStudent);  // 정상 작동
}

4. Manager Service (src/app/core/services/manager.service.ts)

// ❌ 이전 코드
await addDoc(subscriptionHistoryCollection, {
  date: Timestamp.now(),  // 오류 발생
  description: description
});

// ✅ 수정된 코드
import { serverTimestamp } from '@angular/fire/firestore';

await addDoc(subscriptionHistoryCollection, {
  date: serverTimestamp(),  // 서버 타임스탬프 사용
  description: description
});

5. 테스트 파일 수정

Academy Service 테스트

// ❌ 이전 테스트 코드
const mockStudent: Student = {
  uid: 'student-1',
  name: '테스트 학생',
  enrollDate: Timestamp.fromDate(new Date()),  // 테스트에서도 오류 발생 가능
  // ...
};

// ✅ 수정된 테스트 코드
const mockStudent: Student = {
  uid: 'student-1',
  name: '테스트 학생',
  enrollDate: Timestamp.fromDate(new Date()),  // 테스트에서는 사용 가능
  // ...
};

// 하지만 setDoc 테스트 시에는 Date 객체 사용
await setDoc(studentRef, {
  ...mockStudent,
  enrollDate: new Date(),  // setDoc 테스트 시 Date 사용
  punchInTimeToday: new Date(),
  punchOutTimeToday: new Date()
});

테스트 및 검증

1. 유닛 테스트 실행

npm test -- --watch=false --browsers=ChromeHeadless

결과: 55개 중 54개 성공 (1개 의도적 실패 테스트 제외)

2. 빌드 테스트

npm run build

결과: 프로덕션 빌드 성공

3. 타입 검사

npx tsc --noEmit

결과: TypeScript 컴파일 오류 없음

4. 기능 테스트 체크리스트

  • 애플리케이션 실행: npm start
  • 로그인 기능: gcp.js5@gmail.com / 123456
  • 출석 현황 페이지 접근
  • 등원 버튼 클릭 - 정상 작동
  • 하원 버튼 클릭 - 정상 작동
  • 공지사항 발송 기능
  • 콘솔 오류 확인 - Timestamp 관련 오류 없음

문제 해결

자주 발생하는 오류와 해결방법

1. "Unsupported field value: a custom Timestamp object"

원인: Timestamp.now() 또는 Timestamp.fromDate() 사용
해결: serverTimestamp() 또는 new Date() 사용

2. TypeScript 타입 오류

원인: 인터페이스에서 Timestamp | FieldValue 타입 불일치
해결: 저장용/읽기용 인터페이스 분리

// 저장 시
interface StudentInput {
  punchInTimeToday?: Timestamp | FieldValue | Date | null;
}

// 읽기 시  
interface Student {
  punchInTimeToday?: Timestamp | null;
}

3. 기존 데이터 호환성

문제: 기존 Firestore 데이터와의 호환성 우려
답변: 기존 데이터는 영향받지 않음. 새로운 저장 방식만 적용

디버깅 팁

  1. 콘솔 로그 활용
console.log('Before save:', data);
try {
  await setDoc(docRef, data);
  console.log('Save successful');
} catch (error) {
  console.error('Save failed:', error);
}
  1. 타입 가드 사용
function isTimestamp(value: any): value is Timestamp {
  return value && typeof value.toDate === 'function';
}

function isDate(value: any): value is Date {
  return value instanceof Date;
}

모범 사례

1. Timestamp 사용 가이드라인

서버 타임스탬프 사용 시기

  • 문서 생성/수정 시간 기록
  • 로그 기록
  • 순서가 중요한 데이터
// ✅ 서버 타임스탬프 사용
await addDoc(collection, {
  content: "사용자 액션",
  timestamp: serverTimestamp()  // 서버에서 정확한 시간 보장
});

클라이언트 Date 사용 시기

  • 사용자 입력 시간
  • 클라이언트 상태 관리
  • 즉시 표시가 필요한 데이터
// ✅ 클라이언트 Date 사용
const attendanceRecord = {
  studentId: student.id,
  checkInTime: new Date(),  // 즉시 UI에 표시 가능
  status: 'present'
};

2. 인터페이스 설계 패턴

// 1. 기본 데이터 인터페이스
interface BaseEntity {
  id?: string;
  createdAt: Timestamp;
  updatedAt: Timestamp;
}

// 2. 입력 데이터 인터페이스 (저장용)
interface BaseEntityInput {
  id?: string;
  createdAt: Timestamp | FieldValue | Date;
  updatedAt: Timestamp | FieldValue | Date;
}

// 3. 구체적인 엔티티
interface Student extends BaseEntity {
  name: string;
  punchInTimeToday?: Timestamp | null;
}

interface StudentInput extends BaseEntityInput {
  name: string;
  punchInTimeToday?: Timestamp | FieldValue | Date | null;
}

3. 서비스 메서드 패턴

class AcademyService {
  // 저장 메서드는 Input 타입 받기
  async saveStudent(student: StudentInput): Promise<void> {
    await setDoc(doc(this.firestore, 'students', student.id), student);
  }
  
  // 조회 메서드는 일반 타입 반환
  getStudent$(studentId: string): Observable<Student> {
    return docData(doc(this.firestore, 'students', studentId)) as Observable<Student>;
  }
}

4. 마이그레이션 체크리스트

업그레이드 시 확인할 항목들:

  • Timestamp.now() 사용처 모두 확인
  • Timestamp.fromDate() 사용처 모두 확인
  • setDoc, addDoc, updateDoc 호출 시 데이터 타입 확인
  • 인터페이스 분리 필요성 검토
  • 테스트 코드 업데이트
  • 타입 오류 해결
  • 기능 테스트 수행

참고 자료


문서 작성일: 2025년 7월 26일
최종 업데이트: 2025년 7월 26일

profile
교육하고 책 쓰는 개발자

0개의 댓글