관리 메뉴

거니의 velog

(11) 리덕스 미들웨어를 통해 비동기 작업 관리 1 본문

React/React_리액트 심화

(11) 리덕스 미들웨어를 통해 비동기 작업 관리 1

Unlimited00 2023. 12. 13. 21:37

* 리액트 웹 애플리케이션에서 API 서버를 연동할 때는 API 요청에 대한 상태도 잘 관리해야 한다. 예를 들어 요청이 시작되었을 때는 로딩 중임을, 요청이 성공하거나 실패했을 때는 로딩이 끝났음을 명시해야 한다. 요청이 성공하면 서버에서 받아 온 응답에 대한 상태를 관리하고, 요청이 실패하면 서버에서 반환한 에러에 대한 상태를 관리해야 한다.

* 리액트 프로젝트에서 리덕스를 사용하고 있으며 이러한 비동기 작업을 관리해야 한다면, '미들웨어(middleware)'를 사용하여 매우 효율적이고 편하게 상태 관리를 할 수 있다.

* 이 장에서는 리덕스 미들웨어의 개념을 이해하고, 미들웨어를 사용하여 비동기 작업을 처리하는 방법을 알아보자.

* 이번 실습은 다음 흐름으로 진행된다.


1. 작업 환경 준비

* 리덕스를 적용한 간단한 리액트 프로젝트를 준비하자. 이 프로젝트를 통해 리덕스 미들웨어에 대해 알아볼 것이다.

* CRA(create-react-app)를 사용하여 새 리액트 프로젝트를 생성하자.

$ yarn create react-app learn-redux-middleware

* 다음으로 리덕스를 사용하여 카운터를 구현한다. 이에 필요한 라이브러리들을 새 프로젝트에 설치해 주자.

$ yarn add redux react-redux redux-actions

* 이제 리덕스를 위한 코드를 준비하자. 먼저 counter 리덕스 모듈을 작성하자.

import React from "react";
import { createAction, handleActions } from "redux-actions";

const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

const initialState = 0; // 상태는 꼭 객체일 필요는 없다. 숫자도 잘 작동한다.

const counter = handleActions(
  {
    [INCREASE]: (state) => state + 1,
    [DECREASE]: (state) => state - 1,
  },
  initialState
);

export default counter;

* 다음으로 루트 리듀서를 생성하자.

import { combineReducers } from "redux";
import counter from "./counter";

const rootReducer = combineReducers({ counter });

export default rootReducer;

* 리듀서를 다 만들었으면 src 디렉터리의 index.js에서 스토어를 생성한 후, Provider로 리액트 프로젝트에 리덕스를 적용하자.

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { createStore } from "redux";
import rootReducer from "./modules";
import { Provider } from "react-redux";

const store = createStore(rootReducer);

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

reportWebVitals();

* 이어서 카운터 컴포넌트와 카운터 컨테이너 컴포넌트를 만든다. 프레젠테이셔널 컴포넌트는 components 디렉터리에 저장하고, 컨테이너 컴포넌트는 containers 디렉터리에 저장하자.

import React from "react";

const Counter = ({ onIncrease, onDecrease, number }) => {
  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
};

export default Counter;

import React from "react";
import { connect } from "react-redux";
import { increase, decrease } from "../modules/counter";
import Counter from "../components/Counter";

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};

export default connect(
  (state) => ({
    number: state.counter,
  }),
  {
    increase,
    decrease,
  }
)(CounterContainer);

* 다 만들었다면 App 에서 CounterContainer를 렌더링하여 잘 작동하는지 확인해 보자.

[App.js]

import React from "react";
import CounterContainer from "./containers/CounterContainer";

const App = () => {
  return (
    <div>
      <CounterContainer />
    </div>
  );
};

export default App;

* 이제 yarn start 명령어를 입력하여 브라우저에서 확인해 보자.

카운터 작동 확인

* 리덕스 프로젝트가 모두 준비되었다! 이제 리덕스 미들웨어를 본격적으로 배워 보자.


2. 미들웨어란?

* 리덕스 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업들을 실행한다. 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있다.

* 리듀서가 액션을 처리하기 전에 미들웨어가 할 수 있는 작업은 여러 가지가 있다. 전달받은 액션을 단순히 콘솔에 기록하거나, 전달받은 액션 정보를 기반으로 액션을 아예 취소하거나, 다른 종류의 액션을 추가로 디스패치할 수도 있다.


(1) 미들웨어 만들기

* 실제 프로젝트를 작업할 때 미들웨어를 직접 만들어서 사용할 일은 그리 많지 않다. 다른 개발자가 만들어 놓은 미들웨어를 사용하면 되기 때문이다. 하지만 미들웨어가 어떻게 작동하는지 이해하려면 직접 만들어 보는 것이 가장 효과적이다. 간단한 미들웨어를 직접 만들어 보면 미들웨어의 작동 방식을 제대로 이해할 수 있다. 원하는 미들웨어를 찾을 수 없을 때는 상황에 따라 직접 만들거나 기존 미들웨어들을 커스터마이징하여 사용할 수도 있다.

* 여기서는 액션이 디스패치될 때마다 액션의 정보와 액션이 디스패치되기 전후의 상태를 콘솔에 보여 주는 로깅 미들웨어를 작성해 보겠다.

* src 디렉터리에 lib 디렉터리를 생성하고, 그 안에 loggerMiddleware.js 파일을 생성하자.

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

export default loggerMiddleware;

* 위 코드에서 리덕스 미들웨어의 구조를 볼 수 있다. 화살표 함수를 연달아서 사용했는데, 일반 function 키워드로 풀어서 쓴다면 다음과 같은 구조이다.

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

* 미들웨어는 결국 함수를 반환하는 함수를 반환하는 함수이다. 여기에 있는 함수에서 파라미터로 받아 오는 store는 리덕스 스토어 인스턴스를, action은 디스패치된 액션을 가리킨다. 이 두 가지 값은 이미 익숙하다. 반면에 next는 그렇지 않다. next 파라미터는 함수 형태이며, store.dispatch와 비슷한 역할을 한다. 하지만 큰 차이점이 있는데, next(action)을 호출하면 그다음 처리해야 할 미들웨어에게 액션을 넘겨주고, 만약 그다음 미들웨어가 없다면 리듀서에게 액션을 넘겨준다는 것이다.

* 미들웨어 내부에서 store.dispatch를 사용하면 첫 번째 미들웨어부터 다시 처리한다. 만약 미들웨어에서 next를 사용하지 않으면 액션이 리듀서에게 전달되지 않는다. 즉, 액션이 무시되는 것이다.

* 이제 미들웨어를 마저 구현해 보자. 이번에 만들 미들웨어는 다음 정보를 순차적으로 콘솔에 보여 준다.

1. 이전 상태

2. 액션 정보

3. 새로워진 상태

[loggerMiddleware.js]

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(); // 그룹 끝
};

export default loggerMiddleware;

* 만든 리덕스 미들웨어를 스토어에 적용하겠다. 미들웨어는 스토어를 생성하는 과정에서 적용한다.

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { applyMiddleware, createStore } from "redux";
import rootReducer from "./modules";
import { Provider } from "react-redux";
import loggerMiddleware from "./lib/loggerMiddleware";

const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));

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

reportWebVitals();

* 이제 개발자 도구에서 콘솔을 열고 카운터의 버튼을 눌러 보자.

직접 만든 미들웨어

* 액션 정보와 업데이트되기 전후의 상태가 잘 나타난다. 미들웨어에서는 여러 종류의 작업을 처리할 수 있다. 특정 조건에 따라 액션을 무시하게 할 수도 있고, 특정 조건에 따라 액션 정보를 가로채서 변경한 후 리듀서에게 전달해 줄 수도 있다. 아니면 특정 액션에 기반하여 새로운 액션을 여러 번 디스패치할 수도 있다.


(2) redux-logger 사용하기

* 이번에는 오픈 소스 커뮤니티에 이미 올라와 있는 redux-logger 미들웨어를 설치하고 사용해 보자. 방금 만든 loggerMiddleware 보다 훨씬 더 잘 만들어진 라이브러리이며, 브라우저 콘솔에 나타나는 형식도 훨씬 깔끔하다.

* 우선 명령어를 사용하여 redux-logger를 설치하자.

$ yarn add redux-logger

* 이어서 index.js를 다음과 같이 수정해보자.

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { applyMiddleware, createStore } from "redux";
import rootReducer from "./modules";
import { Provider } from "react-redux";
// import loggerMiddleware from "./lib/loggerMiddleware";
import { createLogger } from "redux-logger";

const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger));

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

reportWebVitals();

* 이제 브라우저를 다시 열어서 카운터 버튼을 눌러 보자.

redux-logger

* 콘솔에 색상도 입혀지고, 액션 디스패치 시간도 나타난다. 리덕스에서 미들웨어를 사용할 때는 이렇게 이미 완성된 미들웨어를 라이브러리로 설치해서 사용하는 경우가 많다.