[React] 19. Redux-Saga( TOOLKIT적용 )

dev·2020년 10월 28일
0

React

목록 보기
19/21
post-custom-banner

오늘은 Toolkit에 대해 알아보겠습니다.

React 개발을 할때 Redux를 거의 필수로 사용을 하고 있습니다.
Redux를 사용하면 코드가 늘어나는 거는 어쩔 수 없는 일이죠.
액션 함수, 액션 생성 함수, 리듀서 switch문으로 각 해당 액션함수마다 실행되는 코드들이 나누어져 있습니다.
화면이 복잡해 질수록 더 코드량이 더 늘어납니다.
Redux Toolkit은 이런 Redux를 사용할 때 추가해야하는 코드들을 상당히 줄여줍니다.
하지만 코드를 너무 줄이면 이해하기가 어려워지는 경우도 있겠지만, Redux를 사용한 분들을 쉽게 적용할 수 있을겁니다.

해당 글을 전에 예제를 사용하여 Redux Toolkit을 적용시켜 보겠습니다.

참고 내용은 [React] 18. Redux-Saga(api적용)

를 보셔도 되고, Redux-Saga내용을 알고 계시다면 현재 예제만 봐도 상관없습니다.
단 내용만 보는게 아닌 예제를 따라하실 분은 참고 내용을 보고난 후 보시는게 좋으실겁니다.
코드는 아래에 작성해놓겠습니다.
일단 Toolkit을 사용하려면 설치부터 해야합니다.

Toolkit 설치

해당 명령어를 사용하여 적용할 리액트 프로젝트에 설치를 해줍니다.

npm install @reduxjs/toolkit
또는
yarn add @reduxjs/toolkit

설치가 완료가 되었으면

리액트 프로젝트에 적용시켜보겠습니다.

index.js

일단 index.js 코드를 보겠습니다.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, {rootSaga} from './modules/rootReducer';
import myLogger from './middlewares/myLogger'; // 직접 만든 미들웨어로거 적용시키기
import logger from 'redux-logger';            // redux-logger추가하기
import { composeWithDevTools } from 'redux-devtools-extension'; //Redux devtools 추가
import ReduxThunk from 'redux-thunk'; // redux-thunk 추가
import createSagaMiddleware from 'redux-saga';// redux-saga 추가
import { configureStore } from '@reduxjs/toolkit'; // toolkit 추가
const sagaMiddleware = createSagaMiddleware(); // 사가 미들웨어를 만듭니
 const store = configureStore({reducer : rootReducer, middleware : [ReduxThunk, sagaMiddleware, logger]}); //configureStore 적용
sagaMiddleware.run(rootSaga); // 루트 사가를 실행해줍니다.
// 주의: 스토어 생성이 된 다음에 위 코드를 실행해야합니다.
ReactDOM.render(
  <React.StrictMode>
     <Provider store={store}>
        <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);
serviceWorker.unregister();

우리가 store를 적용 시켜 줄때 redux의 내장함수인 createSotre를 사용하여 reducer(redux)와 middleware(thunk,saga등)를 연결하여 store를 만들어줬습니다.

Redux Toolkit을 적용 시킬 때는 toolkit의 configureStore를 사용하여 store를 만들어줘야합니다.
일단
index.js에 import를 시켜줍니다.

import { configureStore } from '@reduxjs/toolkit'; // toolkit 추가

import 했으면 store를 만들어 줍니다.

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

보시면

const store = configureStore({reducer: 리듀서, middleware : 미들웨어});

reducer부분에는 reducer를, 미들웨어 적용시킬거면 middleware를 작성해주시면 됩니다.

그럼 이제
toolkit을 사용한 modules/counter.js를 보도록 하겠습니다.

modules/counter.js

이 예제에서는 counter.js파일의 전체 코드가 아닌 변경된 부분만을 보도록 하겠습니다. createAction, createReducer를 사용하도록하겠습니다.

사용하려는 createAction과 createReducer를 import를 시켜줘야합니다.

import { createAction, createReducer } from "@reduxjs/toolkit"; // 추가

createAction

createAction은 우리가 리덕스에서 액션 함수를 만들고 액션 생성 함수를 직접 만들었습니다.

//액션함수
const INCREASE_ASYNC = 'INCREASE_ASYNC';
const DECREASE_ASYNC = 'DECREASE_ASYNC';
//액션생성함수
export const increaseAsync = (ten) => ({ type :  INCREASE_ASYNC, number : ten});
export const decreaseAsync = (one) => ({type : DECREASE_ASYNC, number : one});

액션이 추가될 때마다 액션과 액션크리에이터 함수들이 계속 늘어난다고 생각해본다면 상당한 코드의 증가라고 할 수 있다.

이렇게 액션을 정의하는 부분을 createAction을 사용하면

export const increaseAsync = createAction("INCREASE_ASYNC", param => { return {"payload":param}});
//또는
const DECREASE_ASYNC = 'DECREASE_ASYNC';
export const decreaseAsync = createAction(DECREASE_ASYNC, param => { return {"payload": param}});

이렇게 createAction으로 액션을 전달하기만 하면 액션과 액션 크리에이터 함수가 만들어진다.
만약에 인자값을 전달해야 한다면 위 방법 또는 그냥 increaseAsync에 값을 넣어서 전달하면 알아서 payload에 값을 담아서 전달한다.

export const increaseAsync = createAction("INCREASE_ASYNC");
increaseAsync(3);
//// {type: 'DECREASE_ASYNC', payload: 3}

기본적으로 createAction은 타입이 전달되고, 인자값을 전달하면 payload에 담겨서 전달됩니다.

이렇게 createAction함수를 만들어 액션을 정의를 하였습니다.

CreateReducer

다음으로 createReducer를 보겠습니다.

우리가 일반적으로 Reducer를 만들어서 사용할 때 switch문을 통해 액션함수에 맞는 로직을 실행시키게 했습니다.
또, default 를 항상 명시해주어야 해서 굳이 필요하지 않았던 코드가 생겼습니다. 이러한 것들을 createReducer 를 사용하면 해결할 수 있습니다.

일반적인 Reducer

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

이렇게 action의 type으로 switch문을 작성하여 해당 case에 맞게 실행해주는 형식입니다.
다음은 createReducer입니다.

toolkit createReducer

export default createReducer(initialState, {
   [increase] : (state, action) => {   
    return {...state, number: state.number + action.payload.number};
 }, // [increase] createAction 함수 명으로 사용
 [DECREASE] : (state, action) => {
     return {...state, number : state.number - action.payload.number};
 } // [DECREASE] 액션함수 사용
});

createReducer는 인자로 처음 state, 리듀서 정의를 나타냅니다.

createReducer는(state, {리듀서 정의})

리듀서 정의 하는 곳을 보면
[increase], [DECREASE]가 있는데 첫번째 [increase]는 createAction 함수명으로 캐치를 해본거고, [DECREASE]는 액션함수명으로 캐치를 해본겁니다.
둘다 사용이 가능합니다.
[increase]의 인자값을 보면 (state, action) state는 기존 state값을 나타내고, action은 createAction에서 만든 payload값을 보냅니다.
state를 변경할때 payload에서 해당 값을 꺼내서 변경합니다.

이렇게 toolkit을 적용하고 나서 실행을 했을때 기존과 동일하게 작동하는 것을 보실 수 있습니다.

api를 전송하고 받아서 state값에 저장하는 부분은 복습 차원으로 toolkit을 적용시켜 만들어보도록 합시다.

코드

counter.js

import { call, delay, put, takeEvery, takeLatest } from 'redux-saga/effects';
import * as API from '../api/posts';
import { createAction, createReducer } from "@reduxjs/toolkit";

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

const GET_NUMBER = 'GET_NUMBER'; // 요청 시작
const GET_NUMBER_S = 'GET_NUMBER_S' // 성공
const GET_NUMBER_F = 'GET_NUMBER_F' // 실패

export const increase = createAction(INCREASE,param => {return {"payload":param}});
export const increaseAsync = createAction(INCREASE_ASYNC, param => { return {"payload":param}});
export const decrease = createAction(DECREASE, param => { return {"payload": param}});
export const decreaseAsync = createAction(DECREASE_ASYNC, param => { return {"payload": param}});
export const getNumber = createAction(GET_NUMBER);
export const getNumberS = createAction(GET_NUMBER_S);
export const getNumberF = createAction(GET_NUMBER_F);

// 제너레이터 함수

export function counterSaga() {
yield takeEvery(INCREASE_ASYNC, increaseSaga); // 모든 INCREASE_ASYNC 액션을 처리
yield takeEvery(GET_NUMBER, numberSaga);
}
export function
counterSaga2() {

yield takeLatest(DECREASE_ASYNC, decreaseSaga); //가장 마지막으로 디스패치된 DECREASE_ASYNC 액션만
}

function* numberSaga(){
try{
const number = yield call(API.getNumber);
// call 을 사용하면 특정 함수를 호출하고, 결과물이 반환 될 때까지 기다려줄 수 있습니다.
yield put({type: GET_NUMBER_S, error:false, number : number[0]});
}
catch(e){
yield put({type:GET_NUMBER_F, error:true, payload: 0});
}
}

function* increaseSaga(param) {
yield delay(1000); // 1초를 기다립니다.

if(param.type === INCREASE_ASYNC){
yield put(increase(param.payload));
}
else{
yield put(decrease(param.payload));
}
}

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

const initialState = {number:40
};

// toolkit createReducer
export default createReducer(initialState, {
[increase] : (state, action) => {
return {...state, number: state.number + action.payload.number};
},
[decrease] : (state, action) => {
return {...state, number : state.number - action.payload.number};
},
[GET_NUMBER] : (state, action) => {
return state;
},
[GET_NUMBER_S] : (state, action) => {
return {...state, number : action.number, error : action.error};
},
[GET_NUMBER_F] : (state, action) => {
return {...state, number : action.number, error : action.error};
}
});

rootReducer

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

const rootReducer = combineReducers({ counter });

export function* rootSaga() {
yield all([counterSaga(),counterSaga2()]); // all은 배열안의 여러 사가를 동시에 실행시킨다.
}

export default rootReducer;

CounterContainer

import React, {useEffect} from 'react';
import Counter from '../components/Counter';
import { useSelector, useDispatch } from 'react-redux';
import { increase, decrease, increaseAsync, decreaseAsync, getNumber } from '../modules/counter';

function CounterContainer() {
const state = useSelector(state => state.counter);

const dispatch = useDispatch();

const onIncrease = () => {
dispatch(increaseAsync({"number":10}));
};
const onDecrease = () => {
dispatch(decreaseAsync({"number": 1}));
};

useEffect(() => {
dispatch(getNumber());
}, [dispatch]);

return (

);
}

export default CounterContainer;

Counter

import React from 'react';

function Counter({ number, onIncrease, onDecrease }) {

return (

<div>
  <h1>{number}</h1>
  
  <button onClick={onIncrease}>+10</button>
  <button onClick={onDecrease}>-1</button>
</div>

);
}

export default Counter;

posts

import axios from 'axios';

//http://127.0.0.1:5500/src/data.json
// 포스트 목록을 가져오는 비동기 함수
export const getNumber = async () => {
//await sleep(500); // 0.5초 쉬고
//return posts; // posts 배열
const response = await axios.get(http://localhost:4000/number);
return response.data;

};

profile
studying
post-custom-banner

0개의 댓글