@ List
* 8.3 Firebase에 회원 정보 등록하기
* 8.4 UserContext 만들고 로그인 사용자 분기 처리하기
@ Note
1. createContext API 의 사용
- reference : https://ko.reactjs.org/docs/context.html#reactcreatecontext
2. 로그인 관련 setUser를 호출 하는 시점
- 프로필이 등록된 계정으로 로그인했을 때
- Welcome 화면에서 프로필 정보를 등록했을 때
- 앱을 새로 켜서 로그인 상태가 유지됐을 때
3. user 상태에 따른 화면 분기 처리
- SignIn 및 Welcome 화면에서 MainTab 화면으로 전환 시, navigation 또는 push 로 하면 안됨
- 스택에 기존 로그인 또는 프로필 등록 화면이 남아있으므로, 뒤로가기 버튼 클릭 시 화면이 다시 노출될 수 있음
> 방법 1) RootStack에서 아에 불필요한 화면들을 제거하는 방법 권장
> 방법 2) 화면을 띄운 후 navigation의 reset 메서드를 사용하는 방법이 있으나 권자하지 않음
> reference : https://reactnavigation.org/docs/navigation-prop/#reset
4. 비동기처리 시 useEffect의 cleanup function 의 사용
- 책의 내용을 그대로 실습하다보면 로그인 완료 시 "MainTb" 화면으로 전환되는데, 이때 아래 에러가 발생한다
- 시뮬레이터 상에는 Warning 으로 표현되지만, Metro 에서는 에러이므로 디버깅 후 진행
> 디버깅 방안 : https://bocoder.tistory.com/79
@ Result
@ Git
- https://github.com/eunbok-bocoder/PublicGalleryBocoder/commits/main
# Source tree
* user.js 수정 이유 : https://bocoder.tistory.com/78 에서 코드 실수로 놓침
# App.js
import React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import RootStack from './screens/RootStack';
import {UserContextProvider} from './contexts/UserContext';
function App() {
return (
<UserContextProvider>
<NavigationContainer>
<RootStack />
</NavigationContainer>
</UserContextProvider>
);
}
export default App;
# components > SetupProfile.js
import {useNavigation, useRoute} from '@react-navigation/core';
import React, {useState} from 'react';
import {StyleSheet, View} 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';
function SetupProfile() {
const [displayName, setDisplayName] = useState('');
const navigation = useNavigation();
const {setUser} = useUserContext();
const {params} = useRoute();
const {uid} = params || {};
const onSubmit = () => {
const user = {
id: uid,
displayName,
photoURL: null,
};
createUser(user);
setUser(user);
};
const onCancel = () => {
signOut();
navigation.goBack();
};
return (
<View style={styles.block}>
<View style={styles.circle} />
<View style={styles.form}>
<BorderedInput
placeholder="닉네임"
value={displayName}
onChangeText={setDisplayName}
onSubmitEditing={onSubmit}
returnKeyType="next"
/>
<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;
# contexts > UserContext.js
import React, {useContext, createContext, useState} from 'react';
const UserContext = createContext(null);
export function UserContextProvider({children}) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider
children={children}
value={{
user,
setUser,
}}
/>
);
}
export function useUserContext() {
const userContext = useContext(UserContext);
if (!userContext) {
throw new Error('UserContext.Provider is not found');
}
return userContext;
}
# lib > users.js
import firestore from '@react-native-firebase/firestore';
export const usersCollection = firestore().collection('users');
export function createUser({id, displayName, photoURL}) {
return usersCollection.doc(id).set({
id,
displayName,
photoURL,
});
}
export async function getUser(id) {
const doc = await usersCollection.doc(id).get();
return doc.data(); // 추가
}
# screens > MainTab.js
import React from 'react';
import {StyleSheet, Text, View} from 'react-native';
import {useUserContext} from '../contexts/UserContext';
function MainTab() {
const {user} = useUserContext();
return (
<View style={styles.block}>
<Text style={styles.text}>Hello, {user.displayName}</Text>
</View>
);
}
const styles = StyleSheet.create({
block: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 24,
},
});
export default MainTab;
# screens > RootStack.js
import React from 'react';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import SignInScreen from './SignInScreen';
import WelcomeScreen from './WelcomeScreen';
import {useUserContext} from '../contexts/UserContext';
import MainTab from './MainTab';
const Stack = createNativeStackNavigator();
function RootStack() {
const {user} = useUserContext();
return (
<Stack.Navigator>
{user ? (
<>
<Stack.Screen
name="MainTab"
component={MainTab}
options={{headerShown: false}}
/>
</>
) : (
<>
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{headerShown: false}}
/>
<Stack.Screen
name="Welcome"
component={WelcomeScreen}
options={{headerShown: false}}
/>
</>
)}
</Stack.Navigator>
);
}
export default RootStack;
# 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;
}
setLoading(true);
const info = {email, password};
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;
# screens > WelcomeScreen.js
import React from 'react';
import {KeyboardAvoidingView, Platform, StyleSheet, Text} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import SetupProfile from '../components/SetupProfile';
function WelcomeScreen() {
return (
<KeyboardAvoidingView
style={styles.KeyboardAvoidingView}
behavior={Platform.select({ios: 'padding'})}>
<SafeAreaView style={styles.block}>
<Text style={styles.title}>환영합니다!</Text>
<Text style={styles.description}>프로필을 설정하세요.</Text>
<SetupProfile />
</SafeAreaView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
KeyboardAvoidingView: {
flex: 1,
},
block: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 48,
},
description: {
marginTop: 16,
fontSize: 21,
color: '#757575',
},
});
export default WelcomeScreen;
'React Native > React Native_study' 카테고리의 다른 글
[리액트 네이티브를 다루는 기술 #20] 9장 Firebase로 사진 공유 앱 만들기2 (p .479 ~ 510) (0) | 2022.03.21 |
---|---|
[리액트 네이티브를 다루는 기술 #19] 8장 Firebase로 사진 공유 앱 만들기1 (p .466 ~ 478) (0) | 2022.03.16 |
[리액트 네이티브를 다루는 기술 #17] 8장 Firebase로 사진 공유 앱 만들기1 (p .445 ~ 452) (0) | 2022.02.15 |
[리액트 네이티브를 다루는 기술 #16] 8장 Firebase로 사진 공유 앱 만들기1 (p .419 ~ 444) (0) | 2022.01.26 |
[리액트 네이티브를 다루는 기술 #15] 8장 Firebase로 사진 공유 앱 만들기1 (p .407 ~ 418) (0) | 2022.01.20 |