LifeSports Application(ReactNative & Nest.js) - 9. auth-service(3)

#1 postman 테스트

앞서 작성한 auth-service를 테스트해보도록 하겠습니다. auth-service에서의 주요 메서드는 총 3가지입니다.

1) POST /auth-service/login
2) POST /auth-serivce/register
3) GET /auth-service/:userId

이 엔드포인트들을 위주로 테스트를 진행해보도록 하겠습니다.

1) register

정상적으로 회원가입이 되는 모습을 볼 수 있습니다.

그러면 오류 케이스들을 상정하고 테스트를 진행해보겠습니다.
1-1) 데이터가 미입력된 경우

phoneNumber 데이터를 미입력하고 회원가입을 진행할 경우 위와 같이 에러메시지가 출력됩니다.

1-2) 이메일이 중복된 경우

이메일이 중복된 경우 위와 같은 에러메시지를 확인할 수 있습니다.

이어서 로그인 테스트를 진행하도록 하겠습니다.

2) login

로그인의 경우 비밀번호 불일치, 존재하지 않는 회원의 에러케이스가 있습니다. 우선 정상적으로 로그인이 되는 경우를 테스트해보겠습니다.

2-1) 비밀번호 불일치

다른 비밀번호를 입력할 경우 Unauthorized 에러가 발생합니다.

2-2) 존재하지 않는 회원

존재하지 않는 이메일을 입력할 경우 Unauthorized 에러가 발생합니다.

이어서 getUser메서드를 테스트하도록 하겠습니다. getUser의 경우 존재하지 않는 유저아이디, 토큰 미입력의 오류 케이스들을 테스트해보도록 하겠습니다.

3) getUser

3-1) 존재하지 않는 유저아이디

3-2) 토큰 미입력

getUser의 정상적인 요청 그리고 오류케이스들을 테스트해보았습니다. 그러면 이제 react native와의 연동을 진행해보고 테스트를 해보겠습니다.

#2 react nativce, auth-service연동

client와 backend 통신을 위해 app-client 디렉토리에 다음의 라이브러리를 설치하도록 하겠습니다.

 npm install axios redux-saga

그리고 package.json 파일에 다음과 같은 proxy설정을 진행하도록 하겠습니다.

{
  ...
  "proxy": "http://localhost:7000/"
}

우선 7000번 포트에 맞춰두고 추후에 apigateway를 구현하게 되면 apigateway의 포트로 변경하도록 하겠습니다.

axios를 이용하여 endpoint를 연결해줄 api메서드를 만들어보도록 하겠습니다.

  • ./src/lib/api/client.js
import axios from 'axios';

const client = axios.create();

export default client;
  • ./src/lib/api/auth.js
import client from './client';

export const login = ({ 
    email, 
    password 
}) => client.post('http://10.0.2.2:7000/auth-service/login', { 
    email, 
    password 
});

export const register = ({ 
    email, 
    password, 
    phoneNumber, 
    nickname 
}) => client.post('http://10.0.2.2:7000/auth-service/register', { 
    email, 
    password, 
    phoneNumber, 
    nickname 
});

export const getUser = userId => client.get(`http://10.0.2.2:7000/auth-service/${userId}`);

그리고 액션의 시작과 끝을 알리는 loading 모듈을 만들겠습니다.

  • ./src/modules/loading.js
import { createAction, handleActions } from 'redux-actions';

const START_LOADING = 'loading/START_LOADING';
const FINISH_LOADING = 'loading/FINISH_LOADING';

export const startLoading = createAction(
    START_LOADING,
    requestType => requestType
);

export const finishLoading = createAction(
    FINISH_LOADING,
    requestType => requestType
);

const initialState = {};

const loading = handleActions(
    {
        [START_LOADING]: (state, action) => ({
            ...state,
            [action.payload]: true,
        }),
        [FINISH_LOADING]: (state, action) => ({
            ...state,
            [action.payload]: false,
        }),
    },
    initialState,
);

export default loading;

requestSaga함수는 saga를 요청하고 이에 대한 응답을 받기 위한 함수입니다. 하나의 정형화된 형식으로 응답을 받기 때문에 관리 측면에서 용이합니다.

  • ./src/lib/createRequestSaga.js
import { call, put } from 'redux-saga/effects';
import { startLoading, finishLoading } from '../modules/loading';

export const createRequestActionTypes = type => {
    const SUCCESS = `${type}_SUCCESS`;
    const FAILURE = `${type}_FAILURE`;

    return [
        type, 
        SUCCESS, 
        FAILURE
    ];
};

export default function createRequestSaga(type, request) {
    const SUCCESS = `${type}_SUCCESS`;
    const FAILURE = `${type}_FAILURE`;

    return function* (action) {
        yield put(startLoading(type));

        try {
            const response = yield call(request, action.payload);

            yield put({
                type: SUCCESS,
                payload: response.data.payload
            });
        } catch(err) {
            yield put({
                type: FAILURE,
                payload: err,
                error: true,
            });
        }
    
        yield put(finishLoading(type));
    };
}

이제 이 createRequestSaga를 이용해서 auth모듈을 추가적으로 작성하도록 하겠습니다.

  • ./src/modules/auth.js
import { createAction, handleActions } from "redux-actions";
import produce from "immer";
import { takeLatest } from "@redux-saga/core/effects";
import createRequestSaga, { 
    createRequestActionTypes 
} from "../lib/createRequestSaga";
import * as authAPI from '../lib/api/auth';

...

const [
    REGISTER, 
    REGISTER_SUCCESS, 
    REGISTER_FAILURE
] = createRequestActionTypes('auth/REGISTER');

const [
    LOGIN, 
    LOGIN_SUCCESS, 
    LOGIN_FAILURE
] = createRequestActionTypes('auth/LOGIN');

...
export const register = createAction(REGISTER, ({
    email,
    password,
    phoneNumber,
    nickname
}) => ({
    email,
    password,
    phoneNumber,
    nickname
}));
export const login = createAction(LOGIN, ({
    email,
    password
}) => ({
    email,
    password,
}));

const registerSaga = createRequestSaga(REGISTER, authAPI.register);
const loginSaga = createRequestSaga(LOGIN, authAPI.login);

export function* authSaga() {
    yield takeLatest(REGISTER, registerSaga);
    yield takeLatest(LOGIN, loginSaga);
}

...

const auth = handleActions(
    {
        ...
        [REGISTER_SUCCESS]: (state, { payload: auth }) => ({
            ...state,
            authError: null,
            auth,
        }),
        [REGISTER_FAILURE]: (state, { payload: error }) => ({
            ...state,
            authError: error,
        }),
        [LOGIN_SUCCESS]: (state, { payload: auth }) => ({
            ...state,
            authError: null,
            auth
        }),
        [LOGIN_FAILURE]: (state, { payload: error }) => ({
            ...state,
            authError: error,
        }),
    },
    initialState,
);

export default auth;

auth모듈을 index에 rootSaga를 생성하여 넣어주고, index의 rootSaga를 sagaMiddleware에 넣어 실행하도록 하겠습니다.

  • ./src/modules/index.js
import { combineReducers } from "redux";
import { all } from "redux-saga/effects";
import auth, { authSaga } from './auth';
import loading from "./loading";

const rootReducer = combineReducers({
    auth,
    loading,
});

export function* rootSaga() {
    yield all([authSaga()]);
};

export default rootReducer;
  • ./src/App.js
import React from 'react';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from '@redux-saga/core';
import rootReducer, { rootSaga } from './src/modules';
import StackNavigatior from './src/navigator/MainNavigation';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer, 
  composeWithDevTools(applyMiddleware(sagaMiddleware)),
);

sagaMiddleware.run(rootSaga);

const App = () => {
  return(
    <Provider store={ store }>
      <StackNavigatior />
    </Provider>
  );
};

export default App;

auth모듈을 실행할 준비가 되었으니 회원가입을 구현하도록 하겠습니다.

  • ./src/pages/auth/RegisterForm.js
import React, { useEffect, useState } from 'react';
import { View, StyleSheet } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, register } from '../../../modules/auth';
import StyledFullButton from '../../../styles/common/StyledFullButton';
import StyledTextInput from '../../../styles/common/StyledTextInput';
import palette from '../../../styles/palette';
import ErrorMessage from '../../../styles/common/ErrorMessage';

const RegisterForm = ({ navigation }) => {
    const [error, setError] = useState(null);
    const dispatch = useDispatch();
    const { 
        form,
        auth,
        authError 
    } = useSelector(({ auth }) => ({
        form: auth.register,
        auth: auth.auth,
        authError: auth.authError,
    }));

    const onChange = e => {
        const inputAccessoryViewID = e.target._internalFiberInstanceHandleDEV.memoizedProps.inputAccessoryViewID;
        const value = e.nativeEvent.text;
        
        dispatch(
            changeField({
                form: 'register',
                key: inputAccessoryViewID,
                value
            })
        );
    };

    const toSignUp = e => {
        e.preventDefault();

        const { 
            email, 
            password, 
            passwordConfirm,
            nickname,
            phoneNumber
        } = form;

        if(password !== passwordConfirm) {
            dispatch(changeField({ form: 'register', key: 'password', value: '' }));
            dispatch(changeField({ form: 'register', key: 'passwordConfirm', value: '' }));

            setError('Not match password and re-password')
            
            return;
        }

        dispatch(register({
            email,
            password,
            phoneNumber,
            nickname,
        }));
    };

    useEffect(() => {
        dispatch(initializeForm('register'));
    }, [dispatch]);

    useEffect(() => {
        if(auth) {
            if(auth.status === "ERROR") {
                setError(auth.message);
    
                return;
            } else if(auth.status === "SUCCESS") {
                setError(null);

                navigation.navigate('SignIn', {
                        name: 'SignIn'
                    },
                );
            }
        }
        
        if(authError) {
            setError("Not completely fill inputs, check inputs");

            return;
        }
    }, [authError, auth, dispatch]);

    return(
        <View style={ styles.container }>
            <StyledTextInput inputAccessoryViewID="email"
                             placeholder="E-mail"
                             placeholderTextColor={ palette.gray[5] }
                             onChange={ onChange }
                             value={ form.email }
            />
            <StyledTextInput inputAccessoryViewID="password"
                             placeholder="Password"
                             placeholderTextColor={ palette.gray[5] }
                             onChange={ onChange }
                             value={ form.password }
            />
            <StyledTextInput inputAccessoryViewID="passwordConfirm"
                             placeholder="Re-Password"
                             placeholderTextColor={ palette.gray[5] }
                             onChange={ onChange }
                             value={ form.passwordConfirm }
            />
            <StyledTextInput inputAccessoryViewID="nickname"
                             placeholder="Nickname"
                             placeholderTextColor={ palette.gray[5] }
                             onChange={ onChange }
                             value={ form.nickname }
            />
            <StyledTextInput inputAccessoryViewID="phoneNumber"
                             placeholder="Phone-Number"
                             placeholderTextColor={ palette.gray[5] }
                             onChange={ onChange }
                             value={ form.phoneNumber }
            />
            { error && <ErrorMessage text={ error } /> }
            <StyledFullButton onPress={ toSignUp }
                              text="Sign Up"
            />
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
});

export default RegisterForm;

앞서 postman에서 다양한 에러메시지를 테스트해보았습니다. 이 에러메시지는 message키에 담겨 오는데 이 message를 useState를 이용하여 error에 담아주고, 사용자에게 에러메시지를 보여주도록 하겠습니다.

회원가입 폼까지 완료가 되었으니 연동이 잘 되었는지 확인을 하도록 하겠습니다.

#3 테스트

  • package.json의 scripts.android, proxy를 다음과 같이 바꾸도록 하겠습니다.
{
  ...
  "scripts": {
    "android": "react-native run-android adb reverse tcp:8081 tcp:7000",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint ."
  },
  ...
  "proxy": "http://10.0.2.2:7000/"
}

adb reverse tcp:8081 tcp:7000은 react native의 서버 포트인 8081을 7000번 포트를 가진 로컬 서버와 연결하겠다는 의미입니다. 그리고 10.0.2.2는 가상 에뮬레이터의 localhost ip번호입니다. react native를 안드로이드로 실행할 경우 가상 에뮬레이터는 컴퓨터 안에서 동작하는 하나의 네트워크를 가진 기기이므로 자체 로컬호스트 주소를 사용해야 하는 것 같습니다. 마찬가지로 api디렉토리에 있는 auth파일의 localhost를 10.0.2.2로 바꾸도록 하겠습니다.

그리고 npm run android 명령어로 실행을 하겠습니다.

회원가입의 경우 잘 실행되는 모습을 볼 수 있습니다. 이어서 postman에서 테스트했던 에러케이스도 테스트해보겠습니다.

1-1) 데이터가 미입력된 경우

1-2) 이메일이 중복된 경우

1번의 오류케이스가 잘 실행되는 모습을 볼 수 있습니다.

이어서 로그인도 구현해보도록 하겠습니다.

#4 로그인

  • ./src/pages/auth/components/LoginForm.js
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { View, StyleSheet } from 'react-native';
import StyledBorderButton from '../../../styles/common/StyledBorderButton';
import StyledFullButton from '../../../styles/common/StyledFullButton';
import StyledTextInput from '../../../styles/common/StyledTextInput';
import palette from '../../../styles/palette';
import { changeField, initializeForm, login } from '../../../modules/auth';
import ErrorMessage from '../../../styles/common/ErrorMessage';

const LoginForm = ({ navigation }) => {
    const dispatch = useDispatch();
    const [error, setError] = useState(null);
    const { 
        form,
        auth,
        authError, 
    } = useSelector(({ auth }) => ({
        form: auth.login,
        auth: auth.auth,
        authError: auth.authError,
    }));

    const onChange = e => {
        const inputAccessoryViewID = e.target._internalFiberInstanceHandleDEV.memoizedProps.inputAccessoryViewID;
        const value = e.nativeEvent.text;

        dispatch(changeField({
                form: 'login',
                key: inputAccessoryViewID,
                value
            }),
        );
    };

    const toSignUpScreen = e => {
        e.preventDefault();

        navigation.navigate('SignUp');
    };

    const onLogin = e => {
        e.preventDefault();

        const { 
            email,
            password
        } = form;
        
        dispatch(login({ 
            email, 
            password
        }));
    };

    useEffect(() => {
        if(auth) {
            if(auth.status === 200) {
                navigation.navigate('Tab');
                
                setError(null);
                
                dispatch(initializeForm('login'));
            }
        }

        if(authError) {
            setError("Not valid user, check again email and password");

            return;
        }
    }, [auth, authError, dispatch]);

    useEffect(() => {
        dispatch(initializeForm('login'));
    }, [dispatch]);
    
    return(
        <View style={ styles.loginBox }>
            <StyledTextInput 
                inputAccessoryViewID="email"
                placeholder="E-mail"
                placeholderTextColor={ palette.gray[5] }
                onChange={ onChange }
                value={ form.email }
            />
            <StyledTextInput 
                inputAccessoryViewID="password"
                placeholder="Password"
                placeholderTextColor={ palette.gray[5] }
                onChange={ onChange }
                value={ form.password }
            />
            { error && <ErrorMessage text={ error } /> }
            <StyledFullButton 
                onPress={ onLogin }
                text="Sign in"
            />
            <StyledBorderButton
                onPress= { toSignUpScreen } 
                text="Sign up"
            />
        </View>
    );
};

const styles = StyleSheet.create({
    loginBox: {
        flex: 1,
        justifyContent: 'center',
        alignContent: 'center',
    },
});

export default LoginForm;

로그인을 구현을 했으니 정상 케이스, 오류 케이스를 나누어 테스트를 진행해보도록 하겠습니다.

2) login

2-1) 비밀번호 미입력

2-2) 존재하지 않는 회원

로그인 테스트도 정상적으로 진행됨을 확인할 수 있습니다. 다음 포스트에서는 로그인 시 user의 상태를 담을 user 리덕스 모듈, 로그아웃을 구현해보도록 하겠습니다.

좋은 웹페이지 즐겨찾기