본문 바로가기
개념공부/React.js

[Redux-middleware 리덕스 미들웨어 中] Redux-Saga란?

by 29살아저씨 2022. 3. 6.
반응형

Redux-middleware

1. 리액트 미들웨어란 무엇인가?

- 기존의 리덕스는 액션이 발생하게 되면, 디스패치를 통해 스토어에게 상태 변화의 필요성을 알리게 된다. 우리가 알고있는 리덕스는 동기적인 흐름을 통해 아래와 같이 동작한다.

액션 객체 생성 -> 디스패치로 액션을 발생시킴 -> 리듀서는 정해진 로직에 의해 처리한 뒤 새로운 상태 값 반환

하지만, 동기적인 흐름만으로는 처리하기 힘든 작업들이 있다. 예를들어, 시간을 딜레이시켜 동작하게 한다던지, 외부 데이터를 요청하여 그에 따른 응답 화면을 보여주어야 한다면? 리덕스에서 이러한 비동기 작업을 처리하기 위해 사용하는 것이 리덕스 미들웨어 이다.

 

2. 리덕스 미들웨어의 종류

리덕스 미들웨어를 직접 만들어서 처리할 수도 있지만, 흔히 이미 만들어진 미들웨어를 사용한다. 대표적인 미들웨어로는 redux-logger, redux-thunk, redux-saga등이 있다. 이 중 나는 redux-saga를 이용하였다.

 

Redux-Saga

1. 리덕스 사가란?

redux-saga란 리덕스 생태계를 지탱하고 있는 다양한 미들웨어 중 핫한 미들웨어이다. redux-thunk도 많이 사용하지만, api로 여러 비동기적 동작을 처리하는것에 조금 무리가 있어서 제너레이터 문법 기반의 redux-saga를 많이 사용한다.

redux-thunk의 문제점 : action에서 너무 많은 일을 한다. 액션생성자는 type과 payload가 담긴 객체를 생성해서 반환하는 역할을 수행하기로 했는데, thunk 미들웨어에서는 API 요청이나 비동기처리가 껴서 본래의 역할이 모
호해진다. 어떨떄는 객체를 반환하고, 어떨때는 함수를 반환한다.

redux-saga는 redux-thunk처럼 액션 생성자가 함수를 반환하지 않고 일관되게 객체를 반환하기 때문에, 이런 부분에서 오는 모호함이 해결된다.

 

2. 리덕스 사가와 제너레이터

redux-saga는 자바스크립트 ES6의 제너레이터 문법을 이용한다. function* 형태의 특수한 함수로 생성된 제너레이터 객체는 함수 내부에서 yield란고 하는 키워드로 다음 값, 동작을 제어한다. .next() 라는 메서드를 받았을 때 다음 동작을 처리하기 때문에 비동기적인 처리를 하기 좋다.

 

2-1. redux-saga가 제너레이터 문법을 사용하는 이유?

비동기적 처리의 인식과 제어를 잘 통제하기 위함이다. reducer에 정의된 특정한 action을 기다리다가 action이 발생하는 시점에 yield에 등록된 함수나 로직이 동작하게 하는 것이다. 이러한 처리들은 redux-saga에 미리 정의된 여러 부수효과(effect) 함수들로 동작하게 된다.

 

3. 리덕스 사가 시작하기

리덕스에서 동작하는 것이기 때문에 기본적으로 액션, (액션 생성자), 리듀서, 스토어는 구성이 되어있어야 한다. 결국 리덕스를 사용하는 이유는 '상태값 변화'이기 때문이다.

 

3-1. 스토어를 만들 때 미들웨어로 연결하고, 아래와 같이 동작하는 구문을 넣어주기.

// store.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import user from './user';
import rootSaga from './saga';

// 여러 상태값을 변경하는 리듀서들을 하나의 리듀서 함수로 함친다.
const rootReducer = combineReducers({ user });

// 1. 사가 미들웨어를 생성
const sagaMiddleware = createSagaMiddleware();

// 2. store 생성 (reducer, middleware 연결)
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

// 3. 사가 미들웨어에서 통합 사가 함수를 실행시킨다.
sagaMiddleware.run(rootSaga);

export default store;

1, 2, 3 순서대로 동작을 한다.

1. 사가 미들웨어를 생성 -> 생성된 사가 미들웨어를 applyMiddleware 함수의 인자로 넘겨줌(등록된 스토어 상태값을 변경할 때 사가 함수들을 인식할 준비 완료) -> sagaMiddleware.run에 rootSaga 함수를 연결

 

3-2. 사가 함수와 부수효과 함수들 작성

redux saga의 부수효과 함수 대부분이 비동기 처리를 한다고 보면 된다. 부수효과 함수를 사용하려면 yield로 제약을 걸어줄 필요가 있다. redux saga함수의 동작을 보기 전에 부수효과(effect)들에 대해서 먼저 알아보자

 

4. 리덕스 사가의 부수효과(effect)들

1. delay : 설정된 시간 이후에 resolve하는 Promise 객체를 리턴한다.

   delay(1000) : 1초 기다리기

2. put : 특정 액션을 dispatch 한다.

   put({type:'INCREMENT'}) : INCREMENT 액션을 dispatch한다.

3. call : 함수의 첫 번째 파라미터는 함수, 나머지 파라미터는 해당 함수에 넣을 인수이다. (동기 함수 호출, 주로 api 요청 시 사용)

   call(MeetingAPI, action.data) : MeetingAPI 함수에 action.data 파라미터를 넣어 호출한다.

* call과 put의 다른점은 put은 스토어에 인자로 들어온 action을 dispatch하고, call은 주어진 함수를 실행하는 것이다.

4. all : all 함수를 사용해서 제너레이터 함수를 배열 형태로 넣어주면, 제너레이터 함수들이 병행적으로 동시에 실행되고, 전부 resolve될 때 까지 기다린다.

   yield all ([testSaga1(), testSaga2()]) : testSaga1()과 testSaga2()가 동시에 실행되고, 모두 resolve될 때 까지 기다린다.

5. fork : 비동기 함수 호출로 요청을 보내버리고 결과와 상관없이 바로 다음 것이 실행된다.

6. takeEvery : 들어오는 모든 액션에 대해 특정 작업을 처리해준다.

   takeEvery(INCREASE_ASYNC, increaseSaga) : 들어오는 모든 INCREASE_ASYNC 액션에 대해 increaseSaga함수 실행

7. takeLatest : 액션이 여러번 수행될 때 기존에 진행중이던 작업이 있다면 취소 처리하고 가장 마지막으로 실행된 작업만 수행한다.

 

내가 직접 작성한 saga 함수를 가져와봤다. 아래 코드를 보면서 위에 했던 내용을 설명해보겠다.

import { all, fork } from 'redux-saga/effects';
import faq from './faq';
import mypage from './mypage';
import meeting from './meeting';
import member from './member';
import meetingList from './meetingList';
import fan from './fan';
import admin from './admin';

export default function* rootSaga() {
  // sagas 안에 있는 함수 전체 모음
  yield all([
    // all 안에 있는 함수들이 병행적으로 동시에 실행
    // fork : call과 반대로 비동기적으로 함수가 처리될 수 있도록 함 / 다수의 eventListener를 실행한다 해도 실행 순서대로 동작을 안하고 특정 함수가 특정 조건을 만족해야 실행된다.
    fork(faq),
    fork(mypage),
    fork(meeting),
    fork(member),
    fork(meetingList),
    fork(fan),
    fork(admin),
  ]);
}

이 코드는 rootSaga의 코드이다. rootSaga에서는 나눠진 saga함수를 모아서 fork 시키는 역할을 한다. yield all 함수를 이용하여 모든 제너레이터 함수들을 동시에 실행시킨다.

import { all, fork, put, takeLatest, call } from 'redux-saga/effects';
import {
  MeetingGameListAPI,
} from '../apis/Main/meetingList';
import {
  MEETING_GAME_RESULT_REQUEST,
  MEETING_GAME_RESULT_SUCCESS,
  MEETING_GAME_RESULT_FAILURE,
} from '../modules/meetingList';

// 3
function* meetingGame(action) {
  try {
    const result = yield call(MeetingGameListAPI, action.data);
    yield put({
      type: MEETING_GAME_RESULT_SUCCESS,
      data: result.data,
    });
  } catch (err) {
    yield put({
      type: MEETING_GAME_RESULT_FAILURE,
    });
  }
}

// 2
function* watchMeetingGame() {
  yield takeLatest(MEETING_GAME_RESULT_REQUEST, meetingGame);
}

// 1
export default function* meetingSaga() {
  yield all([fork(watchMeetingGame)]);
}

rootSaga에서 호출 된 fork(meeting) 내부이다. 코드는 1->2->3번 순서대로 진행된다.

MEETING_GAME_RESULT_REQUEST 액션이 호출되면 2번에서 meetingGame함수를 호출하고, 3번의 meetingGame 제너레이터 함수가 실행된다. 나는 api 요청 부분을 처리했으므로, 먼저 try내에서 call(동기)함수로 MeetingGameListAPI에 action.data 파라미터를 넣어서 실행시키고 결과를 받아온 뒤 에러가 나면 catch를 실행시키고, 성공했으면 put 함수를 실행시킨다.

 

여기서 redux내 구조를 한번 보자

import produce from 'immer';
const initialState = {
  meetingApplyList: [], // 미팅 참여인원리스트
  meetingGameList: [], // 게임 리스트
  meetingsApplyLoading: false, // 팬미팅 신청 명단
  meetingsApplyDone: false,
  meetingsApplyError: null,

  meetingsGameResultLoading: false,
  meetingsGameResultDone: false,
  meetingsGameResultError: null,
};

export const MEETING_APPLY_REQUEST = 'MEETING_APPLY_REQUEST'; // 전체미팅
export const MEETING_APPLY_SUCCESS = 'MEETING_APPLY_SUCCESS';
export const MEETING_APPLY_FAILURE = 'MEETING_APPLY_FAILURE';

export const MEETING_GAME_RESULT_REQUEST = 'MEETING_GAME_RESULT_REQUEST'; // 미팅 게임정보
export const MEETING_GAME_RESULT_SUCCESS = 'MEETING_GAME_RESULT_SUCCESS';
export const MEETING_GAME_RESULT_FAILURE = 'MEETING_GAME_RESULT_FAILURE';

const reducer = (state = initialState, action) =>
  produce(state, draft => {
    switch (action.type) {
      case MEETING_APPLY_REQUEST:
        draft.meetingsApplyLoading = true;
        draft.meetingsApplyDone = false;
        draft.meetingsApplyError = null;
        break;
      case MEETING_APPLY_SUCCESS:
        draft.meetingsApplyLoading = false;
        draft.meetingsApplyDone = true;
        draft.meetingApplyList = action.data;
        break;
      case MEETING_APPLY_FAILURE:
        draft.meetingsApplyLoading = false;
        draft.meetingsApplyError = action.error;
        break;
      case MEETING_GAME_RESULT_REQUEST:
        draft.meetingsGameResultLoading = true;
        draft.meetingsGameResultDone = false;
        draft.meetingsGameResultError = null;
        break;
      case MEETING_GAME_RESULT_SUCCESS:
        draft.meetingsGameResultLoading = false;
        draft.meetingsGameResultDone = true;
        draft.meetingGameList = action.data.content;
        break;
      case MEETING_GAME_RESULT_FAILURE:
        draft.meetingsGameResultLoading = false;
        draft.meetingsGameResultError = action.error;
        draft.meetingGameList = [];
        break;
      default:
        break;
    }
  });
export default reducer;

하나의 API 요청에 대한 action을 REQUEST, SUCCESS, FAILURE 3가지로 나눴다. 또한 initialState를 Loading, Done, Error로 나눴다. 각 action마다 initialState가 변경되는 것을 알 수 있다.

먼저 dispatch를 통해 REQUEST action을 통해 요청이 들어오면 REQUEST action이 실행되고 saga가 실행된 뒤 결과에 따라 SUCCESS, FAILURE action이 실행된다. 

 

initialState값은 각 컴포넌트에서 useEffect를 이용하여 변경되었을 때 특정 동작을 수행시킬 수 있다. (ex 로그인이 성공하였을 때 -> loginDone : true)

또한 이렇게 작성을 하게되면 redux 확장프로그램에서 원하는 action이 순서대로 동작되었는지를 쉽게 파악할 수 있어서 코드 관리가 용이하다.

 

 

참고문서

fork, call https://carpet-part1.tistory.com/311

 

redux-saga call, fork

call call은 동기 함수 호출(blocking) call을 하면 로그인 api가 return할 때까지 기다려서 result에 값을 넣어줌 call의 경우 👇 function* logIn() { try{ const result = yield call(logInAPI);..

carpet-part1.tistory.com

리덕스 미들웨어, 사가 https://hankyeolk.github.io/2021/02/07/reduxSaga.html#%EB%A6%AC%EB%8D%95%EC%8A%A4-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4

 

😈 Redux-Saga를 동작시키는 기본적인 방법 - HK Blog

블로그 목차 리덕스 미들웨어 redux-saga는 리덕스 생태계를 지탱하고 있는 다양한 미들웨어 중 핫한 미들웨어다. 미들웨어는(middleware Express.js로 서버를 구축할 때 사용되는 여러 미들웨어와 사용

hankyeolk.github.io

https://juhi.tistory.com/25

 

[React] redux-saga 시작하기! 기본적인 effects 활용

💥 redux-saga 비동기 작업을 하는 미들웨어가 필요할 때 사용한다. 특정 액션이 발생했을 때 상태 값이나 응답 상태 등에 따라 다른 액션을 디스패치 하거나 추가적인 로직을 적용 해야될 때 사용

juhi.tistory.com

 

반응형

댓글