Rental Application (React & Spring boot Microservice) - 26 : 로그인

yellow_note·2021년 9월 10일
0

#1 로그인

회원가입에 이어서 로그인에 관한 기능을 완성하도록 하겠습니다. 우선 redux모듈에 header를 저장할 수 있게 store에 관련 state를 만들도록 하겠습니다.

  • 로그인 시 res를 이용하여 localStorage에 저장하는 방법을 사용해봤지만 payload값을 읽어 들이지 못해 부득이하게 header state를 만들어 저장하는 방법을 사용하도록 하겠습니다.
  • ./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,
                headers: response.headers,
            });
        } catch(e) {
            yield put({
                type: FAILURE,
                payload: e,
                error: true,
            });
        }

        yield put(finishLoading(type));
    };
}
  • ./src/modules/auth.js
...

const initialState = {
    ...
    headers: null,
    ...
};

const auth = handleActions(
    {
        ...
        [LOGIN_SUCCESS]: (state, { payload: auth, headers: headers, }) => ({
            ...state,
            authError: null,
            auth,
            headers,
        }),
        ...
    ...
);

export default auth;
  • ./src/components/auth/LoginForm.js
...

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

export default withRouter(LoginForm);

이런 식으로 header의 값을 state에 저장하도록 하겠습니다. header는 로그인시 값을 읽어 들여와 저장되는 값으로 userid, token의 정보가 들어있습니다. 그러면 로그인을 했을 때, 유저의 정보도 읽어들여와 state에 저장하도록 하겠습니다.

  • ./src/lib/api/auth.js
...

export const getUser = userId => client.get(`/auth-service/${userId}/getUser`, {
    headers: {
        'Authorization': 'Bearer ' + JSON.parse(localStorage.getItem('token'))   
    }
});

...

이전에 만들어둔 apigateway-service에서는 Authorization에 토큰 값을 넣어 유저의 권한을 허락했습니다. 이 토큰 값 판별을 위해서 AuthorizationHeaderFilter를 구현했었구요. 그래서 유저의 정보를 얻어올 수 있는 getUser에서는 위의 코드처럼 headers에 토큰의 정보를 담아 GET 요청을 합니다.

그러면 이 API 메서드를 연결하는 액션함수를 만들도록 하겠습니다.

  • ./src/modules/auth.js
...

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

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

...

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

export default auth;

auth.js에 액션함수를 만들어 payload인 유저 정보값을 auth state에 저장하도록 하고, 이를 LoginForm에서 호출하도록 하겠습니다.

-./src/components/auth/LoginForm.js

...

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

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

        const {
            email,
            password
        } = form;

        dispatch(login({ 
            email, 
            password 
        }));
    };

    ...

    useEffect(() => {
        if(headers) {
            const { 
                userid, 
                token 
            } = headers;

            localStorage.setItem('token', JSON.stringify(token));
            
            dispatch(info(userid));
        }
    }, [dispatch, headers]);

    ...
};

export default withRouter(LoginForm);

useEffect훅을 이용했습니다. headers state값이 들어오면 로그인이 되었다는 의미이니 이 때, headers에서 userid, token의 값을 읽어들여와 localStorage에 토큰을 저장하고, 유저 정보를 얻어와 auth에 저장하는 순서로 구현을 진행했습니다.

중간 테스트를 진행하도록 하겠습니다.

로그인 버튼을 누르면 auth에 유저 정보가 잘 저장되는 모습을 볼 수 있습니다.

#2 user 모듈

로그인이 되었으니 새로고침을 한번 눌러보도록 하겠습니다.

사진처럼 모든 state의 값들이 초기화되는 모습을 볼 수 있습니다. 그러면 로그인을 했어도 새로고침 한번이면 auth의 값이 초기화가 되니 아무 의미가 없겠죠. 그래서 이 유저의 값을 localStorage에 저장하여 새로고침을 해도 로그아웃을 하지 않는 이상 로그인을 유지할 수 있도록 만들어보겠습니다.

로그인 유지를 위한 순서는 다음과 같습니다.

1) 로그인 성공 시 auth에 유저 상태 저장
2) 유저 상태가 저장되면 userId를 읽어와 check메서드 수행
3) 성공적으로 값이 반환되면 user에 유저 상태 저장
4) 홈 화면으로 이동

auth-service에 check메서드를 다음과 같이 만들어주도록 하겠습니다.

  • AuthController
@GetMapping("/{userId}/check")
public ResponseEntity<?> check(@PathVariable("userId") String userId) {
    log.info("Auth Service's Controller Layer :: Call getMyRentals Method!");

    if(userId == null) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Not user");
    }

    UserDto userDto = authService.getUser(userId);

    return ResponseEntity.status(HttpStatus.OK).body(ResponseUser.builder()
                                                                 .email(userDto.getEmail())
                                                                 .nickname(userDto.getNickname())
                                                                 .phoneNumber(userDto.getPhoneNumber())
                                                                 .userId(userDto.getUserId())
                                                                 .posts(userDto.getPosts())
                                                                 .build());
}

userId를 읽어와 유저의 상태 값을 반환해주는 메서드입니다. 이 상태값은 redux에서 만들 user모듈에 담기게 됩니다.

auth-service와 연결하기 위한 api 메서드를 만들어 보겠습니다.

  • ./src/lib/api/auth.js
export const check = userId => client.get(`/auth-service/${userId}/check`, {
    headers: {
        'Authorization': 'Bearer ' + JSON.parse(localStorage.getItem('token'))       
    }
});

그리고 이 요청을 연결하기 위한 리덕스, 액션함수 모듈을 만들겠습니다.

  • ./src/modules/user.js
import { createAction, handleActions } from "redux-actions";
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);

function checkFailureSaga() {
    try {
        localStorage.removeItem('user');
    } catch(e) {
        console.log('localStorage is not working');
    }
}

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,
);
  • ./src/modules/index.js
import { combineReducers } from "redux";
import { all } from 'redux-saga/effects';
import auth, { authSaga } from './auth';
import user, { userSaga } from "./user";
import loading from './loading';

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

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

export default rootReducer;

로그인 폼에서 check메서드를 호출하여 user에 유저 정보를 담도록 하겠습니다.

  • ./src/components/auth/LoginForm.js
...

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

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

        const {
            email,
            password
        } = form;

        dispatch(login({ 
            email, 
            password 
        }));
    };

    ...

    useEffect(() => {
        if(authError) {
            return;
        }

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

            dispatch(check(userId));

            history.push('/');
        }
    }, [dispatch, user, auth, authError, history]);

    useEffect(() => {
        if(headers) {
            const { 
                userid, 
                token 
            } = headers;

            localStorage.setItem('token', JSON.stringify(token));
            
            dispatch(info(userid));
        }
    }, [dispatch, headers, history, auth]);

    return (
        <AuthForm
            type='login'
            form={ form }
            onChange={ onChange }
            onSubmit={ onSubmit }
            error={ error }
        />
    );
};

export default withRouter(LoginForm);

여기까지 진행이 되었으니 user에 유저 상태가 저장이 되는지 확인해보도록 하겠습니다.

#3 로그인 유지

이제 새로고침을 해도 로그인을 유지해줄 state를 만들었으니 이를 활용해보도록 하겠습니다.

  • ./src/components/auth/LoginForm.js
const LoginForm = ({ history }) => {
    ...

    useEffect(() => {
        if(authError) {
            return;
        }

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

            dispatch(check(userId));
        }
    }, [dispatch, user, auth, authError, history]);

    useEffect(() => {
        if(user) {
            try {
                localStorage.setItem('user', JSON.stringify(user));
                
                history.push('/');
            } catch(e) {
                console.log('localStorage is not working');
            }
        }
    });

    ...
};

export default withRouter(LoginForm);

check메서드를 호출하면 user state에 유저정보가 저장되도록 구현을 마쳤으니 유저 정보가 기록되면 localStorage에 user정보를 담고 페이지 이동을 합니다.

그리고 user모듈에 코드를 추가하여 CHECK액션에 오류가 생겼을 경우 localStorage에서 user정보를 제거할 수 있도록 메서드를 만들겠습니다.

  • ./src/modules/user.js
...

function checkFailureSaga() {
    try {
        localStorage.removeItem('user');
    } catch(e) {
        console.log('localStorage is not working');
    }
}

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

...

마지막으로 처음 렌더링때 user의 정보를 불러오도록 메서드를 만들도록 하겠습니다.

  • ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
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 './modules';
import { BrowserRouter } from 'react-router-dom';
import { saveUser, check } from './modules/user';

...

function loadUser() {
  try {
    const user = JSON.parse(localStorage.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();

...

user의 상태를 불러오고 만약 user가 존재하지않는다면 return하도록 하였습니다. 그러면 최종적으로 새로고침을 눌렀을 때도 유저의 정보가 존재하는지 확인하도록 하겠습니다.


새로고침을 해도 유저의 정보가 잘 있는 모습을 볼 수 있습니다.

참고

  • 리액트를 다루는 기술 - 저자: 김민준

0개의 댓글

관련 채용 정보