프로젝트/할일목록 앱 (RN)

16. 전체 로그인 로직 수정하기

syleemomo 2023. 10. 27. 11:03
728x90

* 로그인 로직의 현재 문제점과 고려할 부분 

안드로이드의 로컬 스토리지(AsyncStorage)는 사용하지 않는다. 

=> 이유는 사용자가 로그인후 앱을 껐다 키면 로그인이 되어 있는 상태이나 로컬 스토리지에 저장된 사용자 정보는 사라지기 때문에 로그인이 되어있더라도 사용자 정보가 없는 문제가 발생한다. 

{!isLoggedIn && <Stack.Screen name="Landing" component={LandingScreen}/>}

 stackRouter 컴포넌트에서 해당 부분이 문제가 된다. 사용자가 로그인하기 전에는 isLoggedIn 상태가 false 이므로 로딩시 랜딩페이지도 함께 로딩된다. 하지만 사용자가 앱을 종료하고 다시 시작하면 isLoggedIn 은 true 가 되기 때문에 로딩시 랜딩페이지는 로딩되지 않는다. 이렇게 되면 사용자가 로그인후 곧바로 로그아웃을 할때는 랜딩페이지가 메모리에 남아있기 때문에 (로딩이 되었으므로) 아래와 같이 navigation.navigate('Landing') 을 실행하면 "Landing" 이라는 값이 존재한다. 그러나 사용자가 로그아웃하지 않고 앱을 종료했다가 다시 시작하는 경우에는 랜딩페이지는 로딩되지 않으므로 (isLoggedIn 상태가 true 이므로) 아래와 같이 navigation.navigate('Landing') 을 실행하면 "Landing" 이라는 값이 없다는 에러가 발생한다.

const signOutWithGoogle = async () => {
    try {
      await GoogleSignin.signOut()
      setUserInfo(null)
      navigation.navigate('Landing')
    } catch (error) {
      console.error('failed to logout, error: ', error)
    }
  }

 

* 로그인 로직 수정하기

import React from 'react'
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

import LandingScreen from './screens/LandingScreen';
import App from './App'

const Stack = createNativeStackNavigator()

function stackRouter(){
    return (
        <NavigationContainer>
            <Stack.Navigator 
                initialRouteName="Landing"
                screenOptions={{headerShown: false}} // 탭메뉴 헤더와 겹치지 않도록 함
            >
                <Stack.Screen name="Landing" component={LandingScreen}/>
                <Stack.Screen name="App" component={App}/>
            </Stack.Navigator>
        </NavigationContainer>
    )
}
export default stackRouter

stackRouter 컴포넌트를 위와 같이 수정한다. 

import React, { useState, useEffect } from 'react'

기존에는 리액트 훅을 사용하였지만 현재는 사용하지 않을 예정이므로 모두 제거한다.

import React from 'react'

구글 로그인 라이브러리도 해당 컴포넌트에서 사용하지 않을 예정이므로 제거한다. 

import { GoogleSignin } from '@react-native-google-signin/google-signin'

해당 코드는 stackRouter 컴포넌트에서 삭제한다. 이유는 로그인 여부를 판별하는 코드는 랜딩페이지에서 수행할 예정이기 때문이다. 

const [isLoggedIn, setIsLoggedIn] = useState(null)
    checkLoginState = async () => {
        const isSignedIn = await GoogleSignin.isSignedIn()
        setIsLoggedIn(isSignedIn)
    }
    useEffect(() => {
        checkLoginState()
    }, [])

 

{!isLoggedIn && <Stack.Screen name="Landing" component={LandingScreen}/>}

기존 코드를 아래와 같이 수정한다. 로그인 여부와 별개로 로그아웃시 다시 랜딩페이지로 전환되어야 하므로 무조건 로딩이 되도록 한다. 

<Stack.Screen name="Landing" component={LandingScreen}/>

 

* 랜딩페이지에서 이미 로그인이 된 상태이면 홈화면으로 이동하기

import React, { useState, useEffect } from 'react'
import { SafeAreaView, View, Text, StyleSheet, StatusBar, ScrollView, Dimensions, ImageBackground  } from 'react-native'
import LandingPage from '../components/LandingPage'
import landingData from '../data/landingData'
import LoginButton from '../components/LoginButton';

import { GoogleSignin } from '@react-native-google-signin/google-signin'

function LandingScreen({navigation}){
    const { width, height } = Dimensions.get('window')
    const [currentPageIndex, setCurrentPageIndex] = useState(0)
    console.log('페이지 번호: ', currentPageIndex)

    const setCurrentPage = (e) => {
        const { x } = e.nativeEvent.contentOffset // x : 스크롤 위치
        console.log("스크롤 위치: ", x, "화면너비: ", width)
        const nextPageIndex = Math.ceil(x / width) // x / width : 스크롤 위치 / 화면너비 -> 페이지번호
        console.log(nextPageIndex)
        if(nextPageIndex !== currentPageIndex){
            setCurrentPageIndex(nextPageIndex)
        }
    }

    const getUserInfo = async () => await GoogleSignin.getCurrentUser()
    useEffect(() => {
        const user = getUserInfo()
        if(user){
            navigation.navigate('App')
        }
    }, [])

    return (
        <>
            <StatusBar hidden></StatusBar>
            <SafeAreaView style={styles.block}>
                <ScrollView
                    style={{ flex: 1 }}
                    horizontal={true} // 수평 스크롤링
                    scrollEventThrottle={16} // 스크롤 이벤트 감지하는 시간간격 (ms)
                    pagingEnabled={true} // 스크롤시 페이지네이션
                    showsHorizontalScrollIndicator={false} // 스크롤바 숨기기
                    onScroll={setCurrentPage}
                >
                    {landingData.map((page, index) => (
                        <LandingPage
                            width={width}
                            height={height}
                            {...page}
                            key={index}
                        />
                    ))}
                </ScrollView>

                <View style={styles.scrollIndicatorWrapper}>
                    {Array(3).fill(0).map((_, index) => (
                        <View key={index} style={[styles.scrollIndicator, { opacity: currentPageIndex === index ? 1: 0.3}]}></View>
                    ))}
                </View>

                <LoginButton navigation={navigation}/>
            </SafeAreaView>
        </>
    )
}

const styles = StyleSheet.create({
  block: {
    flex: 1
  },
  scrollIndicatorWrapper:{
    position: 'absolute',
    left: 0, right: 0,
    bottom: 50,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center'
  },
  scrollIndicator: {
    height: 10,
    width: 10,
    borderRadius: 10 / 2,
    backgroundColor: '#aaa',
    marginLeft: 10,
  },

})
export default LandingScreen

screens > LandingScreen.js 파일을 위와 같이 수정한다. 

import React, { useState, useEffect } from 'react'

useEffect 훅을 사용하기 위하여 임포트한다. 

import { GoogleSignin } from '@react-native-google-signin/google-signin'

로그인 상태를 조회하기 위하여 구글 라이브러리를 임포트한다. 

const getUserInfo = async () => await GoogleSignin.getCurrentUser()
    useEffect(() => {
        const user = getUserInfo()
        if(user){
            navigation.navigate('App')
        }
    }, [])

로그인 상태를 판별하고 로그인이 된 상태이면 홈화면(App)으로 이동한다. getCurrentUser 메서드는 사용자정보를 조회하는 코드이다. 사용자정보가 존재하면 이미 로그인되어 있다는 의미이므로 곧바로 홈화면으로 이동하면 된다. 로그인 되어 있지 않은 경우에는 랜딩페이지가 화면에 보여진다. 

 

* Settings 화면 수정하기 

import React, { useEffect, useState } from 'react'
import { SafeAreaView, View, Text, StyleSheet, StatusBar, Button, Image, TouchableHighlight } from 'react-native'
import { GoogleSignin } from '@react-native-google-signin/google-signin'
import auth from '@react-native-firebase/auth'

function SettingsScreen({ navigation }){
  const [userInfo, setUserInfo] = useState(null) 
 
  const googleSigninConfigure = () => { 
    GoogleSignin.configure({
      webClientId:
        '137262950194-gcouccatiffjp4ei8aqfi05uruv5j4dl.apps.googleusercontent.com',
    })
  }
  
  const signOutWithGoogle = async () => {
    try {
      await GoogleSignin.signOut()
      setUserInfo(null)
      navigation.navigate('Landing')
    } catch (error) {
      console.error('failed to logout, error: ', error)
    }
  }

  getCurrentUser = async () => {
    const currentUser = await GoogleSignin.getCurrentUser()
    setUserInfo(currentUser)
  }

  useEffect(() => {
    googleSigninConfigure()
    getCurrentUser()
  }, [])

  return (
    <SafeAreaView style={styles.block}>
      <StatusBar backgroundColor="#a8c8ffff"></StatusBar>
      <View>
        {userInfo && userInfo.user && 
        (<View style={styles.profileInfo}>
          <View>
            <Image source={{uri: userInfo.user.photo}} style={styles.profileImg}/>
          </View>
          <View style={styles.profileText}>
            <Text style={[styles.info, { fontWeight: 'bold', fontSize: 20 }]}>{userInfo.user.name}</Text>
            <Text style={styles.info}>{userInfo.user.email}</Text>
          </View>
        </View>)
        }
      </View>
      <TouchableHighlight onPress={signOutWithGoogle} style={styles.logoutBtnWrapper}>
          <View style={[styles.logoutBtn, { backgroundColor: "#a8c8ffff" }]}>
            <Text style={styles.logoutBtnText}>로그아웃</Text>
          </View>
      </TouchableHighlight>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  block: {
    flex: 1,
    justifyContent: 'flex-start',
  },
  profileInfo: {
    marginHorizontal: 'auto',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 20,
    backgroundColor: '#eee',
  },
  profileText: {
    borderRadius: 10,
    padding: 20,
  },
  info: {
    fontSize: 15,
    fontWeight: 'bold'
  },
  profileImg: {
    width: 50,
    height: 50,
    borderRadius: 25,
    marginLeft: 'auto',
    marginRight: 'auto',
  },
  logoutBtnWrapper: {
    flexDirection: 'row',
    position: 'absolute',
    bottom: 0
  },
  logoutBtn: {
    flex: 1,
    height: 35,
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
  },
  logoutBtnText: {
    color: '#fff',
    letterSpacing: 3,
    fontWeight: 'bold',
    fontSize: 15,
    textAlign: 'center',
  },
  
})
export default SettingsScreen

screens > SettingsScreen.js 파일을 위와 같이 수정한다. 

import { GoogleSignin, GoogleSigninButton  } from '@react-native-google-signin/google-signin'

기존에는 설정화면에 구글로그인 버튼이 있었지만 랜딩페이지로 옮겼기 때문에 설정화면에는 로그인 버튼이 필요없다. 그래서 아래와 같이 해당 컴포넌트는 임포트하지 않는다. 

import { GoogleSignin } from '@react-native-google-signin/google-signin'

로그인 여부 판별은 랜딩페이지에서 하기 때문에 관련된 state 를 해당 컴포넌트에서 정의할 필요가 없다. 그래서 아래 코드는 삭제하도록 한다. 

const [isSigningIn, setIsSigningIn] = useState(false) // 로그인 여부

소셜로그인을 진행하는 아래 코드도 components > LoginButton.js 에서 수행하기 때문에 해당 컴포넌트에서는 제거하도록 한다. 

const signInWithGoogle = async () => {
    try {
      await GoogleSignin.hasPlayServices()
      const userInfoFromGoogle = await GoogleSignin.signIn()
      if(userInfoFromGoogle){
        console.log("사용자 사진: ", userInfoFromGoogle.user.photo)
        setUserInfo(userInfoFromGoogle)
        setIsSigningIn(true)
      }
      
    } catch (error) {
      if (error.code === statusCodes.SIGN_IN_CANCELLED) {
        console.log('user cancelled the login flow')
      } else if (error.code === statusCodes.IN_PROGRESS) {
        console.log('sign in is in progress already')
      } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
        console.log('play services not available or outdated')
      } else {
        console.log('some other error happened')
      }
    }
  }

 

const signOutWithGoogle = async () => {
    try {
      await GoogleSignin.signOut()
      setUserInfo(null)
      setIsSigningIn(false) // 삭제
    } catch (error) {
      console.error('failed to logout, error: ', error)
    }
  }

로그인 여부에 대한 상태를 변경하는 setIsSigningIn 함수는 필요없기 때문에 삭제한다. 그리고 아래와 같이 로그아웃 후 랜딩페이지로 이동하도록 아래와 같이 수정한다. 

const signOutWithGoogle = async () => {
    try {
      await GoogleSignin.signOut()
      setUserInfo(null)
      navigation.navigate('Landing') // 수정
    } catch (error) {
      console.error('failed to logout, error: ', error)
    }
  }

구글 로그인 라이브러리를 이용하여 로그인된 사용자정보를 조회한다. 그리고 해당 컴포넌트에서 사용자 정보를 화면에 렌더링하기 위하여 setUserInfo 함수로 userInfo 상태를 변경해준다. 

getCurrentUser = async () => {
    const currentUser = await GoogleSignin.getCurrentUser()
    setUserInfo(currentUser)
  }

 

useEffect(() => {
    googleSigninConfigure()
  }, [])

기존의 코드를 아래와 같이 수정한다. 설정화면이 화면에 렌더링될때 사용자정보를 조회한다. 

useEffect(() => {
    googleSigninConfigure()
    getCurrentUser() // 추가
  }, [])

구글 로그인 버튼은 랜딩페이지로 옮겼기 때문에 이제 설정화면에는 필요가 없다. 그래서 해당 컴포넌트는 삭제하도록 한다. 

<GoogleSigninButton
  size={GoogleSigninButton.Size.Wide}
  color={GoogleSigninButton.Color.Dark}
  onPress={signInWithGoogle}
  disabled={isSigningIn}
  style={styles.signInBtn}
/>

View 컴포넌트 안에 위치하던 로그아웃 버튼을 View 컴포넌트 외부로 빼내었다. 이유는 View 컴포넌트 안에 있으면 로그아웃 버튼을 화면 맨 하단에 배치할 수가 없었기 때문이다. 

<TouchableHighlight onPress={signOutWithGoogle} style={styles.logoutBtnWrapper}>
  <View style={[styles.logoutBtn, { backgroundColor: "#a8c8ffff" }]}>
    <Text style={styles.logoutBtnText}>로그아웃</Text>
  </View>
</TouchableHighlight>

 

* 설정화면 UI 수정하기 

const styles = StyleSheet.create({
  block: {
    flex: 1,
    alignItems: 'center' // 삭제
  },
  signInBtn: {  // signInBtn 스타일 삭제 
    marginTop: 10,
    marginLeft: 'auto',
    marginRight: 'auto'
  },
  profileInfo: {
    marginVertical: 20, // 삭제
    marginHorizontal: 'auto',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 20,
    backgroundColor: '#eee' // 삭제 
  },
  profileText: {
    borderRadius: 10,
    padding: 20,
  },
  info: {
    fontSize: 15,
    fontWeight: 'bold'
  },
  profileImg: {
    width: 50,
    height: 50,
    borderRadius: 25,
    marginLeft: 'auto',
    marginRight: 'auto',
  },
  logoutBtn: {
    flex: 1,
    height: 35,
    borderRadius: 3, // 삭제
    marginLeft: 'auto', // 삭제
    marginRight: 'auto', // 삭제 
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
  },
  logoutBtnText: {
    color: '#fff',
    letterSpacing: 3,
    fontWeight: 'bold',
    fontSize: 15,
    textAlign: 'center',
  },
  
})
export default SettingsScreen

기존의 스타일을 아래와 같이 수정하도록 한다. 

const styles = StyleSheet.create({
  block: {
    flex: 1,
    justifyContent: 'flex-start', // 추가
  },
  profileInfo: {
    marginHorizontal: 'auto',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 20,
    backgroundColor: '#eee', // 추가 
  },
  profileText: {
    borderRadius: 10,
    padding: 20,
  },
  info: {
    fontSize: 15,
    fontWeight: 'bold'
  },
  profileImg: {
    width: 50,
    height: 50,
    borderRadius: 25,
    marginLeft: 'auto',
    marginRight: 'auto',
  },
  logoutBtnWrapper: { // logoutBtnWrapper 전체 추가 
    flexDirection: 'row',
    position: 'absolute',
    bottom: 0
  },
  logoutBtn: {
    flex: 1,
    height: 35,
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
  },
  logoutBtnText: {
    color: '#fff',
    letterSpacing: 3,
    fontWeight: 'bold',
    fontSize: 15,
    textAlign: 'center',
  },
  
})

 

* 로그인 에러 수정하기

import { GoogleSignin, GoogleSigninButton, statusCodes  } from '@react-native-google-signin/google-signin'

components > LoginButton.js 파일에서 구글 로그인 라이브러리 임포트시 statusCodes 를 추가로 임포트한다. 해당 객체가 없으면 로그인시 에러가 발생한다.  

 

* 수정된 로그인 로직 테스트하기 

사용자가 처음에 로그인한다. 앱을 종료하고 다시 켜면 이미 로그인된 상태이므로 곧바로 홈화면으로 이동한다. 로그아웃 버튼을 클릭하면 랜딩페이지로 이동한다. 사용자가 로그인을 취소하면 로그인중 에러처리에 의하여 경고창이 뜬다. 

 

 

728x90