React.js - redux middleware(리덕스 미들웨어, redux-thunk, redux-saga)

Gyu·2022년 9월 6일
0

React.js

목록 보기
16/20
post-thumbnail

React - Redux(리덕스 미들웨어)

미들웨어란?

  • 소프트웨어 공학에서 미들웨어란 운영체제와 응용 소프트웨어 중간에서 조정과 중개의 역할을 수행하는 소프트웨어를 말한다.
  • 리덕스 미들웨어는 액션을 디스패치 했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업을 실행한다. 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있다.
  • 리듀서가 액션을 처리하기 전에 미들웨어가 할 수 있는 작업은 여러가지가 있다. 전달받은 액션을 단순히 콘솔에 기록하거나, 전달받은 액션 정보를 기반으로 액션을 아예 취소하거나, 다른 종류의 액션을 추가로 디스패치할 수 있다.

미들웨어를 왜 쓰는가?

  • 리덕스는 액션이 발생하면 디스패치를 통해 스토어에게 상태 변화의 필요성을 알린다. 개발자는 가끔 디스 패치된 액션을 스토어로 전달하기 전에 로깅, 액션 취소, 다른 액션을 발생 시키는 등의 작업을 하고 싶을 수 있다. 미들웨어는 이러한 작업을 가능하게 해준다.
  • 리덕스는 아래의 과정으로 동작한다.
    • 액션 객체 생성 → 디스패치가 액션 발생을 스토어에게 알림 → 정해진 로직에 따라 리듀서가 액션 처리 → 리듀서가 새로운 상태값 반환 → 스토어가 새로운 상태를 저장
    • 이러한 작업은 동기적으로 동작한다.
  • 리덕스는 동기적으로 작동하고, 위 과정은 순식간에 실행된다. 때문에 위 과정을 시간을 딜레이 시켜 동작하게 하거나 ajax 통신 같은 비동기 작업을 넣을 수가 없다. 예를들어 아래와 같이 액션 생성 함수 내에서 ajax요청을 하고 응답값을 payload에 넣어 액션 객체를 생성하여 반환한다고 하면 에러가 나거나 정상작동하지 않는다.
    // async/await로 만든 액션 생성 함수
    export const fetchData = async () => {
      //임의의 api 주소에 async await으로 (get) 요청을 하였다.
      const response = await APIadress.get('/data')
     
      return {
        type: 'FETCH_DATA',
        payload: response
      }
    }
    // 결과 : 아래 에러 발생
    // action must be plain objects. Use custom middleware for async actions
    // 이유 : reducer로 dispatch 되는 최초의 action 형태는 
    //       plain object가 아니라 async await의 함수의 형태이기 때문
    
    // Promise로 만든 액션 생성 함수
    export const fetchData = async () => {
      //임의의 api 주소에 (get) 요청을 promise 형태로 하였다.
      const promise = await APIadress.get('/data')
     
      return {
        type: 'FETCH_DATA',
        payload: response
      }
    }
    // 결과 : 에러는 나지 않지만 정상 작동하지 않음
    // 이유 : Promise를 사용하면 reducer에서 
    //       '반환된 데이터가 아직 없다'(객체에 아무것도 없다!)라고 판단하기 때문
  • 리덕스에서는 이러한 비동기 작업을 처리하기 위한 지침을 알려주지 않고 있기 때문에 이러한 비동기 작업을 처리하는 데 있어 리덕스 미들웨어를 주로 사용한다.

대표적인 redux middleware

  • redux-logger : 액션 정보를 콘솔에 출력해주는 미들웨어.
  • redux-thunk : 비동기 작업을 처리 할 때 가장 많이 사용하는 미들웨어. 객체가 아닌 함수 형태의 액션을 디스패치할 수 있게 해준다.
  • redux-saga : redux-thunk 다음으로 가장 많이 사용되는 비동기 작업 관련 미들웨어 라이브러리. 특정 액션이 디스 패치 되었을 때 정해진 로직에 따라 다른 액션을 디스패치 시키는 규칙을 작성하여 비동기 작업을 처할 수 있게 해준다.

미들웨어 작동 방식 이해하기

  • 리덕스 미들웨어를 만들때 아래와 같은 템플릿을 사용한다.
// 미들웨어 기본 템플릿
const middleware = store => next => action => {
	// 처리 로직
};

// 일반 함수 형식
const middleware = function middleware(store) {
	return function(next) {
		return function(action) {
			// 처리 로직
		}
	}
}
  • 미들웨어는 함수를 반환하는 함수를 반환하는 함수다.
  • 함수의 첫 번째 매개변수인 store는 리덕스 스토어 인스턴스로, 이 안에 dispatch, getState, subscribe 내장함수들이 들어있다.
  • 두 번째 매개변수인 next는 액션을 다음 미들웨어에게 전달하는 함수로 store.dispatch와 비슷한 역할을 한다. next(action) 형태로 사용하며, next(action) 를 호출하면 그 다음 처리해야할 미들웨어에게 액션을 넘겨주고, 만약 미들웨어가 없다면 리듀서에게 액션을 넘겨준다. 만약 미들웨어 내부에서 next를 사용하지 않으면 액션이 리듀서에게 전달되지 않는다. 즉 액션이 무시되는 것이다.
  • action은 디스패치되어 현재 처리하고 있는 액션을 가리킨다.
  • 미들웨어 내부에서 store.dispatch를 사용하면 첫번째 미들웨어부터 다시 처리한다.

  • 미들웨어는 위와 같은 구조로 작동한다. 리덕스 스토어에는 여러 개의 미들웨어를 등록할 수 있다. 새로운 액션이 디스패치 되면 첫 번째로 등록한 미들웨어가 호출된다. 만약에 미들웨어에서 next(action)을 호출하게 되면 다음 미들웨어로 액션이 넘어간다. 그리고 만약 미들웨어에서 store.dispatch 를 사용하면 다른 액션을 추가적으로 발생시킬 수 도 있다.

logger 만들어 미들웨어 작동방식 이해하기

  • 실제 미들웨어를 직접 만들어서 사용할 일은 많지 않지만, 간단한 미들웨어를 직접 만들어 보면 미들웨어 작동 방식을 이해할 수 있다.
// src/lib/loggerMiddleware.js
// 이전상태, 액션정보, 새로워진 생태를 순차적으로 콘솔에 기록하는 미들웨서
const loggerMiddleware = store => next => action => {
    // 미들웨어 기본 구조
    console.group(action && action.type); // 액션 타입을 그룹명으로 설정
    console.log('이전상태', store.getState());
    console.log('action', action);
    next(action); // 다음 미들웨어 혹은 리듀서에게 전달
	// next 함수를 쓰지 않으면 리듀서에게 전달이 되지 않기 때문에 액션이 무시 됨
	// next 함수를 기준으로 이전이 이전 상태, 이후가 다음 상태
    console.log('다음 상태', store.getState());
    console.groupEnd(); // 그룹 끝
}

export default loggerMiddleware;

// src/index.js - 미들웨어 스토어에 적용
import React from 'react';
import ReactDOM from 'react-dom';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
import loggerMiddleware from './lib/loggerMiddleware';

// 미들웨어는 스토어를 생성하는 과정에서 applyMiddleware() 함수를 사용하여 적용한다.
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));

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

redux-logger 사용하기

  • 설치 : npm i redux-logger
  • src/index.js에 적용하기
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { applyMiddleware, createStore } from 'redux';
    import { Provider } from 'react-redux';
    import './index.css';
    import App from './App';
    import rootReducer from './modules';
    // import loggerMiddleware from './lib/loggerMiddleware';
    import { createLogger } from 'redux-logger';
    
    const logger = createLogger();
    const store = createStore(rootReducer, applyMiddleware(logger));
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    );

redux-thunk

  • 리덕스를 사용하는 프로젝트에서 비동기 작업을 처리할 때 가장 기본적으로 사용하는 미들웨어. 리덕스의 창시자인 댄 아브라모프가 만들었으며, 리덕스 공식 메뉴얼에서도 이 미들웨어를 사용하여 비동기 작업을 다루는 예시를 보여준다.

Thunk란?

  • 컴퓨터 프로그래밍에서, 썽크(Thunk)는 기존의 서브루틴에 추가적인 연산을 삽입할 때 사용되는 서브루틴이다. 썽크는 주로 연산 결과가 필요할 때까지 연산을 지연시키는 용도로 사용되거나, 기존의 다른 서브루틴들의 시작과 끝 부분에 연산을 추가시키는 용도로 사용되는데, 컴파일러 코드 생성 시와 모듈화 프로그래밍 방법론 등에서는 좀 더 다양한 형태로 활용되기도 한다.

    // thunk 예1
    // 1+2를 연산하고 싶다면 알해와 같이 코드를 작성하면 된다.
    const x = 1 + 2;
    
    // 아래와 같이 코드를 작성하면, 연산이 바로 실행되지 않고
    // foo() 함수가 호촐되어야 실행된다.
    const foo = () => 1 + 2;
  • 썽크(Thunk)는 "고려하다"라는 영어 단어인 "Think"의 은어 격 과거분사인 "Thunk"에서 파생된 단어인데, 연산이 철저하게 "고려된 후", 즉 실행이 된 후에야 썽크의 값이 가용해지는 데서 유래된 것이라고 볼 수 있다.

  • Thunk란 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 칭한다.

    // thunk 예2
    
    // 파라미터로 전달받은 값에 1을 더해서 반환하는 함수
    const addOne = x => x + 1;
    addOne(1); // 2
    // 함수 실행 시 바로 1+1을 연산
    
    // 연산 작업을 1초 후애 실행하는 함수
    const addOne = x => x + 1; // 파라미터에 1을 더하는 함수
    
    function addOneThunk(x) { // addOne 함수를 반환하는 함수를 반환하는 함수
    	const thunk = () => addOne(x);
    	return thunk;
    }
    
    const fn = addOneThunk(1); // () => addOne(1);
    setTimeout(() => {
    	const value = fn(); // fn이 실행되는 시점 - 연산이 실행됨
    	console.log(value)
    }, 1000)
    
    // 화살표 함수로 변환
    const addOne = x => x + 1;
    const addOneThunk = x => () => addOne(x);
    
    const fn = addOneThunk(1); // () => addOne(1);
    setTimeout(() => {
    	const value = fn(); // fn이 실행되는 시점
    	console.log(value)
    }, 1000)
    
  • redux-thunk 라이브러리를 사용하면 thunk 함수를 만들어서 디스패치 할 수 있다. 그러면 리덕스 미들웨어가 그 함수를 전달받아 store의 dispatch와 getState를 파라미터로 넣어 호출해준다.

    // redux-thunk에서 사용할 수 있는 thunk 함수 예시
    const simpleThunk = () => (dispatch, getState) => {
    	// 현재 상태를 참조할 수 있고, 새 액션을 디스패치할 수 있다.
    }

redux-thunk란?

  • redux-thunk는 객체 대신 함수를 생성하는 액션 생성 함수를 작성 할 수 있게 해준다. 리덕스에서는 기본적으로 객체 형태의 액션을 디스패치합니다. 일반 액션 생성자는, 다음과 같이 파라미터를 가지고 액션 객체를 생성하는 작업만 한다.
    const actionCreator = (payload) => ({action: 'ACTION', payload});
  • 만약에 특정 액션이 몇 초 뒤에 실행되게 하거나, 현재 상태에 따라 아예 액션이 무시되게 하려면, 일반 액션 생성 함수로는 할 수가 없다. 하지만 redux-thunk를 사용하면 가능하다.
  • redux-thunk를 사용하면 객체가 아닌 함수를 디스패치할 수 있는데, 함수를 디스패치 할 때는 해당 함수는 dispatchgetState를 파라미터로 가질 수 있다.
  • 예제1) - 1초 뒤에 액션 디스패치
    // 액션 타입 선언
    const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
    
    // 액션 생성 함수
    const increase = () => ({action: INCREMENT_COUNTER});
    
    // dispatch를 매개변수로 갖고 있는 함수를 리턴
    // 이 함수를 thunk 함수라고 한다.
    const increaseAsync = () => dispatch => { 
    	// 1초 뒤 dispatch
    	setTimeout(() => {
    		dispatch(increase());
    	}, 1000)
    }
    
    // INCREMENT_COUNTER 액션이 1초 뒤 디스패치
    store.dispatch(increaseAsync());
  • 예제2) - 조건에 따라 액션 디스패치 혹은 무시
    // dispatch와 getState를 매개변수로 갖는 함수를 리턴
    const dispatchIfOdd = () => (dispatch, getState) => {
    	const { counter } = getState();
    
        if (counter % 2 === 0) {
          return;
        }
    
        dispatch(increment());
    }
  • 리턴하는 함수가 dispatch, getState 를 파라미터 갖는다면 스토어의 상태에도 접근 할 수 있다. 따라서, 현재의 스토어 상태의 값에 따라 액션이 dispatch 될 지 무시될지 정할 수 있다.
  • 간단하게 정리를 하자면 redux-thunk 는 일반 액션 생성자에 날개를 달아준다. 보통의 액션생성자는 그냥 하나의 액션객체를 생성 할 뿐이지만 redux-thunk 를 통해 만든 액션생성자는 그 내부에서 여러가지 작업을 할 수 있다. 이 곳에서 네트워크 요청을 해도 무방하며, 이 안에서 액션을 여러번 디스패치 할 수도 있다.

dispatch, getState는 어디서 받아오는가?

  • redux-thunk 미들웨어에서, 전달받은 액션이 함수 형태 일 때, 그 함수에 dispatchgetState 를 넣어서 실행해준다. 실제로, redux-thunk 의 코드는 정말로 간단하다. 아래 그 코드를 보면 더 이해하기 쉽다.
    function createThunkMiddleware(extraArgument) {
      // store에서 dispatch와 getState를 비구조화 할당으로 전달
      return ({ dispatch, getState }) => next => action => {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
    
        return next(action);
      };
    }
    
    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;
    
    export default thunk;

redux-thunk 사용하기(api 요청, 응답처리 예제)

  • 설치 : npm i redux-thunk
  • 미들웨어 스토어에 적용 하기
    // src/index.js
    
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { applyMiddleware, createStore } from 'redux';
    import { Provider } from 'react-redux';
    import './index.css';
    import App from './App';
    import rootReducer from './modules';
    import { createLogger } from 'redux-logger';
    import ReduxThunk from 'redux-thunk'; // 1. import redux-thunk
    
    const logger = createLogger();
    
    // createStore의 매개 변수에 applyMiddleware() 함수 할당
    // applyMiddleware() 함수의 매개변수에 ReduxThunk를 할당
    const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    );

redux-saga

  • redux-thunk 다음으로 많이 사용하는 비동기 작업 관련 미들웨어.
  • redux-thunk는 함수 형태의 액션을 디스패치히여 미들웨어에서 해당 함수에 스토어의 dispatch, getState를 파라미터로 넣어 사용하는 원리다. 그래서 thunk 함수 내부에서 api 요청, 다른 액션 dispatch, 현재 상태 조회 등의 작업을 할 수 있다. 대부분의 경우에서는 redux-thunk로 충분히 기능을 구현할 수 있다.
  • redux-saga는 좀 더 까다로운 상황에서 유용하다. 아래의 상황에서 redux-saga를 쓰는 것이 유용하다.
    • 기존 요청을 취소 처리해야할 때(불필요한 중복요청 방지)
    • 특정 액션이 발생했을 때 다른 액션을 발생시키거나, api 요청 등 리덕스와 관계없는 코드를 실행할 때
    • 웹소켓을 사용할 때
    • api 요청 실패 시 재요청해야 할 때
  • redux-saga는 제너레이터 함수 문법을 기반으로 비동기 작업을 관리해 준다. redux-saga는 디스패치하는 액션을 모니터링해서 그에 따라 필요한 작업을 따로 수행할 수 있는 미들웨어다.

제너레이터

  • 일반 함수는 하나의 값(혹은 0개의 값)만을 반환한다. 하지만 제너레이터(generator)를 사용하면 여러 개의 값을 필요에 따라 하나씩 반환(yield)할 수 있으며, 제너레이터와 이터러블 객체를 함께 사용하면 손쉽게 데이터 스트림을 만들 수 있다.

제너레이터 기본 사용법

  • 제네레이터는 제네레이터 함수로 생성가능하며, 제너레이터 함수를 만들 때는 function* 키워드를 사용한다.
    // 일반 함수 - 아래처럼 여러 개의 값을 반환하는 것은 불가능 하다.
    function generalFunction() {
    	return 1;
    	return 2;
    	return 3;
    }
    
    // 제너레이터 함수 선언 - function* 키워드 사용
    // function와 *를 띄워도 상관없지만 붙이는 것을 권장
    function* generateFunction() {
      yield 1;
      yield 2;
      return 3;
    }
    
    // '제너레이터 함수'는 '제너레이터 객체'를 생성한다.
    let generator = generateFunction();
    alert(generator); // [object Generator]
  • 제너레이터는 next() 라는 메서드를 갖고 있다. 제너레이터가 처음 만들어지면 함수의 흐름은 멈춰 있는 상태인데, next() 메서드를 호출하면 가장 가까운 yield <value>문을 만날 때까지 명령을 실행한다.(value를 생략할 수도 있는데, 이 경우엔 undefined가 된다) 이후, yield <value> 문을 만나면 실행을 멈추고 산출하고자 하는 값인 value가 바깥 코드에 반환된다.
  • 제너레이터 함수를 사용하면 함수를 도중에 멈출 수 있고, 순차적으로 여러 값을 반환시킬 수 있다.
  • next() 는 항상 아래 두 프로퍼티를 가진 객체를 반환합니다.
    • value: 산출 값
    • done: 함수 코드 실행이 끝났으면 true, 아니라면 false
function* generateFunction() {
	console.log('hello');
  yield 1; // 1번 실행 - 여기까지 실행 후 value: 1을 반환
	console.log('generateFunction');
  yield 2; // 2번 실행 - 여기까지 실행 후 value: 2를 반환
	console.log('end')
  return 3; // 3번 실행 - 여기까지 실행 후 value: 2를 반환, 제너레이터가 끝났음을 알림
}

let generator = generateFunction();

// 1번
console.log(generator.next());
// 결과 - hello / {value: 1, done: false}

// 2번
console.log(generator.next());
// 결과 - generateFunction / {value: 2, done: false}

// 3번
console.log(generator.next());
// 결과 - end / {value: 3, done: true}

// 4번
console.log(generator.next());
// 결과 - {value: undefined, done: true}

제너레이터와 이터러블

  • next() 메서드를 보면 짐작할 수 있듯이, 제너레이터는 이터러블이다. 따라서 for..of 반복문을 사용해 값을 얻을 수 있습니다. 이 방식을 이용하면 .next().value 을 호출하는 것 보다 쉽게 모든 value를 얻을 수 있다.
    function* generateSequence() {
      yield 1;
      yield 2;
      return 3;
    }
    
    let generator = generateSequence();
    
    for(let value of generator) {
      alert(value); // 1, 2가 출력됨
    }
  • 다만 주의할 점은 위와 같이 코드를 짤 경우 1, 2만 출력되고 3은 출력되지 않는다. 그 이유는 for..of 이터레이션이 done: true 일 때 마지막 value를 무시하기 때문이다. 그러므로 for..of 를 사용했을 때 모든 값이 출력되길 원한다면 yield 로 값을 반환해야 한다.
    function* generateSequence() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    let generator = generateSequence();
    
    for(let value of generator) {
      alert(value); // 1, 2, 3
    }
  • 제너레이터는 이터러블 객체이므로 제너레이터에도 스프레드 연산자를 사용할 수 있다.
    function* generateSequence() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    let sequence = [0, ...generateSequence()];
    
    alert(sequence); // 0, 1, 2, 3

'yield’를 사용해 제너레이터 안·밖으로 정보 교환하기

  • 제너레이터는 값을 생성해주는 특수 문법을 가진 이터러블 객체와 유사해 보이지만, 사실 제너레이터는 더 강력하고 유연한 기능을 제공한다. 바로 yield 가 양방향 길과 같은 역할을 하기 때문이다. yield 는 결과를 바깥으로 전달할 뿐만 아니라 값을 제너레이터 안으로 전달하기까지 한다.
  • next() 함수에 파라미터를 넣고, 제너레이터 함수에서는 yield를 사용하여 해당 값을 조회하여 값을 안, 밖으로 전달할 수 있다.
    • generator.next(arg) 를 호출할 때 인수 argyield 의 결과가 된다.

      // 제너레이터 함수 선언
      function* gen() {
        // 질문을 제너레이터 밖 코드에 던지고 답을 기다린다.
        let result = yield "2 + 2 = ?"; // (*)
      
        alert(result);
      }
      
      //제너레이터 객체 생성
      let generator = gen();
      
      // generator.next() -> { value: "2 + 2 = ?", done: false }
      // yield의 value를 반환한다.
      let question = generator.next().value;
      
      // next() 함수의 매개변수가 yield에 전달되고, yield는 result에 저장된다.
      // 그 후 alert(result); 가 실행된다.
      generator.next(4); // { value: undefined, done: true }
    1. generator.next() 를 처음 호출할 땐 항상 매개변수가 없어야 한다. 매개변수가 있더라도, 이미 yieldvalue 가 있기 때문에 무시된다. generator.next() 를 호출하면 제너레이터 함수가 실행되고 첫 번째 yield "2+2=?" 의 결과가 반환된다. 이 시점에는 제너레이터가 (*)로 표시한 줄에서 실행을 잠시 멈춘다.
    2. yield 의 결과가 제너레이터를 호출하는 외부 코드에 있는 변수, question 에 할당된다.
    3. generator.next(4) 에서 제너레이터가 다시 시작되고 4는 result 에 할당된다. ( let result = 4 )
      • 제너레이터 외부 코드에서 next(4) 를 즉시 호출하지 않고 있는데, 이는 제너레이터가 기다려주기 때문에 호출을 나중에 해도 문제가 되지 않는다.
  • 일반 함수와 다르게 제너레이터와 외부 호출 코드는 next/yield 를 이용해 결과를 전달 및 교환한다.
    // 제너레이터 함수 정의
    function* gen() {
      let ask1 = yield "2 + 2 = ?";
    
      alert(ask1); // 4
    
      let ask2 = yield "3 * 3 = ?"
    
      alert(ask2); // 9
    }
    
    // 제너레이터 생성
    let generator = gen();
    
    alert( generator.next().value ); // "2 + 2 = ?"
    
    alert( generator.next(4).value ); // "3 * 3 = ?"
    
    alert( generator.next(9).done ); // true
    1. 제너레이터 객체가 만들어지고 첫 번째 next()가 호출되면, 실행이 시작되고 첫 번째 yield에 도달한다.
    2. 산출 값("2 + 2 = ?")은 바깥 코드로 반환된다.
    3. 두 번째 next(4)는 첫 번째 yield의 결과가 될 4를 제너레이터 안으로 전달한다. 그리고 다시 실행이 이어집니다.
    4. 실행 흐름이 두 번째 yield에 다다르고, 산출 값("3 * 3 = ?")이 제너레이터 호출 결과가 됩니다.
    5. 세 번째 next(9)는 두 번째 yield의 결과가 될 9를 제너레이터 안으로 전달합니다. 그리고 다시 실행이 이어지는데, done: true이므로 제너레이터 함수는 종료된다.

제너레이터로 액션 모니터링하기

function* watchGenerator() {
	console.log('모니터링 중...');
	let prevAction = null;
	while(true) {
		const action = yield;
		console.log('이전 액션: ', prevAction);
		prevAction = action;
		if(action.type === 'HELLO') {
			console.log('안녕하세요!');
		}
	}
}

const watch = watchGenerator();

watch.next();
// 모니터링 중...
// { value: undefined, done: false }
watch.next({type: 'TEST'});
// 이전 액션: null
// { value: undefined, done: false }
watch.next({type: 'HELLO'});
// 이전 액션 : { type: 'TEST' }
// 안녕하세요!
// { value: undefined, done: false }
  • redux-saga는 위 코드와 비슷한 원리로 액션을 모니터링 한다.

redux-saga로 비동기 카운터 만들기

redux-saga로 모듈 만들기

// modules/counter.js
import { createAction, handleActions } from "redux-actions";
import { delay, put, takeEvery, takeLatest } from "@redux-saga/core/effects";

// 동기 액션 타입 선언
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// 동기 액션 생성 함수
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

// 비동기 액션 타입 선언
const INCREASE_ASYNC = 'counter/INCREASE_ASYNC';
const DECREASE_ASYNC = 'counter/DECREASE_ASYNC';
// 비동기 액션 생성 함수
// 마우스 클릭 이벤트가 payload 안에 들어가지 않도록
//() => undefined 를 두 번째 파라미터로 넣어준다.
export const increaseAsync = createAction(INCREASE_ASYNC, () => undefined);
export const defreascAsync = createAction(DECREASE_ASYNC, () => undefined);

// 제너레이터 함수 정의. 이 제너레이터 함수를 사가(saga)라고 부른다.
function* increaseSaga() {
    yield delay(1000); // 1초를 기다린다.
    yield put(increase()); // 특정 액션 디스패치
}

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

export function* counterSaga() {
    // takeEvery는 들어오는 모든 액션에 대해 특정 작업을 처리
		// 모든 INCREASE_ASYNC 액션에 대해 increaseSaga 함수 실행
    yield takeEvery(INCREASE_ASYNC, increaseSaga);
    // takeLatest는 기존에 진행 중이던 작업이 있다면 취소하고
    // 가장 마지막으로 실행된 작업만 수행
		// DECREASE_ASYNC액션에 대해서 기존에 진행 중이던 작업이 있다면 
		// 취소 처리하고 가장 마지막으로 실행된 작업에 대해서만 decreaseSaga 함수 실행
    yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}

// 상태는 꼭 객체일 필요가 없다.
const initialState = 0;

const counter = handleActions(
    {
        [INCREASE]: state => state + 1,
        [DECREASE]: state => state - 1
    },
    initialState
)

export default counter;
  • delay(ms) : 작업을 지연 시키는 메서드
  • put(action) : 특정 액션을 디스패치 시키는 메서드
  • takeEvery(actionType, saga) : actionType 을 모니터링 하고 있다가, actionType 이 발생하면 saga 를 실행시키는 메서드. saga 가 아직 종료되지 않았는데 actionType 이 발생하면, 기존 saga 실행을 중지하지 않고 saga 를 또 실행 시킨다. (새로은 saga 태스크 생성)
  • takeLatest(actionType, saga) : actionType 을 모니터링 하고 있다가, actionType 이 발생하면 saga 를 실행시키는 메서드. takeEvery() 와 달리, takeLatest()saga 가 아직 종료되지 않았는데 actionType 이 발생할 경우, 이미 실행 중인 saga 태스크를 종료한 후 마지막으로 발생한 actionType 에 대해서만 saga 를 실행 시킨다. 즉 takeLatest() 는 어느 순간에서도 단 하나의 saga 태스크만 실행되게 한다. (액션이 중첩되어 디스패치 됐을 경우 가장 마지막 액션만 처리)

루트 사가 등록

  • 루트 리듀서를 만드는 것처럼 루트 사가를 만들어야 한다. 추후 다른 리듀서에서도 사가를 만들어 등록할 것이기 때문이다. all() 함수를 이용해 여러 사가를 합칠 수 있다.
  • all(arr) : all() 함수를 사용해서 제너레이터 함수를 배열의 형태로 인자로 넣어주면, 제너레이터 함수들이 병행적으로 동시에 실행되고, 전부 resolve 될때까지 기다린다. Promise.all 과 비슷하다.
    • 예 : yield all([testSaga1(), testSaga2()])
      • testSaga1()testSaga2()가 동시에 실행되고, 모두 resolve될 때까지 기다린다.
// moudule/index.js
import { combineReducers } from "redux";
import { all } from "@redux-saga/core/effects"; // import all method
import counter, { counterSaga } from "./counter"; // import countSaga
import sample from "./sample";
import loading from './loading';

const rootReducer = combineReducers({counter, sample, loading});

// 루트 사가 생성
export function* rootSaga() {
    // all 함수는 여러 사가를 합쳐주는 역할을 한다.
    yield all([counterSaga()]);
}

export default rootReducer;

스토어에 redux-saga 적용

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer, { rootSaga } from './modules'; // import root saga
import { createLogger } from 'redux-logger';
import ReduxThunk from 'redux-thunk';
// import createSagaMiddleware
import createSagaMiddleware from '@redux-saga/core';
import { composeWithDevTools } from 'redux-devtools-extension';

const logger = createLogger();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer, 
  composeWithDevTools(applyMiddleware(logger, ReduxThunk, sagaMiddleware))
);
sagaMiddleware.run(rootSaga);

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

redux-saga 함수(Saga-Effect)

  • delay(ms) : ms 이후에 resolve 하는 Promise 객체를 리턴한다.
  • put(actionType) : actionTypedispatch 하도록 한다.
  • takeEvery() : 위 설명 참고
  • takeLatest() : 위설명 참고
  • all(arr) : 위 설명 참고
  • call(function, arg) : function 에 매개변수로 arg 를 넣어 실행 시키는 함수. API를 호출해야 하는 상황에서는 사가 내부에서 직접 호출 하지 않고 call() 함수를 사용한다.
  • select(selector) : 사가 내부에서 현재 상태를 참조할 때 사용하는 메서드. selector(state[, ...args]) => args 형태의 함수로, 현재 스토어의 상태를 매개변수로 갖는다.
    • 예 : const number = yield select(state ⇒ state.counter);
  • throttle(ms, actionType, saga) : saga 실행 주기를 제한하는 함수로, actionType 이 발생할 경우 saga 를 실행 시키되, ms 에 한 번만 실행 시킨다.
    • 예 : throttle(3000, INCREASE_ASYNC, increaseSaga) : INCREASE_ASYNC 이 발생할 경우 3초에 increaseSaga 를 단 한 번만 호출한다.
profile
애기 프론트 엔드 개발자

0개의 댓글