오티스의개발일기

[REACT NATIVE] 인스타그램 클론 코딩 (12) AsyncStorage 를 사용하여 jwt 토큰 저장하기 본문

개발/react-native

[REACT NATIVE] 인스타그램 클론 코딩 (12) AsyncStorage 를 사용하여 jwt 토큰 저장하기

안되면 될때까지.. 2023. 1. 1. 12:56
728x90


< 이전글

2022.12.31 - [개발/spring boot] - [SPRING BOOT] 리엑트 네이티브 인스타그램 클론 코딩 (11) JWT 를 사용한 회원정보 상태관리 구현 -git참조-

 

[SPRING BOOT] 리엑트 네이티브 인스타그램 클론 코딩 (11) JWT 를 사용한 회원정보 상태관리 구현 -git

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

otis.tistory.com



다음글 >

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

 

[REACT NATIVE] 인스타그램 클론 코딩 (13) 마이페이지 ui 만들기

< 이전글 2023.01.01 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (12) AsyncStorage 를 사용하여 jwt 토큰 저장하기 [REACT NATIVE] 인스타그램 클론 코딩 (12) AsyncStorage 를 사용하여 jwt 토큰 저장하기

otis.tistory.com


 

오늘은 저번 포스팅에서 받은 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

 

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

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

github.com


다음글 >

 

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

 

[REACT NATIVE] 인스타그램 클론 코딩 (13) 마이페이지 ui 만들기

< 이전글 2023.01.01 - [개발/react-native] - [REACT NATIVE] 인스타그램 클론 코딩 (12) AsyncStorage 를 사용하여 jwt 토큰 저장하기 [REACT NATIVE] 인스타그램 클론 코딩 (12) AsyncStorage 를 사용하여 jwt 토큰 저장하기

otis.tistory.com


 

728x90
Comments