Firebase Codelab : 11. 트랜잭션으로 데이터 쓰기
Firebase github : View Source
사용자가 레스토랑에 리뷰를 제출할 수 있는 기능...
리뷰를 제출한 다음 음식점의 평점 count 및 average rating을 업데이트...
둘 중 하나가 실패하고 다른 하나는 실패하지 않으면 데이터베이스의 한 부분에 있는 데이터가 다른 부분의 데이터와 일치하지 않는 일관되지 않은 상태...
이 트랙잭션은 단지 firebase라는 db에 국한되지 않는다. 앱이나 서비스 자체에서도 매우 중요한 개념이기 때문에 꼭 이해,숙지, 외워야 한다.
또 이번 문서 함수에서는 콜백callback 개념이 중요하니 이것 또한 꼭 이해,숙지,외워야한다.
Cloud Firestore는 트랜잭션 기능을 제공하므로 데이터의 일관성을 유지...
java는 @Transaction 어노테이션으로 트랜잭션을 쓰기도 한다
Firestore 제작자가 Firestore에 기본 기능으로 넣어놨으니 (개발자가 따로 안 만들어도 되고) 있는거 쓰라는 의미.
로컬에 클론해서 받았던 'friendlyeats-web' 경로에서 vs code 실행.
vanilla-js \ scripts \ FriendlyEats.Data.js 열기
function addRating
FriendlyEats.prototype.addRating = function(restaurantID, rating) {
var collection = firebase.firestore().collection('restaurants');
var document = collection.doc(restaurantID);
var newRatingDocument = document.collection('ratings').doc();
};
Nosql 형식이 소스 분석만으로 이해하기 힘들다면
일단 리뷰가 있다는 가정 하에 firebase Console를 이미지 형태로 보고 이해한다.
firebase(의 여러 서비스 중).firestore()(firestore 서비스를 가져오고).collection('레스토랑들') 컬렉션 데이터를 가져온다.
그 컬렉션의.doc('화면에서 누른 레스토랑ID를') 넘겨서 그 레스토랑 문서를 가져온다.
그 레스토랑 문서 중' rating'이란 컬렉션 데이터를 가져온다.
위에서 collection('rating')의 문서를 각각 누르면 각 문서의 필드를 볼 수 있다.
update에 필요한 기존 db 데이터를 먼저 가져온 거라고 이해하면 된다.
그 다음에 함수 소스를 본다.
runTransaction하고 함수 안에서 return... function...
return firebase.firestore().runTransaction(function(transaction) {
return transaction.get(document).then(function(doc) {
var data = doc.data();
var newAverage =
(data.numRatings * data.avgRating + rating.rating) /
(data.numRatings + 1);
transaction.update(document, {
numRatings: data.numRatings + 1,
avgRating : newAverage
});
return transaction.set(newRatingDocument, rating);
});
});
처음 접하면 runTransaction이 어떻게 돌아가는 함수인지 모른다.
firebase 뿐만 아니라 java, js, sql, swift, nodejs 등... 잘 모를 땐
제작자가 안내 가이드 마냥 공식 사이트 Document에 잘 정리해서 올려놓아주고 그걸 참고하면 된다.
참고 문헌
공식 문서 - 트랜잭션 및 일괄 쓰기 - 트랜잭션을 사용한 데이터 업데이트 ('Web namespaced API' 참고)
일단 sample 소스의 return 부터 본다. (위 sfDocRef는 아래서 볼거다)
연결해둔 나의 firebase.firestore() => db
.runTransaction(function(transaction) { => sample 화살표 함수와 동일. 주석 This..는 Codelab 최하단 '경고'와 동일한 의미.
transaction.get(document) => 문서 존재여부 확인. if ( !문서.존재여부) = 없으면 { "문서 없어!" 라고 하기 }
궁금한게 그럼 transaction.get(document) 리턴값은 true / false로 바로 갈리는게 아니라 .then까지 들어가서 throw를 던지나보다?
그럼 .then이란 함수가 약간 ajax의 success / error... 아무튼 do catch exception throw 그런건가..?
아무튼 문서가 있으면 다음 소스를 수행하고
transaction.update(document, { n.. , r.. } ) => 앞의 sfDocRef 문서를 뒤의 문서로 덮어써라?
return db.runTransaction((transaction) => {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(sfDocRef).then((sfDoc) => {
if (!sfDoc.exists) {
throw "Document does not exist!";
}
// Add one person to the city population.
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
var newPopulation = sfDoc.data().population + 1;
transaction.update(sfDocRef, { population: newPopulation });
});
}).then(() => {
console.log("Transaction successfully committed!");
}).catch((error) => {
console.log("Transaction failed: ", error);
});
그렇다면 friendlyeats 함수 소스도 대충 풀어낼 수 있다.
transaction.get(document)
(1) 인자 document는 위쪽에서 (restaurantID)를 넘겨서 가져온 '문서'다. 그렇다면 transaction.get의 param 타입은 document 타입이겠구나 도 추측 가능.
(2) 연결시킨 db firebase에 document 문서 존재 여부 .then(function(doc) = 갑툭튀 doc은 보통 이럴때 콜백이 돌려준 데이터다.
컴퓨터 : "연결시킨 db에 그 문서 있구나? 그럼 그 문서, 이름 doc이란 곳에 담아서 돌려줄게." 대충 이런 의미.
고로 (*db에 restaurantID로 문서가 반드시 있다는 가정 하에) 정말!! 이해만 하기 위해 간단하게 보자면
return firebase.firestore().runTransaction(function(transaction) {
return transaction.get(document).then(function(doc) {
var data = doc.data();
var newAverage =
(data.numRatings * data.avgRating + rating.rating) /
(data.numRatings + 1);
transaction.update(document, {
numRatings: data.numRatings + 1,
avgRating : newAverage
});
return transaction.set(newRatingDocument, rating);
});
});
collection.doc(restaurantID) = transaction.get(document) = doc > 다 같은 문서라는 것! (라고만 이해)
그럼 이제 그 문서는 아직 firebase NoSql 포맷 형식이니 인간이 다루기 좋게 .data() data화 해준다.
저 데이터를 console.log로 찍으면 아래와 같은 구조로 보인다.
문서가 갖고 있는 기존 데이터와 , 앱(=firendlyeats 사이트)에서 클라이언트(=리뷰 고객)한테서 넘어온 데이터를 계산한게
newAverage 새로운 평균이 된다.
numRating = 총 리뷰 갯수 = 기존 총 갯수 + 1개(지금 새로 1개 추가된거니까)
avRating = 평균 점수 = newAverage로 덮어버린다.
이 2개 필드를 .update( document에, {값으로}) 한다.
마지막에 return transaction.set(newRatingDocument, rating); 가 있다.
.set함수가 뭔지 모른다. 그러면 또 제작자가 올려준 공식 문서에서 찾아보면 되는데 아까 위에서 말했던 sfDocRef 부분을 지금 보면 된다.
참고 문헌
공식 문서 - 트랜잭션 및 일괄 쓰기 - 트랜잭션을 사용한 데이터 업데이트 ('Web namespaced API' 참고)
// Create a reference to the SF doc.
var sfDocRef = db.collection("cities").doc("SF");
// Uncomment to initialize the doc.
// sfDocRef.set({ population: 0 });
db 컬렉션(도시)에 문서(SF)를 만든다.
밑에 주석 보면 // 만든 문서에.set({ 인구 수는 : 0인 걸 }) 세팅해라~
그렇다면 newRatingDocument는 컬렉션(ratings)의 문서였는데
newRatingDocument = document.collection('ratings').doc();
다른 점은
sfDocRef.set({ population: 0 }); < 여긴 파라미터가 1개지만
transaction.set(newRatingDocument, rating); < 여긴 파라미터가 2개다. 인자 갯수가 다를 땐 주의할 필요가 있다.
참고 문헌
공식 문서 - Cloud Firestore에 데이터 추가 - 문서 설정 ('Web namespaced API' 참고)
문서가 없으면 생성됩니다. 문서가 있으면 새로 제공한 데이터로 내용을 덮어쓰지만, 다음과 같이 데이터를 기존 문서와 병합하도록 지정한 경우는 예외입니다.
var setWithMerge = cityRef.set({capital: true}, { merge: false });
문서가 있는지 확실하지 않은 경우 전체 문서를 실수로 덮어쓰지 않도록 새 데이터를 기존 문서와 병합하는 옵션을 전달하세요. 대상 맵이 포함된 문서가 비어 있으면 대상 문서의 맵 필드를 덮어씁니다.
만약 위 샘플 소스에서 merge가 capital이었다면
var setWithMerge = cityRef.set({capital: true}, { capital: false });
앞의 데이터 true는 뒤 데이터false로 덮어써지는 것이다.
transaction.update는 db에 (뭐를, 뭐로 ) 바꿔라. < 라는 걸 한거지만..
우리가 리뷰를 작성하고 나면 그 리뷰가 추가된 데이터가 새로 화면에 renderer해서 보여야한다. 그 로직은 이제 봐야한다.
return (newRatingDocument라는 이름의 변수에, 새로운 'rating 문서'를 담은) 것은
블록을 나가면 다시 return이 되고
그 return(값은 계속 set(newRatingDocument, rating))은 runTransaction의 return이 된다.
그러면 함수 addRating은 이 return 새로운 문서를 > 어디에 돌려주는 것인가?
vanilla-js \ scripts \ FriendlyEats.Data.js \ function addRating
이 함수가 호출되는 곳을 찾아본다.
(1) vanilla-js \ scripts \ FriendlyEats.View.js \ FriendlyEats.prototype.initReviewDialog
(2) vanilla-js \ scripts \ FriendlyEats.Mock.js \ FriendlyEats.prototype.addMockRatings
각각 함수 쪽에 console.log를 적고, 새로운 rating 리뷰를 추가하면 어느 쪽에서 호출되는 건지 확인할 수 있다.
(1) 에서 호출되고 있으니 (1) 소스를 본다.
Data.js \ function addRating 는 return에 값을 담아서 돌려줬지만
View.js \ function initReviewDialog 는 어느 변수에도 받지 않고 .then .rerender() 한다.
아예 화면을 다시 렌더링해서 그냥 [8. Get() 데이터] Data.js \ function getRestaurants하도록 해놓았다.
FriendlyEats.prototype.initReviewDialog = function() {
var dialog = document.querySelector('#dialog-add-review');
this.dialogs.add_review = new mdc.dialog.MDCDialog(dialog);
var that = this;
this.dialogs.add_review.listen('MDCDialog:accept', function() {
var pathname = that.getCleanPath(document.location.pathname);
var id = pathname.split('/')[2];
that.addRating(id, {
rating: rating,
text: dialog.querySelector('#text').value,
userName: 'Anonymous (Web)',
timestamp: new Date(),
userId: firebase.auth().currentUser.uid
}).then(function() {
that.rerender();
});
});
// ...
};
다르게 세팅하는 로직도 있을텐데 왜 이렇게 했을까 궁금할 수 있다.
firebase 제작자도 온갖 예외 생각을 다 생각해서 만들었을 것이다. 그래서 Codelab 마지막 참고,경고를 본다.
사용자(=고객)이 특정 레스토랑을 비방하기 위해 평점을 조작할 수 있다. '앱'에서 어찌저찌해서 그런 조작을 못하도록
Data.js \ function addRating 에서 newAverage를 계산하고 .update하라고 설명한다.
또 Data.js \ function addRating 의 function() {
return function() {
return... < 이런 형태를 콜백callback 함수라고 하는데
로직을 잘 못 짠 줄 모르고 '앱'을 돌릴 경우, 소스 수행이 실패하게 되어 "콜백 지옥callback hell에 빠진다"라고 한다.
그래서 '경고: 콜백 함수에 '앱' 상태 수정하는거.. 넣으면.. 님 지옥에 빠짐..'라고 친절하게 설명을 적어줬다.
그러면 아하. 다르게 세팅하는 로직도 있을텐데 저런 이슈가 있어서 저렇게 했구나. 하면 된다.
아까 위에서 .then() 함수는 비동기 Promise의 함수인 건 알고 있었는데 정확한 Cycle을 모르고 지나갔다.
'─── Toy Project > Firebase - FriendlyEats' 카테고리의 다른 글
13. 결론 (0) | 2024.10.19 |
---|---|
12. 데이터 보안 (0) | 2024.10.18 |
10. 색인 배포 (0) | 2024.10.11 |
9. 데이터 정렬 및 필터링 (0) | 2024.10.11 |
8-half. FirendlyEats 소스 분석 (0) | 2024.10.03 |