일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- ffmpeg
- 프론트 엔드
- 상태관리
- 스프링
- 인스타그램
- react
- 자바
- 백엔드
- 비전공
- Redux
- 프론트엔드
- 국비지원
- 스타트업
- 개발
- Java
- 리엑트
- expo
- react native
- 스프링 부트
- spring boot
- 클론코딩
- 풀스택 개발자
- 개발자
- 풀스택
- 코딩
- Spring
- react-native
- 국비 지원
- 리엑트 네이티브
- 비전공자
- Today
- Total
오티스의개발일기
[REACT NATIVE] 인스타그램 클론 코딩 (15) 로그아웃 구현하기 본문
< 이전글
2023.01.01 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (14) gorhom/bottom-sheet 를 활용한 바텀 시트 만들기
다음글 >
2023.01.02 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (16) react-native-paper 사용하여 메뉴바 만들기!
이번 시간에는 로그아웃을 구현해볼것이다.
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
다음글 >
2023.01.02 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (16) react-native-paper 사용하여 메뉴바 만들기!
'개발 > react-native' 카테고리의 다른 글
[REACT NATIVE] 인스타그램 클론 코딩 (17) navigation을 이용하여 새 게시물 스택 만들기 (2) | 2023.01.02 |
---|---|
[REACT NATIVE] 인스타그램 클론 코딩 (16) react-native-paper 사용하여 메뉴바 만들기! (5) | 2023.01.02 |
[REACT NATIVE] 인스타그램 클론 코딩 (14) gorhom/bottom-sheet 를 활용한 바텀 시트 만들기 (1) | 2023.01.01 |
[REACT NATIVE] 인스타그램 클론 코딩 (13) 마이페이지 ui 만들기 (1) | 2023.01.01 |
[REACT NATIVE] 인스타그램 클론 코딩 (12) AsyncStorage 를 사용하여 jwt 토큰 저장하기 (1) | 2023.01.01 |