(Redux) Saga

Mirrer·2022년 8월 28일
0

Redux

목록 보기
4/7
post-thumbnail

Redux_Saga

Saga는 제너레이터 문법을 사용해 특정 Action이 발생하면 비동기적으로 Dispatch

기본적으로 ReduxAction Dispatch동기적으로 실행된다.

그래서 여러 Diapatch가 실행되는 경우에는 컴포넌트 파일에서도 Dispatch 로직을 여러번 작성해야하는 단점이 존재하는데 이것을 해결하기 위해 Redux_Saga라는 미들웨어를 사용한다.

Redux_Saga앞선 포스팅에서 소개한 제너레이터 문법을 사용하여 비동기적으로 Dispatch를 수행한다.

Redux_Saga의 내부 메소드를 활용하면 무한의 개념과 이벤트리스너의 역활을 수행할 수 있다.


Saga Effect

Redux_Saga에서는 다음과 같은 내부 메서드를 제공한다.

Effect용도
all함수 배열을 인자로 받은 뒤 동시에 전부 등록
take첫번째 인자로 받은 Action이 실행되기까지 기다린다, 실행이 완료되면 두번째 인자로 받은 함수가 실행
putActionDispatch
delay서버를 구현하기전 비동기적 효과
fork함수를 첫번째 인자로 받은 뒤 비동기 호출, 나머지 인자들은 첫번째 인자 함수에 매개변수로 전달될 값
call함수를 첫번째 인자로 받은 뒤 동기 호출, 나머지 인자들은 첫번째 인자 함수에 매개변수로 전달될 값

fork & call의 차이점

Saga Effectfork는 비동기로 함수를 호출하기때문에 결과값을 기다리지 않고 다음 코드를 실행한다.

하지만 call은 함수를 동기적으로 실행하기 때문에 결과값이 전달될때까지 기다린다.

// fork 
const result = yield fork(logInAPI);
  axios.post('/api/login')
  yield put({
      type: 'LOG_IN_SUCCESS',
      data: result.data
});

// call
const result = yield call(logInAPI);
  axios.post('/api/login')
      .then((result) => {
  yield put({
      type: 'LOG_IN_SUCCESS',
      data: result.data
  });
});

take series, throttle

제너레이터 문법의 yeild를 사용해 이벤트리스너를 구현하면 그 기능은 한번밖에 사용하지 못하는 일회용이다.

이러한 단점은 while무한반복문으로 해결할 수 있지만 직관적이지 않다.

function* watchLogIn() {
  while (true) {
    yield take('LOG_IN_REQUEST', logIn);    
  }
}

그래서 takeEvery, takeLatest, takeLeading, throttle...등과 같은 내부 메서드를 사용해 위와 같은 문제를 해결한다.

  1. takeEvery를 사용하면 위 코드의 while(true)문을 대체할 수 있다.
function* watchLogIn() {  
  yield takeEvery('LOG_IN_REQUEST', logIn);      
}

  1. takeLatest는 마우스 이벤트의 중복 발생을 방지하게 위해 마지막 이벤트만 실행한다.
function* watchLogIn() {  
  yield takeLatest('LOG_IN_REQUEST', logIn);      
}

takeLatest는 요청을 취소하는 것이 아닌 응답을 취소하는 것을 주의


  1. takeLeadingtakeLatest와 다르게 마우스 이벤트의 중복 발생시 첫 이벤트만 실행한다.
function* watchLogIn() {  
  yield takeLeading('LOG_IN_REQUEST', logIn);      
}

  1. throttle요청 제한 시간을 지정해 해당 시간동안 단 한번의 서버 요청만 호출한다.
function* watchLogIn() {  
  yield throttle('LOG_IN_REQUEST', logIn, 10000);      
}

Saga 사용방법

프로젝트에 Redux_Saga를 적용하는 방법은 다음과 같다.

  1. 아래 npm명령어를 통해 Saga를 설치한다.
npm i redux-saga

  1. configureStore.jssaga middleware를 추가
// store/configureStore.js
import { createWrapper } from 'next-redux-wrapper';
import { applyMiddleware, compose, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga'; // redux-saga불러오기

import reducer from '../reducers/index';
import rootSaga from '../sagas'; // saga파일 불러오기

const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware(); // redux-saga변수에 할당
  const middlewares = [sagaMiddleware]; // redux-saga middleware에 추가
  const enhancer = process.env.NODE_ENV === 'production'
    ? compose(applyMiddleware(...middlewares))
    : composeWithDevTools(applyMiddleware(...middlewares))
  const store = createStore(reducer, enhancer);
  store.sagaTask = sagaMiddleware.run(rootSaga); // store에 추가
  return store;
};

const wrapper = createWrapper(configureStore, {
  debug: process.env.NODE_ENV === 'development',
});

export default wrapper;

  1. Saga 모듈 생성
// sagas/index.js
import { all, fork, take, call, put } from 'redux-saga/effects'; // effect앞에는 yield사용
import axios from 'axios';

function logInAPI(data) {
  return axios.post('/api/login', data); // 서버에 로그인 요청을 보낸다.
}

function* logIn(action) { // 요청결과에 따라 try ~ catch문작성
  try {
    // fork는 비동기함수 호출, call은 동기함수 호출    
    const result = yield call(logInAPI, action.data); // logInAPI함수의 요청결과를 변수에 저장
    yield put({
      type: 'LOG_IN_SUCCESS',
      data: result.data // 서버요청 성공의 실제 데이터
    })
  } catch(err) {
    yield put({ // put은 action객체를 dispatch
      type: 'LOG_IN_FAILURE',
      data: err.response.data // 서버요청 실패의 실제 데이터
    })
  }
  
}

function* watchLogIn() {
  // LOG_IN action이 실행될때까지 기다린다.
  // LOG_IN action이 실행되면 logIn함수를 실행한다.
  // thunk와 다르게 비동기 action creator 직접실행하는 것이 아닌 이벤트리스너와 같은 역활
  yield take('LOG_IN_REQUEST', logIn); 
}

function* watchLogOut() {
  yield take('LOG_OUT_REQUEST');
}

function* watchAddPost() {
  yield take('ADD_POST_REQUEST');
}

export default function* rootSaga() {
  yield all([ // all은 배열안에 제너레이터 함수를 동시에 실행
    fork(watchLogIn), // fork는 제너레이터 함수룰 실행
    fork(watchLogOut),
    fork(watchAddPost),
  ]);
}

Saga Split

앞선 포스팅에서 소개한 Reducer와 마찬가지로 Saga 또한 코드의 양이 길어진다는 단점이 존재한다.

그래서 SagaAction 코드를 가독성을 위해 Reducer와 동일한 방식으로 분리한다.

다만 ReducercombineReducers로 파일들을 병합했지만 Saga는 위 과정을 생략하여 좀 더 간편하게 병합이 가능하다.

// sagas/index.js
import { all, fork } from 'redux-saga/effects';

import userSaga from './user';
import postSaga from './post';

export default function* rootSaga() {
  yield all([ 
    fork(userSaga), 
    fork(postSaga),    
  ]);
}
// sagas/post.js
import { all, fork, delay, put, takeLatest } from 'redux-saga/effects';
import { 
  ADD_POST_REQUEST, ADD_POST_SUCCESS, ADD_POST_FAILURE,   
} from '../reducers/post';

function* addPost(action) { 
  try {
    yield delay(1000);
    yield put({
      type: ADD_POST_SUCCESS,
      data: action.data,
    })
  } catch(err) {
    yield put({ 
      type: ADD_POST_FAILURE,
      data: err.response.data 
    })
  }  
}

function* watchAddPost() {
  yield takeLatest(ADD_POST_REQUEST, addPost, 2000);
}

export default function* postSaga() {
  yield all([
    fork(watchAddPost),
  ]);
}
// sagas/user.js
import { all, fork, delay, put, takeLatest } from 'redux-saga/effects';
import { 
  LOG_IN_REQUEST, LOG_IN_SUCCESS, LOG_IN_FAILURE,   
} from '../reducers/user';

function* logIn(action) {
  try {    
    yield delay(1000);
    yield put({
      type: LOG_IN_SUCCESS,       
      data: action.data,
    })
  } catch(err) {
    yield put({
      type: LOG_IN_FAILURE,
      error: err.response.data
    })
  }  
}

function* watchLogIn() {   
  yield takeLatest(LOG_IN_REQUEST, logIn);
}

export default function* userSaga() {
  yield all([
    fork(watchLogIn),
  ]);
}

참고 자료

Redux 공식문서
React로 NodeBird SNS 만들기 - 제로초

profile
memories Of A front-end web developer

0개의 댓글