Redux의 핵심, 미들웨어

권세원·2023년 5월 28일
0
post-thumbnail

리덕스의 꽃, 미들웨어

리덕스에 대해 알아보기 시작하였다면 여기서 멈출 수 없지.
리덕스의 핵심, 미들웨어에 대해 알아보자.


Before - 빌드업

우리는 무엇을 위해 프론트엔드를 공부하는가?

계산기를 만들기 위해서?

아니

투두리스트를 만들기 위해서?

아니

우리는 하나의 서비스를 만들기 위해 공부하는 것이 아닌가?
프론트엔드의 꽃은 역시 백엔드와 상호작용하여 의미있는 무언가를 만들어내는 것이지.

그렇다면 API를 파싱, 핸들링 해야지!!

What(1) - 미들웨어란 무엇인가

미들웨어란 통상적으로

운영체제와 응용 소프트웨어, 어플리케이션과 어플리케이션 사이와 같이 둘 이상을 연결시켜주는 매개 역할을 하는 소프웨어

정도가 될 것이다.

한 프로젝트에서, 유저에게 인터페이스를 제공하는 프론트와 그 데이터를 저장하는 DB 사이에서 중간 과정을 처리하며 둘을 매끄럽게 연결시켜주는 백엔드의 존재라고 생각하면 될 것이다.

그중에서 우리는 오늘 리덕스의 미들웨어에 대해 알아보자

Why - 리덕스 미들웨어는 왜 필요할까

리액트 애플리케이션에서 API 서버를 연동할 때는 API 요청에 대한 상태도 잘 관리해주어야 한다.
이러한 비동기 작업을 관리를 해야 한다면, 리덕스 미들웨어만 한 게 없다.

리덕스 미들웨어는 비동기 작업을 매우 효율적으로 관리할 수 있도록 도와준다.

How - 리덕스 미들웨어는 어떻게 쓸까

미들웨어의 구조

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

export default loggerMiddleware;

구조를 보면 화살표 함수를 연달아 사용하는 것을 볼 수 있는데 이를 일반 함수로 풀면 아래와 같다.

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

미들웨어는 결국 함수를 반환하는 함수를 반환하는 함수이다.

여기서 파라미터로 받아오는 스토어는 리덕스 스토어 객체를, 액션은 디스패치된 액션을 가리킨다.

그리고 처음 보는 next는 함수 형태로 store.dispatch와 비슷한 역할을 한다.
다만 차이점이라면, next(action)을 호출하면 그다음 처리해야 할 미들웨어에게 액션을 넘겨주고, 만약 그다음 미들웨어가 없다면 리듀서에게 액션을 넘겨준다.

미들웨어를 스토어에 추가

미들웨어를 스토어에 추가해주는 방법은 아래와 같다.

...
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { loggerMiddleware } from './미들웨어 주소';

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

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

Provider에 루트 리듀서를 추가해줬었듯 미들웨어 또한 applyMiddleware 함수를 통해 추가해줄 수 있다.

redux-logger

앞서 만든 loggerMiddleware 따위 보다 훨씬 더 잘 만들어진 라이브러리이다.

아래와 같이 적용시킬 수 있다.

...
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { createLogger } from 'redux-logger';

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

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

콘솔만 찍어보아도 훨씬 자세하고 깔끔하게 나온다.

Which - 이미 나온 미들웨어들

위에서 알아보았듯 이미 누군가 만든 아주 좋은 미들웨어들이 존재한다.
우린 그걸 사용하기만 해도 반은 가니 따라해보자.

redux-thunk

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

thunk 란?

특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것

아래 코드를 보고 thunk의 동작을 살펴보자

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

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

위와 같이 작성해주면 연산을 미룰 수 있다.

thunk 함수

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

const thunk = () => (dispatch, getState) => {
 	// 현재 상태를 참조할 수 있고,
  	// 새 액션을 디스패치할 수도 있다.
}

스토어에 thunk 적용하기

...
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { createLogger } from 'redux-logger';
import ReduxThunk from 'redux-thunk';

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

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

logger라는 미들웨어를 적용했듯, thunk도 유사한 방식으로 스토어에 추가할 수 있다.

thunk 생성 함수 만들기

redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신 함수를 반환해준다.
increaseAsync와 decreaseAsync 함수를 통해 알아보자.

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

실행시켜보면 각 이벤트가 발생하고 1초 후에 숫자가 변경되는 것을 확인할 수 있다.

thunk로 비동기 작업 처리하기

export const getPost = (id) => {
 	axios.get('url'); 
}

const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_FAILURE = 'GET_POST_FAILURE';

export const getAsyncPost = (id) => {
 	dispatch({ type: GET_POST })  // 요청 시작
  	try {
     	const response = (await getPost(id))
      	dispatch({
        	type: GET_POST_SUCCESS,
          	payload: response.data
        })
    } catch (err) {
     	dispatch({
          	type: GET_POST_FAILURE,
          	payload: err,
          	error: true
        })
      throw err;
    }
};

const initState = { // 기본 값 지정 (= 상태 초기화)
 	loading: {
      GET_POST: false,
    },
  	post: null,
}

// reducer
const reducer = handleAction({
  [GET_POST]: state => ({
  	...state,
    loading: {
     ..state.loading,
      GET_POST: true // 요청 시작
    }
  }),
  [GET_POST_SUCCESS]: (state, action) => ({
  	...state,
    loading: {
     ..state.loading,
      GET_POST: false // 요청 완료
    },
    post: action.payload
  }),
  [GET_POST_FAILURE]: (state, action) => ({
  	...state,
    loading: {
     ..state.loading,
      GET_POST: true // 요청 완료
    }
  }),
  initState
});

복잡하다...
솔직히 눈으로만 봐서는 완전히 알지는 못하겠다.
프로젝트에서 직접 사용하면서 알아보도록 해야겠다.

redux-saga

redux-saga는 redux-thunk 다음으로 많이 사용하는 비동기 처리 관련 미들웨어이다.

대부분의 처리를 redux-thunk로 처리할 수 있다.
그럼 saga는 언제 쓰는 걸까?

redux-saga는 좀 더 까다로운 사황에 특화되어 있다.
아래와 같은 상황들에서 사용하는 것이 유리하다.

  • 기존 요청을 취소해야 할 때(불필요한 중복 요청 방지)
  • 특정 액션이 발생했을 때 다른 액션을 발생시키거나, API 요청 등 리덕스와 관계없는 코드를 실행할 때
  • 웹소켓을 사용할 때
  • API 요청 실패 시 재요청해야 할 때

제너레이터 함수 이해하기

redux-saga는 제너레이터 함수를 사용한다.

그렇다면 제너레이터 함수란 무엇인가

선언

function* 키워드를 통해 선언할 수 있으며 제너레이터 객체를 반환한다.

function*이라는 특별한 키워드를 사용하기에 화살표 함수를 쓸 수 없다.

특징

한 번에 하나의 값씩 순차적으로 반환하며, 중간에 일시 정지하거나 그 시점에서 다시 실행시킬 수도 있다.

yield

yield 키워드는 제너레이터 함수를 중지하거나 재게하는 데에 사용된다.

또한 하나의 시점 느낌으로 특정 부분으로 돌아가거나 값을 안이나 밖으로 전달이 가능하도록 해준다.

이를 통해 제너레이터 함수의 안과 밖을 양방향으로 연결시켜준다.

next()

next() 메서드는 제너레이터 함수에서 상당히 중요한 역할을 한다.

순차적으로 값을 반환시키는 함수의 특성상 다음 yield로 호출시켜주는, 그러니까 함수를 진행시켜주는 역할을 합니다.

예시

function* generator() {
  console.log('생성되었습니다');
  yield 1;
  console.log('A');
  yield 2;
  console.log('B');
  yield 3;
  return 4;
}

const fn = generator();

fn.next();
// 생성되었습니다
// { value: 1, done: false }
fn.next();
// A
// { value: 2, done: false }
fn.next();
// B
// { value: 3, done: false }
fn.next() 
// { value: 4, done: true }

redux-saga 사용하기

...
import { delay, put, takeEvery, takeLatest } from 'redux-saga/effects';

...

function* increaseSaga() {
  yield delay(1000); // delay(밀리초) : 밀리초만큼 대기
  yield put(increase()) // put(액션()) : 액션을 디스패치
}

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

function* counterSaga() {
  yield takeEvery(INCREASE_ASYNC, increaseSaga)
  // takeEvery : 들어오는 모든 작업에 특정 작업을 처리
  yield takeLatest(DECREASE_ASYNC, decreaseSaga)
  // takeLatest : 기존에 진행 중인 작업이 있다면 취소 처리하고
  // 가장 마지막으로 실행된 작업만 수행(가장 최근에 들어온 작업)
}

function* rootSaga() { // 루트 리듀서 만들듯이 saga도 루트 saga로 묶어준다.
  yield all([counterSaga()]) 
}

redux-saga 적용

...
import createSagaMiddleware from 'redux-saga';

const logger = createLogger();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  applyMiddleware(logger, ReduxThunk, sagaMiddleware) // 미들웨어들 적용
);
sagaMiddleware.run(rootSaga);

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

여태까지 미들웨어나 리듀서, 루트 리듀서를 적용시켜줬던 방식과 크게 다르지 않다.

자, 이제 redux-saga도 사용할 수 있게 되었다.




결론

미들웨어에 대해 알아보며,
리덕스 미들웨어는 확실히 리덕스의 핵심이라는 말이 맞다는 생각이 들었다.

하지만 나에겐 아직 좀 어려운 감이 있다. 좀이 아니라 많이 일지도
리덕스 라이브러리만 사용하는건 어렵지 않은데, 미들웨어까지 확장을 하려니 머리가 상당히 아프다.

미들웨어는 본다고 해결되는 부분은 확실히 아닌 것 같고, 많이 사용해보며 익히는게 나에겐 최선책이라는 생각이든다.

어째 한입 해보려다가 되려 뱉을뻔 했다.

일단은 그냥 리덕스라도 열심히 써보자.

profile
rnsjtpdnjs

0개의 댓글