@ List
* 9.5 사용자 프로필 화면 구현하기
** 9.5.1 Firestore 데이터 조회할 때 조건 추가하기
** 9.5.2 포스트 조회 함수 리팩토링하기
** 9.5.3 Firestore에서 색인 추가하기
** 9.5.4 Profile 컴포넌트 만들기
** 9.5.5 그리드 뷰 만들기
** 9.5.6 페이지네이션 구현하기
** 9.5.7 커스텀 Hook을 작성해 컴포넌트 리팩토링하기
** 9.5.8 포스트 열기
** 9.5.9 내 프로필 화면 구현하기
@ Note
1. default parameter 문법
- 파라미터가 주어지지 않았다면 기본값으로 빈 객체 {} 를 사용하도록 함
- 설정하지 않을 시 구조 분해 과정에서 오류 발생
export async function getPosts({userId, mode, id} = {}) {
...
return posts;
}
2. Firestore에서 색인 추가하기
- 특정 사용자의 포스트를 조회하는 과정에서 user.id 를 찾아서 조회하고 createdAt을 내림차순으로 정렬하고 있는데, 이렇게 특정 조건이 붙고 특정 속성으로 정렬할 때는 Firestore에서 색인(index)을 추가해야 함
3. margin 관련 참고 사항
- 프로필 화면에서 한 열에 3개의 항목이 보여지고, 각 컴포넌트에 0.5의 여백을 주면 추가한 여백이차지하는 크기는 총 3px (컴포넌트 사이에 1px, 화면의 양 끝에 0.5px) 이므로, dimensions.width 에서 3px을 차감함
...
function PostGridItem({post}) {
const size = (dimensions.width - 3) / 3;
...
}
const styles = StyleSheet.create({
block: {margin: 0.5},
...
});
export default PostGridItem;
4. 커스텀 Hook 작성을 통한 리팩토링
- 컴포넌트에서 비슷한 UI 가 반복된다면 컴포넌트를 분리해 재사용하면 되고, 컴포넌트의 로직만 반복된다면 커스텀 Hook을 만들어서 로직을 재사용함
- 컴포넌트와의 차이점으로 커스텀 Hook은 JSX를 반환하지 않음
...
export default function usePosts(userId) {
...
return {posts, noMorePost, refreshing, onLoadMore, onRefresh};
}
5. 셀렉터 함수 (selector function)
- 상태에서 어떤 값을 조회할지 정하는 함수
...
function PostCard({user, photoURL, description, createdAt, id}) {
const routeNames0 = useNavigationState(state => state); // 전체 상태 조회
console.log(routeNames0); // {"index": 0, "key": "stack-...", "routeNames": ["Feed", "Profile", "Post"], "routes": [{"key": "Feed-...", "name": "Feed", "params": undefined}], "stale": false, "type": "stack"} or ...
const routeNames = useNavigationState(state => state.routeNames); // 객체 내부의 routeNames 값만 조회
console.log(routeNames); // ["Feed", "Profile", "Post"] or ["MyProfile", "Post"]
...
}
...
@ Result
@ Git
- https://github.com/eunbok-bocoder/PublicGalleryBocoder/commits/main
# Source tree
* lib > _posts.js : 기존 소스 참고용으로 주석 처리
# components > Avatar.js
import React from 'react';
import {Image} from 'react-native';
function Avatar({source, size, style}) {
return (
<Image
source={source || require('../assets/user.png')}
resizeMode="cover"
style={[
style,
{
width: size,
height: size,
borderRadius: size / 2,
},
]}
/>
);
}
Avatar.defaultProps = {
size: 32,
};
export default Avatar;
# components > PostCard.js
import React, {useMemo} from 'react';
import {View, StyleSheet, Text, Image, Pressable} from 'react-native';
import Avatar from './Avatar';
import {useNavigation, useNavigationState} from '@react-navigation/native';
function PostCard({user, photoURL, description, createdAt, id}) {
const routeNames = useNavigationState(state => state.routeNames);
// console.log(routeNames); // ["Feed", "Profile", "Post"] or ["MyProfile", "Post"]
const date = useMemo(
() => (createdAt ? new Date(createdAt._seconds * 1000) : new Date()),
[createdAt],
);
const navigation = useNavigation();
const onOpenProfile = () => {
// MyProfile이 존재하는지 확인
if (routeNames.find(routeName => routeName === 'MyProfile')) {
navigation.navigate('MyProfile');
} else {
navigation.navigate('Profile', {
userId: user.id,
displayName: user.displayName,
});
}
};
return (
<View style={styles.block}>
<View style={[styles.head, styles.paddingBlock]}>
<Pressable style={styles.profile} onPress={onOpenProfile}>
<Avatar source={user.photoURL && {uri: user.photoURL}} />
<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,
},
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;
# components > PostGridItem.js
import {useNavigation} from '@react-navigation/native';
import React from 'react';
import {StyleSheet, useWindowDimensions, Image, Pressable} from 'react-native';
function PostGridItem({post}) {
const dimensions = useWindowDimensions();
const size = (dimensions.width - 3) / 3;
const navigation = useNavigation();
const onPress = () => {
// TODO: 단일 포스트 조회 화면 띄우기
navigation.navigate('Post', {post});
};
return (
<Pressable
onPress={onPress}
style={({pressed}) => [
{
opacity: pressed ? 0.6 : 1,
width: size,
height: size,
},
styles.block,
]}>
<Image
style={styles.image}
source={{uri: post.photoURL}}
resizeMethod="resize"
resizeMode="cover"
/>
</Pressable>
);
}
const styles = StyleSheet.create({
block: {margin: 0.5},
image: {
backgroundColor: '#bdbdbd',
width: '100%',
height: '100%',
},
});
export default PostGridItem;
# components > Profile.js
import React, {useEffect, useState} from 'react';
import {
ActivityIndicator,
FlatList,
StyleSheet,
Text,
View,
RefreshControl,
} from 'react-native';
import {getUser} from '../lib/users';
import Avatar from './Avatar';
import PostGridItem from './PostGridItem';
// import {getNewerPosts, getOlderPosts, getPosts, PAGE_SIZE} from '../lib/posts';
import usePosts from '../hooks/usePosts';
function Profile({userId}) {
const [user, setUser] = useState(null);
const {posts, noMorePost, refreshing, onLoadMore, onRefresh} =
usePosts(userId);
// const [posts, setPosts] = useState(null);
// const [noMorePost, setNoMorePost] = useState(false);
// const [refreshing, setRefreshing] = useState(false);
// const onLoadMore = async () => {
// if (noMorePost || !posts || posts.length < PAGE_SIZE) {
// return;
// }
// const lastPost = posts[posts.length - 1];
// const olderPosts = await getOlderPosts(lastPost.id, userId);
// if (olderPosts.length < PAGE_SIZE) {
// setNoMorePost(true);
// }
// setPosts(posts.concat(olderPosts));
// };
// const onRefresh = async () => {
// if (!posts || posts.length === 0 || refreshing) {
// return;
// }
// const firestPost = posts[0];
// setRefreshing(true);
// const newerPosts = await getNewerPosts(firestPost.id, userId);
// setRefreshing(false);
// if (newerPosts.length === 0) {
// return;
// }
// setPosts(newerPosts.concat(posts));
// };
useEffect(() => {
getUser(userId).then(setUser);
// getPosts({userId}).then(setPosts);
}, [userId]);
if (!user || !posts) {
return (
<ActivityIndicator style={styles.spinner} size={32} color="#6200ee" />
);
}
return (
<FlatList
style={styles.block}
data={posts}
renderItem={renderItem}
numColumns={3}
keyExtractor={item => item.id}
ListHeaderComponent={
<View style={styles.userInfo}>
<Avatar source={user.photoURL && {uri: user.photoURL}} size={128} />
<Text styles={styles.username}>{user.displayName}</Text>
</View>
}
onEndReached={onLoadMore}
onEndReachedThreshold={0.25}
ListFooterComponent={
!noMorePost && (
<ActivityIndicator
style={styles.bottomSpinner}
size={32}
color="#6200ee"
/>
)
}
refreshControl={
<RefreshControl onRefresh={onRefresh} refreshing={refreshing} />
}
/>
);
}
const renderItem = ({item}) => <PostGridItem post={item} />;
const styles = StyleSheet.create({
spinner: {
flex: 1,
justifyContent: 'center',
},
block: {
flex: 1,
},
userInfo: {
paddingTop: 80,
paddingBottom: 64,
alignItems: 'center',
},
username: {
matginTop: 8,
fontSize: 24,
color: '#424242',
},
bottomSpineer: {
height: 128,
},
});
export default Profile;
# components > SetupProfile.js
import {useNavigation, useRoute} from '@react-navigation/core';
import React, {useState} from 'react';
import {
Image,
Pressable,
StyleSheet,
View,
Platform,
ActivityIndicator,
} from 'react-native';
import {signOut} from '../lib/auth';
import {createUser} from '../lib/users';
import BorderedInput from './BorderedInput';
import CustomButton from './CustomButton';
import {useUserContext} from '../contexts/UserContext';
import {launchImageLibrary} from 'react-native-image-picker';
import storage from '@react-native-firebase/storage';
import Avatar from './Avatar';
function SetupProfile() {
const [displayName, setDisplayName] = useState('');
const navigation = useNavigation();
const {setUser} = useUserContext();
const {params} = useRoute();
const {uid} = params || {};
const [response, setResponse] = useState(null);
const [loading, setLoading] = useState(false);
const onSubmit = async () => {
setLoading(true);
let photoURL = null;
if (response) {
const asset = response.assets[0];
const extension = asset.fileName.split('.').pop(); // 확장자 추출
const reference = storage().ref(`/profile/${uid}.${extension}`);
if (Platform.OS === 'android') {
await reference.putString(asset.base64, 'base64', {
contentType: asset.type,
});
} else {
await reference.putFile(asset.uri);
}
photoURL = response ? await reference.getDownloadURL() : null;
}
const user = {
id: uid,
displayName,
photoURL,
};
createUser(user);
setUser(user);
};
const onCancel = () => {
signOut();
navigation.goBack();
};
const onSelectImage = () => {
launchImageLibrary(
{
mediaType: 'photo',
maxWidth: 512,
maxHeight: 512,
// android의 경우 uri에서 직접 파일을 읽는 과정에서 권한 오류가 발생 할 수 있기 때문에 설정
includeBase64: Platform.OS === 'android',
},
res => {
if (res.didCancel) {
//취소했을 경우
return;
}
// console.log(res);
setResponse(res);
},
);
};
return (
<View style={styles.block}>
<Pressable onPress={onSelectImage}>
<Avatar
source={response && {uri: response.assets[0].uri}}
style={styles.circle}
size={128}
/>
</Pressable>
<View style={styles.form}>
<BorderedInput
placeholder="닉네임"
value={displayName}
onChangeText={setDisplayName}
onSubmitEditing={onSubmit}
returnKeyType="next"
/>
{loading ? (
<ActivityIndicator size={32} color="#6200ee" style={styles.spinner} />
) : (
<View style={styles.buttons}>
<CustomButton title="다음" onPress={onSubmit} hasMarginbottom />
<CustomButton title="취소" onPress={onCancel} theme="secondary" />
</View>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
block: {
alignItems: 'center',
marginTop: 24,
paddingHorizontal: 16,
width: '100%',
},
circle: {
backgroundColor: '#cdcdcd',
borderRadius: 64,
width: 128,
height: 128,
},
form: {
marginTop: 16,
width: '100%',
},
buttons: {
marginTop: 48,
},
});
export default SetupProfile;
# hooks > usePosts.js
import {useEffect, useState} from 'react';
import {getNewerPosts, getOlderPosts, getPosts, PAGE_SIZE} from '../lib/posts';
export default function usePosts(userId) {
const [posts, setPosts] = useState(null);
const [noMorePost, setNoMorePost] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const onLoadMore = async () => {
if (noMorePost || !posts || posts.length < PAGE_SIZE) {
return;
}
const lastPost = posts[posts.length - 1];
const olderPosts = await getOlderPosts(lastPost.id, userId);
if (olderPosts.length < PAGE_SIZE) {
setNoMorePost(true);
}
setPosts(posts.concat(olderPosts));
};
const onRefresh = async () => {
if (!posts || posts.length === 0 || refreshing) {
return;
}
const firstPost = posts[0];
setRefreshing(true);
const newerPosts = await getNewerPosts(firstPost.id, userId);
setRefreshing(false);
if (newerPosts.length === 0) {
return;
}
setPosts(newerPosts.concat(posts));
};
useEffect(() => {
getPosts({userId}).then(_posts => {
setPosts(_posts);
if (_posts.length < PAGE_SIZE) {
setNoMorePost(true);
}
});
}, [userId]);
return {posts, noMorePost, refreshing, onLoadMore, onRefresh};
}
# lib > _posts.js
//before refactoring
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 = 12;
// 포스트 불러오기
export async function getPosts(userId) {
// const snapshot = await postsCollection
// .orderBy('createdAt', 'desc') // 시간순 내림차순 정렬
// .limit(PAGE_SIZE) // 페이지 수 제한
// .get();
let query = postsCollection.orderBy('createdAt', 'desc').limit(PAGE_SIZE);
if (userId) {
query = query.where('user.id', '==', userId);
}
const snapshot = await query.get();
const posts = snapshot.docs.map(doc => ({
id: doc.id, // key 추가
...doc.data(),
}));
return posts;
}
// 이전 포스트 불러오기
export async function getOlderPosts(id, userId) {
const cursorDoc = await postsCollection.doc(id).get();
// const snapshot = await postsCollection
// .orderBy('createdAt', 'desc')
// .startAfter(cursorDoc) // 특정 포스트의 id 값 이전에 작성한 포스트를 불러옴
// .limit(PAGE_SIZE)
// .get();
let query = postsCollection
.orderBy('createdAt', 'desc')
.startAfter(cursorDoc)
.limit(PAGE_SIZE);
if (userId) {
query = query.where('user.id', '==', userId);
}
const snapshot = await query.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();
let query = postsCollection
.orderBy('createdAt', 'desc')
.endBefore(cursorDoc)
.limit(PAGE_SIZE);
if (userId) {
query = query.where('user.id', '==', userId);
}
const snapshot = await query.get();
const posts = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
return posts;
}
# 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 = 12;
// 포스트 불러오기
export async function getPosts({userId, mode, id} = {}) {
let query = postsCollection.orderBy('createdAt', 'desc').limit(PAGE_SIZE);
if (userId) {
query = query.where('user.id', '==', userId);
}
if (id) {
const cursorDoc = await postsCollection.doc(id).get();
query =
mode === 'older'
? query.startAfter(cursorDoc)
: query.endBefore(cursorDoc);
}
const snapshot = await query.get();
const posts = snapshot.docs.map(doc => ({
id: doc.id, // key 추가
...doc.data(),
}));
return posts;
}
// 이전 포스트 불러오기
export async function getOlderPosts(id, userId) {
return getPosts({
id,
mode: 'older',
userId,
});
}
// 최신 포스트 불러오기
export async function getNewerPosts(id, userId) {
return getPosts({
id,
mode: 'newer',
userId,
});
}
# 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';
import usePosts from '../hooks/usePosts';
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 () => {
// 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));
// };
const {posts, noMorePost, refreshing, onLoadMore, onRefresh} = usePosts();
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={
<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 > HomeStack.js
import React from 'react';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import FeedScreen from './FeedScreen';
import ProfileScreen from './ProfileScreen';
import PostScreen from './PostScreen';
const Stack = createNativeStackNavigator();
function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Feed" component={FeedScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen
name="Post"
component={PostScreen}
options={{title: '게시물'}}
/>
</Stack.Navigator>
);
}
export default HomeStack;
# screens > MyProfileScreen.js
import {useNavigation} from '@react-navigation/native';
import React from 'react';
import {useEffect} from 'react';
import Profile from '../components/Profile';
import {useUserContext} from '../contexts/UserContext';
function MyProfileScreen() {
const {user} = useUserContext();
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({
title: user.displayName,
});
}, [navigation, user]);
return <Profile userId={user.id} />;
}
export default MyProfileScreen;
# screens > MyProfileStack.js
import React from 'react';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import MyProfileScreen from './MyProfileScreen';
import PostScreen from './PostScreen';
const Stack = createNativeStackNavigator();
function MyProfileStack() {
return (
<Stack.Navigator>
<Stack.Screen name="MyProfile" component={MyProfileScreen} />
<Stack.Screen
name="Post"
component={PostScreen}
options={{title: '게시물'}}
/>
</Stack.Navigator>
);
}
export default MyProfileStack;
# screens > PostScreen.js
import {useRoute} from '@react-navigation/native';
import React from 'react';
import {ScrollView, StyleSheet} from 'react-native';
import PostCard from '../components/PostCard';
function PostScreen() {
const route = useRoute();
const {post} = route.params;
return (
<ScrollView contentContainerStyle={StyleSheet.contentContainer}>
<PostCard
user={post.user}
photoURL={post.photoURL}
description={post.description}
createdAt={post.createdAt}
id={post.id}
/>
</ScrollView>
);
}
const styles = StyleSheet.create({
block: {flex: 1},
contentContainer: {
paddingBottom: 40,
},
});
export default PostScreen;
# screens > ProfileScreen.js
import {useNavigation, useRoute} from '@react-navigation/native';
import React from 'react';
import {useEffect} from 'react';
import Profile from '../components/Profile';
function ProfileScreen() {
const route = useRoute();
const navigation = useNavigation();
const {userId, displayName} = route.params ?? {};
useEffect(() => {
navigation.setOptions({
title: displayName,
});
}, [navigation, displayName]);
return <Profile userId={userId} />;
}
export default ProfileScreen;
# screens > SignInScreen.js
import React, {useState, useEffect} from 'react';
import {
Alert,
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
View,
} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import SignButtons from '../components/SignButton';
import SignForm from '../components/SignForm';
import {signIn, signUp} from '../lib/auth';
import {getUser} from '../lib/users';
import {useUserContext} from '../contexts/UserContext';
function SignInScreen({navigation, route}) {
useEffect(() => {
return () => setLoading(false); // unmounted 시에 cleanup 기능 (하단 finally{} 구문의 에러 디버깅)
}, []);
const {isSignUp} = route.params ?? {}; // null 병합 연산자
const [form, setForm] = useState({
email: '',
password: '',
confirmPassword: '',
});
const [loading, setLoading] = useState();
const {setUser} = useUserContext();
const createChangeTextHandler = name => value => {
setForm({...form, [name]: value});
};
const onSubmit = async () => {
Keyboard.dismiss();
// console.log('[Log] form : ', form);
const {email, password, confirmPassword} = form;
if (isSignUp && password !== confirmPassword) {
Alert.alert('실패', '비밀번호가 일치하지 않습니다.');
return;
}
if (email == '') return Alert.alert('실패', '이메일을 입력해주세요.');
else if (password == '')
return Alert.alert('실패', '비밀번호를 입력해주세요.');
setLoading(true);
const info = {email, password};
// console.log('[LOG] info : ', info);
try {
const {user} = isSignUp ? await signUp(info) : await signIn(info);
console.log('[LOG] user : ', user);
const profile = await getUser(user.uid);
if (!profile) {
console.log('[LOG] profile : ', profile); // "undefined"
navigation.navigate('Welcome', {uid: user.uid});
} else {
console.log('[LOG] profile : ', profile);
setUser(profile);
}
} catch (e) {
const message = {
'auth/email-already-in-use': '이미 가입된 이메일입니다.',
'auth/wrong-password': '잘못된 비밀번호입니다.',
'auth/user-not-found': '존재하지 않는 계정입니다.',
'auth/invalid-email': '유효하지 않은 이메일 주소입니다.',
};
const msg = message[e.code] || `${isSignUp ? '가입' : '로그인'} 실패`;
Alert.alert('실패', msg);
console.log('[Log] error :', e);
} finally {
setLoading(false); // 에러발생 : "Can't perform a React state update on an unmounted component"
}
};
return (
<KeyboardAvoidingView
style={styles.KeyboardAvoidingView}
behavior={Platform.select({ios: 'padding'})}>
<SafeAreaView style={styles.fullscreen}>
<Text style={styles.text}>PublicGallery</Text>
<View style={styles.form}>
<SignForm
isSignUp={isSignUp}
onSubmit={onSubmit}
form={form}
createChangeTextHandler={createChangeTextHandler}
/>
<SignButtons
isSignUp={isSignUp}
onSubmit={onSubmit}
loading={loading}
/>
</View>
</SafeAreaView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
KeyboardAvoidingView: {
flex: 1,
},
fullscreen: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 32,
fontWeight: 'bold',
},
form: {
marginTop: 64,
width: '100%',
paddingHorizontal: 16,
},
// buttons: {
// marginTop: 64,
// },
});
export default SignInScreen;
'React Native > React Native_study' 카테고리의 다른 글
[리액트 네이티브를 다루는 기술 #25] 11장 네이티브 UI 컴포넌트 사용하기 (p .645 ~ 682) (0) | 2022.04.19 |
---|---|
[리액트 네이티브를 다루는 기술 #23] 9장 Firebase로 사진 공유 앱 만들기2 (p .559 ~ 608) (0) | 2022.04.10 |
[리액트 네이티브를 다루는 기술 #21] 9장 Firebase로 사진 공유 앱 만들기2 (p .511 ~ 528) (0) | 2022.03.23 |
[리액트 네이티브를 다루는 기술 #20] 9장 Firebase로 사진 공유 앱 만들기2 (p .479 ~ 510) (0) | 2022.03.21 |
[리액트 네이티브를 다루는 기술 #19] 8장 Firebase로 사진 공유 앱 만들기1 (p .466 ~ 478) (0) | 2022.03.16 |