[React.js]리액트_redux-saga

2021. 8. 3. 00:52Web_Programming/React

 

 

💻 redux-saga를 이용한 비동기 액션처리

 

비동기 액션을 처리하는 라이브러리

리덕스에서 액션 처리후, 비동기 처리를 통해 상태값을 변경시키고 싶을 경우 사용

  • redux-thunk
  • redux-observable (RxJS 패키지 기반)
  • redux-saga

: 제너레이터 기반, 활용도가 높고/ 테스트 코드 작성에 쉬움.

 

💡 제너레이터

 

제너레이터는 function*와 같이 작성.

function* f1() {
  console.log('f1-1');
  yield 10;
  console.log('f1-2');
  yield 20;
  console.log('f1-3');
  return 'finished';
}
const gen = f1();
console.log(gen.next());
/*
  실행구간
  console.log('f1-1');
  yield 10;

  반환객체
  { value: 10, done: false }
*/

console.log(gen.next());
/*
  실행구간
  console.log('f1-2');
  yield 20;

  반환객체
  { value: 20, done: false }
*/

console.log(gen.next());
/*
  실행구간
  console.log('f1-3');
  yield 30;

  반환객체
  { value: 'finished', done: true }
*/

📍 next 함수 (제너레이터 객체 내의 함수)

 

제너레이터의 yield 구간을 기준으로 나뉘어 호출

value: yield 오른쪽 값 / done: 제너레이터 함수가 모두 완료되었는지 여부

 

 

iterator vs iterable

iterator( 반복자) iterable(반복 가능)
next 함수 사용가능 Symbol.iteraor() 속성값으로 가짐
next 함수: value/done 속성값의 객체를 반환 Symbol.iteraor() 호출시, iterator 반환
작업 종료시, done ==true  

 

👉 제너레이터는 iterator이면서 iterable입니다.

 

이로인해, for of문과 전개연산자 사용가능

function* f1() {
  yield 10;
  yield 20;
  yield 30;
}

for (const v of f1()) {
  console.log(v);
}

const arr = [...f1()];
console.log(arr); // [ 10, 20, 30 ]

제너레이터 예제

function* user() {
  const userMessageList = ['Hi, I am keeper!', 'Nice to meet you!'];
  for (const message of userMessageList) {
    console.log('User: ', yield message);
  }
}

function kepper() {
  const gen = user();

  const robotMessageList = ['', 'Hi, I am user!'];
  for (const message of robotMessageList) {
    console.log('Keeper: ', gen.next(message).value);
  }
}

❗ 제너레이터 next에 인자로 값을 전달하면 전달된 인자는 yield의 반환값

  1. gen.next('')=> 로그: Keeper: Hi, I am keeper!
  2. gen.next('Hi, I am user!')=> 로그: User: Hi, I am User!
  3. 반복

리덕스 사가 함수 : user()

리덕스 사가 미들웨어 : keepper()

 

💡 리덕스 사가의 이펙트 함수

 

put : 액션 발생

call : 서버 API 호출

 

takeLeading (<-> takeLatest)

두번째 인자 함수가 아직 진행중에 있다면, 그 사이에 들어온 액션은 무시하고 처음에 들어온 액션을 우선으로 처리하도록 관리해줍니다.

import { all, call, put, takeLeading } from 'redux-saga/effects';

export function* fetchData(action) {
  yield put(actions.setLoading(true));
  yield put(actions.addLike(action.timeline.id, 1));
  yield call(callApiLike);
  yield put(actions.setLoading(false));
}

export default function* () {
  yield all([
    takeLeading(type.REQUEST_LIKE, fetchData)
    // takeLeading(type.REQUEST_LIKE1, fetchData1)
    // takeLeading(type.REQUEST_LIKE2, fetchData2)
    // takeLeading(type.REQUEST_LIKE3, fetchData3)
  ]);
}

📍 take함수 반환객체

var takeReturn = take(types.REQUEST_LIKE);
var takeReturnLog = {
  TAKE: {
    pattern: 'timeline/REQUEST_LIKE'
  }
}

📍 put 함수 반환객체

var putReturn = put(actions.setLoading(false));
var putReturnLog = {
  PUT: {
    channel: null,
    action: {
      type: 'timeline/SET_LOADING',
      isLoading: false
    }
  }
}

📍 call 함수 반환객체

var callReturn = call(callApiLike);
var callReturnLog = {
  CALL: {
    context: null,
    fn: callApiLike,
    args: [],
  }
}

이렇게 반환된 객체는 yield를 호출시 👉 사가 미들웨어에 전달

 

사가 미들웨어는 부수 효과 완료 후 👉 그 결과와 함께 실행흐름을 다시 제네레이터(fetchData) 쪽으로 전달

 

💡 리덕스 사가 미들웨어 추가하기

import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
import { createSagaMiddleware } from 'redux-saga';
import { all } from 'redux-saga/effects';

const reducer = combineReducers({
  timeline: timelineReducer,
  friend: friendReducer,
})

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  reducer,
  composeEnhancers(
    applyMiddleware(sagaMiddleware)
  )
)

function* rootSaga() {
  yield all([
    timelineSaga(),
    friendSaga()
  ]);
}
sagaMiddleware.run(rootSage);

export default store;
  • 기존 리덕스 적용 코드에서 createSagaMiddleware() 를 이용하여 생성
  • createStore에서, 미들웨어를 등록하는 composeEnhancers 함수를 입력, applyMiddleware 함수에 미들웨어 객체를 넣어줍니다.(applyMiddleware 여러개 입력가능)
  • all 함수를 통해서 모든 사가를 추가시켜준 다음 사가 미들웨어를 실행

 

💡 다수 액션 처리

LOGIN액션과 LOGOUT액션 처리

function* loginFlow() {
  while(true) {
    // [로그인]
    // 로그인 액션을 기다림
    // 로그인 API 호출
    // 로그인 정보를 저장
    const { id, password } = yield take(types.LOGIN);
    const userInfo = yield call(callApiLogin, id, password);
    yield put(types.SET_USER_INFO, userInfo);
    
    // [로그아웃]
    // 로그아웃 액션을 기다림
    // 로그아웃 API 호출
    // 로그인 정보를 제거
    yield take(types.LOGOUT);
    yield call(callApiLogout, id);
    yield put(types.SET_USER_INFO, null);
  }
}

💡 리덕스 사가 예외 처리

try/catch문을 이용하여 예외 처리

import { all, call, put, takeLeading } from 'redux-saga/effects';

export function* fetchData(action) {
  yield put(actions.setLoading(true));
  yield put(actions.addLike(action.timeline.id, 1));
  
  // 에러처리
  try {
    yield call(callApiLike);
  } catch (error) {
    // 에러객체 저장
    // Like 원래상태로 되돌림
    yield put(actions.setValue('error', error));
    yield put(actions.addLike(action.timeline.id, 1));
  }
  
  yield put(actions.setLoading(false));
}

export default function* () {
  yield all([
    takeLeading(type.REQUEST_LIKE, fetchData)
  ]);
}

💡 디바운스

: 같은 함수가 여러번 연속해서 호출 될 때, 첫번째 또는 마지막 호출만 실행하는 기능

 

리덕사 사가의 디바운스 함수

일정 시간 내에 제너레이터 함수가 여러번 호출 되더라도 한번만 실행

 

REQUEST_LIKE 액션이 발생했을 때 바로 함수를 호출하지 않고 1초를 기다렸다가 더이상 액션이 발생하지 않으면 fetchData 함수를 실행

import { all, call, put, takeLeading } from 'redux-saga/effects';

export function* fetchData(action) {
  yield put(actions.setLoading(true));
  yield put(actions.addLike(action.timeline.id, 1));
  try {
    yield call(callApiLike);
  } catch (error) {
    yield put(actions.setValue('error', error));
    yield put(actions.addLike(action.timeline.id, 1));
  }
  yield put(actions.setLoading(false));
}

export default function* () {
  yield all([
    debounce(1000, type.REQUEST_LIKE, fetchData)
  ]);
}
반응형