React + Spring - API 연동 (리액트편)

김태훈·2023년 1월 18일
0

이제, 리액트로 구현했던 회원가입/로그인 폼을 Spring으로 가져와서 회원가입과 로그인 기능을 직접 구현해보자.

1. 목표

브라우저에 렌더링 되어 있는 회원가입 폼에 사용자가 정보를 입력하고 가입하면, 해당 정보를 담은 API가 백엔드 쪽으로 가서 데이터로 저장되게 하는 것이 목표이다.

2. 리덕스(미들웨어)

리액트 웹 어플리케이션에서 API 서버를 연동할 때에는 리덕스가 매우 유용하다. API 서버를 연동할 때 API 요청에 대한 상태를 잘 관리해야 한다. 예를 들어, 요청이 시작되었을 때에는 로딩 중임을, 요청이 성공하거나 실패했을 때에는 로딩이 끝났음을 명시해야 한다. 이를 도와주는 것이 리덕스의 '미들웨어' 이다.

1. 미들웨어란?

미들웨어는 액션을 dispatch 했을 때(액션 발생) 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업을 실행한다. 즉, 액션과 리듀서 사이의 중간자 역할을 한다.
실제 프로젝트에서는 미들웨어를 직접 만들어서 사용할 일은 많지 않다. 이미 만들어진 미들웨어를 사용하면 되기 때문이다.

액션 -> 미들웨어 -> 리듀서 -> 스토어

2. 미들웨어의 구조를 코드로 보기

const loggerMiddleware = store => next => action => {
  //미들웨어 기본구조
}

일반 function 함수를 사용하게되면,

const loggerMiddleware = function loggerMiddleware(store){
  return function(next){
    return function(action){
      //미들웨어 기본구조
    };
  };
};

결국 미들웨어는 함수를 반환하는 함수라고 할 수 있다. store 인자는 리덕스 스토어 인스턴스를, actino은 dispatch된 action을 가리킨다. 이때 next는 무엇인가?
next는 함수형태이며, store.dispatch와 비슷한 역할을 하지만, next(action)을 호출하면 그 다음 처리해야할 미들웨어에게 액션을 넘겨주고, 만약 그 다음 미들웨어가 없다면 리듀서에게 액션을 넘겨준다.

액션 -> store.dispatch -> 미들웨어1 -> next -> 미들웨어2 ->next -> 리듀서 -> 스토어

3. 미들웨어 구현 예시 코드

const loggerMiddleware = store => next => action => {
  console.group(action && action.type) //액션 타입으로 log를 그룹화
  console.log('이전 상태',store.getState());
  console.log('액션',action);
  next(action); //다음 미들웨어 혹은 리듀서에게 전달
  console.log('다음 상태', store.getState()); //업데이트된 상태
  console.groupEnd(); //그룹 끝
};

이러한 구조를 이용하여 미들 웨어에서 특정 조건에 따라 액션을 무시하게할 수도, 또는 특정 조건에 따라 액션 정보를 가로채서 변경한 후 리듀서에게 전달해줄 수 있다. 이를 활용하여 비동기 작업을 관리한다.

4. 미들웨어 종류

  1. redux-logger
    오픈소스로 올라와 있는거
  2. redux-thunk
    비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어. 객체가 아닌 함수 형태의 액션을 디스패치할 수 있게 한다.
  3. redux-saga
    redux-thunk 다음으로 가장 많이 사용되는 비동기 작업 관련 미들웨어 라이브러리로, 특정 액션이 디스패치되었을 때 정해진 로직에 따라 다른 액션을 디스패치 시키는 규칙을 작성하여 비동기 작업을 처리할 수 있게 한다.

3. axios 사용한 API 연동

리덕스에서 비동기 작업을 쉽게 하기 위해 redux-saga를 사용하겠다.

1. axios 인스턴스 생성

import axios from 'axios';

const client = axios.create();

/*
  글로벌 설정 예시:
  
  // API 주소를 다른 곳으로 사용함
  client.defaults.baseURL = 'https://external-api-server.com/' 

  // 헤더 설정
  client.defaults.headers.common['Authorization'] = 'Bearer a1b2c3d4';

  // 인터셉터 설정
  axios.intercepter.response.use(\
    response => {
      // 요청 성공 시 특정 작업 수행
      return response;
    }, 
    error => {
      // 요청 실패 시 특정 작업 수행
      return Promise.reject(error);
    }
  })  
*/

export default client;

이렇게 axios 인스턴스를 만들면 나중에 API 클라이언트에 공통된 설정을 쉽게 넣어줄 수 있다. 인스턴스를 만들지 않아도 되지만, 그렇게 되면 모든 API 요청에 응답하므로, 또다른 API서버를 사용할 때 곤란해질 수 있다. 따라서 처음부터 이렇게 인스턴스를 만들어 작업하자.

2. 프록시 추가 설정

백엔드에서 프론트엔드로의 프록시 설정은 되어있지만, 아직 프론트에서 백으로 넘어오는 API에 대한 프록시 설정은 되어 있지 않다. REACT 설정 JSON 에서 추가해주자(8080포트로)

3. API 함수 작성

  • auth.js 파일
import client from './client';

//로그인
export const login = ({username,password}) =>
    client.post('/api/auth/login', {username,password});

//회원 가입
export const register = ({username,password}) =>
    client.post('/api/auth/register',{username,password});

//로그인 상태 확인
export const check = () => client.get('/api/auth/check');

4. redux-saga 통한 쉬운 API요청

1. loading.js 설정하기

import { createAction, handleActions } from 'redux-actions';

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

/*
 요청을 위한 액션 타입을 payload로 설정합니다 (예: "sample/GET_POST")
*/

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;

이후에 반드시 root리듀서(index.js)에 등록하도록 하자.

2. createRequestSaga 함수 작성하기

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)); // 로딩 끝
  };
}

이때 createRequestActionTypes는 auth 리덕스 모듈에서 중복을 막기 위해 함수를 선언하여 export 한 것이다.

5. auth 리덕스 모듈에서 API 사용하기

방금 만든 유틸함수 (createRequestSaga)를 사용하여 auth 리덕스 모듈에서 API를 사용할수 있도록 해보자.

import { createAction, handleActions } from 'redux-actions';
import produce from 'immer';
import { takeLatest } from 'redux-saga/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 [REGISTER, REGISTER_SUCCESS, REGISTER_FAILURE] = createRequestActionTypes(
  'auth/REGISTER'
);

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

export const changeField = createAction(
  CHANGE_FIELD,
  ({ form, key, value }) => ({
    form, // register , login
    key, // username, password, passwordConfirm
    value // 실제 바꾸려는 값
  })
);
export const initializeForm = createAction(INITIALIZE_FORM, form => form); // register / login
export const register = createAction(REGISTER, ({ username, password }) => ({
  username,
  password
}));
export const login = createAction(LOGIN, ({ username, password }) => ({
  username,
  password
}));

// saga 생성
const registerSaga = createRequestSaga(REGISTER, authAPI.register);
const loginSaga = createRequestSaga(LOGIN, authAPI.login);
export function* authSaga() {
  yield takeLatest(REGISTER, registerSaga);
  yield takeLatest(LOGIN, loginSaga);
}

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

const auth = handleActions(
  {
    [CHANGE_FIELD]: (state, { payload: { form, key, value } }) =>
      produce(state, draft => {
        draft[form][key] = value; // 예: state.register.username을 바꾼다
      }),
    [INITIALIZE_FORM]: (state, { payload: form }) => ({
      ...state,
      [form]: initialState[form],
      authError: null // 폼 전환 시 회원 인증 에러 초기화
    }),
    // 회원가입 성공
    [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;

이 때, 로딩에 관련된 상태는 이미 loading 리덕스 모듈에서 관리하므로, 무시하자.
마찬가지로 root 리듀서에 적용시켜서 rootSaga를 만들어주자.

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;

그후, 스토어에 redux-saga 미들웨어를 적용시킨다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter} from 'react-router-dom';
import {Provider} from 'react-redux';
import {applyMiddleware, createStore} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import rootReducer, {rootSaga} from './modules';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMiddleware)));
sagaMiddleware.run(rootSaga);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

6.회원가입 기능 구현

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, register } from '../modules/auth';
import AuthForm from '../components/auth/AuthForm';

const RegisterForm = () => {
  const dispatch = useDispatch(); //스토어에서 컨테이너 컴포넌트를 가져옴
  const { form,auth,authError } = useSelector(({ auth }) => ({
    form: auth.register,
    auth:auth.auth,
    authError:auth.authError
  }));

  // 인풋 변경 이벤트 핸들러
  const onChange = (e) => {
    const { value, name } = e.target;
    dispatch(
      changeField({
        form: 'register',
        key: name,
        value,
      }),
    );
  };

  // 폼 등록 이벤트 핸들러
  const onSubmit = (e) => {
    e.preventDefault();
    const {username,password,passwordConfirm} = form; // state 에 존재하는 form에 따른 username,password,passwordconfirm key-value를 객체비구조화로 나타냄.
    if (password!== passwordConfirm){
      //오류처리 해야함
      return;
    }
    dispatch(register({username,password}));
  };

  //컴포넌트가 처음 렌더링될 때 form 을 초기화함
  useEffect(()=>{
    dispatch(initializeForm('register'));},[dispatch]);
  
  //회원가입 성공/실패 처리
  useEffect(()=>{
    if (authError){
      console.log('오류 발생');
      console.log(authError);
      return;
    }
    if (auth){
      console.log('회원가입 성공');
      console.log(auth);
    }
  },[auth,authError]);

  return (
    <AuthForm
      type="register"
      form={form}
      onChange={onChange}
      onSubmit={onSubmit}
    />
  );
};

export default RegisterForm;

자 이제 백엔드에서 해당 정보를 받아 데이터베이스에 저장해보자!!

profile
기록하고, 공유합시다

0개의 댓글