본문으로 바로가기

서버 구현

 

JWT Refresh Token을 이용한 로그인 (with Node.js, React Native) - 1. 서버

진행중인 프로젝트에 로그인 기능을 구현하기 위해 JWT를 사용하여 refresh token/ access token을 구현했다. 글이 길어져 서버 구현 / 클라이언트 구현으로 나누어 작성한다. 또한 JWT의 개념적인 내용보

tae-jun.tistory.com

지난 서버 구현에 이어 React Native로 로그인 과정을 구현에 대한 글을 작성한다.

모든 구현 내용을 블로그에 담을 순 없어서 화면 구성에 대한 내용은 제외하고 로그인 로직에 중점을 맞추어 글을 작성했다.

 

1. 라이브러리 설치

# 내부 저장소 사용
npm install @react-native-async-storage/async-storage

# http 통신 사용
npm install axios

# 토스트 사용
npm install react-native-toast-message

# 화면 전환(navigation) 사용
npm install @react-navigation/native 
npm install @react-navigation/stack

 

2. tokenUtils.js

import AsyncStorage from "@react-native-async-storage/async-storage";
import axios from "axios";
import { Toast } from "react-native-toast-message/lib/src/Toast";
const URL = 'http://IP:PORT'

const showToast = (text) =>{
    Toast.show({
        type: 'error',
        position: 'bottom',
        text1: text,
      });
};

export const getTokens = (email, password, navigation) => {
    axios.post(`${URL}/login`,
    {
      "userId":email,
      "userpw":password
    })
    .then(res =>{{
          //accessToken, refreshToken 로컬에 저장
          if (res.status === 200){
            AsyncStorage.setItem('Tokens', JSON.stringify({
              'accessToken': res.data.accessToken,
              'refreshToken': res.data.refreshToken,
              'userId': res.data.userId
            }))
            navigation.navigate('HomeTab');
          }

    }})
    .catch(error =>{
            if(error.response.status === 401){
                showToast(error.response.data)
            }
            else{
                showToast("알수없는 오류")
            } 
          
    })
};

const getTokenFromLocal = async () => {
  try {
    const value = await AsyncStorage.getItem("Tokens");
    if (value !== null) {
      return JSON.parse(value)
    }
    else{
      return null;
    }
  } catch (e) {
    console.log(e.message);
  }
};


export const verifyTokens = async (navigation) => {
  const Token = await getTokenFromLocal();

  // 최초 접속
  if (Token === null){
    navigation.reset({routes: [{name: "AuthPage"}]});
  }
  // 로컬 스토리지에 Token데이터가 있으면 -> 토큰들을 헤더에 넣어 검증 
  else{
    const headers_config = {
      "refresh": Token.refreshToken,
      Authorization: `Bearer ${Token.accessToken}`   
    };

    try {
      const res = await axios.get(`${URL}/refresh`, {headers: headers_config})

      // accessToken 만료, refreshToken 정상 -> 재발급된 accessToken 저장 후 자동 로그인
      AsyncStorage.setItem('Tokens', JSON.stringify({
        ...Token,
        'accessToken': res.data.data.accessToken,
      }))
      navigation.reset({routes: [{name: "HomeTab"}]});

    } catch(error){
      const code = error.response.data.code; 

      // accessToken 만료, refreshToken 만료 -> 로그인 페이지
      if(code === 401){
        navigation.reset({routes: [{name: "AuthPage"}]});
      }
      // accessToken 정상, refreshToken 정상 -> 자동 로그인
      else{
        navigation.reset({routes: [{name: "HomeTab"}]});
      }
    }

  }
};

- showToast(text)

text를 인자로 받아 해당 text를 토스트로 띄워주는 함수이며 해당 라이브러리에 대한 자세한 사용법은 링크에 설명되어 있다.

 

- getTokens(email, password, navigation)

사용자가 로그인 버튼을 클릭 했을 때 email, password, navigation을 인자로 받아 토큰을 로컬 저장소에 저장하고 화면을 전환해주는 함수이다.

1. axios.post() 함수를 이용하여 서버의 URL/login 경로로  email, password 정보를 json형식으로 담아 post한다. 

2. 서버로부터 응답을 받아 상태 코드가 200(정상)이면 AsyncStorage.setItem() 함수를 통해 로컬 저장소에 accessToken, refreshToken, userId를 Tokens라는 이름으로 저장한 후 홈화면으로 이동한다.

3. 상태 코드가 200이 아니라면 showToast() 함수를 호출하여 사용자에게 오류가 발생했다는 토스트를 띄운다.

 

- getTokenFromLocal()

로컬 저장소에서 accessToken, refreshToken, userId를 가져오는 함수이다.

AsyncStorage.getItem("Tokens")를 호출하여 Tokens라는 이름으로 저장된 데이터를 가져온다. 오류 없이 데이터를 가져오면 해당 데이터를 JSON 객체로 변환하여 반환하고 오류가 발생하여 가져온 데이터가 없다면 null을 반환한다.

 

- verifyTokens(navigation)

splash 화면에서 토큰값을 검증하는 함수이다.

1. getTokenFromLocal() 함수를 호출하여 로컬 저장소에 있는 토큰값을 불러온다.

2. 만약 Token값이 null이라면 앱에 최초 접속했다는 의미이므로 로그인 화면으로 전환한다.

3. Token값이 있다면 헤더에 refreshToken과 accessToken을 넣어 서버의 URL/refresh 경로로 get 요청을 보낸다.

4. 이때 accessToken이 만료되고 refreshToken이 정상이면 서버에서 상태 코드(200)과 함께 새로운 accessToken을 발급하여 응답을 보내오고 재발급된  accessToken은 로컬 저장소에 저장시킨 후 홈화면으로 이동하여 자동 로그인한다.

5. 서버에서 상태 코드(401)로 응답한다면 accessToken이 만료되고 refreshToken도 만료되었다는 의미이며 이는 다시 로그인하여 새로운 accessToken, refreshToken을 발급 받아야 한다. 따라서 로그인 페이지로 이동한다.

6. 서버에서 401 이외의 상태 코드로 응답한다면 accessToken은 정상, refreshToken도 정상이므로 홈화면으로 이동해 자동 로그인한다.

 

AuthStackNavigator.js

import React from "react";
import { createStackNavigator } from "@react-navigation/stack";

import { HomeTapNavigator } from "./HomeTabNavigator";
import AuthPage from "../pages/AuthPage";
import SignUpPage from "../pages/SignUpPage";
import SplashPage from "../pages/SplashPage";
import ResendMailPage from "../pages/ResendMailPage";

const AuthStackNavigator = ({navigation}) => {
    const Stack = createStackNavigator();

    return (
        <Stack.Navigator>
            <Stack.Screen name='SplashPage' component={SplashPage} options={{ headerShown: false}} navigation={navigation}/>
            <Stack.Screen name='AuthPage' component={AuthPage} options={{ headerShown: false}} navigation={navigation}/>
            <Stack.Screen name='HomeTab' component={HomeTapNavigator} options={{ headerShown: false }}/>
            <Stack.Screen name='SignUpPage' component={SignUpPage} options={{ headerShown: false}}/>
            <Stack.Screen name='ResendMailPage' component={ResendMailPage} options={{ headerShown: false}}/>
        </Stack.Navigator>
  );
}

export { AuthStackNavigator };

SignUpPage와 ResendMailPage는 회원가입에 필요한 화면이므로 로그인 과정에는 사용하지 않는다.

Stack navigator에 대한 자세한 사용법은 링크를 참고하면 된다.

 

SplashPage.js

import React, { useEffect } from 'react';
import { Text, View, } from 'react-native';
import { verifyTokens } from '../../utils/networks/tokenUtils';

const SplashPage = ({navigation}) => {
    useEffect(()=>{
        verifyTokens(navigation);
    },[])
    
  return (
    <View style ={{padding: 50}}>
      <Text style={{padding: 10, fontSize: 42}}>
        SPLASH
      </Text>
    </View>
    );
}

export default SplashPage;

앱에 최초 접속했을 때 보여지는 화면이다.

userEffect() 함수를 사용하여 화면이 처음 그려질 때 verifyTokens() 를 호출하여 토큰을 검증하고 상황에 맞게 홈화면, 로그인 페이지로 이동한다.

 

AuthPage.js

import { View} from 'react-native';
import { getTokens } from '../../utils/networks/tokenUtils';
import AuthTemplate from "../templates/AuthTemplate";
import { useState } from "react";

const AuthPage = ({navigation}) => {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    
    const onPressLoginButton = () =>{
        getTokens(email, password, navigation);
    }

    const onPressButton = () =>{
      navigation.navigate('SignUpPage');
      }
    const onPressResendMail = () =>{
      navigation.navigate('ResendMailPage');
    }

  return (
    <View style ={{padding: 50}}>
      <AuthTemplate 
        onPressButton={onPressButton} 
        onPressLoginButton={onPressLoginButton}
        onPressResendMail={onPressResendMail}
        email={email}
        setEmail={setEmail}
        password={password}
        setPassword={setPassword}
        />
    </View>
    );
}

export default AuthPage;

로그인 페이지이며 useState()를 사용하여 email과 password에 대한 상태값을 갖는다. 사용자가 TextInput에 이메일과 패스워드를 입력하면 email, password의 상태값이 변하며 사용자가 로그인 버튼을 누르면 두 상태값을 getTokens()함수에 넣어 호출하여 로그인 과정을 진행한다.

 

 


결과

로그인

자동로그인

 

참고

React Native : AsyncStorage 쉽게 사용해보기. (tistory.com)

[ RN] 로그인 및 JWT (velog.io)