HDomi/domi-chat-portfolio 프로젝트에서 RAG 시스템의 핵심인 포트폴리오 데이터를 Firestore에 저장하는 과정에서 예상치 못한 문제를 겪었습니다. upload-resume.mjs 스크립트를 통해 DATA_CHUNKS 상수에 정의된 텍스트 데이터와 임베딩 벡터를 resume_chunks 컬렉션에 저장하려고 할 때, 특정 필드의 값이 undefined로 설정되어 있으면 Firebase Firestore가 해당 문서의 쓰기 작업을 거부하는 현상이 발생했습니다.
문제 발생: undefined 필드와 Firestore의 엄격함
Firebase Firestore는 NoSQL 클라우드 데이터베이스로, 유연한 스키마를 제공하지만 데이터 타입에 대해서는 엄격한 규칙을 가지고 있습니다. 특히, 문서 필드의 값으로 undefined를 허용하지 않습니다. 이는 null과는 다른 개념으로, null은 유효한 값으로 취급되어 저장할 수 있지만, undefined는 필드 자체가 존재하지 않음을 의미하기 때문에 Firestore에서 허용하지 않습니다. 만약 객체 내에 undefined 값을 가진 필드가 포함된 채로 set 또는 update 메서드를 호출하면, Firestore는 Function Document.set() called with invalid data. Unsupported field value: undefined와 같은 오류 메시지와 함께 쓰기 작업을 거부합니다.
우리 프로젝트에서는 DATA_CHUNKS를 기반으로 문서 객체를 구성하고 Gemini Embedding 모델을 통해 벡터 데이터를 생성한 뒤, 이 모든 데이터를 한 번에 Firestore에 저장하고 있었습니다. 이 과정에서 일부 필드가 조건부로 생성되거나, TypeScript 인터페이스에서 선택적(optional)으로 정의된 필드가 실제 객체 생성 시 누락되어 undefined가 되는 경우가 있었습니다. 예를 들어, resume_chunks 컬렉션에 저장될 문서 스키마는 text, embedding, 그리고 추가적인 메타데이터 필드를 포함할 수 있는데, 특정 메타데이터 필드가 항상 존재하지 않을 경우 undefined가 됩니다.
초기 시도: 수동 필터링과 그 한계
처음에는 오류 메시지를 확인하고 어떤 필드가 undefined인지 특정하기 위해 디버깅을 진행했습니다. 문제가 되는 필드를 찾아 수동으로 제거하거나 null로 변경하는 방법을 시도했습니다. 예를 들어 다음과 같은 방식으로 코드를 수정했습니다.
interface ResumeChunk {
text: string;
embedding: number[];
category?: string; // 선택적 필드
}
const documentData: ResumeChunk = {
text: "...",
embedding: [...],
// category가 없을 경우 undefined가 됨
};
// 수동 필터링 시도
const cleanedData: Record<string, any> = {};
for (const key in documentData) {
if (documentData[key] !== undefined) {
cleanedData[key] = documentData[key];
}
}
// firestore.collection('resume_chunks').add(cleanedData);
이 방법은 단순한 객체에서는 작동했지만, 저장해야 할 데이터가 중첩된 객체 구조를 가질 가능성이 있었고, 필드의 수가 많아지거나 스키마가 변경될 때마다 수동으로 코드를 수정해야 하는 번거로움이 있었습니다. 또한, upload-resume.mjs 스크립트 외에 다른 부분에서도 비슷한 문제가 발생할 수 있었기 때문에, 더 범용적이고 유지보수가 용이한 해결책이 필요하다고 판단했습니다.
해결책: 범용 유틸리티 함수를 통한 필드 정리
수동 필터링의 한계를 인지하고, 객체 내의 모든 undefined 필드를 재귀적으로 제거하는 범용 유틸리티 함수를 구현하기로 결정했습니다. 이 함수는 어떤 객체가 주어져도 내부의 undefined 필드를 찾아 제거하고, 중첩된 객체나 배열의 경우에도 동일하게 처리할 수 있도록 설계했습니다. 이 유틸리티 함수는 src/utils/index.ts와 같은 공통 유틸리티 파일에 정의하여 프로젝트 전반에서 재활용할 수 있도록 했습니다.
// src/utils/index.ts (예시)
/**
* 객체에서 undefined 값을 가진 필드를 재귀적으로 제거합니다.
* Firestore에 저장하기 전에 데이터를 정리하는 데 유용합니다.
* @param obj 처리할 객체
* @returns undefined 필드가 제거된 새로운 객체
*/
export function removeUndefinedFields<T extends object>(obj: T): T {
if (typeof obj !== 'object' || obj === null) {
return obj; // 객체가 아니거나 null이면 그대로 반환
}
if (Array.isArray(obj)) {
// 배열인 경우 각 요소를 재귀적으로 처리
return obj.map(item => removeUndefinedFields(item)) as T;
}
const newObj: Partial<T> = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
if (value !== undefined) {
// 값이 객체이면 재귀 호출, 아니면 직접 할당
newObj[key] = (typeof value === 'object' && value !== null)
? removeUndefinedFields(value)
: value;
}
}
}
return newObj as T;
}
이 removeUndefinedFields 함수를 upload-resume.mjs 스크립트에서 Firestore set 메서드를 호출하기 직전에 적용했습니다. 예를 들어:
// upload-resume.mjs (예시)
import { initializeApp, cert } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import { removeUndefinedFields } from './src/utils'; // 유틸리티 함수 임포트
// ... Firebase 초기화 코드 ...
async function uploadData() {
const db = getFirestore();
for (const chunk of DATA_CHUNKS) {
const embedding = await generateEmbedding(chunk.text);
const docData = {
text: chunk.text,
embedding: embedding,
category: chunk.category, // category가 없을 경우 undefined가 될 수 있음
// ... 기타 필드 ...
};
const cleanedDocData = removeUndefinedFields(docData); // 중요: 여기서 필터링
await db.collection('resume_chunks').add(cleanedDocData);
console.log(`Document added for chunk: ${chunk.text.substring(0, 30)}...`);
}
}
uploadData().catch(console.error);
이러한 접근 방식을 통해 undefined 필드로 인한 Firestore 쓰기 거부 문제를 효과적으로 해결할 수 있었습니다. 이제 데이터 소스에서 어떤 필드가 undefined로 들어오더라도, Firestore에 저장되기 전에 자동으로 정리되므로 안정적인 데이터 ingestion 파이프라인을 구축할 수 있게 되었습니다.
다음 과제
현재 removeUndefinedFields 함수는 undefined 값만 제거하지만, 경우에 따라 빈 문자열(''), 빈 배열([]), 또는 빈 객체({})와 같은 '의미 없는' 값들도 제거해야 할 수 있습니다. 다음 단계에서는 이러한 값들까지도 선택적으로 처리할 수 있도록 유틸리티 함수를 확장하거나, Firestore 스키마 유효성 검사 라이브러리(예: Zod)를 도입하여 데이터 유효성을 더욱 강화하는 방안을 고려하고 있습니다. 이는 데이터 일관성을 높이고 애플리케이션의 견고성을 향상시키는 데 기여할 것입니다.