실시간 리스너 및 오프라인 유지
onSnapshot으로 Firestore 컬렉션을 구독하고 UI에서 실시간 업데이트를 받으며, 연결이 없어도 앱이 캐시된 데이터를 읽도록 오프라인 유지를 활성화합니다.
실시간 리스너 및 오프라인 유지은(는) CoddyKit의 무료 React Native Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 React Native Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Real-Time Listeners vs One-Time Reads
Firestore offers two ways to read data: one-time reads with .get() and real-time listeners with .onSnapshot(). A real-time listener keeps a persistent connection open and fires your callback every time the document or query result changes, including the initial data load.
Use .get() when you only need data once (like loading user settings on app start). Use .onSnapshot() when data changes frequently and the UI must stay up to date, like a chat feed or a live score.
Listening to a Single Document
Call .onSnapshot(callback) on a DocumentReference to listen for changes to a specific document. The callback fires immediately with the current data and again whenever the document changes.
.onSnapshot() returns an unsubscribe function. Store it in a variable and call it in your useEffect cleanup to stop the listener when the component unmounts and avoid memory leaks.
import { useEffect, useState } from 'react';
import firestore from '@react-native-firebase/firestore';
export function usePost(postId: string) {
const [post, setPost] = useState(null);
useEffect(() => {
const unsubscribe = firestore()
.collection('posts')
.doc(postId)
.onSnapshot((snapshot) => {
if (snapshot.exists) {
setPost({ id: snapshot.id, ...snapshot.data() });
}
});
return unsubscribe; // Stop listener on unmount
}, [postId]);
return post;
}Listening to a Collection Query
Attach .onSnapshot() to a collection query to receive live updates whenever any document in the result set is added, modified, or removed. The QuerySnapshot passed to the callback contains all matching documents — not just the changed ones.
This means each time a document changes, you rebuild the entire list from snapshot.docs. This is fine for small to medium lists, and Firestore's local cache makes it fast even without a network round-trip for cached data.
useEffect(() => {
const unsubscribe = firestore()
.collection('messages')
.where('roomId', '==', roomId)
.orderBy('createdAt', 'desc')
.limit(50)
.onSnapshot((snapshot) => {
const messages = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setMessages(messages);
});
return unsubscribe;
}, [roomId]);Using DocumentChange for Efficiency
Instead of rebuilding the entire list on every snapshot, you can inspect the changes array to see exactly what was added, modified, or removed. Each entry in snapshot.docChanges() has a type property: 'added', 'modified', or 'removed'.
This approach is more efficient for large collections where only a few documents change at a time. You surgically update the local state array instead of replacing it entirely.
snapshot.docChanges().forEach((change) => {
const item = { id: change.doc.id, ...change.doc.data() };
if (change.type === 'added') {
setItems((prev) => [item, ...prev]);
} else if (change.type === 'modified') {
setItems((prev) => prev.map((i) => (i.id === item.id ? item : i)));
} else if (change.type === 'removed') {
setItems((prev) => prev.filter((i) => i.id !== item.id));
}
});Handling Listener Errors
.onSnapshot() accepts a second argument for error handling. When the listener fails — typically due to a permission error from Firestore Security Rules — the error callback fires with a FirebaseError.
Common error codes include permission-denied (security rules blocked the read) and unavailable (offline and no cached data). Always provide an error handler so you can display a fallback UI instead of silently failing.
const unsubscribe = firestore()
.collection('private-data')
.onSnapshot(
(snapshot) => {
setData(snapshot.docs.map((doc) => doc.data()));
},
(error) => {
if (error.code === 'permission-denied') {
setError('You do not have access to this data.');
} else {
setError('Could not load data. Check your connection.');
}
}
);Firestore Offline Persistence
React Native Firebase enables offline persistence by default on mobile. This means Firestore caches all documents you read or listen to in a local SQLite database on the device. When the device has no internet, reads return cached data immediately.
You do not need any configuration to enable this — it just works. You can control the cache size limit (default 40 MB). When the cache is full, Firestore evicts the least recently used documents. You can also set it to unlimited with cacheSizeBytes: firestore.CACHE_SIZE_UNLIMITED.
import firestore from '@react-native-firebase/firestore';
// Configure cache size (call before any Firestore operations)
await firestore().settings({
cacheSizeBytes: firestore.CACHE_SIZE_UNLIMITED,
persistence: true, // enabled by default on mobile
});
// After this, all reads and writes work offline automaticallyReading from Cache vs Server
By default, .get() tries the server first and falls back to the cache if offline. You can override this by specifying a source option: { source: 'cache' } always reads from local cache (instant, no network) and { source: 'server' } always goes to the network.
Cache-only reads are useful for optimistic UI patterns where you want to render existing data immediately while a fresh network request is in progress. If the document is not in the cache, a cache-only read throws an error.
// Always use cached data (instant, works offline)
const snapshot = await firestore()
.collection('posts')
.doc(postId)
.get({ source: 'cache' });
// Always fetch from server (throws if offline)
const freshSnapshot = await firestore()
.collection('posts')
.doc(postId)
.get({ source: 'server' });
// Default: server first, cache fallback
const snapshot2 = await firestore()
.collection('posts')
.doc(postId)
.get();Pagination with Query Cursors
For large collections, load data in pages using query cursors: .startAfter(lastDocument). You keep a reference to the last document you loaded and pass it to the next query to get the next page. This is known as cursor-based pagination.
Store the last document snapshot in state. When the user scrolls to the bottom of the list (FlatList's onEndReached), trigger a new query starting after the last document. Append the new results to the existing list.
const [posts, setPosts] = useState([]);
const [lastDoc, setLastDoc] = useState(null);
const [hasMore, setHasMore] = useState(true);
async function loadMore() {
if (!hasMore) return;
let query = firestore()
.collection('posts')
.orderBy('createdAt', 'desc')
.limit(20);
if (lastDoc) {
query = query.startAfter(lastDoc);
}
const snapshot = await query.get();
if (snapshot.empty) {
setHasMore(false);
return;
}
setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
const newPosts = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setPosts((prev) => [...prev, ...newPosts]);
}Composite Indexes
When you combine .where() and .orderBy() on different fields, Firestore requires a composite index. Without it, the query throws a failed-precondition error with a URL to create the index automatically.
Composite indexes are configured in firestore.indexes.json and deployed with the Firebase CLI. During development, just click the URL from the error log — Firebase creates the index for you. For production, always manage indexes in code so they are version controlled.
// This query requires a composite index on (userId ASC, createdAt DESC)
const snapshot = await firestore()
.collection('posts')
.where('userId', '==', userId)
.orderBy('createdAt', 'desc')
.get();
// If the index is missing, you see:
// FirebaseError: The query requires an index.
// https://console.firebase.google.com/project/.../firestore/indexes?...
// Click the link to auto-create the indexDetaching Listeners on Screen Change
Real-time listeners consume bandwidth and battery. Detach them when the user navigates away from a screen by using React Navigation's useFocusEffect hook instead of useEffect. useFocusEffect runs the effect when the screen comes into focus and cleans up when the screen loses focus.
This way, you only maintain an active Firestore listener when the relevant screen is visible, and the listener is paused (and re-established) as the user navigates away and back.
import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';
export function ChatScreen({ route }) {
useFocusEffect(
useCallback(() => {
// Start listener when screen is focused
const unsubscribe = firestore()
.collection('messages')
.onSnapshot(handleSnapshot);
// Stop listener when screen loses focus
return unsubscribe;
}, [route.params.roomId])
);
}Optimistic Updates with Local State
For a snappy user experience, apply changes to local state immediately before the Firestore write completes — this is called an optimistic update. Show the change right away, then let the real-time listener confirm or correct it when the server response arrives.
For example, when the user likes a post, increment the like count in local state immediately. If the server write fails, revert the local state by decrementing the count and showing an error toast. This avoids perceived lag on slow connections.
async function toggleLike(postId: string, currentlyLiked: boolean) {
// 1. Optimistically update local state
setLiked(!currentlyLiked);
setLikeCount((prev) => prev + (currentlyLiked ? -1 : 1));
try {
// 2. Persist to Firestore
await firestore().collection('posts').doc(postId).update({
likesCount: firestore.FieldValue.increment(currentlyLiked ? -1 : 1),
});
} catch (error) {
// 3. Revert on failure
setLiked(currentlyLiked);
setLikeCount((prev) => prev + (currentlyLiked ? 1 : -1));
Alert.alert('Error', 'Could not update like. Try again.');
}
}Quick Check
Test your understanding of React Native Mobile Development concepts from this lesson.
Lesson Recap
In this lesson you learned: how to attach real-time listeners with .onSnapshot() for live data updates, how Firestore offline persistence automatically caches data for offline use, and how to paginate large collections using startAfter cursor-based pagination. Next up we profile React Native apps and eliminate performance bottlenecks.
AI 튜터와 함께 JavaScript을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 30
- 레슨
- 120
자주 묻는 질문
“실시간 리스너 및 오프라인 유지” 강의는 무료인가요?
네 — “실시간 리스너 및 오프라인 유지” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 React Native Academy 강의 전체를 잠금 해제할 수 있습니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“실시간 리스너 및 오프라인 유지”에서 뭘 배우나요?
onSnapshot으로 Firestore 컬렉션을 구독하고 UI에서 실시간 업데이트를 받으며, 연결이 없어도 앱이 캐시된 데이터를 읽도록 오프라인 유지를 활성화합니다. 브라우저에서 직접 실행하는 실습 코드로 React Native Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
React Native Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 React Native Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“실시간 리스너 및 오프라인 유지” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 React Native Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 React Native Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.