[말로 풀어쓴 React] 리덕스 미들웨어를 통한 비동기 작업 관리 1

DongGu·2021년 2월 26일
0

목차

18.1 작업 환경 준비

18.2 미들웨어란?

18.3 비동기 작업을 처리하는 미들웨어 사용

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

리액트 프로젝트에서 리덕스를 사용하고 있으며 이러한 비동기 작업을 과니해야 한다면, 미들웨어를 사용하여 매우 효율적이고 편하게 상태관리를 할 수 있다. 이번 포스팅에서는 리덕스 미들웨어의 개념을 이해하고, 미들웨어를 사용하여 비동기작업을 처리하는 방법을 알아볼 것이다.

18.1 작업 환경 준비

yarn create react-app learn-redux-middleware
yarn add redux react-redux redux-actions

counter 리덕스 모듈을 작성한다.
createAction: 액션생성 자동화 (액션 생성을 더 간단히 해줌)
handleActions: 리듀서 함수에서 type에 따라 해야 하는 작업이 다르다. switch-case로 코드를 작성하면 scope 문제가 발생할 수 있고, 더 번거롭다. 이를 해결해주는 함수다.

// modules/counter.js
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;
// modules/index.js
import {combineReducers} from 'redux';
import counter from './counter';
const rootReducer = combineReducers({counter});
export default rootReducer;

스토어를 생성한 후, Provider로 리액트 프로젝트에 리덕스를 적용한다.

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

const store = createStore(rootReducer);
ReactDOM.render(
  <Provider store={store}>
  	<App/>
  </Provider>
  document.getElementById('root')
);

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

// components/Counter.js
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;

connect는 리액트 컴포넌트와 리덕스 스토어를 연결시켜준다.

// containers/CounterContainer.js
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.js
import React from 'react';
import CounterContainer from './cotainers/CounterContainer';

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

18.2 미들웨어란?

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

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

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

18.2.1 미들웨어 만들기

실제 프로젝트에서 미들웨어를 직접 만드는 경우는 드물다. 보통 다른 개발자가 만들어놓은 미들웨어를 사용한다. 미들웨어의 작동원리를 보기 위해 직접 만들어 볼 것이다. 액션이 디스패치될 때마다 액션의 정보와 액션이 디스패치기 되기 전 후의 상태를 콘솔에 보여주는 로깅 미들웨어를 작성할 것이다.

// lib/loggerMiddleware.js
const loggerMiddleware = store => next => action {
	// 미들웨어 기본 구조
};
export default loggerMiddleware

화살표 함수를 일반 홤수로 풀어쓰면 다음과 같다.

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

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

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

이번에 만들 미들웨어는 '이전 상태, 액션 정보, 새로워진 상태'를 순차적으로 콘솔에서 보여줄 것이다.

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

만든 리덕스 미들웨어를 스토어에 적용할 것이다. 미들웨어는 스토어를 생성하는 과정에 적용한다.

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

const store = createStore(rootReucer, applyMiddleware(loggerMiddlewrare));

ReactDOM.render(
  <Provider store={store}>
  	<App/>
  </Provdier>,
  document.getElemntById('root')
);

18.2.2 redux-logger 사용하기

직접 만든 loggerMiddleware보다 더 시각적으로 뛰어난 라이브러리이다. 직접 만들 필요 없이 설치 후 불러와 아래처럼 작성하면 된다.

yarn add redux-logger

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './moduels';
import {createLogger} from 'redux-logger';

18.3 비동기 작업을 처리하는 미들웨어 사용

이번에 다뤄볼 미들웨어는 다음과 같다.

  • redux-thunk: 비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어다. 객체가 아닌 함수 형태의 액션을 디스패치할 수 있게 해준다.
    액션을 함수형태로 만들 수 있으니까 setTime을 통해 지연을 줄 수 있다.

  • redux-saga: redux-thunk 다음으로 가장 많이 사용되는 비동기 작업 관련 미들웨어 라이브러리이다. 특정 액션이 디스패치되었을 때 정해진 로직에 따라 다른 액션을 디스패치시키는 규칙을 작성하여 비동기작업을 처리할 수 있게 해준다.

18.3.1 redux-thunk

redux-thunk는 리덕스를 사용하는 프로젝트에서 비동기 작업을 처리할 때 가장 기본적으로 사용하는 미들웨어다.

  • 18.3.1.1 Thunk란?
    Thunk는 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미한다. 예를 들어 주어진 파라미터에 1을 더하는 함수를 만들고 싶다면 다음과 같이 작성하면 된다.
const addOne = x => x +1;
addOne(1); // 2

이 코드를 실행하면 addOne을 호출했을 때 바로 1+1이 연산된다. 그런데 이 연산작업에 지연시간을 주고 싶다면 어떻게 해야 할까?

const addOne = x => x+1;
function addOneThunk(x){
  const thunk = () => addOne(x);
  return thunk;
}

const fn = addOneThunk(1);
setTimeout(()=>{
  const value = fn(); // fn이 실행되는 시점에 연산
  onsole.log(Value);
}, 1000);

이렇게 하면 특정 작업을 나중에 하도록 미룰 수 있다.
만약 addOneThunk를 화살표 함수로만 사용한다면 다음과 같이 구현할 수 있다.

const addOne = x => x+1;
const addOneThunk = x => () =>  addOne(x);

const fn = addOneThunk(1);
setTimeout(()=>{
  const value = fn();
  console.log(value);
}, 1000);

redux-thunk 라이브러리를 사용하면 thunk 함수를 만들어서 디스패치할 수 있다. 그러면 리덕스 미들웨어가 그 함수를 전달받아 store의 dipatch와 getState를 파라미터로 넣어서 호출해준다.

다음은 redux-thunk에서 사용할 수 있는 예시 thunk 함수이다.

const sampleThunk = () => (dispatch, getState) => {
  // 현재 상태를 참조할 수 있고,
  // 새 액션을 디스패치할 수도 있다.
}
  • 18.3.1.2 미들웨어 적용하기
    redux-thunk 미들웨어를 설치하고 프로젝트에 적용해볼 것이다.
    yarn add redux-thunk 후 스토어를 만들 때 redux-thunk를 적용한다.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
import {createLogger} from 'redux-logger';
import ReduxThunk from 'redux-thunk';

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

ReactDOM.redner(
  <Provider store={store}>
  	<App/>
  </Provider>
document.getElementById('root')
);
  • 18.3.1.3 Thunk 생성 함수 만들기
    redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신에 함수를 반환한다. increaseAsync와 decreaseAsync 함수를 만들어 카운터 값을 비동기적으로 변형시켜 볼 것이다.

dispatch(increase())로 전달되면 액션을 '객체'로 전달된다. increaseAsync는 액션을 '함수'로 전달한다. setTimeout을 이용해 지연을 줬다.

// modules/counter.js
import {createAction, handleActions} from 'redux-actions';

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

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

// 1초 뒤에 increase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => dispatch => {
  setTimeout(()=>{
    dispatch(increase());
  }, 1000);
};

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

const initialState = 0; // 상태는 꼭 객체일 필요 없다. 숫자도 작동한다.
const counter = handleActions(
  {
    [INCREASE]: state => state+1,
    [DECREASE]: state => state-1
  },
  initialState
);
export default counter;

리덕스 모듈을 수정했으면 CounterContainer에서 호출하던 액션생성함수도 수정해야 한다.

// container/CounterContainer.js
const CounterContainer = ({number, IncreaseAsync, DecreaseAsync}) => {
  return (
    <Counter number={number} onIcrease={IncreaseAsync} onDecrease={DecreaseAsync} />
  );
};

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

코드를 저장하고 yarn start를 하고 버튼을 눌러보면, 숫자가 1초 뒤에 바뀔 것이다. 처음 디스패치되는 액션은 함수 형태이고, 두 번째는 객체 형태다.

profile
코딩하는 신방과생

0개의 댓글