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