맨 땅에 프론트엔드 개발자 되기

Redux-Saga 사용방법 정리 본문

코딩 공부 일지/React JS

Redux-Saga 사용방법 정리

헬로코딩 2022. 7. 13. 19:35
728x90

나는 어떤 일을 할 때, 그 일을 왜 하는지가 이해가 되어야 한다. Redux-Saga를 처음 배울 때 비동기를 처리하기 위해 왜 Redux-Saga를 사용할까 이해가 되지 않았다. 컴포넌트 내부에서 비동기 로직을 실행하고 난 뒤에 디스패치를 하면 되는 거 아닌가? 라고 생각했었다.

왜 Redux-Saga를 사용하는가?

디자인 패턴이라고 들어보았는가? 진짜 잘하는 개발자는 소프트웨어 설계부터 비즈니스 로직이 안정적으로 구현되고 가독성 좋은 코드를 짜기 위해 고민한다고 한다. Redux-Saga는 갑자기 어디서 뿅하고 튀어나온 게 아니라 이 디자인 패턴을 고민하다가 나온 결과물이라고 할 수 있다.

- Saga 패턴

Saga 패턴에 대한 설명을 찾아보면, "saga 패턴은 분산 애플리케이션의 일관성을 유지하고 여러 마이크로서비스 간의 트랜잭션을 조정하여 데이터 일관성을 유지하는 데 도움이 되는 장애 관리 패턴입니다. 마이크로서비스는 모든 트랜잭션에 대한 이벤트를 게시하고 다음 트랜잭션은 이벤트의 결과에 따라 시작됩니다." 라고 되어있다.

이게 무슨 말이야???

내가 이해한 대로 설명해보자면, DB가 여러 개로 분산되어 있는데(e.g. user 정보를 관리하는 DB, 상품 정보를 관리하는 DB 등) 동시에 여러 DB를 업데이트 해야되는 상황일 때, 한 곳에서 관리해서 전체 서비스의 데이터 일관성을 유지하는 방식이 적용된 것이 Saga 패턴이다.

Saga 패턴에 대해 이해하고 싶으신 분들은 아래 글을 읽어보면 좋을 것 같다.

 

마이크로서비스 분산 트랜잭션 관리 (Saga Pattern)

개요 앞선 포스팅에서 마이크로서비스 분산DB 환경에서 고려되어야 할 사항에 대해 살펴보았다. 자세한 내용은 아래 포스팅을 참고하기 바란다. 마이크로서비스 분산 트랜잭션 관리 (2Phase Co

waspro.tistory.com

Redux-Saga는 여러 작업을 병렬로 분기하고 실행 중인 작업을 취소하거나 에러가 발생했을 때 그에 대한 처리를 할 수 있도록 도와준다.

Generator 함수

Generator 함수는 잘 쓰이지 않기도 하고 처음 접할 때는 생소해서 이해가 잘 되지 않는데, Redux-Saga가 Generator 함수를 기반으로 만들어졌기 때문에 러닝 커브가 높다고 얘기하기도 한다. Generator 함수는 function 뒤에 *을 붙이고, yield 를 통해 실행 구간을 나눈다.

function* generatorFunction() {
    yield 1;
    yield 2;
    yield 3;
    return 4;
}

const generator = generatorFunction();

위와 같은 함수가 있을 때, 콘솔에서 실행해보면 아래와 같은 결과를 얻을 수 있다.

generator.next() 를 통해 다음 구간을 호출할 수 있다.

Redux-Saga 사용방법 (로그인 구현)

React를 사용한다는 가정하에 Redux 미들웨어이므로 당연하게도 redux를 설치해주어야 한다.

npm i redux react-redux redux-saga

modules/login.js

import { put, all, call, takeLatest } from "redux-saga/effects";
import Cookies from "universal-cookie";

// 액션 타입
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';

// 액션 생성 함수
export function loginRequest(payload) {
  return {type: LOGIN_REQUEST, payload};
}
export function loginSuccess() {
  return {type: LOGIN_SUCCESS};
}
export function loginFailure(error) {
  return {type: LOGIN_FAILURE, error};
}
export function logoutRequest() {
  return {type: LOGOUT_REQUEST};
}
export function logoutSuccess() {
  return {type: LOGOUT_SUCCESS};
}
export function logoutFailure(error) {
  return {type: LOGOUT_FAILURE, error};
}

// 로그인
const cookies = new Cookies();

function* login(payload) {
  const getToken = (payload) => {
    // 실제로는 payload에 들어있는 아이디와 패스워드를 post 요청으로 보내서 서버에서 검증을 받고 액세스 토큰을 얻음.
    if(payload.id && payload.pw){
      setTimeout(() => {
        return 'fake jwt accessToken';
      }, 1000);
    }
    return null;
  }
  try {
    const token = yield call(() => getToken(payload.payload)); // call은 함수를 실행해줌.
    cookies.set("token", token); // 다음 실행 전에 쿠키에 값을 저장하는 등 다른 작업을 할 수 있음.
    yield put(loginSuccess()); // put은 특정 액션을 디스패치 해줌.
  } catch (err) {
    yield put(loginFailure(err));
  }
}

// 로그아웃
function* logout() {
  try {
    yield put(logoutSuccess());
    cookies.remove("token"); // 쿠키 제거
  } catch (err) {
    yield put(logoutFailure(err));
  }
}

export function* getLoginSaga() {
  yield all([
  	takeLatest(LOGIN_REQUEST, login), // takelatest는 만약 동일한 디스패치 요청이 여러 번 있었다면 제일 마지막에 들어온 요청만 실행함. debounce와 동일
  	takeLatest(LOGOUT_REQUEST, logout)
  ]);
}

// 초깃값
const initiaState = {
  error: null,
  isLoggingIn: null,
};

export default function login(state = initiaState, action) {
  switch (action.type) {
    case LOGIN_REQUEST:
      return {
        ...state,
        isLoggingIn: null,
        error: null
      };  
    case LOGIN_SUCCESS:
      return {
        ...state,
        isLoggingIn: true,
        error: null,
      }
    case LOGIN_FAILURE:
      return {
        ...state,
        error: action.error
      }
    case LOGOUT_REQUEST:
      return {
        ...state,
        error: null
      }
    case LOGOUT_SUCCESS:
      return {
        ...state,
        isLoggingIn: false,
        error: null
      }
    case LOGOUT_FAILURE:
      return {
        ...state,
        error: action.error
      }   
    default:
      return state;
  }
}

modules/index.js

import { combineReducers } from 'redux';
import login, { getLoginSaga } from './login';
import { all } from 'redux-saga/effects';

const rootReducer = combineReducers({ 
  login 
});

export function* rootSaga() {
  yield all([
    getLoginSaga()
  ]); // all은 배열 안의 여러 사가를 동시에 실행시켜줍니다.
}

export default rootReducer;

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import createSagaMiddleware from '@redux-saga/core';
import { configureStore } from '@reduxjs/toolkit';

const sagaMiddleware = createSagaMiddleware(); // 사가 미들웨어를 만든다.

const store = configureStore({
  reducer: rootReducer, 
  middleware: [sagaMiddleware, logger]
});

sagaMiddleware.run(rootSaga); // 루트 사가를 실행해준다.
// 주의: 스토어 생성이 된 다음에 위 코드를 실행해야 한다.

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

Redux-Saga의 주요 기능들

- delay

설정된 시간 이후에 실행하는 Promise 객체를 리턴한다.

예시: delay(1000)

=> 1초 기다리기

- put

특정 액션을 dispatch 하도록 한다.

예시: put({ type: 'INCREMENT'})

=> INCREMENT 액션을 dispatch 한다.

- takeEvery

짧은 시간 안에 여러 번 들어오는 모든 액션에 대해 특정 작업을 처리해준다.

예시: takeEvery(INCREASE_ASYNC, increaseSaga)

=> 들어오는 모든 INCREASE_ASYNC 액션에 대해 increaseSaga 함수를 실행한다.

- takeLatest

짧은 시간 안에 여러 번 들어오는 모든 액션 중에 가장 마지막으로 들어온 액션만 수행한다. (이전 액션은 취소처리)

예시: takeLatest(INCREASE_ASYNC, increaseSaga)

=> 들어오는 모든 INCREASE_ASYNC 중 가장 마지막 액션에 대해 increaseSaga 함수를 실행한다.

- call

함수의 첫 번째 파라미터는 함수, 나머지 파라미터는 해당 함수에 넣은 인수다.

예시: call(() => func(), 1000)

=> 1초 뒤에 func 함수를 실행한다. 두 번째 파라미터가 없을 경우 기본 값은 0이다.

- all

all 함수를 사용해서 제너레이터 함수를 배열의 형태로 넣어주면, 제너레이터 함수들이 병행적으로 동시에 실행된다.

예시: yield all([testSaga1(), testSaga2()])

=> testSaga1과 testSaga2가 동시에 실행되고, 모두 resolve 될 때까지 기다린다.

728x90