Rental Application (React & Spring boot Microservice) - 25 : redux-saga

yellow_note·2021년 9월 7일
0

#1 redux-saga적용

redux-saga는 액션들을 모니터링하고, 특정 액션이 발생시 이 액션을 처리하기 위한 작업을 수행합니다. 우선 redux-saga, axios를 설치하도록 하겠습니다. axios는 API를 연동하기 위한 라이브러리입니다.

npm install redux-saga axios
  • ./src/lib/api/client.js
import axios from 'axios';

const client = axios.create();

export default client;

프록시를 8900번 포트(apigateway-service)로 설정하여 CORS 오류를 예방하도록 하겠습니다.

  • pacakage.json
{
  ...
  "proxy": "http://localhost:8900/"
}

다음과 같이 회원 관리에 필요한 api를 작성하도록 하겠습니다. 기존에 만들어둔 auth-service의 endpoint url을 활용합니다.

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

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

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

export const getUser = ({ userId }) => client.getUser('/auth-service/:userId/getUser', { 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;
  • ./src/modules/index.js
import { combineReducers } from "redux";
import auth from './auth';
import loading from './loading';

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

export default rootReducer;

createRequestSaga는 제네레이터 함수를 이용하여 액션 함수가 정상적으로 작동이 되었을 때, API의 데이터에 대한 요청과 응답을 처리하기 위한 유틸함수입니다.

  • ./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,
            });
        } catch(e) {
            yield put({
                type: FAILURE,
                payload: e,
                error: true,
            });
        }

        yield put(finishLoading(type));
    };
}

1) createRequestActionTypes: 액션타입에 대한 코드입니다. 예를 들어 로그인에 관한 액션 타입을 'auth/LOGIN', 'auth/LOGIN_SUCCESS', 'auth/LOGIN_FAILURE'라고 했을 때 해당 메서드를 사용하여 액션타입을 변수에 부여합니다.

2) createRequestSaga: Saga를 요청하는 코드입니다. 예를 들어 'auth/LOGIN'이라는 타입과 아이디, 비밀번호를 가진 ./src/lib/api/auth.js에 존재하는 login request가 들어왔을 때, 이에 대한 요청을 제네레이터 함수를 이용하여 처리합니다.

그러면 이 코드들을 이용해서 api처리를 위한 코드를 완성하도록 하겠습니다.

  • ./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 CHANGE_FIELD = 'auth/CHANGE_FIELD';
const INITIALIZE_FORM = 'auth/INITIALIZE_FORM';

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

export const changeField = createAction(
    CHANGE_FIELD, ({
        form,
        key,
        value
    })  => ({
        form,
        key,
        value,
    }),
);

export const initializeForm = createAction(INITIALIZE_FORM, form => form);

export const login = createAction(LOGIN, ({ 
    email, 
    password 
}) => ({
    email,
    password
}));

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

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

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

const initialState = {
    register: {
        email: '',
        password: '',
        passwordConfirm: '',
        nickname: '',
        phoneNumber: '',
    },
    login: {
        email: '',
        password: '',
    },
    auth: null,
    authError: null,
};

const auth = handleActions(
    {
        [CHANGE_FIELD]: (state, { payload: { form, key, value }}) => 
        produce(state, draft => {
            draft[form][key] = value;
        }),
        [INITIALIZE_FORM]: (state, { payload: form }) => ({
            ...state,
            [form]: initialState[form],
            authError: null,
        }),
        [LOGIN_SUCCESS]: (state, { payload: auth }) => ({
            ...state,
            authError: null,
            auth
        }),
        [LOGIN_FAILURE]: (state, { payload: error }) => ({
            ...state,
            authError: error,
        }),
        [REGISTER_SUCCESS]: (state, { payload: auth }) => ({
            ...state,
            authError: null,
            auth,
        }),
        [REGISTER_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';

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

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

export default rootReducer;

./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/core';
import rootReducer, { rootSaga } from './modules';
import { BrowserRouter } from 'react-router-dom';

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

sagaMiddleware.run(rootSaga);

ReactDOM.render(
  <Provider store={ store }>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById('root')
);
reportWebVitals();

리덕스 관련 코드들을 작성했으므로 Form 컴포넌트에서 회원가입과 로그인을 진행할 수 있는 코드를 작성하겠습니다.

#2 회원가입

  • ./src/components/auth/RegisterForm.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, register } from '../../modules/auth';
import AuthForm from './AuthForm';
import { withRouter } from 'react-router-dom';

const RegisterForm = ({ history }) => {
    const dispatch = useDispatch();
    const { 
        form, 
        auth, 
        authError
    } = useSelector(({ 
        auth,
    }) => ({
        form: auth.register,
        auth: auth.auth,
        authError: auth.authError,
    })); 

    ...

    // Handler that registers form
    const onSubmit = e => {
        e.preventDefault();

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

        if(password !== passwordConfirm) {
            // setError
            
            return;
        }

        if([
            email, 
            password,
            passwordConfirm,
            nickname,
            phoneNumber,
        ].includes('')) {
            // setError
            return;
        }

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

        history.push('/');
    };

    ...
};

export default withRouter(RegisterForm);

discovery-service, apigateway-service, config-server, auth-service를 구동시키고 테스트 결과를 보도록 하겠습니다.



회원가입이 잘 된 모습을 볼 수 있습니다.

다음 포스트에서는 로그인 관련 redux를 작성해보도록 하겠습니다.

참고

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

0개의 댓글