본문 바로가기

프로그래밍/React

React(리액트) - redux 미들웨어 알아보기(redux-thunk, redux-saga)

공부하면서 쓰는 글이니 오류가 있을 수 있습니다. 있다면 알려주시면 감사하겠습니다.!

 

리덕스에는 미들웨어라는 기능을 지원합니다.

 

리덕스에서 미들웨어는 액션과 리듀서 사이에 있는 중간 처리기입니다. 이 미들웨어는 액션 작업을 처리하기 전 액션과 리듀서 사이에서 필요한 작업 로그 기록이나 액션을 취소시킨다던가 액션을 받고 다른 액션도 호출할 수 있습니다. 

 

많이 알려진 미들웨어로는 로그를 기록하는 redux-logger, 비동기 처리를 해주는 redux-thunk, redux-saga가 있습니다.

 

미들웨어 동작 원리

미들웨어의 기본적인 구조와 작동 순서를 알아봅니다.

 

기본적인 리덕스 미들웨어는 함수의 함수를 리턴하는 구조로 되어있습니다. 

function exampleMiddleware(storeAPI) {
  return function wrapDispatch(next) {
    return function handleAction(action) {

      return next(action)
    }
  }
}

해당 미들웨어는 리덕스의 applyMiddleware 함수의 의해 호출됩니다. 

 

첫 번째 함수는 스토어를 인자로 받고 두 번째 함수는 next라는 인자를 받습니다. 세 번째는 알고 있는 액션을 받습니다.

 

두 번째 함수의 인자 next는 다음 미들웨어에 액션을 전달하는 함수를 전달받습니다. 만약 다음 미들웨어가 없다면 리듀서 함수에 전달됩니다.

 

액션 발생 -> 미들웨어 -> 리듀서 순으로 작동됩니다.

여기서 미들웨어가 여러 개 있다면 next함수로 미들웨어에 액션이 전달되면서 해당하는 작업이 진행되고 리듀서로 넘어갑니다.

 

아래 미들웨어와 액션 생성 함수와 리듀서를 만들었습니다.

// lib/prinMiddle.js
const printMiddle = store => next => action => {
    console.log(`현재 액션: ${action.type}`);
    next(action);
    console.log(`next후 상태: ${store.getState().current}`);
}

export default printMiddle;

// modules/printReducer.js
const PRINT_ACTION = "print/PRINT_ACTION";

export const printAction = () => ({ type: PRINT_ACTION });

const initalState = {
    current: null
}

function printReducer(state=initalState, action) {
    switch (action.type) {
        case PRINT_ACTION:
            return {
                ...state,
                current:"ACTION 1"
            }
        default:
            return state
    }
}

export default printReducer;

 

이번에는 컨테이너와 화면 컴포넌트를 만들고 연결합니다.

// components/Print.jsx
import React from "react";

const Print = ({ current, action }) => {
    return (
        <div>
            <button onClick={action}>클릭</button>
            <div>현재 상태 {current}</div>
        </div>
    );
};

export default Print;


// containers/PrintContainer.jsx
import React from "react";
import { connect } from "react-redux";
import Print from "../components/Print";
import { printAction } from "../modules/printReducer";

const PrintContainer = ({ current, printAction }) => {
    return (
        <Print current={current} action={printAction}/>
    );
};

const mapStateToProps = (state) => ({
    current: state.current
});

const mapDispatchToProps = {
    printAction
}

export default connect(mapStateToProps, mapDispatchToProps)(PrintContainer);

그리고 마지막으로 App.js 수정과 index에서 스토어를 만들고 미들웨어를 적용합니다.

// App.js
import PrintContainer from './container/PrintContainer';

function App() {
  return (
    <div>
      <PrintContainer />
    </div>
  );
}

export default App;

// index.js
// 기존꺼에서 추가하기
import printMiddle from './lib/printMiddle';
import printReducer from './modules/printReducer';

const store = createStore(printReducer, applyMiddleware(printMiddle));

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

이렇게 완성이 됩니다.

 

이제 버튼을 클릭하면 미들웨어가 적용되어 콘솔에 로그가 나타납니다.

 

이 로그 미들웨어는 간단하게 액션과 액션이 처리가 된 상태를 보여줍니다.

const printMiddle = store => next => action => {
    console.log(`현재 액션: ${action.type}`);
    next(action);
    console.log(`next후 상태: ${store.getState().current}`);
}

next함수를 호출하여 다음 미들웨어에 액션을 전달하지만 현재는 다음 미들웨어가 없기 때문에 리듀서로 액션이 넘어갑니다. 따라서 리듀서에서 액션이 처리되고 다시 콘솔에 상태가 어떻게 바뀌었는지 나타납니다.

 

이것으로 기본적인 미들웨어 방식을 알아보았습니다.

 

redux-thunk 미들웨어

redux-thunk는 리덕스에서 지원하는 미들웨어이며 비동기 작업을 처리할 때 사용되는 미들웨어입니다.

 

라이브러리 설치

아래 명령어를 통해 라이브러리를 설치합니다.

npm install redux-thunk

thunk 함수는?

thunk 함수는 미들웨어에서 사용될 비동기 작업을 하는 함수를 반환하는 함수입니다. 비동기 작업으로는 서버에서 데이터를 가져오는 상황이 될 수 있습니다.

 

thunk 함수는 액션이 호출될 때 미들웨어에서 받아 state와 dispatch를 전달받아 비동기 작업이 완료되면 dispatch를 이용해 새로운 액션을 디스패치 할 수 있습니다.

 

redux-thunk 비동기 작업

위에서 사용하던 코드에서 살짝만 변경하여 사용합니다. 

 

redux-thunk 미들웨어를 사용하기 위해 미들웨어를 적용해줍니다.

// index.js
import ReduxThunk from "redux-thunk";

const store = createStore(printReducer, applyMiddleware(ReduxThunk));

위에서 사용하던 코드에서 살짝만 변경하여 사용합니다. 

 

액션 시작 타입과 비동기 작업을 하는 함수를 반환하는 함수 만들어 주었습니다.

const PRINT_ACTION_START = "print/PRINT_ACTION_START";
const PRINT_ACTION = "print/PRINT_ACTION";

export const printAction = () => ({ type: PRINT_ACTION });

export const printActionAsync = () => dispatch => {
    dispatch({ type: PRINT_ACTION_START });
    setTimeout(() => {
        console.log("1초 후");
        dispatch(printAction());
    }, 1000);
};

const initalState = {
    current: null
}

function printReducer(state=initalState, action) {
    switch (action.type) {
        case PRINT_ACTION_START:
            return {
                ...state,
                current: "ACTION START",
            };
        case PRINT_ACTION:
            return {
                ...state,
                current:"ACTION 1"
            }
        default:
            return state
    }
}

export default printReducer;

버튼을 클릭하면 printActionAsync 함수를 호출하여 시작을 알리는 PRINT_ACTION_START 액션을 디스패치 하고 1초 후 PRINT_ACTION 액션을 디스패치 합니다.

 

프로젝트가 커지면 printActionAsync 같은 함수를 따로 만들어 재사용 가능하게 만들어 사용하면 되며 해당 작업을 이용해서 서버에서 데이터를 가져올 때 비동기 작업을 처리하면 됩니다.

 

redux-saga 미들웨어

redux-saga는 좀 더 세부적인 작업을 처리할 수 있습니다. 

  • 반복된 요청을 방지하거나 기존 요청을 취소할 때.
  • 특정 액션을 발생했을 때 다른 액션을 발생시킬 때
  • 웹 소켓을 사용할 때
  • API 요청 실패 시 다시 시도할 때

redux-saga는 ES6에서 지원하는 Generator 함수를 이용합니다. 제너레이터 함수는 코드 실행을 잠시 중단하고 다시 이어갈 수 있는 문법입니다.

 

제너레이터의 내용은 아래 링크에 정리하였습니다.

https://bearcomputer.tistory.com/31

 

자바스크립트(JavaScript) - 이터레이터(Iterator), 이터러블(Iterable), 제너레이터(Generator)

글에 오류가 있을 수도 있으니 알려주시면 감사하겠습니다. ES6부터 추가된 이터레이터와 제너레이터는 반복적인 처리를 할 때 사용되는 기능입니다. 이터레이터(Iterator) 이터레이터는 반복 처

bearcomputer.tistory.com

 

라이브러리 설치

아래 명령어를 통해 redux-saga를 프로젝트에 설치합니다.

npm install redux-saga

redux-saga 비동기 작업

위에서 사용된 print 코드를 변경합니다.

 

먼저 saga 함수를 만들어야 합니다. 이 saga 함수는 제너레이터 함수이며 작업 처리와 작업을 처리하는 saga 함수를 액션 타입에 지정하는 saga 함수를 만듭니다.

// modules/printReducer.js
import { delay, put, takeEvery } from "redux-saga/effects";

const PRINT_ACTION_START = "print/PRINT_ACTION_START";
const PRINT_ACTION = "print/PRINT_ACTION";

export const printAction = () => ({ type: PRINT_ACTION });

export const printActionAsync = () => ({ type: PRINT_ACTION_START });

function* printActionSaga() {
    yield delay(3000);
    yield put(printAction());
}

export function* printSaga() {
    yield takeEvery(PRINT_ACTION_START,printActionSaga);
}

액션 생성 함수와 작업을 처리할 printActionSaga 제너레이터 함수, 액션 타입과 연결하는 printSaga 제너레이터 함수를 만들어 주었습니다.

 

printSaga 제너레이터 함수를 이용해 PRIINT_ACTION_START 액션이 디스패치 되면 printActionSaga 제너레이터 함수가 호출되도록 설정하고 합니다.

 

그 후 PRIINT_ACTION_START 액션이 디스패치되면 printActionSaga 함수가 호출됩니다.

 

printActionSaga 제너레이터 함수는 delay 함수를 이용해 3초간 중단되고 put 함수를 이용해 printAction 함수를 호출하여 액션을 디스패치 합니다.

 

서버에서 데이터를 가져와야 하는 경우 call 함수를 이용해 데이터를 가져올 때까지 대기할 수 있습니다.

 

saga에서 지원하는 함수

  • delay - delay 함수는 지정한 초까지 일시 중단하는 함수입니다. 위 코드에서는 3초 정지합니다.
  • call - call 함수는 인수로 받은 함수를 미들웨어에서 호출되도록 effect를 생성하여 전달합니다. 이 effect는 함수의 정보를 객체로 만들어 미들웨어에 전달되어 호출됩니다. 전달된 함수가 Promise기반이라면 작업이 완료할 때까지 기다립니다.
  • put - put은 effect를 생성합니다. effect는 사용할 액션의 정보를 객체로 만들어 넘겨져 미들웨어에서 사용됩니다. 액션을 발생시키는 디스패치입니다.
  • takeEvery - 인수에 전달된 액션이 디스 패치되면 인수에 전달한 saga 함수를 호출하도록 설정합니다.
  • takeLatest - 인수에 전달된 액션이 디스패치 되면 인수에 전달한 saga 함수를 호출하도록 설정합니다. 하지만 takeEvery와는 달리 중복된 액션을 한 번씩만 실행됩니다.

이것보다 더 많지만 간략하게 알아보았습니다. 더 자세한 정보 또는 더 많은 함수는 문서에서 확인하시기 바랍니다.

https://redux-saga.js.org/docs/api

 

API Reference | Redux-Saga

Middleware API

redux-saga.js.org

 

미들웨어와 연결하기

redux-saga를 사용하려면 만들어둔 saga를 미들웨어에 연동해야 합니다. 

 

아래 코드를 사용하여 미들웨어를 생성하고 saga를 연동합니다.

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

// redux
import { applyMiddleware, createStore } from "redux";
import { Provider } from "react-redux";
import createSagaMiddleware from "redux-saga";
import printReducer, { printSaga } from "./modules/printReducer";

const sagaMiddleware = createSagaMiddleware();
const store = createStore(printReducer, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(printSaga);

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

createSagaMiddleware 함수를 이용해 미들웨어를 생성하고 store에 적용합니다. 그 후 run 함수를 이용해 만들었던 saga 함수와 연동합니다.

 

현재는 컴포넌트가 하나뿐이어서 연동할 saga 함수가 하나뿐이지만 여러 개 일 경우 modules 폴더에서 index.js 파일을 만들어 여러 개의 saga를 합쳐 하나로 만들어 사용합니다.

// modules/index.js
import { all } from "redux-saga/effects";
import { saga1 } from "./saga1";
import { saga2 } from "./saga2";
import { printSaga } from "./printReducer";

export function* rootSaga() {
    yield all([saga1(),saga2(),printSaga()]);
}

export default rootReducer;

// index.js
import { rootSaga } from './modules';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(printReducer, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(rootSaga);

 

all 함수를 통해 연동할 saga 함수 모두 호출하여 한번에 설정합니다.

 

하나로 합치는 것은 redux에 reducer를 하나로 합칠 때도 사용하며 redux-saga와 살짝 다르니 참고하시기 바랍니다.

 

이렇게 코드를 작성하고 실행하면 버튼을 누르면 화면에 ACTION START가 나타나고 3초 후 ACTION 1으로 변경됩니다.

 

state 값 가져오기

saga 함수에서 state값을 가져오려면 select 함수를 사용해야 합니다.

// modules/printReducer.js
// 기존코드에서 변경
import { delay, put, takeEvery, select } from "redux-saga/effects";

function* printActionSaga() {
    yield delay(3000);
    yield put(printAction());
    let current = yield select(state => state.current);
    console.log(current);
}

select 함수의 인수로 state 반환하는 함수를 전달하면 지정한 state의 값을 반환합니다.

 

참고

리액트를 다루는 기술(개정판) - 김민준 지음

https://redux-saga.js.org/docs/basics/DeclarativeEffects

https://redux-saga.js.org/docs/api