LifeSports Application(ReactNative & Nest.js) - 10. auth-service(4)

yellow_note·2021년 10월 1일
0

#1 user 리덕스 모듈

react native에서는 localStorage사용이 안되므로 다음의 패키지를 설치하도록 하겠습니다.

npm install @react-native-async-storage/async-storage
  • ./src/modules/user.js
import { createAction, handleActions } from "redux-actions";
import AsyncStorage from '@react-native-async-storage/async-storage';
import { takeLatest } from "redux-saga/effects";
import * as authAPI from '../lib/api/auth';
import createRequestSaga, {
    createRequestActionTypes,
} from "../lib/createRequestSaga";

const SAVE_USER = 'user/SAVE_USER';
const [
    CHECK,
    CHECK_SUCCESS,
    CHECK_FAILURE
] = createRequestActionTypes('user/CHECK');

export const saveUser = createAction(SAVE_USER, user => user);
export const check = createAction(CHECK, userId => userId);

const checkSaga = createRequestSaga(CHECK, authAPI.check);

async function checkFailureSaga() {
    try {
        await AsyncStorage.removeItem('user');
    } catch(e) {
        console.log(e);
    }
}

export function* userSaga() {
    yield takeLatest(CHECK, checkSaga);
    yield takeLatest(CHECK_FAILURE, checkFailureSaga);
}

const initialState = {
    user: null,
    checkError: null,
};

export default handleActions(
    {
        [SAVE_USER]: (state, { payload: user }) => ({
            ...state,
            user,
        }),
        [CHECK_SUCCESS]: (state, { payload: user }) => ({
            ...state,
            user,
            checkError: null
        }),
        [CHECK_FAILURE]: (state, { payload: error }) => ({
            ...state,
            user: null,
            checkError: error,
        }),
    },
    initialState,
);

user 모듈의 기능을 보겠습니다. saveUser는 로그인 시 유저의 상태를 리덕스 state에 저장해주는 역할을 합니다. 그리고 check는 userId를 매개로 하여 체크하고자 하는 유저가 타당한 유저인지 확인해주는 메서드입니다. checkFailureSaga의 경우 user state를 제거해주는 역할을 합니다.

그리고 auth모듈에 다음의 액션을 만들어주도록 하겠습니다.

  • ./src/modules/auth.js
...
const [
    INFO,
    INFO_SUCCESS,
    INFO_FAILURE,
] = createRequestActionTypes('auth/INFO');

...
export const info = createAction(INFO, userId => userId);

...
const infoSaga = createRequestSaga(INFO, authAPI.getUser);

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

...

const auth = handleActions(
    {
        ...
        [INFO_SUCCESS]: (state, { payload: auth }) => ({
            ...state,
            authError: null,
            auth,
        }),
        [INFO_FAILURE]: (state, { payload: error }) => ({
            ...state,
            authError: error,
        }),
    },
    initialState,
);

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

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

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

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

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

async function loadUser() {
  try {
    const user = JSON.parse(await AsyncStorage.getItem('user'));
    
    if(!user) {
      return;
    }

    store.dispatch(saveUser(user));

    const { userId } = user;
    
    store.dispatch(check(userId));
  } catch(e) {
    console.log(e);
  }
}

sagaMiddleware.run(rootSaga);
loadUser();

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

export default App;

loadUser 메서드를 만들어 애플리케이션이 구동되자마자 유저의 상태를 불러올 수 있도록 합니다.

  • ./src/lib/api/auth.js
...
export const getUser = async userId => client.get(`http://10.0.2.2:7000/auth-service/${userId}`, {
    headers: {
        'Authorization': 'Bearer ' + JSON.parse(await AsyncStorage.getItem('token'))       
    }
});

export const check = async userId => client.get(`http://10.0.2.2:7000/auth-service/${userId}/check`, {
    headers: {
        'Authorization': 'Bearer ' + JSON.parse(await AsyncStorage.getItem('token'))       
    }
});

check 메서드는 헤더에 jwt토큰을 실어 타당한 유저인지 확인합니다.

auth-service의 컨트롤러에 check에 관한 메서드를 추가하도록 하겠습니다.

  • ./src/app.controller.ts
...

@Controller('auth-service')
export class AppController {
    ...

    @Get(':userId/check')
    public async check(@Param('userId') userId: string): Promise<any> {
        if(userId === null) {
            return await Object.assign({
                status: HttpStatus.UNAUTHORIZED,
                payload: null,
                message: "Not valid user"
            });
        }

        const result: any = await this.userService.getUser(userId);

        return await Object.assign({
            status: HttpStatus.OK,
            payload: Builder(ResponseUser).email(result.payload.email)
                                          .nickname(result.payload.nickname)
                                          .phoneNumber(result.payload.phoneNumber)
                                          .userId(result.payload.userId)
                                          .build(),
            message: "Check user!"
        });
    }
}

유저 모듈이 완성되었습니다. 이어서 유저모듈을 로그인 폼에 불러오도록 하겠습니다.

  • ./src/pages/auth/components/
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { View, StyleSheet } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
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, info, initializeForm, login } from '../../../modules/auth';
import ErrorMessage from '../../../styles/common/ErrorMessage';
import { check } from '../../../modules/user';

const LoginForm = ({ navigation }) => {
    ...

    useEffect(() => {
        if(auth) {
            const { userId } = auth;

            dispatch(check(userId));
        }

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

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

    useEffect(async () => {
        if(user) {
            try {
                setError(null);
        
                dispatch(initializeForm('login'));
            
                await AsyncStorage.setItem('user', JSON.stringify(user))
            
                dispatch(initializeForm('auth'));

                navigation.navigate('Tab');
            } catch(e) {
                console.log(e);
            }
        }
    }, [user]);

    useEffect(async () => {
        if(auth) {
            if(auth.token) {
                const { token } = auth;
                const { userId } = auth;

                await AsyncStorage.setItem('token', JSON.stringify(token));
                
                dispatch(info(userId));
            }
        }
    }, [dispatch, auth]);
    
    ...
};

...

export default LoginForm;

그러면 이 코드들을 바탕으로 유저 state가 잘 저장이 되는지 확인을 해보도록 하겠습니다.


로그인 결과 유저의 state가 잘 담겨있는 모습을 볼 수 있습니다. 그러면 마이페이지에서 이 유저의 상태 값을 유저에게 보여주고, 로그아웃도 구현을 해보도록 하겠습니다.

  • ./src/pages/user/components/MyPageHeader.js
import React from 'react';
import {
    StyleSheet,
    View,
    Text
} from 'react-native';
import { useSelector } from 'react-redux';
import palette from '../../../styles/palette';

const MyPageHeader = () => {
    const { user } = useSelector(({ user }) => ({ user: user.user }));

    return(
        <View style={ styles.container }>
            <Text style={ styles.text }>
                { user ? user.nickname : null }
            </Text>
            <Text>
                { user ? user.email : null }
            </Text>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        width: 420,
        height: 100,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: palette.gray[3],
    },
    text: {
        fontWeight: 'bold',
        fontSize: 17,
    },
});

export default MyPageHeader;

MyPageHeader에 나타나는 간단한 유저의 정보들을 표시를 해보았고, 로그아웃 관련 모듈, 메서드를 만들도록 하겠습니다.

우선 auth-service 컨트롤러에 다음의 메서드를 정의하겠습니다.

  • ./src/app.controller.ts
...

@Controller('auth-service')
export class AppController {
    ...

    @Post('logout')
    public async logout(): Promise<any> {
        try {
            return await Object.assign({
                status: HttpStatus.NO_CONTENT,
                payload: null,
                message: "Success logout!"
            });
        } catch(err) {
            return await Object.assign({
                status: statusConstants.ERROR,
                payload: null,
                message: "Error message: " + err
            });
        }
    }
}

그리고 react native에서 이와 연결하기 위한 API endpoint 메서드를 정의하겠습니다.

  • ./src/lib/api/auth.js
...
export const logout = () => client.post('http://10.0.2.2:7000/auth-service/logout');

로그아웃 모듈입니다.

  • ./src/modules/user.js
import { createAction, handleActions } from "redux-actions";
import AsyncStorage from '@react-native-async-storage/async-storage';
import { takeLatest, call } from "redux-saga/effects";
import * as authAPI from '../lib/api/auth';
import createRequestSaga, {
    createRequestActionTypes,
} from "../lib/createRequestSaga";

...
const LOGOUT = 'user/LOGOUT';

...
export const logout = createAction(LOGOUT);

...

async function logoutSaga() {
    try {
        call(authAPI.logout);

        await AsyncStorage.removeItem('user');
    } catch(err) {
        console.log(err);
    }
}

export function* userSaga() {
    yield takeLatest(CHECK, checkSaga);
    yield takeLatest(CHECK_FAILURE, checkFailureSaga);
    yield takeLatest(LOGOUT, logoutSaga);
}

...

export default handleActions(
    {
        ...
        [LOGOUT]: state => ({
            ...state,
            user: null
        }),
    },
    initialState,
);

이 모듈을 LogoutButton 컴포넌트에 불러와 테스트를 진행해보겠습니다.

  • ./src/pages/user/components/LogoutButton.js
import React from 'react';
import { 
    StyleSheet,
    Text, 
    TouchableOpacity 
} from 'react-native';
import { useDispatch } from 'react-redux';
import { logout } from '../../../modules/user';
import palette from '../../../styles/palette';

const LogoutButton = () => {
    const navigation = useNavigation();
    const dispatch = useDispatch();
    const onLogout = e => {
        e.preventDefault();

        dispatch(logout());

        navigation.navigate('SignIn');
    };

    return(
        <TouchableOpacity style={ styles.container }
                          onPress={ onLogout }
        >
            <Text style={ styles.text }>
                Logout
            </Text>
        </TouchableOpacity>
    );
};

const styles = StyleSheet.create({
    container: {
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'center',
        width: 350,
        height: 50,
        borderRadius: 30,
        borderColor: palette.blue[4],
        borderWidth: 2,
        backgroundColor: palette.white[0],
        marginTop: 10,
    },
    text: {
        fontWeight: 'bold',
        fontSize: 15,
        color: palette.blue[4],
    },
});

export default LogoutButton;



테스트 결과 유저의 state가 잘 제거되는 모습을 볼 수 있습니다. 다음 포스트에서는 대관 데이터 관련 local server를 띄워보고 map 서비스에서 이 데이터를 요청받아보도록 하겠습니다.

0개의 댓글

관련 채용 정보