firebase의 firestore를 db로 사용할 것이다.
파이어베이스 연동
server 디렉토리에서 파이어베이스 dependency를 설치한다.
yarn add firebase
▼ server/firebase.js
파이어베이스의 튜토리얼을 따라 아래 내용이 포함되도록 파이어베이스 설정을 한다.
파이어베이스 쿼리 작성
상품 쿼리 작성
▼ server/resolvers/product.ts
- createAt 기준으로 내림차순 정렬
- startAt 옵션은 쿼리 결과의 시작 지점을 지정한다.
- unshift() : 배열 앞에 새로운 값을 추가하고 배열의 전체 개수를 리턴함 /
shift() : 배열의 맨 앞 값을 삭제하고 삭제한 값을 리턴함 - where 옵션은 쿼리를 필터링하는 데 사용된다. (특정 조건을 만족하는 데이터만 가져오거나 쿼리함)
- query에 limit를 주어 15개씩만 가져오도록 한다.
- getDocs는 콜렉션의 모든 문서를 가져오는데 사용되며 비동기로 동작한다.(promise를 반환)
- getDoc은 문서의 위치를 가리키는 레퍼런스로, 해당 (개별)문서의 데이터를 읽거나 쓰는 데 사용됨
Query: {
products: async (
parent,
{ cursor = "", showDeleted = false } /*{ db }*/
) => {
const products = collection(db, "products");
const queryOptions: any[] = [orderBy("createdAt", "desc")];
if (cursor) {
const snapshot = await getDoc(doc(db, "products", cursor));
queryOptions.push(startAfter(snapshot));
}
if (!showDeleted) {
queryOptions.unshift(where("createdAt", "!=", null));
}
const q = query(products, ...queryOptions, limit(PAGE_SIZE));
const snapshot = await getDocs(q);
const data: DocumentData[] = [];
snapshot.forEach((doc) => data.push({ id: doc.id, ...doc.data() }));
return data;
},
product: async (parent, { id }) => {
const snapshot = await getDoc(doc(db, "products", id));
return {
...snapshot.data(),
id: snapshot.id,
};
},
},
장바구니 쿼리 작성
▼ server/src/cart.ts
- getDocs로 cart db를 모두 가져온 다음 forEach로 data에 하나씩 push해준다.
- CartItem인 product는 id값으로 찾아 리턴한다.
Query: {
cart: async (parent, args) => {
const cart = collection(db, "cart");
const cartsnap = await getDocs(cart);
const data: DocumentData[] = [];
cartsnap.forEach((doc) => {
const d = doc.data();
data.push({ id: doc.id, ...d });
});
return data;
// return db.cart;
},
},
CartItem: {
product: async (cartItem, args) => {
const product = await getDoc(cartItem.product);
const data = product.data() as any;
return {
...data,
id: product.id,
};
// return db.products.find((product) => product.id === cartItem.id);
},
},
상품 mutation 작성
- addProduct는 addDoc을 사용한다.
- updateProduct는 updateDoc을 사용한다.
- deleteProduct는 updateDoc을 사용해 createdAt 값을 null로 수정한다.
▼ server/src/resolvers/product.ts
addProduct: async (parent, { imageUrl, price, title, description }) => {
const newProduct = {
price,
imageUrl,
title,
description,
createdAt: serverTimestamp(),
};
const result = await addDoc(collection(db, "products"), newProduct);
const snapshot = await getDoc(result);
return {
...snapshot.data(),
id: snapshot.id,
};
}
updateProduct: async (parent, { id, ...data }) => {
const productRef = doc(db, "products", id);
if (!productRef) throw new Error("존재하지 않는 상품입니다.");
await updateDoc(productRef, data);
const snap = await getDoc(productRef);
return {
...snap.data(),
id: snap.id,
};
}
deleteProduct: async (parent, { id }) => {
const productRef = doc(db, "products", id);
if (!productRef) throw new Error("존재하지 않는 상풉입니다.");
await updateDoc(productRef, { createdAt: null });
return id;
}
장바구니 mutation 작성
- addCart는 addDoc을 사용한다.
- updateCart는 updateDoc을 사용한다.
- deleteCart는 deleteDoc을 사용한다.
▼ server/src/resolvers/cart.ts
addCart: async (parent, { productId }) => {
if (!productId) throw Error("상품 productId가 없습니다.");
const productRef = doc(db, "products", productId);
const cartCollection = collection(db, "cart");
const exist = (
await getDocs(
query(collection(db, "cart"), where("product", "==", productRef)))).docs[0];
let cartRef;
if (exist) {
cartRef = doc(db, "cart", exist.id);
await updateDoc(cartRef, { amount: increment(1) });
} else {
cartRef = await addDoc(cartCollection, {
amount: 1,
product: productRef,
});
}
const cartSnapshot = await getDoc(cartRef);
return {
...cartSnapshot.data(),
product: productRef,
id: cartSnapshot.id,
};
}
updateCart: async (parent, { cartId, amount }) => {
if (amount < 1) throw new Error("수량은 1이상이어야 합니다.");
const cartRef = doc(db, "cart", cartId);
if (!cartRef) throw new Error("장바구니 정보가 없습니다");
await updateDoc(cartRef, { amount });
const cartSnapshot = await getDoc(cartRef);
return {
...cartSnapshot.data(),
id: cartSnapshot.id,
};
}
deleteCart: async (parent, { cartId }) => {
const cartRef = doc(db, "cart", cartId);
if (!cartRef) throw new Error("장바구니 정보가 없습니다");
await deleteDoc(cartRef);
return cartId;
}
결제처리 mutation
- ids로 전달받은 id들을 for문으로 돌면서 cartData를 얻고, 만약 createAt이 있으면(결제가 가능하면) cartRef에서 삭제한다.
executePay: async (parent, { ids }) => { const deleted = []; for await (const id of ids) { const cartRef = doc(db, "cart", id); const cartSnapshop = await getDoc(cartRef); const cartData = cartSnapshop.data(); const productRef = cartData?.product; if (!productRef) throw new Error("상품 정보가 없습니다."); const product = (await getDoc(productRef)).data() as Product; if (product.createdAt) { await deleteDoc(cartRef); deleted.push(id); } } return deleted; }
클라이언트에 DB변경 적용하기
삭제복구 기능 추가
- 삭제한 상품을 다시 목록으로 복귀하기 위해서 기존의 수정버튼을 누르면 다시 복구하도록 할 것이다.
- 따라서 updateProduct를 할 때 createdAt을 다시 지정해주어 복구하도록 한다.
▼ server/src/resolvers/product.ts
updateProduct: async (parent, { id, ...data }) => {
const productRef = doc(db, "products", id);
if (!productRef) throw new Error("존재하지 않는 상품입니다.");
await updateDoc(productRef, { ...data, createdAt: serverTimestamp() });
const snap = await getDoc(productRef);
return {
...snap.data(),
id: snap.id,
};
},
삭제된 상품에 위 사진과 같이 수정 및 복구 버튼이 뜨고 수정완료를 누르면 원래대로 복구된다.