토이프로젝트II 서버는 Firebase에서 제공하는 Firestore를 사용 !
토이프로젝트II는 짧은 기간인 3주동안 진행되어 서버까지 구축하지않고 firebase에서 제공되는 서버 firstore를 사용하기로 했다.
firestore 구조는 컬렉션 > 문서 > 필드 & 컬렉션 > 문서 > 필드 & 컬렉션 ... 이런 반복되는 구조로 되어있다. firebase/firestore에서 제공되는 다양한 메서드를 통해 CRUD 작업을 쉽게 할 수 있다.
CRA는 process.env.REACT_APP_FIREBASE_API_KEY
로 작성하지만 vite는 import.meta.env.VITE_FIREBASE_API_KEY
로 작성한다. 이 부분에서 많이 헤맸었다....🥲
Firebase_Config.ts
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
declare global {
interface ImportMeta {
readonly env: {
readonly VITE_FIREBASE_DATABASE_URL: string;
readonly VITE_FIREBASE_API_KEY: string;
readonly VITE_FIREBASE_AUTH_DOMAIN: string;
readonly VITE_FIREBASE_PROJECT_ID: string;
readonly VITE_FIREBASE_STORAGE_BUCKET: string;
readonly VITE_FIREBASE_MESSAGING_ID: string;
readonly VITE_FIREBASE_APP_ID: string;
};
}
}
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
};
const firebase = initializeApp(firebaseConfig);
const db = getFirestore(firebase);
export { db };
SDK는 개인 정보로 공유되면 안되기때문에 env파일에 따로 작성해주고 gitignore에 추가로 작성한다.
.env
VITE_FIREBASE_API_KEY=******
VITE_FIREBASE_AUTH_DOMAIN=******
VITE_FIREBASE_PROJECT_ID=******
VITE_FIREBASE_STORAGE_BUCKET=******
VITE_FIREBASE_MESSAGING_ID=******
VITE_FIREBASE_APP_ID=******
VITE_FIREBASE_MEASUREMENT_ID=******
import { collection, addDoc, getDocs } from 'firebase/firestore';
import { db } from './Firebase_Config.tsx';
const createPayrollCorApp = async (name: string, month: number, option: string, correctionDetails: string) => {
try {
const membersSnapshot = await getDocs(collection(db, 'members'));
for (const memberDoc of membersSnapshot.docs) {
const collectionSnapshot = await getDocs(collection(db, `members/${memberDoc.id}/payrollCorApp`));
for (const payDataDoc of collectionSnapshot.docs) {
const payData = payDataDoc.data();
if (payData.name === name) {
await addDoc(collection(db, `members/${memberDoc.id}/payrollCorApp`), {
name: payData.name,
month,
correctionState: 'standBy',
correctionDetails,
reasonForApplication: option,
});
break;
}
}
}
} catch (error) {
console.log(error);
}
};
export default createPayrollCorApp;
위의 로직은 members컬렉션 안에 parameter로 받은 유저의 정보와 같은 유저의 payrollCorApp 컬렉션의 필드를 addDoc()메서드를 사용하여 생성한다. addDoc은 문서 ID를 고유한 값으로 자동 생성되며 같은 데이터가 있으면 덮어씌워지는 것이 아닌 새로 생성한다.
import { collection, getDocs } from 'firebase/firestore';
import { db } from './Firebase_Config.tsx';
interface CollectionData {
[key: string]: any;
}
const getUserData = async (collectionName: string) => {
const docData: CollectionData[] = [];
try {
const querySnapshot = await getDocs(collection(db, collectionName));
docData.push(querySnapshot.docs.map((doc) => doc.data()));
} catch (error) {
console.error(error);
}
return docData;
};
getUserdata( )는 collectionName을 인자로 받아 getDocs메서드를 사용하여 firestore database에 있는 collectionName의 데이터를 읽을 수 있다.
const getCollectionData = async () => {
const allDocData: CollectionData[] = [];
try {
const membersSnapshot = await getDocs(collection(db, 'members'));
for (const memberDoc of membersSnapshot.docs) {
const userDocId = memberDoc.id;
const collectionSnapshot = await getDocs(collection(db, `members/${userDocId}/payrollDetails`));
collectionSnapshot.docs.forEach((obj) => {
allDocData.push(obj.data());
});
}
} catch (error) {
console.log(error);
}
return allDocData;
};
상위 컬렉션을 읽고 컬렉션의 문서 ID를 조회하여 하위 컬렉션을 읽는다.
import { collection, doc, setDoc, getDocs } from 'firebase/firestore';
import { db } from './Firebase_Config.tsx';
const updateCorrectionState = async (name: string, month: number, state: string, correctionDetails: string) => {
try {
const membersSnapshot = await getDocs(collection(db, 'members'));
for (const memberDoc of membersSnapshot.docs) {
const collectionSnapshot = await getDocs(collection(db, `members/${memberDoc.id}/payrollCorApp`));
for (const payDataDoc of collectionSnapshot.docs) {
const payDataId = payDataDoc.id;
const payData = payDataDoc.data();
if (payData.name === name && payData.month === month && payData.correctionDetails === correctionDetails) {
await setDoc(doc(db, `members/${memberDoc.id}/payrollCorApp`, payDataId), {
name: payData.name,
month: payData.month,
correctionState: state,
correctionDetails: payData.correctionDetails,
reasonForApplication: payData.reasonForApplication,
});
break;
}
}
}
} catch (error) {
console.log(error);
}
};
export default updateCorrectionState;
읽기와 동일하게 getDocs메서드를 사용하여 컬렉션의 문서를 조회하고 해당 문서 중 조건에 맞는 문서를 찾아 setDoc메서드를 사용하여 데이터를 업데이트한다. 추가로 doc 메서드를 사용하여 컬렉션 내의 문서 ID를 참조한다. 여기서 동일한 값의 필드는 조회된 값과 동일하게 넣어주고 바꿀 필드만 값을 넣어주어야 기존 필드를 유지하면서 수정할 수 있다.
addDoc처럼 setDoc도 필드를 새로 생성하지만 다른점은 이미 존재하는 데이터를 덮어쓴다는 점이다. addDoc과 setDoc의 차이점을 알고 적절히 사용해야 한다.
import { collection, deleteDoc, doc, getDocs } from 'firebase/firestore';
import { db } from './Firebase_Config.tsx';
const deleteSalaryCorrectionAPI = async (name: string, month: number, correctionDetails: string) => {
try {
const membersSnapshot = await getDocs(collection(db, 'members'));
for (const memberDoc of membersSnapshot.docs) {
const collectionSnapshot = await getDocs(collection(db, `members/${memberDoc.id}/payrollCorApp`));
for (const payDataDoc of collectionSnapshot.docs) {
const payData = payDataDoc.data();
if (payData.name === name && payData.correctionDetails === correctionDetails) {
await deleteDoc(doc(db, `members/${memberDoc.id}/payrollCorApp`, payDataDoc.id));
break;
}
}
}
} catch (error) {
console.log(error);
}
};
export default deleteSalaryCorrectionAPI;
Update방식처럼 doc메서드를 통해 문서의 ID를 참조하고 deleteDoc메서드를 사용하여 해당 ID를 가진 문서와 그 필드를 삭제한다.