회원가입에 이어서 로그인에 관한 기능을 완성하도록 하겠습니다. 우선 redux모듈에 header를 저장할 수 있게 store에 관련 state를 만들도록 하겠습니다.
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));
};
}
...
const initialState = {
...
headers: null,
...
};
const auth = handleActions(
{
...
[LOGIN_SUCCESS]: (state, { payload: auth, headers: headers, }) => ({
...state,
authError: null,
auth,
headers,
}),
...
...
);
export default auth;
...
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에 저장하도록 하겠습니다.
...
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 메서드를 연결하는 액션함수를 만들도록 하겠습니다.
...
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에 유저 정보가 잘 저장되는 모습을 볼 수 있습니다.
로그인이 되었으니 새로고침을 한번 눌러보도록 하겠습니다.
사진처럼 모든 state의 값들이 초기화되는 모습을 볼 수 있습니다. 그러면 로그인을 했어도 새로고침 한번이면 auth의 값이 초기화가 되니 아무 의미가 없겠죠. 그래서 이 유저의 값을 localStorage에 저장하여 새로고침을 해도 로그아웃을 하지 않는 이상 로그인을 유지할 수 있도록 만들어보겠습니다.
로그인 유지를 위한 순서는 다음과 같습니다.
1) 로그인 성공 시 auth에 유저 상태 저장
2) 유저 상태가 저장되면 userId를 읽어와 check메서드 수행
3) 성공적으로 값이 반환되면 user에 유저 상태 저장
4) 홈 화면으로 이동
auth-service에 check메서드를 다음과 같이 만들어주도록 하겠습니다.
@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 메서드를 만들어 보겠습니다.
export const check = userId => client.get(`/auth-service/${userId}/check`, {
headers: {
'Authorization': 'Bearer ' + JSON.parse(localStorage.getItem('token'))
}
});
그리고 이 요청을 연결하기 위한 리덕스, 액션함수 모듈을 만들겠습니다.
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,
);
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에 유저 정보를 담도록 하겠습니다.
...
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에 유저 상태가 저장이 되는지 확인해보도록 하겠습니다.
이제 새로고침을 해도 로그인을 유지해줄 state를 만들었으니 이를 활용해보도록 하겠습니다.
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정보를 제거할 수 있도록 메서드를 만들겠습니다.
...
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의 정보를 불러오도록 메서드를 만들도록 하겠습니다.
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하도록 하였습니다. 그러면 최종적으로 새로고침을 눌렀을 때도 유저의 정보가 존재하는지 확인하도록 하겠습니다.
새로고침을 해도 유저의 정보가 잘 있는 모습을 볼 수 있습니다.