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

11. 설정화면 - 구글 로그인 이후 사용자 프로필 보여주기

syleemomo 2023. 10. 25. 09:48
728x90

https://github.com/react-native-google-signin/google-signin

 

GitHub - react-native-google-signin/google-signin: Google Sign-in for your React Native applications

Google Sign-in for your React Native applications. Contribute to react-native-google-signin/google-signin development by creating an account on GitHub.

github.com

 

* 구글 로그인후 프로필 정보 보여주기 & 로그아웃 구현하기

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

function SettingsScreen({navigation}){
  const [userInfo, setUserInfo] = useState(null) 
  const [isSigningIn, setIsSigningIn] = useState(false) // 로그인 여부

  const googleSigninConfigure = () => { 
    GoogleSignin.configure({
      webClientId:
        '137262950194-gcouccatiffjp4ei8aqfi05uruv5j4dl.apps.googleusercontent.com',
    })
  }
  
  const signInWithGoogle = async () => {
    try {
      setIsSigningIn(true)
      await GoogleSignin.hasPlayServices()
      const userInfoFromGoogle = await GoogleSignin.signIn()
      if(userInfoFromGoogle){
        console.log("사용자 사진: ", userInfoFromGoogle.user.photo)
        setUserInfo(userInfoFromGoogle)
        setIsSigningIn(false)
      }
      
    } 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)
    } catch (error) {
      console.error('failed to logout, error: ', error)
    }
  }

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

  return (
    <SafeAreaView style={styles.block}>
      <StatusBar backgroundColor="#a8c8ffff"></StatusBar>
      <View>
        <GoogleSigninButton
          size={GoogleSigninButton.Size.Wide}
          color={GoogleSigninButton.Color.Dark}
          onPress={signInWithGoogle}
          disabled={isSigningIn}
          style={styles.signInBtn}
        />
        {userInfo && userInfo.user && 
        (<View style={styles.profileInfo}>
          <View style={styles.profileText}>
            <Text style={styles.info}>사용자 이메일 - {userInfo.user.email}</Text>
            <Text style={styles.info}>사용자 아이디 - {userInfo.user.id}</Text>
            <Text style={styles.info}>사용자 이름 - {userInfo.user.name}</Text>
          </View>
          <Image source={{uri: userInfo.user.photo}} style={styles.profileImg}/>
        </View>)
        }
        <TouchableHighlight onPress={signOutWithGoogle}>
          <View style={[styles.logoutBtn, { backgroundColor: userInfo ? "#a8c8ffff" : 'lightgrey' }]}>
            <Text style={styles.logoutBtnText}>구글 로그아웃</Text>
          </View>
        </TouchableHighlight>
      </View>
    </SafeAreaView>
  )
}

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

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

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

useState, Image, TouchableHighlight, GoogleSigninButton 이 추가되었다. 

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

로그인후 프로필 정보를 저장할 userInfo 와 로그인중인지 아닌지 판별하는 isSigningIn 상태를 정의한다.

async function onGoogleButtonPress() {
    // Check if your device supports Google Play
    await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });
    // Get the users ID token
    const { idToken } = await GoogleSignin.signIn();
    console.log("구글 토큰: ", idToken)
  
    // Create a Google credential with the token
    const googleCredential = auth.GoogleAuthProvider.credential(idToken);
  
    // Sign-in the user with the credential
    return auth().signInWithCredential(googleCredential);
  }

기존의 코드는 로그인후 사용자 토큰만 조회한다. 해당 코드를 아래와 같이 수정한다.

const signInWithGoogle = async () => {
    try {
      setIsSigningIn(true)
      await GoogleSignin.hasPlayServices()
      const userInfoFromGoogle = await GoogleSignin.signIn()
      if(userInfoFromGoogle){
        console.log("사용자 사진: ", userInfoFromGoogle.user.photo)
        setUserInfo(userInfoFromGoogle)
        setIsSigningIn(false)
      }
      
    } 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')
      }
    }
  }

로그인후 전체 프로필 정보를 조회한다. 또한, 로그인 중에 에러가 발생한 경우 try, catch 로 다양한 에러처리를 수행하도록 코드를 수정하였다. 로그인이 시작될때 setIsSigningIn 함수로 로그인 상태를 true 로 변경하여 로그인중이라고 설정한다. 로그인이 성공하고, 프로필 정보가 존재하면 로그인 상태를 false 로 변경하여 로그인이 완료되었음을 알려준다. 

{
  idToken: string,
  serverAuthCode: string,
  scopes: Array<string>
  user: {
    email: string,
    id: string,
    givenName: string,
    familyName: string,
    photo: string, // url
    name: string // full name
  }
}

프로필 정보는 위와 같은 형태이다. 사용자 토큰(idToken), 이메일(user.email), 프로필사진(user.photo), 사용자이름(user.name) 등이 존재한다. 

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

구글에서 제공되는 signOut 메서드로 로그아웃을 진행하고, userInfo 를 null 로 설정하여 사용자정보를 지워준다. 

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

구글에서 기본적으로 제공되는 로그인 버튼이다. disabled 는 버튼을 활성화하거나 비활성화한다. 자세한 속성들은 문서를 참고하기 바란다. 

{userInfo && userInfo.user && 
    (<View style={styles.profileInfo}>
      <View style={styles.profileText}>
        <Text style={styles.info}>사용자 이메일 - {userInfo.user.email}</Text>
        <Text style={styles.info}>사용자 아이디 - {userInfo.user.id}</Text>
        <Text style={styles.info}>사용자 이름 - {userInfo.user.name}</Text>
      </View>
      <Image source={{uri: userInfo.user.photo}} style={styles.profileImg}/>
    </View>)
}

프로필 정보가 존재하는 경우 해당 정보를 화면에 보여준다. 사용자 연락처, 아이디, 이름, 프로필 사진 등을 디스플레이한다.

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

TouchableHighlight 컴포넌트를 이용하여 로그아웃 버튼을 화면에 보여준다. 사용자 정보가 존재하는 경우는 이미 로그인이 완료된 것이므로 로그아웃 버튼 색상을 보라색으로 설정하여 로그아웃 할 수 있도록 한다. 사용자 정보가 없으면 로그인을 하지 않았거나 로그아웃을 한 경우이므로 버튼 색상을 회색으로 설정하였다. 

const styles = StyleSheet.create({
  block: {
    flex: 1,
    alignItems: 'center'
  },
  signInBtn: {        // 로그인 버튼 
    marginTop: 10,
    marginLeft: 'auto',
    marginRight: 'auto'
  },
  profileInfo: {     
    marginVertical: 20,
    marginHorizontal: 'auto',
  },
  profileText: {   
    backgroundColor: '#eee',
    borderRadius: 10,
    padding: 20,
  },
  info: {
    fontSize: 15,
    fontWeight: 'bold'
  },
  profileImg: {
    width: 200,
    height: 200,
    borderRadius: 100,
    marginLeft: 'auto',
    marginRight: 'auto',
  },
  logoutBtn: {
    width: 200,
    height: 35,
    borderRadius: 3,
    marginLeft: 'auto',
    marginRight: 'auto',
    marginTop: 30,
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
  },
  logoutBtnText: {
    color: '#fff',
    letterSpacing: 3,
    fontWeight: 'bold',
    fontSize: 15,
    textAlign: 'center',
  },
  
})

프로필 정보를 화면에 보여주기 위한 스타일 코드이다. 현재는 정보를 화면에 보여주기 위하여 임시적인 스타일을 적용하였고, 추후에 제대로된 프로필 정보를 보여주기 위하여 스타일을 변경할 예정이다.

구글 로그인에 성공한 경우
구글 로그아웃에 성공한 경우

 

* 로그인 버튼 활성화/비활성화 기능 리팩토링하기 & 프로필 UI 수정하기

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

function SettingsScreen({navigation}){
  const [userInfo, setUserInfo] = useState(null) 
  const [isSigningIn, setIsSigningIn] = useState(false) // 로그인 여부

  const googleSigninConfigure = () => { 
    GoogleSignin.configure({
      webClientId:
        '137262950194-gcouccatiffjp4ei8aqfi05uruv5j4dl.apps.googleusercontent.com',
    })
  }
  
  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)
    }
  }

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

  return (
    <SafeAreaView style={styles.block}>
      <StatusBar backgroundColor="#a8c8ffff"></StatusBar>
      <View>
        <GoogleSigninButton
          size={GoogleSigninButton.Size.Wide}
          color={GoogleSigninButton.Color.Dark}
          onPress={signInWithGoogle}
          disabled={isSigningIn}
          style={styles.signInBtn}
        />
        {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>)
        }
        <TouchableHighlight onPress={signOutWithGoogle} style={{flexDirection: 'row'}}>
          <View style={[styles.logoutBtn, { backgroundColor: userInfo ? "#a8c8ffff" : 'lightgrey' }]}>
            <Text style={styles.logoutBtnText}>구글 로그아웃</Text>
          </View>
        </TouchableHighlight>
      </View>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  block: {
    flex: 1,
    alignItems: 'center'
  },
  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

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

 const signInWithGoogle = async () => {
    try {
      setIsSigningIn(true)
      await GoogleSignin.hasPlayServices()
      const userInfoFromGoogle = await GoogleSignin.signIn()
      if(userInfoFromGoogle){
        console.log("사용자 사진: ", userInfoFromGoogle.user.photo)
        setUserInfo(userInfoFromGoogle)
        setIsSigningIn(false)
      }
      // 중략
}

기존에는 로그인중인 경우 로그인버튼을 비활성화하고, 로그인에 성공하면 로그인버튼을 활성화한다. 하지만 이렇게 되면 이미 로그인했는데 사용자가 다시 로그인 버튼을 누를수 있다. 그러므로 로그인중인 경우에는 아무것도 하지않고, 로그인에 성공한 경우 로그인버튼을 비활성화하는게 논리적으로 이치에 맞다. 그래서 기존코드를 아래와 같이 수정하도록 한다.

const signInWithGoogle = async () => {
    try {
      await GoogleSignin.hasPlayServices()
      const userInfoFromGoogle = await GoogleSignin.signIn()
      if(userInfoFromGoogle){
        console.log("사용자 사진: ", userInfoFromGoogle.user.photo)
        setUserInfo(userInfoFromGoogle)
        setIsSigningIn(true) // 수정
      }
      // 중략
 }

로그아웃할때는 사용자가 다시 로그인할 수 있도록 로그인버튼을 활성화해준다.  

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

프로필 정보를 보여주는 UI 는 아래와 같이 수정하였다. Image 컴포넌트를 위로 올리고 수평 정렬을 위하여 View 로 한번 감싸주었다. 사용자 아이디를 보여주는 Text 컴포넌트는 제거하고, 사용자 이름을 보여주는 Text 컴포넌트를 이메일 위쪽으로 이동하였다. 또한, 사용자 이름은 이메일과 다르게 조금 더 두드러지게 보이도록 하였다. 

 {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>)
}

로그아웃 버튼은 수평으로 길게 보여주기 위하여 flexDirection 을 "row"로 설정하였다.

<TouchableHighlight onPress={signOutWithGoogle} style={{flexDirection: 'row'}}>
  // 중략
</TouchableHighlight>

프로필 정보에 대한 UI 스타일이다. 내부 컴포넌트(프로필사진/사용자정보)를 수평정렬하고, 가로/세로 중앙에 배치한다. 

profileInfo: {
    marginVertical: 20,
    marginHorizontal: 'auto',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 20,
    backgroundColor: '#eee'
  },
profileText: {
	// 배경색 제거
    borderRadius: 10,
    padding: 20,
  },

프로필 사진은 Circle 형태로 보여주기 위하여 아래와 같이 스타일링한다.

profileImg: {
    width: 50,
    height: 50,
    borderRadius: 25,
    marginLeft: 'auto',
    marginRight: 'auto',
  },

width, marginTop 속성은 제거하고 아래와 같이 flex: 1 로 설정하여 로그아웃 버튼을 수평으로 길게 보여준다.

logoutBtn: {
    flex: 1,
    height: 35,
    borderRadius: 3,
    marginLeft: 'auto',
    marginRight: 'auto',
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
  },

 

로그아웃 화면
로그인 화면

 

* 로딩화면 구현하기

// if (loading) {
  //   return (
  //     <View>
  //       <Text>로딩중...</Text>
  //     </View> 
  //   )
  // }

screens > HomeScreen.js 파일에서 해당부분은 주석처리한다. 로딩이 진행되는 동안에는 탭메뉴를 누르지 못하도록 구현할 예정이므로 홈화면에서 실행되던 로딩화면을 App 컴포넌트로 옮기도록 한다. 어차피 loading 속성은 App 컴포넌트에서 정의하였으므로 App 컴포넌트로 코드를 곧바로 옮길수 있다.

import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'

App.js 파일에 Text, StyleSheet, ActivityIndicator 컴포넌트를 임포트한다.

if (loading) {
    return (
      <View style={styles.block}>
        <ActivityIndicator size="large" color="#0047AB"/>
        <Text style={styles.loadingText}>Loading ...</Text>
      </View> 
    )
  }

App.js 파일에 위 코드를 추가한다. 로딩중인 경우 로딩화면 UI를 보여준다. 이때 간단하게 로딩 아이콘을 보여주기 위하여 ActivityIndicator 라는 리액트 네이티브 컴포넌트를 사용한다. 

const styles = StyleSheet.create({
  block: {
    flex: 1,
    backgroundColor: '#a8c8ffff',
    justifyContent: 'center',
    alignItems: 'center',
  },
  loadingText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#fff',
    marginTop: 10,
    textAlign: 'center'
  }
})

로딩화면에 대한 스타일 코드이다. 

로딩화면

맨 처음 화면이 로딩될때 보여진다.

 

728x90