23_리액트 리덕스 미들웨어(2)
FrontEnd/React

23_리액트 리덕스 미들웨어(2)

728x90

JSON Server

 

가짜 Rest api를 호출하여 연습용 서버를 만들 수 있다

 

먼저 root프로젝트에 data.json을 만든다 (src폴더보다도 밖)

//data.json
{
    "posts" : [
        {
            "id" : 1,
            "title" : "정민규의 나이",
            "body" : "25살"
        },
        {
            "id" : 2,
            "title" : "정민규의 성별",
            "body" : "남자"
        },
        {
            "id" : 3,
            "title" : "정민규의 최애메뉴",
            "body" : "치킨"
        }
    ]
}

 

 

npx json-server ./data/json --port 4000

그 다음 터미널을 열어서 서버를 개통시켜준다.

 

 

 

터미널에 나온 해당 링크를 클릭해주고

 

주소를 localhost:4000/posts 로 이동해주면

 

 

입력한 데이터가 잘 출력되어 나온다.

 

https://www.postman.com/downloads/?utm_source=postman-home 

 

Download Postman | Get Started for Free

Try Postman for free! Join 17 million developers who rely on Postman, the collaboration platform for API development. Create better APIs—faster.

www.postman.com

위 프로그램을 사용하면 보다 쉽게 json파일에 접근하고, 정보를 수정시키고 할 수 있다.

 

이제 전 글에서 작성했던 프로젝트를 이렇게 만든 api를 사용하여 접근해보자.

 

yarn add axios

예전에 배웠던 axios라이브러리를 사용할 것이기에 먼저 설치해준다.

 

//api/post.js
import axios from 'axios';

export const getPosts = async () => {
    const response = await axios.get('http://localhost:4000/posts');
    return response.data;
}

export const getPostById = async (id) => {
    const response = await axios.get(`http://localhost:4000/posts/${id}`);
    return response.data;
}

기존 가짜api코드를 정상적으로 바꾸고 실행시키면

 

실행도 잘되고 네트워크 요청도 잘 되는것을 확인할 수 있다.

 

 

 

 

 

 

CORs and Webpack DevServer Proxy

 

현재 프로젝트에서 사용하는 주소는 http://localhost:3000/

api는 http://localhost:4000/posts이다.

 

포트가 다르기에 원래는 api를 사용할수 없어서 따로 설정해줘야하는데 json에는 미리 그것이 되어있어 별 문제없이 사용할 수 있다. 

 

하지만 따로 데이터파일을 만들거나 협업을 하거나 할때는 서버에 따로 설정을 해줘야하는데, 프록시 설정을 해두면 이런 번거로움을 피할 수 있다.

 

프록시 설정을 해보자.

 

//package.json   의 맨 끝
   "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },

  "proxy" : "http://localhost:4000"
}

다음처럼 "proxy" 를 입력하고 로컬 주소를 입력해준다. 받아오는 api주소를 입력하면 되겠다.

 

그 이후, 서버를 껏다 켜주면 된다.

 

 

 

redux-saga

 

redux thunk 다음으로 가장 많이 사용되는 미들웨어이다.

액션을 모니터링하고 있다가 액션이 발생하면 어떠한 작업 ( 함수실행,액션실행,상태조회 등)을 실행하는 것이다.

 

비동기 작업을 진행할 때 기존요청을 취소할수도 있다. thunk처럼 함수를 타입의 값을 디스패치할 필요가 없다. 비동기 작업이 실패했을때 재시도 하는 기능을 추가하는등 다양한 기능을 쓸 수 있다.

 

Generator라는 문법을 사용하기에 이에 대해 간단히 알아보자.

 

 

Generator

함수의 흐름을 특정 구간에 멈춰놓았다가 다시실행할 수 있다.

결과값을 여러번 내보낼 수 있다.

 

function* generatorFunction() {
    console.log('안녕하세요?');
    yield 1; //함수의 흐름을 멈추고 1을 반환
    console.log('제너레이터 함수');
    yield 2;
    console.log('function*');
    yield 3;
    return 4;
}

위함수는 아래와 같이 동작한다.

 

즉, 함수를 한번 실행할때마다 멈추게 되며, yield에 써진 값이 반환된다.

 

 

이번엔 아래 예시를 한번 보자

function* sumGenerator() {
    console.log('sumGenerator이 시작됐습니다.');
    let a = yield;
    console.log('a값을 받았습니다.');
    let b = yield;
    console.log('b값을 받았습니다.');
    yield a + b;
}

역시 비슷한 맥락으로 작동된다.

 

 

끝나지 않는 Generator도 만들 수 있다.

 

function* inifinitedAddGenerator() {
    let result = 0;
    while(true) {
    	result += yield result;
    }
}

이처럼 적으면 무한이 작동하는 Generator가 된다.

 

 

이처럼 redux-saga는 Generator에 기반되어 있다. 어렵긴 하지만 유틸함수들이 이미 잘 만들어있으니 사용하는 법을 한번 알아보자.

 

 

yarn add redux-saga

리덕스 사가 라이브러리를 설치하고 시작해보자.

 

그다음 이전에 만들었던 thunk를 이용해 만들었던 아래 counter예제를 redux-saga로 만들어 보자.

//counter.js
export const increaseAsync = () => (dispatch) => {
    setTimeout(() => {
        dispatch(increase());
    },1000)
}

export const decreaseAsync = () => (dispatch) => {
    setTimeout(() => {
        dispatch(decrease());
    },1000)
}

 

 

아래와 같이 Generator문법에 따라서 기본적인 Saga를 작성해주었다.

//counter.js
import { delay,put, takeEvery, takeLatest } from 'redux-saga/effects' //effects : reduxsaga가 수행하도록 작업을 명령하는 것

const INCREASE = 'INCREASE';  //action type
const DECREASE = 'DECREASE';
const INCREASE_ASYNC = 'INCREASE_ASYNC' //redux-saga
const DECREASE_ASYNC = "DECREASE_ASYNC"

export const increase = () => ({ type : INCREASE}); //action생성함수
export const decrease = () => ({ type : DECREASE});
export const increaseAsync = () => ({ type : INCREASE_ASYNC });
export const decreaseAsync = () => ({ type : DECREASE_ASYNC });


function* increaseSaga() {
    yield delay(1000);
    yield put(increase()); //dispatch와 유사

}

function* decreaseSaga() {
    yield delay(1000);
    yield put(decrease());
}

export function* counterSaga() { //내보내줘야함. 결국 rootsaga가 있어야함.
    yield takeEvery(INCREASE_ASYNC, increaseSaga); //INCREASE_ASYNC가 디스패치될때마다 increaseSaga실행
    yield takeLatest(DECREASE_ASYNC,decreaseSaga); //기존건 무시하고 가장 마지막것만 처리
}


const initialState = 0; //초기상태


export default function counter (state = initialState,action) { //리듀서
    switch (action.type) {
        case INCREASE:
            return state +1;
        case DECREASE:
            return state - 1;
        default:
            return state;
    }
}

 

counterSaga는 내보내줘야하 기때문에 export해주었다.

 

//modules/index.js
import { combineReducers } from "redux"; //root리듀서를 만들기 위해
import counter, { counterSaga } from "./counter";
import posts from './posts';
import { all } from 'redux-saga/effects'

const rootReducer = combineReducers({counter,posts});

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

export default rootReducer;

그 이후 rootReducer를 만들었던 것처럼 rootSaga도 만들어주어야 한다.

 

//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore , applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import {composeWithDevTools} from 'redux-devtools-extension';  //개발자도구 적용
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import {createBrowserHistory} from 'history';
import createSagaMiddleware from 'redux-saga';

const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  rootReducer, 
  composeWithDevTools(
    applyMiddleware(
      sagaMiddleware,
      ReduxThunk.withExtraArgument({history : customHistory}),
      logger
    )));//미들웨어 적용

    sagaMiddleware.run(rootSaga) //rootSaga를 호출할 필요는



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

// 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();

이전에 만들었던 index.js에 createSagaMiddleware를 불러오고 스토어에 sagamiddleware를 추가시켜주고

sagaMiddleware.run 함수를 rootSaga를 파라미터로 해서 적어준다.

 

 

그다음 App.js에서 다음처럼 Counter예제를 불러오면

function App() {
  return (
    <>
      <CounterContainer/>
      <Route path = "/" component={PostListPage} exact />
      <Route path="/:id" component={PostPage} />
    </>
  );
}

 

 

잘 작동이 되는것을 알 수 있다. 

 

이때 +버튼은 3번연속 누르면 +3이 결국 되지만, -는 3번연속누르면 1만 감소하는것을 볼 수 있는데, 이는 takeEvery와 takeLatest의 차이라고 볼 수 있다.

 

 

redux-saga로 Promise 다루기

 

이전에 reduxthunk같은경우는

 

export const getPosts = () => async dispatch => {
  dispatch({ type: GET_POSTS }); // 요청이 시작됨
  try {
    const posts = postsAPI.getPosts(); // API 호출
    dispatch({ type: GET_POSTS_SUCCESS, posts }); // 성공
  } catch (e) {
    dispatch({ type: GET_POSTS_ERROR, error: e }); // 실패
  }
};

다음과 같이 작동했다. 즉, 함수를 만들어서 해당함수로 비동기작업을 처리한 후에, 필요 시점에 특정액션이 디스패치 되는 방식으로 작동했다. saga는 이와는 약간 다르게 작동한다.

 

 

 

export const getPost = id => ({ type: GET_POST, payload: id, meta: id });

function* getPostsSaga() {
  try {
    const posts = yield call(postsAPI.getPosts); // call 을 사용하면 특정 함수를 호출하고, 결과물이 반환 될 때까지 기다려줄 수 있습니다.
    yield put({
      type: GET_POSTS_SUCCESS,
      payload: posts
    }); // 성공 액션 디스패치
  } catch (e) {
    yield put({
      type: GET_POSTS_ERROR,
      error: true,
      payload: e
    }); // 실패 액션 디스패치
  }
}

 

 

 

저번에 만들었던 Post예제를 바꾸어보겠다. Counter예제를 바꾼것과 상당히 유사하다.

 

//.modules/posts.js

import * as postsAPI from '../api/post';//postsAPI.getpost등으로 사용가능하게
import { createPromiseThunk, createPromiseThunkById, handleAsyncActions, handleAsyncActionsById, reducerUtils } from '../lib/asyncUtils';
import {call ,put, takeEvery} from 'redux-saga/effects';

/*액션*/
const GET_POSTS = 'GET_POSTS';
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS';
const GET_POSTS_ERROR = 'GET_POSTS_ERROR';

const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';  

const CLEAR_POST = 'CLEAR_POST';   //이전 포스트가 잠깐 보이는 문제를 해결하기 위해

/* saga */
export const getPosts = () => ({ type : GET_POSTS });
export const getPost = (id) => ({
    type : GET_POST,
    payload : id,
    meta : id,
});

function* getPostsSaga() {
    try {
        const posts = yield call(postsAPI.getPosts);
        yield put({
            type : GET_POSTS_SUCCESS,
            payload : posts
        })

    } catch (e) {
        yield put({
            type : GET_POSTS_ERROR,
            payload : e,
            error : true
        })
    }
}

function* getPostSaga(action) {
    const id = action.payload;
    try{
        const post = yield call(postsAPI.getPostById,id);
        yield put({
            type: GET_POST_SUCCESS,
            payload : post,
            meta : id,
        })
    } catch(e) {
        yield put({
            type: GET_POST_ERROR,
            payload : e,
            error : true
        })
    }
}

export function* postsSaga() {
    yield takeEvery(GET_POSTS, getPostsSaga);
    yield takeEvery(GET_POST,getPostSaga);
}


export const goToHome = () => (dispatch,getState,{history}) => {   // 3번째 파라미터안에 history가있어 비구조화 할당으로 가져옴
    history.push('/');                           //withExtraargument에서 extra가 들어오는데 그안에 history가 있다
}

export const clearPost = () => ({ type : CLEAR_POST});


const initialState = {
    posts:reducerUtils.initial(),
    post: {}
}


const getPostsReducer = handleAsyncActions(GET_POSTS,'posts',true);
const getPostReducer = handleAsyncActionsById(GET_POST,'post',true);


export default function posts(state = initialState,action) {
    switch(action.type){
        case GET_POSTS:
        case GET_POSTS_SUCCESS:
        case GET_POSTS_ERROR:
            return getPostsReducer(state,action)
        case GET_POST:
        case GET_POST_SUCCESS:
        case GET_POST_ERROR:
            return getPostReducer(state,action);
        case CLEAR_POST:   
        return{
            ...state,
            post : reducerUtils.initial(),
        }
        default:
            
            return state;
    }
}

 

이후 index.js에서

 

//modules/index.js
import { combineReducers } from "redux"; //root리듀서를 만들기 위해
import counter, { counterSaga } from "./counter";
import posts, { postsSaga } from './posts';
import { all } from 'redux-saga/effects'

const rootReducer = combineReducers({counter,posts});

export function* rootSaga() {
    yield all([counterSaga(), postsSaga]);
}

export default rootReducer;

만들어둔 root Saga를 추가해주기만 하면 된다.

 

 

이 코드역시 getpostSaga와 getpostsSaga가 굉장히 유사한점을 알 수 있는데, 코드 리팩토링을 한번 진행해 보도록 하자

 

 

이전에 thunk함수 리팩토링한 과정과 상당히 유사하다.

 

//asyncUtils.js
import {call,put} from 'redux-saga/effects';

export const createPromiseSaga = (type, promiseCreator) => {
    const [SUCCESS, ERROR ] = [`${type}_SUCCESS`, `${type}_ERROR`];

    return function* saga(action) {
        try{
            const result = yield call(promiseCreator, action.payload);
            yield put({
                type : SUCCESS,
                payload : result
            });
        } catch (e) {
            yield put({
                type : ERROR,
                payload : e,
                error : true
            });
        }
    } 
}

export const createPromiseSagaById = (type,promiseCreator) => {
    const [SUCCESS, ERROR ] = [`${type}_SUCCESS`, `${type}_ERROR`];

    return function* saga(action) {  
        const id = action.meta;
        try{
            const result = yield call(promiseCreator, action.payload);
            yield put({
                type : SUCCESS,
                payload : result,
                meta : id,
            });
        } catch (e) {
            yield put({
                type : ERROR,
                payload : e,
                error : true,
                meta : id
            });
        }
    } 
}

 

후에, post.js에서 saga를 선언했던 부분을 단 두줄로 줄일 수 있다!!

 

/* saga */
export const getPosts = () => ({ type : GET_POSTS });
export const getPost = (id) => ({
    type : GET_POST,
    payload : id,
    meta : id,
});

const getPostsSaga = createPromiseSaga(GET_POSTS,postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST,postsAPI.getPostById);

export function* postsSaga() {
    yield takeEvery(GET_POSTS, getPostsSaga);
    yield takeEvery(GET_POST,getPostSaga);
}

 

기능역시 원래와 똑같이 잘 작동하는것을 알 수 있다.

 

 

이전 글에서 thunk와 라우터를 history를 이용해서 연동했었는데 saga또한 비슷하게 할 수 있다.

 

먼저 index.js에서 context에 history를 다음과 같이 추가해준다.

 

//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore , applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import {composeWithDevTools} from 'redux-devtools-extension';  //개발자도구 적용
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import {createBrowserHistory} from 'history';
import createSagaMiddleware from 'redux-saga';

const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware({
  context : {
    history : customHistory   // saga에서 조회가 가능
  }
});

const store = createStore(
  rootReducer, 
  composeWithDevTools(
    applyMiddleware(
      sagaMiddleware,
      ReduxThunk.withExtraArgument({history : customHistory}),
      logger
    )));//미들웨어 적용

    sagaMiddleware.run(rootSaga) //rootSaga를 호출할 필요는



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

// 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();

context안에 history를 customhistory로 추가해주었다.

 

그후 effects안의 getContext 를 import해준 후에, 이를 사용해서 history를 불러온 후에 사용하면 된다.

 

 

더보기
//.modules/posts.js

import * as postsAPI from '../api/post';//postsAPI.getpost등으로 사용가능하게
import {  createPromiseSaga, createPromiseSagaById, handleAsyncActions, handleAsyncActionsById, reducerUtils } from '../lib/asyncUtils';
import { takeEvery, getContext} from 'redux-saga/effects';

/*액션*/
const GET_POSTS = 'GET_POSTS';
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS';
const GET_POSTS_ERROR = 'GET_POSTS_ERROR';

const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';  
const GO_TO_HOME = 'GO_TO_HOME'

const CLEAR_POST = 'CLEAR_POST';   //이전 포스트가 잠깐 보이는 문제를 해결하기 위해

/* saga */
export const getPosts = () => ({ type : GET_POSTS });
export const getPost = (id) => ({
    type : GET_POST,
    payload : id,
    meta : id,
});

const getPostsSaga = createPromiseSaga(GET_POSTS,postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST,postsAPI.getPostById);
function* goToHomeSaga() {
    const history = yield getContext('history');  //history 사용가능
    history.push('/');
}

export function* postsSaga() {
    yield takeEvery(GET_POSTS, getPostsSaga);
    yield takeEvery(GET_POST,getPostSaga);
    yield takeEvery(GO_TO_HOME,goToHomeSaga)
}


export const goToHome = () => ({ type : GO_TO_HOME})  //thunk와는 달리 순수 객체로

export const clearPost = () => ({ type : CLEAR_POST});


const initialState = {
    posts:reducerUtils.initial(),
    post: {}
}


const getPostsReducer = handleAsyncActions(GET_POSTS,'posts',true);
const getPostReducer = handleAsyncActionsById(GET_POST,'post',true);


export default function posts(state = initialState,action) {
    switch(action.type){
        case GET_POSTS:
        case GET_POSTS_SUCCESS:
        case GET_POSTS_ERROR:
            return getPostsReducer(state,action)
        case GET_POST:
        case GET_POST_SUCCESS:
        case GET_POST_ERROR:
            return getPostReducer(state,action);
        case CLEAR_POST:   
        return{
            ...state,
            post : reducerUtils.initial(),
        }
        default:
            
            return state;
    }
}

 

 

Saga에서 현재 상태를 조회할 수도 있다.

 

//.modules/posts.js

import * as postsAPI from '../api/post';//postsAPI.getpost등으로 사용가능하게
import {  createPromiseSaga, createPromiseSagaById, handleAsyncActions, handleAsyncActionsById, reducerUtils } from '../lib/asyncUtils';
import { takeEvery, getContext, select} from 'redux-saga/effects';

/*액션*/
const GET_POSTS = 'GET_POSTS';
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS';
const GET_POSTS_ERROR = 'GET_POSTS_ERROR';

const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';  
const GO_TO_HOME = 'GO_TO_HOME'

const CLEAR_POST = 'CLEAR_POST';   //이전 포스트가 잠깐 보이는 문제를 해결하기 위해

const PRINT_STATE = 'PRINT_STATE'  //saga로 상태조회


/* saga */
export const getPosts = () => ({ type : GET_POSTS });
export const getPost = (id) => ({
    type : GET_POST,
    payload : id,
    meta : id,
});
export const printState = () => ({ type : PRINT_STATE });

const getPostsSaga = createPromiseSaga(GET_POSTS,postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST,postsAPI.getPostById);
function* goToHomeSaga() {
    const history = yield getContext('history');  //history 사용가능
    history.push('/');
}
function* printStateSaga() {
    const state = yield select(state => state.posts);
    console.log(state);
}

export function* postsSaga() {
    yield takeEvery(GET_POSTS, getPostsSaga);
    yield takeEvery(GET_POST,getPostSaga);
    yield takeEvery(GO_TO_HOME,goToHomeSaga);
    yield takeEvery(PRINT_STATE,printStateSaga)
}


export const goToHome = () => ({ type : GO_TO_HOME})  //thunk와는 달리 순수 객체로

export const clearPost = () => ({ type : CLEAR_POST});


const initialState = {
    posts:reducerUtils.initial(),
    post: {}
}


const getPostsReducer = handleAsyncActions(GET_POSTS,'posts',true);
const getPostReducer = handleAsyncActionsById(GET_POST,'post',true);


export default function posts(state = initialState,action) {
    switch(action.type){
        case GET_POSTS:
        case GET_POSTS_SUCCESS:
        case GET_POSTS_ERROR:
            return getPostsReducer(state,action)
        case GET_POST:
        case GET_POST_SUCCESS:
        case GET_POST_ERROR:
            return getPostReducer(state,action);
        case CLEAR_POST:   
        return{
            ...state,
            post : reducerUtils.initial(),
        }
        default:
            
            return state;
    }
}
728x90

'FrontEnd > React' 카테고리의 다른 글

26_타입스크립트 & 리덕스  (0) 2022.01.07
25_타입스크립트 & 리액트  (0) 2022.01.07
22_리덕스 미들웨어  (0) 2022.01.05
21_리액트_리덕스  (0) 2022.01.02
20_리액트_라우터  (0) 2021.12.30