axios를 사용하여 API를 연동하겠습니다.
Axios
< axios 인스턴스 생성 >
// src/lib/api/client.js import axios from 'axios'; const client = axios.create(); /** * 글로벌 설정 예시: * API 주소를 다른 곳으로 사용함 * client.defaults.baseURL = `https://external-api-server.com/` * * 헤더 설정 * client.defaults.headers.common['Authorization'] = 'Bearer alb2c3d4'; * * 인터셉터 설정 * axios.intercepter.response.use( \ * response => { * 요청 성공 시 특정 작업 수행 * return response; * }, * error => { * 요청 실패 시 특정 작업 수행 * return Promise.reject(error); * } * ) * */ export default client;
이렇게 axios인스턴스를 만들면 나중에 API클라이언트에 공통된 설정을 쉽게 넣어 줄 수 있습니다. 사실 인스턴스를 만들지 않아도 이러한 작업을 할 수 있습니다. 하지만 인스턴스를 만들지 않으면 애플리케이션에서 발생하는 모든 요청에 대해 설정하게 되므로, 또 다른 API서버를 사용할 때 곤란해질 수 있습니다.
< 프록시 설정 >
현재 백엔드 서버는 4000포트, 리액트 개발 서버는 3000포트로 열려 있기 때문에 별도의 설정 없이 API를 호출하려고 하면 오류가 발생합니다. 이 오류를 CORS(Cross Origin Request)라고 합니다. 네트워크 요청을 할 때 주소가 다른 경우에 발생합니다. 이 오류를 해결하려면 다른 주소에서도 API를 호출할 수 있도록 서버 쪽 코드를 수정해야 합니다. 그런데 최종적으로 프로젝트를 다 완성하고 나면 결국 리액트 앱도 같은 호스트에서 제공할 것이기 때문에 이러한 설정을 하는 것은 불필요 합니다.
그 대신 프록시라는 기능을 사용할 것입니다. 웹팩 개발 서버에서 지원하는 기능입니다. 개발 서버로 요청하는 API들을 우리가 프록시로 정해 둔 서버로 그대로 전달해 주고 그 응답을 웹 애플리케이션에서 사용할 수 있게 해 줍니다.
CRA에서 만든 프로젝트는 package.json에서 서중 가능합니다.
{ "name": "blog-frontend", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.2.1", "axios": "^0.26.1", "immer": "^9.0.12", "open-color": "^1.9.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "^7.2.6", "react-router-dom": "^6.2.2", "react-scripts": "5.0.0", "redux": "^4.1.2", "redux-actions": "^2.6.5", "redux-devtools-extension": "^2.13.9", "redux-saga": "^1.1.3", "styled-components": "^5.3.3", "web-vitals": "^2.1.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "proxy": "http://localhost:4000" }
이제 리액트 애플리케이션에서 client.get('/api/posts')를 하게 되면, 웹팩 개발 서버가 프록시 역할을 해서 http://localhost:4000/api/posts에 대신 요청한 뒤 결과물을 응답해 줍니다.
< API 함수 작성 >
// src/lib/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');
이 코드는 이제 외부 서버에서 요청할 api를 담은 것이다. 또한 우리는 이제 react-saga로 비동기 처리를 할 것이므로 api처리를 위한 saga함수들을 작성해 보도록 하겠다.
// src/lib/api/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* ({ payload }) { yield put(startLoading(type)); try { const { data } = yield call(request, payload); yield put({ type: SUCCESS, payload: data, }); } catch (e) { yield put({ type: FAILURE, payload: e, error: true, }); } yield put(finishLoading(type)); }; }
이는 반복되는 사가문을 리팩토링 해서 다른 파일로 빼낸 것입니다.
// src/modules/lauth.js import { createAction, handleActions } from 'redux-actions'; import produce from 'immer'; import { takeLatest } from 'redux-saga/effects'; import createRequestSaga, { createRequestActionTypes, } from '../lib/api/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 export const register = createAction(REGISTER, ({ username, password }) => ({ username, password, })); export const login = createAction(LOGIN, ({ username, password }) => ({ username, password, })); // 사가 생성 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], }), // 회원가입 성공 [REGISTER_SUCCESS]: (state, { payload: auth }) => ({ ...state, authError: null, auth, }), [REGISTER_FAILURE]: (state, { payload: error }) => ({ ...state, authError: error, }), [LOGIN_SUCCESS]: (state, { payload: auth }) => ({ ...state, authErorr: null, auth, }), [LOGIN_FAILURE]: (state, { payload: error }) => ({ ...state, authError: error, }), }, initialState, ); export default auth;
우선 차례대로 createRequestActionTypes를 이용해서 action type3개를 한번에 반환 시키도록 하였습니다. 또한 당연히 REGISTER와 LOGIN액션을 실행시켜 주기위한 액션 생성함수인 register와 login을 만들어 주었습니다. 그 후에는 사가함수를 각각 만들어 주었는데 registerSaga와 loginSaga는 아까 리팩토링 하였던 createReqestSaga를 통해 사가 함수를 반환하게 했습니다. 이 사가 함수에서 api를 비동기적으로 호출하는 것입니다. 그 후에는 authSaga함수를 작성해서 둘을 takeLatest를 활용하여 불러와 줍니다.
그리고 initialState에서는 api를 불러온 결괏값을 담을 auth와 에러가 발생했을 때 이를 담을 authError을 만들어 넣어 주었습니다.그리고 각각에 사가 함수를 실행하면서 발생할 SUCCESS, FAILURE함수들에 대한 리덕스를 만들어 줍니다.
// 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({ auth, loading, }); export function* rootSaga() { yield all([authSaga()]); } export default rootReducer;
// src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import { BrowserRouter as Router } from 'react-router-dom'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import rootReducer, { rootSaga } from './modules'; import createSagaMiddleware from '@redux-saga/core'; const sagaMiddleware = createSagaMiddleware(); const store = createStore( rootReducer, composeWithDevTools(applyMiddleware(sagaMiddleware)), ); sagaMiddleware.run(rootSaga); ReactDOM.render( <Provider store={store}> <Router> <App /> </Router> </Provider>, document.getElementById('root'), );
그 다음에는 redux-saga미들웨어를 프로젝트에 적용을 해주는 작업입니다.
그리고 아까 설명하지 못한 loading을 관리하는 리덕스도 써줍니다.
// src/modules/loading.js import { createAction, handleActions } from 'redux-actions'; const START_LOADING = 'loading/START_LOADING'; const FINISH_LOADING = 'loading/FINISH_LOADING'; /** * 요청을 위한 액션 타입을 payload로 설정합니다. */ 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;
그 다음에는 RegisterForm에 register action을 발생시켜서 mongodb에 회원 정보를 저장하는 작업을 진행해 줍니다.
// src/containers/auth/RegisterForm.js 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; if (password !== passwordConfirm) { // TODO: 오류처리 return; } dispatch(register({ username, password })); }; // 회원가입 성공/실패 처리 useEffect(() => { if (authError) { console.log('오류 발생'); console.log(authError); return; } if (auth) { console.log('회원가입 성공'); console.log(auth); } }, [auth, authError]); // 컴포넌트가 처음 렌더링될 떄 form을 초기화함 useEffect(() => { dispatch(initializeForm('register')); }, []); return ( <AuthForm type="register" form={form} onChange={onChange} onSubmit={onSubmit} /> ); }; export default RegisterForm;
form에 해당하는 username, password, passwordConfirm을 받아서 password confirm한다음 register action parmater로 넘겨주었습니다.



'React > ReactJs' 카테고리의 다른 글
React - React Router v6 (0) | 2022.03.18 |
---|---|
React - FrontEnd Project - 3 (0) | 2022.03.17 |
React - FrontEnd Project - 1 (0) | 2022.03.15 |
React - JWT (0) | 2022.03.13 |
React - API의 활용을 위한 prototype (0) | 2022.03.13 |