오티스의개발일기

[REACT NATIVE] 인스타그램 클론 코딩 (15) 로그아웃 구현하기 본문

개발/react-native

[REACT NATIVE] 인스타그램 클론 코딩 (15) 로그아웃 구현하기

안되면 될때까지.. 2023. 1. 2. 06:47
728x90

 


< 이전글

2023.01.01 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (14) gorhom/bottom-sheet 를 활용한 바텀 시트 만들기

 

[REACT NATIVE] 인스타그램 클론 코딩 (14) gorhom/bottom-sheet 를 활용한 바텀 시트 만들기

< 이전글 2023.01.01 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (13) 마이페이지 ui 만들기 [REACT NATIVE] 인스타그램 클론 코딩 (13) 마이페이지 ui 만들기 < 이전글 2023.01.01 - [개발/react-native] -

otis.tistory.com



다음글 >

 

2023.01.02 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (16) react-native-paper 사용하여 메뉴바 만들기!

 

[REACT NATIVE] 인스타그램 클론 코딩 (16) react-native-paper 사용하여 메뉴바 만들기!

< 이전글 2023.01.02 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (15) 로그아웃 구현하기 [REACT NATIVE] 인스타그램 클론 코딩 (15) 로그아웃 구현하기 < 이전글 2023.01.01 - [개발/react-native] - [REACT

otis.tistory.com


 

 

이번 시간에는 로그아웃을 구현해볼것이다.

useEffect 를 사용하지않아 오류때문에 삽질을 꽤많이 했다... 

 

원래 목표는 AsyncStorage 를 활용하여 AuthNavigation.js 안에서 initialRouteName 을 교체해주고

들어왔을때 Login 페이지를 거치지 않고 바로 Index(HomeScreen.js) 페이지를 보여주려했다

이렇게되면 훨씬 UI 가 부드럽기때문이다.

 

결과적으로 안된다 아무리해도 안된다....

포기하고 LoginScreen.js 안에서 로그인 유무를 판단하고 바로 Index 페이지로 넘겨주기로 결정하였다..

 

그럼 시작해보도록 하겠다.

 


# 오늘 작업할 파일목록

  • userActions.js
  • LoginScreen.js
  • MyPageBottomNavigation.js
  • userSlicer.js

 

# 0. 폴더 구조



 

 

 

# 1. userAction.js

import {createAsyncThunk} from '@reduxjs/toolkit'
import axios from 'axios'
import {Alert} from 'react-native'
import * as $Util from '../constants/utils'
import { ROUTES } from '../constants/routes';
const snsLogin = async (params) => {
    console.log('로그인 요청 파마미터')
    console.log(params)
    return new Promise (function (resolve, reject) {
     axios.post('http://localhost:8080/api/common/user/snsLogin', params, {withCredentials: true})
        .then(function(res) {
            if (res.data.code === 200) { // 정상 코드가 들어올시 비지니스로직 진행
                let result =  {
                    accessToken: res.headers.accesstoken,
                    refreshToken: res.headers.refreshtoken,
                    loading: false,
                }
                $Util.setStoreData('token', {
                    accessToken: res.headers.accesstoken,
                    refreshToken: res.headers.refreshtoken,
                });
                alert(res.data.message);
                resolve(result);
            }
        }).catch(error => {
            if (error.response.data.code !== 303) { // 일반회원이 아니면
                alert(error.response.data.message);
                reject (error.response.data) ;
            } else {
                Alert.alert(
                    error.response.data.message,
                    'sns 로그인 연동하기',
                    [
                        {
                            text: '연동하기',
                            onPress: async () => await updateProvider(params)
                        },
                        {
                            text: '취소',
                            onPress: () => {reject (error.response.data);},
                            style: "cancel"
                        },
                    ],
                    { cancelable: false }
                )
            }
            
        })
    })
}



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

const logOutRequest = createAsyncThunk('userLogOut', async (navigation, {dispatch, getState, rejectWithValue, fulfillWithValue}) => {
    // try catch 는 하지말아야 에러를 캐치할수 있다.
    // 상단 파라미터중 data는 요청시 들어온 파라미터이다. 저 파라미터를 가지고 서버에 데이터 요청하면된다.
    const state = getState(); // 상태가져오기
    let data = {
        accessToken: null,
        refreshToken: null,
    }
    await $Util.setStoreData("token", data)
    return data;
})

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

    let result = await snsLogin(data);

    return result;
})

export {snsLoginRequset, logOutRequest}

 

logOuRequest 를 보면 $Util.setStoreData 를 사용하여 토큰들을 Null 로 만들어준다.

왜 remove 를 사용하지 않지?

 

그이유는 간단하다 우리가 LoginScreen.js 에서 로그인 유무를 판단해야하는데 아에 삭제를 해버리면 token 이라는

storage 를 검색했을때 오류가 발생한다 삭제를 해버리면 오브젝트 뿐만아니라 token 이라는 이름도 같이 삭제 되기때문이다.

 

우리가 예측할수있는 null 값을 넣어줘야 문제없이 개발이 가능하다.

 

action을 만들었으니 slicer에서 데이터를 변경해보자

 

 

#2.UserSlicers.js

 

import { createSlice } from '@reduxjs/toolkit' // toolkit 추가된 임포트
import {snsLoginRequset, logOutRequest} from '../actions/userAction'

const initialState = {
    accessToken: null,
    refreshToken: null,
    loading: false,
}

const themeSlicer = createSlice({
    name: 'userSlice',
    initialState: initialState,
    reducers: { // 동기적인  액션을 넣는다.   내부적인 액션
        setToken (state, action) {
            state.accessToken = action.payload.accessToken;
            state.refreshToken = action.payload.refreshToken;
        },
    },
    extraReducers: (builder) => { // 비동적인 엑션을 넣는다  외부적인 액션 (예를들어 userSlice에서 post의 액션을 써야할때 이곳에 적는데 그때는 동기가아니고 비동기여도 넣는다.)
        builder.addCase(snsLoginRequset.pending, (state, action) => {
            state.loading = true;
        });
        builder.addCase(snsLoginRequset.fulfilled, (state, action) => {
            state.accessToken = action.payload.accessToken;
            state.refreshToken = action.payload.refreshToken;
            state.loading = false;
        });
        builder.addCase(snsLoginRequset.rejected, (state, action) => {
            state.loading = false;
            state.accessToken = null;
            state.refreshToken = null;
        });
        builder.addCase(logOutRequest.pending, (state, action) => {
            state.loading = true;
        });
        builder.addCase(logOutRequest.fulfilled, (state, action) => {
            console.log(action)
            state.loading = false;
            state.accessToken = action.payload.accessToken;
            state.refreshToken = action.payload.refreshToken;
        });
        builder.addCase(logOutRequest.rejected, (state, action) => {
            state.loading = false;
            state.accessToken = null;
            state.refreshToken = null;
        });
    },
});
export default themeSlicer;

크게 달라진건없고 아래에 logOutRequest 를 추가시켜줬다.

실패했을때는 무조건 로그아웃될수있도록 만들어놨다.

 

action 과 slicers 를 만들었으니 사용할 페이지를 만들어 보자

 

# 3. MyPageBottomSheet.js

import styled from "styled-components/native";
import React, { useEffect } from 'react'
import {Divider} from 'react-native-elements'
import Ionicons from "react-native-vector-icons/Ionicons";
import {ICONS} from '../../constants/icons'
import {Alert} from 'react-native'
import { useDispatch, useSelector } from "react-redux"; // userDispatch = 데이터 변경시 사용 // useSelector = 데이터 가져올때 사용
import {logOutRequest} from '../../actions/userAction'
import * as $Util from '../../constants/utils'
import {ROUTES} from '../../constants/routes'
const myPageBottomSheet = ({navigation}) => {

    const dispatch = useDispatch();
    const userSliceData = useSelector(state => state.userSlicer)
    useEffect(() => {
        if ($Util.isEmpty(userSliceData.accessToken)) {
            navigation.dispatch(
                StackActions.replace(ROUTES.LOGIN)
              )
        }
    })

    const openLogOutAlert = () => {
        return (
            Alert.alert(
                '로그아웃 하시겠습니까?',
                '로그아웃',
                [
                    {
                        text: '로그아웃',
                        onPress: () => {dispatch(logOutRequest(navigation))}
                    },
                    {
                        text: '취소',
                        style: "cancel"
                    },
                ],
                { cancelable: false }
            )
        )
    }

  return (
    <Container>
        <TouchableOpacity onPress={openLogOutAlert}>
            <Box>
                <TextBox>
                    <Ionicons name="log-out-outline" size={28} color="black" />
                    <Text>로그아웃</Text>
                </TextBox>
                <Divider width={1}/>
            </Box>
        </TouchableOpacity>
    </Container>
  )
}

export default myPageBottomSheet

const Container = styled.View`
    padding: 10px 15px;
`
const Box = styled.View``

const TextBox = styled.View`
    flex-direction: row;
    align-items: center;
    margin-bottom: 10px;
`

const Text = styled.Text`
    margin-left: 10px;
`

const TouchableOpacity = styled.TouchableOpacity`

`;

 

단순히 로그아웃 눌렀을때 dispatch 안에 action을 넣어 실행되도록 하였다. 코드는 간단하니 확인하면된다.

 

이제마지막으로 로그인부분에서 로그인 유무를 판단하고 route 를 넘겨주는 작업을 해보자

 

 

 

# 4 . LoginScreen.js

import { StyleSheet } from 'react-native'
import React, {useEffect} 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 {snsLoginRequset} from '../../actions/userAction'
import { useDispatch, useSelector } from 'react-redux';
import Loader from '../common/Loader';
import * as $Util from '../../constants/utils'
import {   
  GoogleSignin,
  statusCodes,
} from '@react-native-google-signin/google-signin'; 

import userSlicer from '../../slicers/userSlicer'
import { StackActions } from '@react-navigation/native';

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 userSliceData = useSelector(state => state.userSlicer)
  const dispatch = useDispatch();
  const { navigation } = props; // 네비게이션
  
  $Util.getStoreData('token').then(function(token) {
    if (!$Util.isEmpty(token.accessToken)) {
        dispatch(userSlicer.actions.setToken({
          accessToken: token.accessToken,
          refreshToken: token.refreshToken,
      }))
    }
  })

  useEffect(() => { // 마운트
    if (!$Util.isEmpty(userSliceData.accessToken)) {
      navigation.dispatch(
        StackActions.replace(ROUTES.INDEX)
      )
    }
  })
  
  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
      }
    }
  };

  
  return (
    <SafeAreaView>
      {userSliceData.loading ? <Loader/> : null}
      <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`
`

 

일단 항상 이벤트를 받아야하기때문에 useSeleteor 를 활용하였고

보이는것처럼 useEffect를 사용하였다. 저것을 안쓰면 

 ERROR  Warning: Cannot update a component (`LoginScreen`) while rendering a different component (`AuthNavigaition`). To locate the bad setState() call inside `AuthNavigaition`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render

 

이러한 오류를 보게될것이다. 뭐 한마디로 페이지가 렌딩되자마자 다르곳으로 넘어가니 뭔가 문제가 있다.

이런이야기인데 useEffect 를 사용하면 간단하게 해결된다.

이렇게 간단한건데 몇시간이 걸렸다..ㅋㅋ

 

마지막으로 제대로 동작하는지 확인해보겠다.

 

 

 

1. 로그인후 재 시작

 

 

2. 로그아웃 후 재시작

         

 

정상적으로 동작하는걸 볼수있다.

 

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

 

다음 포스팅에는 ...뭘할지 좀 고민해보도록 하겠다..ㅋㅋㅋ

 

 

 

 

 

# 깃허브 주소


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


다음글 >

 


2023.01.02 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (16) react-native-paper 사용하여 메뉴바 만들기!

 

[REACT NATIVE] 인스타그램 클론 코딩 (16) react-native-paper 사용하여 메뉴바 만들기!

< 이전글 2023.01.02 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (15) 로그아웃 구현하기 [REACT NATIVE] 인스타그램 클론 코딩 (15) 로그아웃 구현하기 < 이전글 2023.01.01 - [개발/react-native] - [REACT

otis.tistory.com

 

728x90
Comments