Firestore의 1MB 제한: 서브컬렉션을 활용한 데이터 관리 전략
Firestore의 1MB 제한
Firestore는 문서당 데이터를 1MB로 제한하고있다. 이는 대규모 응용 프로그램에서 데이터 구조를 설계할 때 고려해야 할 중요한 제한 사항이라고 생각한다.
이번에는 Firestore의 데이터 용량 제한에 대해 자세히 살펴보고, 이를 극복하기 위한 방법 중 하나인 서브컬렉션을 알아보자.
Firestore의 1MB 제한, 서브컬렉션의 개념과 활용
서브컬렉션은 Firestore에서 문서 내에 존재하는 하위 컬렉션을 의미한다. 이를 통해 데이터를 더 작은 단위로 분할하고 관리할 수 있다.
이렇게만 말하면 알기 힘들다!
블로그에 올릴 글과 그 글에 달릴 댓글에 대해 생각해보자. (Post – Comment[])
블로그 글은 1개지만, 댓글은 무수히 많아질 수 있다. 이를 하나의 배열 형태의 컬럼으로 만든다면, 블로그 글의 정보를 담고 있는 데이터는 금방 1MB를 초과할 것이다.
// 한 컬렉션에 댓글 모두 추가 (용량 1MB 초과)
const newPost = {
title: '블로그 글 제목',
comments: ['잘 보고 갑니다 1', ... ,'잘 보고 갑니다 1000000000'],
}
const postRef = await db.collection('posts').add(postData);
이런 경우를 고려하기 위해서, firestore를 쓰기 위해서는 서브컬렉션을 활용해야 한다.
// 서브컬렉션에 댓글 모두 추가 (용량 1MB 미만)
const newPost = {
title: '블로그 글 제목',
comments: ['잘 보고 갑니다 1', ... ,'잘 보고 갑니다 1000000000'],
}
const commentsRef = db.collection('posts').doc(postRef.id).collection('comments');
commentsData.forEach(async comment => {
await commentsRef.add({ text: comment });
});
firestore를 사용할 때, 데이터가 많아지는 경우에는 이렇게 서브컬렉션을 나누어서 데이터를 넣는 방법으로 해야만 용량 초과를 막을 수 있다.
Firestore의 1MB 제한, 서브컬렉션 최적화를 위한 데이터 캐싱
댓글과 같은 서브컬렉션의 데이터를 자주 조회하는 경우, 클라이언트 측에서 데이터를 캐싱하여 조회 성능을 향상시킬 수 있다.
데이터를 캐싱하다!
참 어려운 말이다. 조금 더 쉽게 표현하자면, 데이터베이스의 데이터를 불러오지 않고, 사용중인 기기 내부에 데이터를 저장해서 인터넷 없이 데이터를 사용할 수 있도록 만드는 형태이다.
우선, 단순하게 포스트만 가지고 예시를 들어보자.
포스트의 제목이나 내용을 입력하는 순간, 상태값이 변경될 수 있도록 변수를 정의한다.
const [post, setPost] = useState({
title: '',
content: ''
});
이어서, 포스트의 캐시된(이전에 저장해둔) 데이터를 가져온다.
const cachedPost = await mmkv.getMap('cachedPost');
저장된 데이터가 있다면, 얼른 상태값이 넣어주자.
if (cachedPost) {
setPost(cachedPost);
}
firestore에서는 onSnapshot 이라는 핸들러 함수를 통해서, 해당 데이터가 변경될 때마다 호출되도록 하는 함수가 존재한다.
이 함수를 이용하여 최근 변경된 데이터를 가져와서 상태값과 캐시를 업데이트 해준다.
const postRef = db.collection('posts').doc(postId);
postRef.onSnapshot(snapshot => {
// 최근 변경된 데이터베이스 데이터
const postData = snapshot.data();
// 상태값 업데이트
setPost(postData);
// 캐시 업데이트
mmkv.setMap('cachedPost', postData);
});
이제 완벽한 데이터 처리 과정이 완성되었다.
- 캐시된 데이터를 가져온다
- 상태값을 업데이트해서, 사용자에게 보여준다
- 데이터베이스의 최신 데이터가 변경될 때마다 불러온다
- 상태값 및 캐시 데이터를 업데이트해서, 사용자에게 보여준다.
Firestore의 1MB 제한, 서브컬렉션 최적화를 클라우드 함수
마지막으로 클라우드 함수를 만들어서 연산을 처리하는 방법이 있지만, 이 방법은 사용량에 비례하여 요금이 청구된다. 하지만, 좋은점도 많이 있기 때문에 한번 고려해볼만 하다.
클라우드 함수를 만들어서 사용하면 좋은점
- 실시간 데이터 업데이트: Firestore는 실시간 업데이트를 지원하므로, 데이터 변경 시 클라이언트에 자동으로 업데이트된다. 이를 통해 실시간으로 변화하는 데이터를 쉽게 처리하고 화면에 반영할 수 있다.
- 서버리스 아키텍처: 클라우드 함수를 사용하면 서버를 구축하고 관리하는 번거로움을 줄일 수 있다. 필요한 로직을 클라우드 함수로 구현하여 Firebase의 인프라에서 실행되므로, 서버 유지보수와 관련된 복잡성이 감소한다.
- 확장성: Firestore와 클라우드 함수를 함께 사용하면 애플리케이션의 확장성을 높일 수 있다. Firestore는 자동으로 확장되며, 클라우드 함수를 사용하여 복잡한 로직을 처리할 수 있다. 이를 통해 사용자 증가에 따라 애플리케이션을 쉽게 확장할 수 있다.
- 보안: Firebase는 사용자 인증 및 데이터 보안을 위한 다양한 기능을 제공한다. Firestore 규칙을 사용하여 데이터에 대한 접근을 제어하고, 클라우드 함수에서 보안 로직을 구현하여 데이터를 안전하게 처리할 수 있다.
- 개발 생산성 향상: Firebase의 다양한 기능을 활용하면 개발 생산성을 높일 수 있다. Firebase SDK를 사용하여 간단한 코드로 데이터를 처리하고 클라우드 함수를 호출할 수 있으며, Firebase 콘솔을 사용하여 앱을 모니터링하고 분석할 수 있다.
클라이언트에서 클라우드 함수 호출하는 방법
import functions from '@react-native-firebase/functions';
// 클라우드 함수 호출
const updateCommentCount = functions().httpsCallable('updateCommentCount');
updateCommentCount({ postId: 'postId', commentId: 'commentId' })
.then(result => {
// 클라우드 함수 실행 결과 처리
console.log(result.data);
})
.catch(error => {
// 에러 처리
console.error(error);
});
마지막으로, 포스팅과 그 댓글에 대한 데이터를 서브컬렉션과 데이터 캐싱을 사용하여 다루는 코드를 아래에 첨부해 두었다.
코드예제) Post – Comments
import React, { useState, useEffect } from 'react';
import MMKVStorage from 'react-native-mmkv-storage';
import firebase from 'firebase/app';
import 'firebase/firestore';
const db = firebase.firestore();
const mmkv = new MMKVStorage.Loader().initialize();
const App = () => {
const [post, setPost] = useState({
title: '',
content: ''
});
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchPost = async () => {
try {
const cachedPost = await mmkv.getMap('cachedPost');
if (cachedPost) {
setPost(cachedPost);
}
const postRef = db.collection('posts').doc(postId);
postRef.onSnapshot(snapshot => {
const postData = snapshot.data();
setPost(postData);
setLoading(false);
// 캐시 업데이트
mmkv.setMap('cachedPost', postData);
});
} catch (error) {
setError(error.message);
setLoading(false);
}
};
const fetchComments = async () => {
try {
const cachedComments = await mmkv.getArray('cachedComments');
if (cachedComments) {
setComments(cachedComments);
}
const commentsRef = db.collection('posts').doc(postId).collection('comments');
commentsRef.onSnapshot(snapshot => {
const commentsData = snapshot.docs.map(doc => doc.data());
setComments(commentsData);
setLoading(false);
// 캐시 업데이트
mmkv.setArray('cachedComments', commentsData);
});
} catch (error) {
setError(error.message);
setLoading(false);
}
};
fetchPost();
fetchComments();
}, []);
const handlePostEdit = (key, value) => {
setPost(prevPost => ({
...prevPost,
[key]: value
}));
};
const handleCommentEdit = (index, key, value) => {
setComments(prevComments => {
const newComments = [...prevComments];
newComments[index][key] = value;
return newComments;
});
};
const handleSubmit = async () => {
try {
// 게시물 업데이트
await db.collection('posts').doc(postId).update(post);
// 댓글 업데이트
await Promise.all(comments.map((comment, index) => {
return db.collection('posts').doc(postId).collection('comments').doc(comment.id).update(comments[index]);
}));
// 캐시 업데이트
await mmkv.setMap('cachedPost', post);
await mmkv.setArray('cachedComments', comments);
} catch (error) {
setError(error.message);
}
};
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<div>
<input
type="text"
value={post.title}
onChange={e => handlePostEdit('title', e.target.value)}
/>
<textarea
value={post.content}
onChange={e => handlePostEdit('content', e.target.value)}
/>
<button onClick={handleSubmit}>Save</button>
<h2>Comments</h2>
{comments.map((comment, index) => (
<div key={comment.id}>
<input
type="text"
value={comment.text}
onChange={e => handleCommentEdit(index, 'text', e.target.value)}
/>
<button onClick={handleSubmit}>Save</button>
</div>
))}
</div>
);
};
export default App;
Firestore를 공식 문서의 사용법을 주로 참고하여 앱을 개발해 왔으나, 이번 기회를 통해 데이터 1MB 용량 제한과 최적화를 위한 데이터캐싱 및 클라우드 함수를 알게 되었다.
궁금해 하는 것. 이게 가장 중요한 것 같다. 아니라면, 내가 하는 방식 외에 새로운 것들은 배울 수 없다. (어쨌든, 돌아가긴 하니까 …)