일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 스프링
- 풀스택 개발자
- 비전공
- spring boot
- 개발
- 리엑트 네이티브
- react-native
- 풀스택
- 클론코딩
- 비전공자
- 프론트엔드
- 상태관리
- ffmpeg
- Redux
- 리엑트
- 국비지원
- 스프링 부트
- react
- 자바
- 인스타그램
- 국비 지원
- 프론트 엔드
- 스타트업
- 개발자
- Spring
- expo
- react native
- 코딩
- 백엔드
- Java
- Today
- Total
오티스의개발일기
[REACT NATIVE] 인스타그램 클론 코딩 (12) AsyncStorage 를 사용하여 jwt 토큰 저장하기 본문
[REACT NATIVE] 인스타그램 클론 코딩 (12) AsyncStorage 를 사용하여 jwt 토큰 저장하기
안되면 될때까지.. 2023. 1. 1. 12:56
< 이전글
다음글 >
2023.01.01 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (13) 마이페이지 ui 만들기
오늘은 저번 포스팅에서 받은 jwt 토큰을 클라이언트에 보내 저장해볼예정이다.
처음에는 redux 를 활용해 저장할생각이였지만
로그인후 앱을 나갔을때 상태가 초기화된다는것을 간과하였다...
그래서 async-storage 를 활용해 앱을 종료하더라도 로그인이 유지되도록 작업할것이다
# async-storage 란?
암호화 되지 않은 비동기적인 데이터를 관리하는 Key-Value 저장 시스템이다
원래는 react-native 에서 지원하였지만 현재는 지원하지않고
community packages에 오픈 소스로 만들어진 AsyncStorage 를 사용하여야 한다.
# 사용하는 이유
현재처럼 로그인을 유지시켜주어야 할때 사용하는게 적절하다.
웹이라면 로그아웃시키는것이 맞지만 앱같은경우 로그아웃이 되면 불편하기때문이다.
장점
- 사용이 매우 간편하다
- 앱을 종료한 후에도 데이터를 보존할수있다.
단점
- 암호화가 되어있지않기때문에 보안에 이슈가있다.
- 상태관리를 redux로 하기때문에 사용할때마다 따로 임포트시켜야하는 불편함이있다.
나는 단순 임포트후 사용하지않고
util.js 라는 폴더를 만들어
사용할예정이다.
추후에 이러한 모듈들은 이곳에 담아서 사용할것이다. 그래야 가독성도 증가하고 정리도 확실히 되기때문이다.
# 오늘 작업할 파일목록
- utils.js
- LoginScreen.js
- userAction.js
# 0. 폴더 구조
# 1 . util.js
import AsyncStorage from '@react-native-async-storage/async-storage'
export const isEmpty = (value) => {
return (value === '' || value === null || value === undefined || (value != null && typeof value === 'object' && !Object.keys(value).length))
}
export const setStoreData = async (key, value) => {
try {
const stringValue = JSON.stringify(value);
await AsyncStorage.setItem(key, stringValue);
} catch (e) {
console.log(e.message)
}
}
export const getStoreData = async (key) => {
try {
const value = await AsyncStorage.getItem(key);
if (value !== null) {
const data = JSON.parse(value);
return data;
} else {
return null;
}
} catch (e) {
console.log(e.message)
}
}
export const removeStoreData = async (key) => {
try {
await AsyncStorage.removeItem(key);
} catch (e) {
console.log(e.message)
}
}
사용 방법은 간단한다.
web 개발을 해본사람이라면 localstorage 를 사용해본 경험이 있을것이다.
그것과 거의 유사하다.
저장할때는
JSON.stringify(value) 를 사용하여 json -> string 으로 변환후 저장하고
뺄때는
JSON.parse(value). String ->. json 으로 변환하여 빼서 쓰면된다.
삭제도 매우 간단하다 키만 넣어주며 끝이난다.
이제 userActions.js 를 작업해보겠다.
# 2. userAction.js
import {createAsyncThunk} from '@reduxjs/toolkit'
import axios from 'axios'
import {Alert} from 'react-native'
import * as $Util from '../constants/utils'
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) {
console.log(res.data.code)
if (res.data.code === 200) { // 정상 코드가 들어올시 비지니스로직 진행
let result = {
accesstoken: res.headers.accesstoken,
refreshtoken: res.headers.refreshtoken
}
$Util.setStoreData('token', result);
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 snsLoginRequset = createAsyncThunk('user', async (data, {dispatch, getState, rejectWithValue, fulfillWithValue}) => {
// try catch 는 하지말아야 에러를 캐치할수 있다.
// 상단 파라미터중 data는 요청시 들어온 파라미터이다. 저 파라미터를 가지고 서버에 데이터 요청하면된다.
const state = getState(); // 상태가져오기
let result = await snsLogin(data);
return result;
})
export {snsLoginRequset}
axios 에서 데이터를 받아 headers에서 토큰들을 빼낸후
우리가 초반에 만든 utils 를 활용하여 저장하는 모습이다.
너무 간단하지 않은가???
저거 한줄이면 작업은 끝난것이다.
자 이제 로그인 페이지에서 사용해보자
# 3. 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 Loader from '../common/Loader';
import * as $Util from '../../constants/utils'
import {
GoogleSignin,
GoogleSigninButton,
statusCodes,
} from '@react-native-google-signin/google-signin';
import userSlice from '../../slicers/userSlicer'
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 { navigation } = props; // 네비게이션
const data = useSelector(state => state.userSlicer)
$Util.getStoreData('token').then(function(res) { // <-- 작업한부분
console.log(res)
if (res != null) { // <-- 토큰이 존재한다면
navigation.replace(ROUTES.INDEX) // <-- 인덱스페이지로 이동
}
})
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
}
}
};
return (
<SafeAreaView>
{data.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`
`
상단에 작업한 부분을 확인해보면된다.
주석으로 어느정도 설명되었으니 확인바란다.
이렇게하면 앱을 들어왔을시 로그인이 되어있다면 바로 인덱스 페이지로 넘어가게된다.
실제 구동되는 모습을 확인해보자.
정상적으로 동작하는걸 확인할수있다.
다음시간에는 이 토큰을 사용하여 사용자 정보를 불러오고
오늘 사용한 스토리지의 remove 를 사용하여 로그아웃 하도록 하겠다.
아그리고 LoginScreen.js 뿐만아니라 AuthNavigation.js 에도 토큰 유무에따른 이동을 작업해놓을것이다.
이것으로 포스팅을 마치도록 하겠다.
# 깃허브 주소
https://github.com/1domybest/react-native-ig-clone.git
다음글 >
2023.01.01 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (13) 마이페이지 ui 만들기