오티스의개발일기

[REACT NATIVE] 리엑트 네이티브 인스타그램 클론 코딩 (9) 구글 로그인후 데이터베이스에 저장하기 + 예외 처리를 통한 비동기 처리 -git 참조- 본문

개발/react-native

[REACT NATIVE] 리엑트 네이티브 인스타그램 클론 코딩 (9) 구글 로그인후 데이터베이스에 저장하기 + 예외 처리를 통한 비동기 처리 -git 참조-

안되면 될때까지.. 2022. 12. 30. 18:05
728x90

 


< 이전글

2022.12.29 - [개발/spring boot] - [SPRING BOOT + REACT-NATIVE] (8) jpaQueryFactory 와 queryDsl 를 사용하여 이메일 중복확인 기능 만들기 -git 첨부 -

 

[SPRING BOOT + REACT-NATIVE] (8) jpaQueryFactory 와 queryDsl 를 사용하여 이메일 중복확인 기능 만들기 -git 첨

< 이전글 2022.12.29 - [개발/spring boot] - [SPRING BOOT + REACT-NATIVE] jpaRepository 를 사용하여 유저 데이터베이스에 저장하기 [SPRING BOOT + REACT-NATIVE] jpaRepository 를 사용하여 유저 데이터베이스에 저장하기 오

otis.tistory.com

 



다음글 >

2022.12.30 - [개발/spring boot] - [SPRING BOOT] 리엑트 네이티브 인스타그램 클론 코딩 (10) 백엔드 회원가입 api 만들기 -git 첨부-

 

[SPRING BOOT] 리엑트 네이티브 인스타그램 클론 코딩 (10) 백엔드 회원가입 api 만들기 -git 첨부-

< 이전글 2022.12.30 - [개발/react-native] - [REACT NATIVE] 리엑트 네이티브 인스타그램 클론 코딩 (9) 구글 로그인후 데이터베이스에 저장하기 + 예외 처리를 통한 비동기 처리 -git 참조- [REACT NATIVE] 리엑트

otis.tistory.com


오늘은 드디어 백엔드와 프론트엔드를 연결 하는날이다....

여기까지 오느라 고생 내가 제일 많았다 ㅎㅎ

 

오늘은 구글로 로그인을 시도후 구글로 받은 정보를 실제 데이터베이스에 저장하고

일반 회원으로 가입한사람 이라면 (아직 구현은 안함)

일반회원이니 일반 로그인을 해달라고 요청

그게아니고 가입한 이력이 있으면 로그인

가입한 이력이없고 최초 sns를 통한 로그인이면 자동으로 가입시키는 로직을 만들것이다.

 

spring boot 와 react-native 포스팅을 따로 진행할 예정이기때문에

 오늘은 프론트엔드쪽을 구현하도록 하겠다.

 

 

 

오늘 작업할 파일은

LoginScreen.js 

userAction.js

 

딱 2개이다 그리고 작업할 양은 그리 많지않아 짧을 예정이다.

 

 

#0. 폴더 구조

 

 

 

 

 

# 1. LoginScreen.js 작성

 

전체 코드

더보기
import { StyleSheet } from 'react-native'
import React from 'react'
import { ROUTES } from '../../constants/routes'
import styled, { css } from 'styled-components/native';
import { Divider } from 'react-native-elements';
import * as yup from 'yup'
import { Formik } from 'formik'
import axios from 'axios'
import Ionicons from "react-native-vector-icons/Ionicons";
import {snsLoginRequset} from '../../actions/userAction'
import { useDispatch, useSelector } from 'react-redux';
import {   
  GoogleSignin,
  GoogleSigninButton,
  statusCodes,
} from '@react-native-google-signin/google-signin'; 

GoogleSignin.configure({
  scopes: ['https://www.googleapis.com/auth/drive.readonly'], // what API you want to access on behalf of the user, default is email and profile
  webClientId: '757490347484-2ps65bgpecot0uiuhpuofd17k88che4d.apps.googleusercontent.com', // client ID of type WEB for your server (needed to verify user ID and offline access)
});    

const LoginScreen = (props) => {
  const dispatch = useDispatch();

  const googleLogin = async () => { 
    console.log('구글 로그인 시작');
    try {
      await GoogleSignin.hasPlayServices();
      const userInfo = await GoogleSignin.signIn();
      let params = {};
      params['userName'] = userInfo.user.name;
      params['email'] = userInfo.user.email;
      params['provider'] = 'google';
      params['providerId'] = userInfo.user.id;
      
      dispatch(snsLoginRequset(params));

    } catch (error) {
      console.log(error);
      if (error.code === statusCodes.SIGN_IN_CANCELLED) {
        // user cancelled the login flow
      } else if (error.code === statusCodes.IN_PROGRESS) {
        // operation (e.g. sign in) is in progress already
      } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
        // play services not available or outdated
      } else {
        // some other error happened
      }
    }
  };

  const { navigation } = props; // 네비게이션
  return (
    <SafeAreaView>
      <Container>
        <ImageBox>
          <Image source={require("../../../assets/whiteLogo.png")} />
        </ImageBox>
        <Formik
          initialValues={{ email: '', password: '' }}
          validateOnMount={true}
          onSubmit={values => {
            navigation.navigate(ROUTES.INDEX)
          }}
          validationSchema={loginValidationSchema}
        >
          {({ handleChange, handleBlur, handleSubmit, values, touched, errors, isValid }) => (
            <>
              <TextInputBox>
                <TextInput
                  placeholder="이메일을 입력해주세요"
                  onChangeText={handleChange('email')}
                  onBlur={handleBlur('email')}
                  keyboardType="email-address"
                  value={values.email}
                  autoFocus={true}
                />
                <ValidationTextBox>
                  <ValidationText>
                    {
                      values.email.length > 0 ?
                        errors.email
                        :
                        ''
                    }
                  </ValidationText>
                </ValidationTextBox>
                <TextInput
                  autoCapitalize="none"
                  textContentType="password"
                  secureTextEntry={true}
                  placeholder="비밀번호를 입력해주세요"
                  onChangeText={handleChange('password')}
                  onBlur={handleBlur('password')}
                  value={values.password}
                />
                <ValidationTextBox>
                  <ValidationText>
                    {
                      values.password.length > 0 ?
                        errors.password
                        :
                        ''
                    }
                  </ValidationText>
                </ValidationTextBox>
              </TextInputBox>
              <ForgetPasswordBox>
                <TouchableOpacity onPress={() => navigation.navigate(ROUTES.FORGOTPASSWORD)}>
                  <ForgetPasswordText>비밀번호를 잊으셨나요?</ForgetPasswordText>
                </TouchableOpacity>
              </ForgetPasswordBox>
              <LoginButtonBox>
                {
                  isValid ?
                    <LoginButton onPress={handleSubmit}>
                      <LoginButtonText>
                        로그인
                      </LoginButtonText>
                    </LoginButton>
                    :
                    <InActiveLoginButton>
                      <LoginButtonText>
                        로그인
                      </LoginButtonText>
                    </InActiveLoginButton>
                }
              </LoginButtonBox>
            </>
          )}
        </Formik>
        <SnsLoginBox>
          <SnsLoginOrBox>
            <SnsLoginDividerBox>
              <Divider />
            </SnsLoginDividerBox>
            <Text>또는</Text>
            <SnsLoginDividerBox>
              <Divider />
            </SnsLoginDividerBox>
          </SnsLoginOrBox>
          <SnsLoginTextBox>
              <GoogleIcon source={require('../../../assets/googleIcon.png')}/>
              <TouchableOpacity onPress={() => googleLogin()}>
              <SnsLoginText>
                구글로 로그인
              </SnsLoginText>
            </TouchableOpacity>
          </SnsLoginTextBox>
        </SnsLoginBox>
      </Container>
      <JoinBox>
        <JoinBoxNormalText>
          계정이 없으신가요?
        </JoinBoxNormalText>
        <TouchableOpacity onPress={() => navigation.navigate(ROUTES.REGISTER)}>
          <JoinBoxText>
            가입하기
          </JoinBoxText>
        </TouchableOpacity>
      </JoinBox>
    </SafeAreaView>
  )
}
const styles = StyleSheet.create({})

const loginValidationSchema = yup.object().shape({
  email: yup.string().required("이메일을 입력해주세요").email("올바른 이메일을 작성해주세요"),
  password: yup.string().min(8, ({ min }) => "비밀번호는 최소 " + min + " 자리 이상입니다.").required("비밀번호를 입력해주세요")
})



export default LoginScreen

const GoogleIcon = styled.Image`
  width: 20px;
  height: 20px;
  margin-right: 5px;
`;

const SnsLoginTextBox = styled.View`
  margin-top: 20px;
  align-items: center;
  flex-direction: row;
  justify-content: center;
`;

const SnsLoginText = styled.Text`
  color: #0095F6;
  align-items: center;
`;

const SnsLoginBox = styled.View`
  margin-top: 20px;
`;

const SnsLoginOrBox = styled.View`
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
`;

const SnsLoginDividerBox = styled.View`
  width: 40%;
`;

const ValidationTextBox = styled.View`
  margin-top: 8px;
  margin-bottom: 8px;
`

const ValidationText = styled.Text`
  color: red
`

const Container = styled.View`
  width: 100%;
  padding: 0px 20px;
`
const LoginButtonBox = styled.View`
  margin-top: 20px;
`

const JoinBox = styled.View`
  flex-direction: row;
  position: absolute;
  bottom: 40px;
  left: 0;
  width: 100%;
  justify-content: center;
  align-items: center;
`

const JoinBoxNormalText = styled.Text`
  color: gray;
  font-size: 12px;
  margin-right: 10px;
`

const JoinBoxText = styled.Text`
  color: #0095F6;
`

const InActiveLoginButton = styled.View`
  background-color: #014068d1;
  height: 50px;
  border-radius: 5px;
  font-size: 12px;
`;

const LoginButton = styled.TouchableOpacity`
  background-color: #0095F6;
  height: 50px;
  border-radius: 5px;
  font-size: 12px;
`;


const LoginButtonText = styled.Text`
  color: white;
  text-align: center;
  margin: auto;
  font-weight: 600;
`

const ForgetPasswordBox = styled.View`
  margin-top: 15px;
  align-items: flex-end;
`

const ForgetPasswordText = styled.Text`
  color: #0095F6;
  font-size: 12px;
`

const SafeAreaView = styled.SafeAreaView`
  justify-content: center;
  flex: 1;
  align-items: center;
  background-color: ${props => props.theme.backgroundColor};
`

const TextInputBox = styled.View`
  margin-top: 20px;
  width: 100%;
`

const TextInput = styled.TextInput`
  padding: 0px 10px;
  color: ${props => props.theme.TextColor};
  background-color: #282828a6;
  border: 1px solid #7a7a7a5c;
  width: 100%;
  height: 40px;
  border-radius: 5px;
`

const Image = styled.Image`
  width: 200px;
  height: 55px;
`

const ImageBox = styled.View`
  width: 100%;
  align-items: center;
`


const View = styled.View`
`

const Text = styled.Text`
  color: ${props => props.theme.TextColor}; 
`

const TouchableOpacity = styled.TouchableOpacity`
`

 

 

 

전체적인 코드는 이러하다

 

이번에 작성한 함수를 자세히 살펴보자

 

일단 버튼 부분이다.

 

<SnsLoginTextBox>
              <GoogleIcon source={require('../../../assets/googleIcon.png')}/>
              <TouchableOpacity onPress={() => googleLogin()}>
              <SnsLoginText>
                구글로 로그인
              </SnsLoginText>
            </TouchableOpacity>
          </SnsLoginTextBox>

 

구글로 로그인을 클릭했을시 googleLogin() 이라는 함수가 실행된다.

 

 

 

이제 googleLogin 함수를 살펴보자

 

  const googleLogin = async () => { 
    console.log('구글 로그인 시작');
    try {
      await GoogleSignin.hasPlayServices();
      const userInfo = await GoogleSignin.signIn();
      let params = {};
      params['userName'] = userInfo.user.name;
      params['email'] = userInfo.user.email;
      params['provider'] = 'google';
      params['providerId'] = userInfo.user.id;
      
      dispatch(snsLoginRequset(params));

    } catch (error) {
      console.log(error);
      if (error.code === statusCodes.SIGN_IN_CANCELLED) {
        // user cancelled the login flow
      } else if (error.code === statusCodes.IN_PROGRESS) {
        // operation (e.g. sign in) is in progress already
      } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
        // play services not available or outdated
      } else {
        // some other error happened
      }
    }
  };

 

기본적으로 로그인을 중간에 멈추거나 어떠한 에러가생기면 저기 catch 부분에서 오류 로그가 찍힐것이다.

이코드는 api에서 제공해주는 가이드라인을 그대로 가져온것이니 참고하기 바란다.

 

userInfo 안에 수많은 데이터가있는데 확인해보고싶은사람은 console.log(userInfo); 로 확인하면되고

그안에 내가 써야하는 정보들은 user라는 오브젝트에담겨있다.

 

그것을빼 dispatch 를 사용해 비동기 api 요청을 시도한것이다.

 

dispatch 안에있는 snsLoginRequest(params) 의 함수를 살펴보자

 

 

# 1. userAction.js 생성

 

import {createAsyncThunk} from '@reduxjs/toolkit'
import axios from 'axios'
import {Alert} from 'react-native'

const snsLogin = async (params) => {
    console.log('로그인 요청 파마미터')
    console.log(params)
    await axios.post('http://localhost:8080/api/user/snsLogin', params, {withCredentials: true})
    .then(function(res) {
        console.log(res.data.code)
        if (res.data.code === 200) { // 정상 코드가 들어올시 비지니스로직 진행
            alert(res.data.message);
        }
    }).catch(error => {
        if (error.response.data.code !== 303) { // 일반회원일때
            alert(error.response.data.message);
        } else {
            return (
                Alert.alert(
                    error.response.data.message,
                    'sns 로그인 연동하기',
                    [
                        {
                            text: '연동하기',
                            onPress: async () => await updateProvider(params)
                        },
                        {
                            text: '취소',
                            onPress: () => false,
                            style: "cancel"
                        },
                    ],
                    { cancelable: false }
                )
            )
        }
        
    })
}

const updateProvider = async (params) => {
    await axios.post('http://localhost:8080/api/user/updateProvider', params, {withCredentials: true})
    .then(function(res) {
        if (res.data.code === 200) { // 정상 코드가 들어올시 비지니스로직 진행
            snsLogin(params)
        }
    }).catch(error => {
        alert(error.response.data.message);
    })
}

const snsLoginRequset = createAsyncThunk('user', async (data, {dispatch, getState, rejectWithValue, fulfillWithValue}) => {
    // try catch 는 하지말아야 에러를 캐치할수 있다.
    // 상단 파라미터중 data는 요청시 들어온 파라미터이다. 저 파라미터를 가지고 서버에 데이터 요청하면된다.
    console.log('data = ', data) // 파라미터
    const state = getState(); // 상태가져오기
    console.log('state = ', state)

    let result = snsLogin(data);
    
})

export {snsLoginRequset}

 

 

주석에 어느정도 설명이 적혀있다.

 

일단 어느정도의 흐름을 알려주겠다.

 

일단 로그인하는 이메일을 백엔드에서 확인한후

같은 이메일을 사용하는 유저가 없으면  처음 가입이라고 인식하고 자동으로 가입을 시킨다.

그게 아니라면 provider의 유무를 확인하고

provider가 존재하면 그전 다른 sns로 가입한걸로 가정하고

지금 로그인한 snsproviderproviderId 로 변경시킨다

원래는 다른 sns로 가입한 경로가있다고 에러를 보내는게 요즘쓰는 로직인데 

귀찮기도하고 이렇게하면 확실하게 1인 1계정이 되기때문에 그냥 이렇게 개발해놨다.

마지막으로 provider 가 없다면 일반회원이라고 가정하고

일반회원 으로 가입한이력이 있습니다. 지금 바로 연동하시겠습니까? 라는 것을 알려주고

확인버튼을 누르면 데이터베이스에 providerproviderID 를 저장해주고 로그인 시킬것이다.

 

정상적인 로그인은 code 200을 반환하도록하였고

나머지는 300 이상의 code를 반환하도록 하였기에

catcherror 부분에 함수가 실행될것이다.

 

이제 상황에따른 테스트를 진행해보겠다.

 

상황은 이렇다.

 

1. 가입한적이 없을때 자동 가입

2. 가입한 적이있고 다른 sns로 가입했을때 자동으로 변경

3. 가입한 적이있고 일반회원으로 가입한적이 있을때 연동여부 물어보고 연동시키기

 

 

1. 가입한 적이 없을때 자동 가입

 

 

 

 

mysql 에 정상적으로 저장된걸 확인할수있다.

 

2. 가입한 적이있고 다른 sns로 가입했을때 자동으로 변경

 

임시적으로 내 어플리케이션에 카카오톡 로그인이 있다고 가정하고 db를 바꿔보겠다.

 

 

 

 

 

자 다시 데이터베이스를 확인해보면 kakao -> google 로 바뀌어있을것이다.

 

 

 

 

 

 

이제 마지막으로 provider 와 providerid 를지우고 3번째 상황을 테스트해보겠다.

 

 

 

데이터를 먼저 삭제하였다

 

 

 

 

 

 

정상적으로 데이터가 저장된걸 확인할수있다

 

이것으로 포스팅을 마치도록 하겠다.

 

다음 포스팅은 이 작업에대한 api 를 spring boot 에서 만드는 작업을 진행할것이다.

 

 

코드는 아래 깃에 있다

확인바란다.

 

https://github.com/1domybest/react-native-ig-clone.git

 

GitHub - 1domybest/react-native-ig-clone

Contribute to 1domybest/react-native-ig-clone development by creating an account on GitHub.

github.com


다음글 >

2022.12.30 - [개발/spring boot] - [SPRING BOOT] 리엑트 네이티브 인스타그램 클론 코딩 (10) 백엔드 회원가입 api 만들기 -git 첨부-


 

728x90
Comments