React Native/React Native_study

[리액트 네이티브를 다루는 기술 #18] 8장 Firebase로 사진 공유 앱 만들기1 (p .453 ~ 465)

bocoder
728x90
반응형

@ 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

iOS / android - welcome 화면 테스트

 

01
iOS / android - Firestore 닉네임 등록 테스트

 

앱에서 createUser(user) 호출 시 Firestore 정상 등록 확인

 

@ 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;

 

728x90
반응형