redux-saga, redux-thunk 알아보기

세바님·2023년 5월 27일
0
post-thumbnail

리덕스의 핵심 기능이라고 할 수 있는 리덕스 미들웨어에 대해 알아보자.

리덕스 미들웨어란?

미들웨어는 액션이 디스패치되어 리듀서에서 처리되기 전에 지정된 작업을 한다. 즉, 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있다.
우리는 이 미들웨어를 직접 만들어 쓰거나, 외부 라이브러리에서 들고올 수 있다.

미들웨어 만들어보기

먼저, 미들웨어를 직접 만들어서 쓰는 법을 알아보자!
다음은 미들웨어의 기본 구조이다.

const middleware = (store) => (next) => (action) => {
    // 미들웨어가 실행할 작업들
}

함수인건 알겠는데... 뭐지? 싶을 수 있다. 이는 다음과 같이 표현도 가능하다.

function middleware(store) {
  return function (next) {
    return function (action) {
      // 미들웨어가 실행할 작업들
    };
  };
};

이렇게 풀어서 써 보니 구조를 알기가 더 쉬운 모습이다.

이번에는 미들웨어가 실행할 작업을 적어보자!
먼저 간단히 콘솔에 찍어보는 일부터 시작하자.

const middleware = (store) => (next) => (action) => {
  	console.log(`current state : ${store.getState()}`)
  	console.log(`action : ${action}`)
   	// 액션을 다음 미들웨어, 혹은 리듀서로 넘기는 작업
  	next(action);
  	console.log(`next state : ${store.getState()}`)
}

생각보다 간단하다! 이번엔 직접 만든 미들웨어를 추가해 볼 차례이다.
이때는 applyMiddleware 라는 함수를 이용하여 추가하면 된다.

import { createStore, applyMiddleware } from 'redux';

{ ... }

const store = createStore(reducer, applyMiddleware(middleware))

이제 실행을 한 뒤 콘솔을 보면, 출력이 잘 된걸 볼 수있다!

redux-logger

그런데, 우리가 앞에서 만든 미들웨어보다 더 이쁘고, 잘 만들어진 미들웨어 라이브러리가 이미 있다. 간단하게 짚어만 보고 넘어가자.

import { createLogger } from 'redux-logger';

{ ... }

const logger = createLogger(); 

const store = createStore(reducer, applyMiddleware(logger))

미들웨어 라이브러리 알아보기

이번엔 라이브러리에서 미들웨어를 들고와서 사용해 보자.
이 글에서는 일반적으로 많이 쓰이는 redux-thunkredux-saga 에 대해 다룰 것이다.

redux-thunk

먼저 redux-thunk에 대해 알아보자. redux-thunk는 리덕스에서 비동기 작업을 처리 할 때 사용하는 미들웨어이다.

thunk란?

redux-thunk에서 thunk는 특정 작업을 나중에 하도록 미루기 위해서 함수형태로 감싼것을 말한다.

예를 들어, 두 수를 입력받아서 그 곱을 알기 위해 함수를 만들었다고 해 보자.

const multiplyFunction = (a, b) => a * b;

이런 식으로 하면 multiplyFunction 을 호출했을 때만 연산이 실행될 것이다. 이런 것들을 thunk라고 생각하면 된다.

redux-thunk가 하는 일

redux-thunk는 액션 생성함수가 객체가 아닌 함수를 생성하도록 하게 해 준다.
그렇다면, redux-thunk를 사용했을때 장점이 뭘까?
그건 바로 액션 생성함수의 내부에서 여러가지 작업을 할 수 있게 해 준다는 점이다. 이런 점을 이용해 비동기적으로 작업을 처리 할 수 있다.

간단한 카운터를 예시로 들어보자.

const INCREASE = "INCREASE";
const DECREASE = "DECREASE";

export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

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

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

이렇게 한다면, 카운트 값이 1초 뒤에 변경이 될 것이다.

사용법도 간단하다. thunk 를 import하고, applyMiddleware 함수의 첫번째 매개변수로 넘겨주면 된다. `

import thunk from "redux-thunk";

{ ... }

const store = createStore(reducer, applyMiddleware(thunk, logger));

redux-thunk로 api 처리

이번엔 api를 한번 처리해 보자.

import * as API from "./api";

const GET_POST = "GET_POST";
const GET_POST_SUCCESS = "GET_POST_SUCCESS";
const GET_POST_ERROR = "GET_POST_ERROR";

export const getPost = id => async dispatch => {
  dispatch({ type: GET_POST }); // 요청
  try {
    const post = await API.getPostById(id); // API 호출
    dispatch({ type: GET_POST_SUCCESS, post }); // 성공
  } catch (e) {
    dispatch({ type: GET_POST_ERROR, error: e }); // 실패
  }
};

const initialState = {
  posts: {
    loading: false,
    data: null,
    error: null
  },
  post: {
    loading: false,
    data: null,
    error: null
  }
};

export default function posts(state = initialState, action) {
  switch (action.type) {
    case GET_POST:
      return {
        ...state,
        post: {
          loading: true,
          data: null,
          error: null
        }
      };
    case GET_POST_SUCCESS:
      return {
        ...state,
        post: {
          loading: true,
          data: action.post,
          error: null
        }
      };
    case GET_POST_ERROR:
      return {
        ...state,
        post: {
          loading: true,
          data: null,
          error: action.error
        }
      };
    default:
      return state;
  }
}

이런 식으로 redux-thunk에서 api를 처리할 수 있다!

redux-saga

이번엔 redux-saga를 배워보자. redux-saga 또한 redux-thunk처럼 비동기 작업을 처리할 때 사용된다.
redux-saga의 사용법을 배우기 전에, redux-saga에 쓰이는 Generator 에 대해 배워보자.

Generator

제너레이터 함수는 yield 라는 키워드를 사용하는데, 이는 여러개의 값을 순차적으로 반환할 수 있게 한다.
또한, 제네레이터 함수를 선언할 때는 function* 키워드를 이용해야 한다.

function* generator() {
  console.log("첫번째");
  yield 1;
  console.log("두번째");
  yield 2;
  console.log("마지막");
  return 3;
}

간단하게 제네레이터 함수를 선언했다. 이번엔 제네레이터 함수를 사용해보자.

const generated = generator(); // 제네레이터 생성

generated.next();
// 첫번째
// {value: 1, done: false}
generated.next();
// 두번째
// {value: 2, done: false}
generated.next();
// 마지막
// {value: 3, done: true}
generated.next();
// {value: undefined, done: true}
// 이후 a.next() 사용시 계속 위와 같은 값 출력

정말 순차대로 값을 반환하고, 순차적으로 작업을 실행한다는 것을 알게 되었다.
그리고 추가로, redux-saga에서는 이 제네레이터 함수를 "사가" 라고 부른다.

redux-saga 사용하기

이제 진짜로 redux-saga를 직접 써볼때다!

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

{ ... }

const INCREASE_ASYNC = "INCREASE_ASYNC";
const DECREASE_ASYNC = "DECREASE_ASYNC";
 
export const increaseAsync = () => ({ type: INCREASE_ASYNC });
export const decreaseAsync = () => ({ type: DECREASE_ASYNC });

function* increaseSaga() { 
  yield delay(1000);
  yield put(increase());
}

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

export function* counterSaga() {
  yield takeEvery(INCREASE_ASYNC, increaseSaga);
  yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}

여기서 takeEvery , takeLatest 라는 함수는 액션을 모니터링하는 함수이다. takeEvery는 인수로 받은 액션 타입을 디스패치되는 모든 액션들을 처리하고, takeLatest는 인수로 받은 액션 타입에 대하여 가장 마지막으로 디스패치된 액션을 처리한다.
또한 delay 는 인자로 받은 수 만큼 (ms기준) 기다린다. put 은 인자로 받은 액션을 디스패치한다.

여튼 저튼 쨌든 사가를 정의했다. 이번엔 미들웨어를 만들어 스토어에 적용까지 해 보자!

import createSagaMiddleware from "redux-saga";

{ ... }

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware, logger));
sagaMiddleware.run(counterSaga); // 꼭 스토어 생성 후 실행할 것!

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

이러면 redux-saga 적용까지 끝이 났다!

글을 마치며

이렇게 리덕스 미들웨어 라이브러리중 가장 잘 쓰이는 redux-thunk와 redux-saga의 사용법에 대해 알아보았다.
미들웨어까지 공부를 하며, 막상 적용시키려니 이전 코드와 맞물리면서 정말 어지러웠다...
리덕스는 나중에 다시 한 번 정독을 해보든 해야 할 것 같다...

이렇게 교내 프론트 스터디의 첫 파트를 무사히..? 아무튼 마치게 되었다! 다음 파트까지 푹 쉬어 재정비를 할 수 있도록 해야겠다.

profile
아아

0개의 댓글