본 문서는 AlimiPro Angular 프로젝트에서 Firebase JavaScript SDK를 버전 11에서 12로 업그레이드하는 과정과 주요 breaking change 해결 방법을 기록합니다.
11.7.1
→ 12.0.0
4.0.1
→ 5.0.0
문제: 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
import { serverTimestamp } from '@angular/fire/firestore';
// ✅ 서버에서 타임스탬프를 생성하는 경우
await setDoc(docRef, {
createdAt: serverTimestamp(), // Firebase 서버에서 생성
updatedAt: serverTimestamp() // Firebase 서버에서 생성
});
// ✅ 클라이언트에서 타임스탬프를 생성하는 경우
await setDoc(docRef, {
createdAt: new Date(), // JavaScript Date 객체 직접 사용
updatedAt: new Date() // JavaScript Date 객체 직접 사용
});
{
"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
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
};
src/app/core/services/auth.service.ts
)// ❌ 이전 코드
await updateDoc(userRef, {
lastLoginTime: Timestamp.now() // 오류 발생
});
// ✅ 수정된 코드
import { serverTimestamp } from '@angular/fire/firestore';
await updateDoc(userRef, {
lastLoginTime: serverTimestamp() // 서버 타임스탬프 사용
});
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); // 정상 작동
}
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
});
// ❌ 이전 테스트 코드
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()
});
npm test -- --watch=false --browsers=ChromeHeadless
결과: 55개 중 54개 성공 (1개 의도적 실패 테스트 제외)
npm run build
결과: 프로덕션 빌드 성공
npx tsc --noEmit
결과: TypeScript 컴파일 오류 없음
npm start
원인: Timestamp.now()
또는 Timestamp.fromDate()
사용
해결: serverTimestamp()
또는 new Date()
사용
원인: 인터페이스에서 Timestamp | FieldValue
타입 불일치
해결: 저장용/읽기용 인터페이스 분리
// 저장 시
interface StudentInput {
punchInTimeToday?: Timestamp | FieldValue | Date | null;
}
// 읽기 시
interface Student {
punchInTimeToday?: Timestamp | null;
}
문제: 기존 Firestore 데이터와의 호환성 우려
답변: 기존 데이터는 영향받지 않음. 새로운 저장 방식만 적용
console.log('Before save:', data);
try {
await setDoc(docRef, data);
console.log('Save successful');
} catch (error) {
console.error('Save failed:', error);
}
function isTimestamp(value: any): value is Timestamp {
return value && typeof value.toDate === 'function';
}
function isDate(value: any): value is Date {
return value instanceof Date;
}
// ✅ 서버 타임스탬프 사용
await addDoc(collection, {
content: "사용자 액션",
timestamp: serverTimestamp() // 서버에서 정확한 시간 보장
});
// ✅ 클라이언트 Date 사용
const attendanceRecord = {
studentId: student.id,
checkInTime: new Date(), // 즉시 UI에 표시 가능
status: 'present'
};
// 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;
}
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>;
}
}
업그레이드 시 확인할 항목들:
Timestamp.now()
사용처 모두 확인Timestamp.fromDate()
사용처 모두 확인 setDoc
, addDoc
, updateDoc
호출 시 데이터 타입 확인문서 작성일: 2025년 7월 26일
최종 업데이트: 2025년 7월 26일