@ List
** 9.3.4 Firestore에 포스트 등록하기
* 9.4 포스트 목록 조회하기
** 9.4.1 PostCard 커포넌트 만들기
** 9.4.2 Firestore로 포스트 목록 조회하기
** 9.4.3 FeedScreen에서 getPost 호출 후 FlatList로 보여주기
** 9.4.4 페이지네이션 및 시간순 정렬하기
@ Note
1. FeedScreen 외부에 renderItem 선언
- FlatList에서 보여지는 컴포넌트의 개수가 많을 경우, 컴토넌트 내부에서 renderItem을 렌더링할 때마다 매번 renderItem을 생성함
- 외부에 renderItem 을 한 번만 만들고 재사용하는 것이 성능 면에서 좋음
- 다른 방법으로 useMemo 사용 가능
2. Firesestore().collection() 활용
- 데이터 정렬 및 제한
> orderBy() : 데이터 정렬 순서 지정
> limit() : 검색된 문서 수 제한
* reference : https://firebase.google.com/docs/firestore/query-data/order-limit-data?hl=ko
- 쿼리 커서로 데이터 페이지화
> startAfter() : 파라미터로 넣은 정보 이후의 정보를 불러옴
> startAt() : 파라미터로 넣은 정보를 포함한 이후의 정보를 불러옴
> endBefore() : 파라미터로 넣은 정보 이전의 정보를 불러봄
* reference : https://firebase.google.com/docs/firestore/query-data/query-cursors?hl=ko
3. 비동기 처리 시, then() 참고 사항
useEffect(() => {
// res 생략 시
getPosts().then(setPosts);
// res 생략 하지 않을 시
getPosts().then(res => {
setPosts(res);
});
}, []);
4. posts 의 response 데이터 확인
export async function getPosts() {
...
const posts = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
return posts;
}
@ Result
@ Git
- https://github.com/eunbok-bocoder/PublicGalleryBocoder/commits/main
# Source tree
# components > PostCard.js
import React, {useMemo} from 'react';
import {View, StyleSheet, Text, Image, Pressable} from 'react-native';
function PostCard({user, photoURL, description, createdAt, id}) {
const date = useMemo(
() => (createdAt ? new Date(createdAt._seconds * 1000) : new Date()),
[createdAt],
);
const onOpenProfile = () => {
// TODO: 사용자 프로필 화면 열기
};
return (
<View style={styles.block}>
<View style={[styles.head, styles.paddingBlock]}>
<Pressable style={styles.profile} onPress={onOpenProfile}>
<Image
source={
user.photoURL
? {
uri: user.photoURL,
}
: require('../assets/user.png')
}
resizeMode="cover"
style={styles.avatar}
/>
<Text style={styles.displayName}>{user.displayName}</Text>
</Pressable>
</View>
<Image
source={{uri: photoURL}}
style={styles.image}
resizeMethod="resize"
resizeMode="cover"
/>
<View style={styles.paddingBlock}>
<Text style={styles.description}>{description}</Text>
<Text date={date} style={styles.date}>
{date.toLocaleDateString()}
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
block: {
paddingTop: 16,
paddingBottom: 16,
},
avatar: {
width: 32,
height: 32,
borderRadius: 16,
},
paddingBlock: {
paddingHorizontal: 16,
},
head: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 16,
},
profile: {
flexDirection: 'row',
alignItems: 'center',
},
displayName: {
lineHeight: 16,
fontSize: 16,
marginLeft: 8,
fontWeight: 'bold',
},
image: {
backgroundColor: '#bdbdbd',
width: '100%',
aspectRatio: 1,
marginBottom: 16,
},
description: {
fontSize: 16,
lineHeight: 24,
marginBottom: 8,
},
date: {
color: '#757575',
fontSize: 12,
lineHeight: 18,
},
});
export default PostCard;
# index.js
/**
* @format
*/
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import 'react-native-get-random-values';
AppRegistry.registerComponent(appName, () => App);
# lib > posts.js
import firestore from '@react-native-firebase/firestore';
const postsCollection = firestore().collection('posts');
// 포스트 생성
export function createPost({user, photoURL, description}) {
return postsCollection.add({
user,
photoURL,
description,
createdAt: firestore.FieldValue.serverTimestamp(),
});
}
export const PAGE_SIZE = 3;
// 포스트 불러오기
export async function getPosts() {
const snapshot = await postsCollection
.orderBy('createdAt', 'desc') // 시간순 내림차순 정렬
.limit(PAGE_SIZE) // 페이지 수 제한
.get();
const posts = snapshot.docs.map(doc => ({
id: doc.id, // key 추가
...doc.data(),
}));
return posts;
}
// 이전 포스트 불러오기
export async function getOlderPosts(id) {
const cursorDoc = await postsCollection.doc(id).get();
const snapshot = await postsCollection
.orderBy('createdAt', 'desc')
.startAfter(cursorDoc) // 특정 포스트의 id 값 이전에 작성한 포스트를 불러옴
.limit(PAGE_SIZE)
.get();
const posts = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
return posts;
}
// 최신 포스트 불러오기
export async function getNewerPosts(id) {
const cursorDoc = await postsCollection.doc(id).get();
const snapshot = await postsCollection
.orderBy('createdAt', 'desc')
.endBefore(cursorDoc) // 특정 포스트의 id 값 이후에 작성한 포스트를 불러옴
.limit(PAGE_SIZE)
.get();
const posts = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
return posts;
}
# screens > FeedScreen.js
import React, {useEffect, useState} from 'react';
import {
ActivityIndicator,
FlatList,
RefreshContol,
RefreshControl,
StyleSheet,
} from 'react-native';
import PostCard from '../components/PostCard';
import {getNewerPosts, getOlderPosts, getPosts, PAGE_SIZE} from '../lib/posts';
function FeedScreen() {
const [posts, setPosts] = useState(null);
// 마지막 포스트까지 조회했음을 명시하는 상태
const [noMorePost, setNoMorePost] = useState(false);
// refreshing 이 true 일때만 새로고침 표시
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
// 컴포넌트가 처음 마운트될 때 포스트 목록을 조회한 후 'posts' 상태에 담기
getPosts().then(setPosts);
// 동일한 표현
// getPosts().then(res => {
// setPosts(res);
// });
}, []);
const onLoadMore = async () => {
if (noMorePost || !posts || posts.length < PAGE_SIZE) {
return;
}
const lastPost = posts[posts.length - 1];
const olderPosts = await getOlderPosts(lastPost.id);
if (olderPosts.length < PAGE_SIZE) {
setNoMorePost(true);
}
setPosts(posts.concat(olderPosts));
};
const onRefresh = async () => {
refreshing;
if (!posts || posts.length === 0 || refreshing) {
return;
}
const firstPost = posts[0];
setRefreshing(true);
const newerPosts = await getNewerPosts(firstPost.id);
setRefreshing(false);
if (newerPosts.length === 0) {
return;
}
setPosts(newerPosts.concat(posts));
};
return (
<FlatList
data={posts}
renderItem={renderItem}
keyExtractor={item => item.id}
contentContainerStyle={styles.container}
onEndReached={onLoadMore}
onEndReachedThreshold={0.75} // 출발지(0) ~ 끝지점(1), 설정값에 도달 시 onEndReached 실행
ListFooterComponent={
// 모든 항목의 맨 아래 구현할 값
!noMorePost && (
<ActivityIndicator style={styles.spinner} size={32} color="6200ee" />
)
}
refreshControl={
(console.log('[LOG] refresh'),
(<RefreshControl onRefresh={onRefresh} refreshing={refreshing} />))
}
/>
);
}
const renderItem = ({item}) => (
<PostCard
createdAt={item.createdAt}
description={item.description}
id={item.id}
user={item.user}
photoURL={item.photoURL}
/>
);
const styles = StyleSheet.create({
container: {
paddingBottom: 48,
},
spinner: {
height: 64,
},
});
export default FeedScreen;
# screens > UploadScreen.js
import React, {useEffect, useRef, useState, useCallback} from 'react';
import {
StyleSheet,
TextInput,
View,
Animated,
Keyboard,
useWindowDimensions,
Platform,
KeyboardAvoidingView,
} from 'react-native';
import {useNavigation, useRoute} from '@react-navigation/native';
import IconRightButton from '../components/IconRightButton';
import storage from '@react-native-firebase/storage';
import {useUserContext} from '../contexts/UserContext';
import {v4} from 'uuid';
import {createPost} from '../lib/posts';
function UploadScreen() {
const route = useRoute();
if (route) console.log('[route] : ' + JSON.stringify(route));
const {res} = route.params || {};
const {width} = useWindowDimensions();
const animation = useRef(new Animated.Value(width)).current;
const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
const [description, setDescription] = useState('');
const navigation = useNavigation();
const {user} = useUserContext();
const onSubmit = useCallback(async () => {
// TODO: 포스트 작성 로직 구현
navigation.pop();
const asset = res.assets[0];
const extension = asset.fileName.split('.').pop();
const reference = storage().ref(`/photo/${user.id}/${v4()}.${extension}`);
if (Platform.OS === 'android') {
await reference.putString(asset.base64, 'base64', {
contentType: asset.type,
});
} else {
await reference.putFile(asset.uri);
}
const photoURL = await reference.getDownloadURL();
await createPost({description, photoURL, user});
// TODO : 포스트 목록 새로고침
}, [res, user, description, navigation]);
useEffect(() => {
const didShow = Keyboard.addListener('keyboardDidShow', () =>
setIsKeyboardOpen(true),
);
const didHide = Keyboard.addListener('keyboardDidHide', () =>
setIsKeyboardOpen(false),
);
return () => {
didShow.remove();
didHide.remove();
};
}, []); // 컴포넌트가 화면에 나타날 때 이벤트를 등로갛고, 사라질 때 이벤트를 해제해야 하므로 deps 배열은 비워둠
useEffect(() => {
Animated.timing(animation, {
toValue: isKeyboardOpen ? 0 : width,
useNativeDriver: false,
duration: 150,
delay: 100,
}).start();
}, [isKeyboardOpen, width, animation]);
useEffect(() => {
navigation.setOptions({
headerRight: () => <IconRightButton onPress={onSubmit} name="send" />,
});
}, [navigation, onSubmit]);
return (
<KeyboardAvoidingView
behavior={Platform.select({ios: 'height'})}
style={styles.block}
keyboardVerticalOffset={Platform.select({
ios: 180, // iOS 에서 텍스트 입력 시, 'enter'를 많이 입력하면 텍스트가 화면 밖에 보이는 현상 방지
})}>
<View style={styles.block}>
<Animated.Image
source={{uri: res.assets[0]?.uri}}
style={[styles.image, {height: animation}]}
resizeMode="cover"
/>
<TextInput
style={styles.input}
multiline={true}
placeholder="이 사진에 대한 설명을 입력하세요..."
textAlignVertical="top"
value={description}
onChangeText={setDescription}
/>
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
block: {flex: 1},
image: {width: '100%'},
input: {
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: 16,
flex: 1,
fontSize: 16,
},
});
export default UploadScreen;
'React Native > React Native_study' 카테고리의 다른 글
[리액트 네이티브를 다루는 기술 #23] 9장 Firebase로 사진 공유 앱 만들기2 (p .559 ~ 608) (0) | 2022.04.10 |
---|---|
[리액트 네이티브를 다루는 기술 #22] 9장 Firebase로 사진 공유 앱 만들기2 (p .529 ~ 558) (0) | 2022.03.29 |
[리액트 네이티브를 다루는 기술 #20] 9장 Firebase로 사진 공유 앱 만들기2 (p .479 ~ 510) (0) | 2022.03.21 |
[리액트 네이티브를 다루는 기술 #19] 8장 Firebase로 사진 공유 앱 만들기1 (p .466 ~ 478) (0) | 2022.03.16 |
[리액트 네이티브를 다루는 기술 #18] 8장 Firebase로 사진 공유 앱 만들기1 (p .453 ~ 465) (0) | 2022.02.18 |