본문 바로가기

React 실습/관련 개념 정리

리덕스사가 - Generator, redux-saga, combineReducers

728x90

리덕스사가

 

redux-saga의 경우엔, 액션을 모니터링하고 있다가, 특정 액션이 발생하면 이에 따라 특정 작업을 하는 방식으로 사용합니다. 여기서 특정 작업이란, 특장 자바스크립트를 실행하는 것 일수도 있고, 다른 액션을 디스패치 하는 것 일수도 있고, 현재 상태를 불러오는 것 일수도 있습니다.

 

redux-saga는 redux-thunk로 못하는 다양한 작업들을 처리 할 수 있습니다. 예를 들자면..

  1. 비동기 작업을 할 때 기존 요청을 취소 처리 할 수 있습니다
  2. 특정 액션이 발생했을 때 이에 따라 다른 액션이 디스패치되게끔 하거나, 자바스크립트 코드를 실행 할 수 있습니다.
  3. 웹소켓을 사용하는 경우 Channel 이라는 기능을 사용하여 더욱 효율적으로 코드를 관리 할 수 있습니다 
  4. API 요청이 실패했을 때 재요청하는 작업을 할 수 있습니다

이 외에도 다양한 까다로운 비동기 작업들을 redux-saga를 사용하여 처리 할 수 있답니다.

 

redux-saga는 다양한 상황에 쓸 수 있는 만큼, 제공되는 기능도 많고, 사용방법도 진입장벽이 꽤나 큽니다. 자바스크립트 초심자라면 생소할만한 Generator 문법을 사용하는데요, 이 문법을 이해하지 못하면 redux-saga를 배우는 것이 매우 어려우니, 이 문법부터 작동방식을 이해해보도록 하겠습니다.

 

Generator - JavaScript | MDN

Generator 객체는 generator function 으로부터 반환되며, 반복 가능한 프로토콜과 반복자 프로토콜을 모두 준수합니다.

developer.mozilla.org

 

Generator 문법 배우기

이 문법의 핵심 기능은 함수를 작성 할 떄 함수를 특정 구간에 멈춰놓을 수도 있고, 원할 때 다시 돌아가게 할 수도 있습니다. 그리고 결과값을 여러번 반환 할 수도 있습니다.

 

function weirdFunction() {
  return 1;
  return 2;
  return 3;
  return 4;
  return 5;
}

제너레이터 함수를 사용하면

제너레이터 함수를 만들 때에는 function* 이라는 키워드를 사용합니다.

function* generatorFunction() {
    console.log('안녕하세요?');
    yield 1;
    console.log('제너레이터 함수');
    yield 2;
    console.log('function*');
    yield 3;
    return 4;
}
const generator = generatorFunction();

제너레이터 함수를 호출한다고 해서 해당 함수 안의 코드가 바로 시작되지는 않습니다. generator.next() 를 호출해야만 코드가 실행되며, yield를 한 값을 반환하고 코드의 흐름을 멈춥니다.

코드의 흐름이 멈추고 나서 generator.next() 를 다시 호출하면 흐름이 이어서 다시 시작됩니다.


redux-saga에서는 이러한 원리로 액션을 모니터링하고, 특정 액션이 발생했을때 우리가 원하는 자바스크립트 코드를 실행시켜준답니다.

리덕스 사가 설치 및 비동기 카운터 만들기

우리가 기존에 thunk를 사용해서 구현했던 비동기 카운터 기능을 이번에는 redux-saga를 사용하여 구현해봅시다. 우선 redux-saga 라이브러리를 설치해주세요.

$ yarn add redux-saga

그 다음에는, counter.js 리덕스 모듈을 열어서 기존 increaseAsync  decreaseAsync thunk 함수를 지우고, 일반 액션 생성 함수로 대체시키세요.

modules/counter.js

// 액션 타입
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const INCREASE_ASYNC = 'INCREASE_ASYNC';
const DECREASE_ASYNC = 'DECREASE_ASYNC';

// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseAsync = () => ({ type: INCREASE_ASYNC });
export const decreaseAsync = () => ({ type: DECREASE_ASYNC });

// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
const initialState = 0;

export default function counter(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return state + 1;
    case DECREASE:
      return state - 1;
    default:
      return state;
  }
}

그 다음엔, increaseSaga와 decreaseSaga 를 다음과 같이 만들어주세요. redux-saga 에서는 제너레이터 함수를 "사가" 라고 부릅니다.

modules/counter.js

import { delay, put } from 'redux-saga/effects';

// 액션 타입
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const INCREASE_ASYNC = 'INCREASE_ASYNC';
const DECREASE_ASYNC = 'DECREASE_ASYNC';

// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseAsync = () => ({ type: INCREASE_ASYNC });
export const decreaseAsync = () => ({ type: DECREASE_ASYNC });

function* increaseSaga() {
  yield delay(1000); // 1초를 기다립니다.
  yield put(increase()); // put은 특정 액션을 디스패치 해줍니다.
}
function* decreaseSaga() {
  yield delay(1000); // 1초를 기다립니다.
  yield put(decrease()); // put은 특정 액션을 디스패치 해줍니다.
}

// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
const initialState = 0;

export default function counter(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return state + 1;
    case DECREASE:
      return state - 1;
    default:
      return state;
  }
}

'redux-saga/effects' 에는 다양한 유틸함수들이 들어있습니다. 여기서 사용한 put 이란 함수가 매우 중요한데요, 이 함수를 통하여 새로운 액션을 디스패치 할 수 있습니다.

그 다음엔, takeEvery, takeLatest 라는 유틸함수들을 사용해보겠습니다. 이 함수들은 액션을 모니터링하는 함수인데요, takeEvery는 특정 액션 타입에 대하여 디스패치되는 모든 액션들을 처리하는 것 이고, takeLatest는 특정 액션 타입에 대하여 디스패치된 가장 마지막 액션만을 처리하는 함수입니다. 예를 들어서 특정 액션을 처리하고 있는 동안 동일한 타입의 새로운 액션이 디스패치되면 기존에 하던 작업을 무시 처리하고 새로운 작업을 시작합니다.

counterSaga라는 함수를 만들어서 두 액션을 모니터링 해보세요.

modules/counter.js

import { delay, put, takeEvery, takeLatest } from 'redux-saga/effects';

// 액션 타입
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const INCREASE_ASYNC = 'INCREASE_ASYNC';
const DECREASE_ASYNC = 'DECREASE_ASYNC';

// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseAsync = () => ({ type: INCREASE_ASYNC });
export const decreaseAsync = () => ({ type: DECREASE_ASYNC });

function* increaseSaga() {
  yield delay(1000); // 1초를 기다립니다.
  yield put(increase()); // put은 특정 액션을 디스패치 해줍니다.
}
function* decreaseSaga() {
  yield delay(1000); // 1초를 기다립니다.
  yield put(decrease()); // put은 특정 액션을 디스패치 해줍니다.
}

export function* counterSaga() {
  yield takeEvery(INCREASE_ASYNC, increaseSaga); // 모든 INCREASE_ASYNC 액션을 처리
  yield takeLatest(DECREASE_ASYNC, decreaseSaga); // 가장 마지막으로 디스패치된 DECREASE_ASYNC 액션만을 처리
}

// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
const initialState = 0;

export default function counter(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return state + 1;
    case DECREASE:
      return state - 1;
    default:
      return state;
  }
}

counterSaga 함수의 경우 다른 곳에서 불러와서 사용해야 하기 때문에 export 키워드를 사용해주세요.

이제, 루트 사가를 만들어주겠습니다. 프로젝트에서 여러개의 사가를 만들게 될텐데, 이를 모두 합쳐서 루트 사가를 만듭니다.

modules/index.js

import { combineReducers } from 'redux';
import counter, { counterSaga } from './counter';
import posts from './posts';
import { all } from 'redux-saga/effects';

const rootReducer = combineReducers({ counter, posts });
export function* rootSaga() {
  yield all([counterSaga()]); // all 은 배열 안의 여러 사가를 동시에 실행시켜줍니다.
}

export default rootReducer;

이제 리덕스 스토어에 redux-saga 미들웨어를 적용해봅시다. 


그러기 전에 combineReducers? 가 머지라는 생각이든다. 알아보고 가기로 하자

 

combineReducers

핵심 개념
리덕스 앱에서 상태의 가장 일반적인 형태는 각 최상위 키마다 도메인별 데이터조각을 포함한 일반적인 자바스크립트 객체입니다. 이처럼, 일반적으로 상태의 형태에 대한 리듀서 로직을 작성하는 방법은 "슬라이스 리듀서"기능을 가지고, 각각은 동일한 (state, action)의 형태를 가지며, 특정 상태의 부분을 업데이트 하는 겁니다. 여러 슬라이스 리듀서가 하나의 액션에 응답할 수 있고 독립적으로 필요에 따라 자체적으로 업데이트할 수도 있습니다. 또한 업데이트된 슬라이스는 새로운 객체로 합쳐집니다.
이런 패턴이 매우 흔하기 때문에 리덕스는 이를 구현한 유틸리티인 combineReducers를 제공합니다. 이는 슬라이스 리듀서들로 이루어진 객체를 취하는 고차 리듀서의 예입니다.
 
상태의 형태를 정의하기
 
초기의 형태와 스토어 상태의 형태의 개념을 정의하는 것은 두 가지 방법이 있습니다. 첫 번째는 createStore가 두번째 매개변수로 취하는 preloadedState입니다. 이는 주로 브라우저의 localStorage처럼 이전의 상태로 스토어를 초기화하기 위한 것입니다. 또 다른 벙법은 루트 리듀서의 상태 매개변수가 undefined 일때 초기 상태를 반환하는 것입니다. 이 두 가지 접근방식은 상태 초기화에서 더 자세히 설명합니다. 여기선 combineReducers를 사용할 때 주의해야할 몇가지 사항이 있습니다.
combineReducers는 슬라이스 리듀서 함수들로 이루어진 객체를 취하며, 동일한 키를 가진 상태 객체를 출력하는 함수를 만듭니다. 이는 createStore에 사전 로드된 상태가 제공되지 않는다면, 인풋 슬라이스 리듀서 객체의 키의 이름에 따라 아웃풋 상태객체의 키의 이름이 결정됨을 의미합니다. 이 이름 간의 관계는 항상 명확하진 않으며, default module exports나 객체리터럴과 같은 ES6의 기능을 사용할 때 특히 그렇습니다.
 
아래는 combineReducers에서 ES6의 객체리터럴로 상태의 형태를 정의한 예제입니다.
 
// reducers.js

export default theDefaultReducer = (state = 0, action) => state;
export const firstNamedReducer = (state = 1, action) => state;
export const secondNamedReducer = (state = 2, action) => state;

// rootReducer.js
import {combineReducers, createStore} from "redux";
import theDefaultReducer, {firstNamedReducer, secondNamedReducer} from "./reducers";


// 객체의 형태를 정의하기 위해 ES6의 객체리터럴 단축문법을 사용

const rootReducer = combineReducers({
    theDefaultReducer,
    firstNamedReducer,
    secondNamedReducer
});

const store = createStore(rootReducer);
console.log(store.getState());

// {theDefaultReducer : 0, firstNamedReducer : 1, secondNamedReducer : 2}

 


index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import createSagaMiddleware from 'redux-saga';

const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware(); // 사가 미들웨어를 만듭니다.

const store = createStore(
  rootReducer,
  // logger 를 사용하는 경우, logger가 가장 마지막에 와야합니다.
  composeWithDevTools(
    applyMiddleware(
      ReduxThunk.withExtraArgument({ history: customHistory }),
      sagaMiddleware, // 사가 미들웨어를 적용하고
      logger
    )
  )
); // 여러개의 미들웨어를 적용 할 수 있습니다.

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

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

serviceWorker.unregister();

그 다음엔, App 컴포넌트에서 CounterContainer 를 렌더링해보세요.

App.js

import React from 'react';
import { Route } from 'react-router-dom';
import PostListPage from './pages/PostListPage';
import PostPage from './pages/PostPage';
import CounterContainer from './containers/CounterContainer';

function App() {
  return (
    <>
      <CounterContainer />
      <Route path="/" component={PostListPage} exact={true} />
      <Route path="/:id" component={PostPage} />
    </>
  );
}

export default App;

이제 브라우저를 열어서 +1 버튼과 -1 버튼을 연속해서 눌러보시고, 서로 다른 패턴으로 작동하는것을 확인해보세요.

카운터가 잘 작동하는것을 확인하셨으면, App 에서 CounterContainer 를 지워주세요.

이제 redux-saga의 첫 예시 기능 구현이 끝났습니다. 다음 섹션에서는 redux-saga에서 프로미스를 다루는 방법에 대해서 알아보도록 하겠습니다.

 

출처:

https://react.vlpt.us/redux-middleware/10-redux-saga.html

 

10. redux-saga · GitBook

10. redux-saga 소개 redux-saga는 redux-thunk 다음으로 가장 많이 사용되는 라이브러리입니다. redux-thunk의 경우엔 함수를 디스패치 할 수 있게 해주는 미들웨어였지요? redux-saga의 경우엔, 액션을 모니터

react.vlpt.us

https://lunit.gitbook.io/redux-in-korean/recipes/structuringreducers/usingcombinereducers

 

combineReducers 사용하기 - Redux

리덕스 앱에서 상태의 가장 일반적인 형태는 각 최상위 키마다 도메인별 데이터조각을 포함한 일반적인 자바스크립트 객체입니다. 이처럼, 일반적으로 상태의 형태에 대한 리듀서 로직을 작성

lunit.gitbook.io

 

728x90