본문 바로가기

react

[리액트를 다루는 기술] 18장 리덕스 미들웨어를 통한 비동기 작업 관리(2)

18.3.2 redux-saga

 

- 기존 요청을 취소 처리해야 할 때 (불필요한 중복 요청 방지)

- 특정 액션이 발생했을 때 다른 액션을 발생시키거나, API 요청 등 리덕스와 관계없는 코드를 실행할 때

- 웹소켓을 사용할 떄 

- API 요청 실패 시 재요청해야 할 때 

 

 

18.3.2.1 제너레이터 함수 이해하기 

 

제너레이터(generator) 함수 

 

- 함수에서 값을 순차적으로 반환가능

- function* 키워드 사용 

- 제너리에터 함수를 호출했을 때 반환되는 객체 = 제너레이터 

- 제너레이터가 처음 만들어지면 함수의 흐름은 멈춰 있는 상태 

- next()가 호출되면 다음 yield가 있는 곳까지 호출하고 다시 함수가 멈춤 

 

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

const generator = generatorFunction();

generator.next();
generator.next();
generator.next();
generator.next();

 

 

 

- redux-saga는 제너레이터 함수 문법을 기반으로 비동기 작업을 관리 

- redux-saga는 우리가 디스패치하는 액션을 모니터링해서 그에 따라 필요한 작업을 따로 수행할 수 있는 미들웨어 

 

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();
watch.next();
watch.next();

 

 

18.3.2.2 비동기 카운터 만들기 

 

- 라이브러리 설치 

npm install redux-saga

 

module/counter

import {createAction, handleActions} from 'redux-actions';
import {delay, put, takeEvery, takeLatest} from 'redux-saga/effiects';


const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
const INCREASE_ASYNC = 'counter/INCREASE_ASYNC';
const DECREASE_ASYNC = 'counter/DECREASE_ASYNC';


//액션 생성
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
export const increaseAsync = createAction(INCREASE_ASYNC, ()=>undefined);
export const decreaseAsync = createAction(DECREASE_ASYNC, ()=>undefined);

//사가 생성
function* increaseSaga() {
    yield delay(1000); //1초 기다림
    yield put(increase()); //특정 액션을 디스패치
}

function* decreaseSaga() {
    yield delay(1000); //1초 기다림
    yield put(decrease()); //특정 액션을 디스패치
}

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

const initialState = 0;

//리듀서 함수 
const counter = handleActions(
    {
        [INCREASE]: state => state +1,
        [DECREASE]: state => state -1
    },
    initialState
);

export default counter;

 

 

moduels/index.js

- 루트 사가 만들기 

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

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

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

export default rootReducer;

 

 

index.js

- 스토어에 redux-saga 미들웨어 적용 

import ReactDOM from 'react-dom/client';
import App from './App';
import { applyMiddleware, createStore } from 'redux';
import rootReducer, { rootSaga } from './modules';
import { Provider } from 'react-redux';
import  {createLogger} from 'redux-logger';
import {thunk} from 'redux-thunk';
import createSagaMiddleware from 'redux-saga';

const logger = createLogger();
const sagaMiddleware = createSagaMiddleware();

const store = createStore(rootReducer, applyMiddleware(logger, thunk, sagaMiddleware)); //스토어 생성

sagaMiddleware.run(rootSaga);

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

);

 

 

- 리덕스 개발자 도구 라이브러리 설치

npm install redux-devtools-extension

 

 

 

18.3.2.3  API 요청 상태 관리하기 

 

 

lib/createRequestSaga.js

- API를 호출해야 하는 상황에는 사가 내부에서 직접 호출하지 않고 call 함수를 사용

- call 함수의 경우, 첫 번째 인수는 호출하고 싶은 함수이고, 그 뒤에 오는 인수들은 해당 함수에 넣어 주고 싶은 함수 

import {call,put} from 'redux-saga/effects';
import { startLoading, finishLoading } from '../modules/loading';

export default function createRequestSaga(type,request) {
    const SUCCESS = `${type}_SUCCESS`;
    const FAILURE = `${type}_FAILURE`;

    return function*(action) {
        yield put(startLoading(type)); //로딩 시작
        try {
            const response = yield call(request, action.payload);
            yield put({
                type:SUCCESS,
                payload:response.data
            });
        } catch (e) {
            yield put({
                type:FAILURE,
                payload:e,
                error:true
            });
        }
        yield put(finishLoading(type)); //로딩 끝 
    }
}

 

 

modules/index.js

- sampleSaga를 루트 사가에 등록

import { combineReducers } from "redux";
import {all} from "redux-saga/effects";
import counter,{ counterSaga}  from "./counter";
import sample, {sampleSaga} from "./sample";
import loading from "./loading";

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

//루트 사가 
export function* rootSaga() {
    //all 함수는 여러 사가를 합쳐 주는 역할
    yield all([counterSaga(), sampleSaga()]);
}

export default rootReducer;

 

 

modules/sample.js

import { createAction, handleActions } from "redux-actions";
import { takeLatest } from "redux-saga/effects";
import * as api from '../lib/api';
import createRequestSaga from "../lib/createRequestSaga";

//액션 타입을 선언 

const GET_POST = 'sample/GET_POST'; //시작
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS'; //성공

const GET_USERS = 'sample/GET_USERS'; //시작
const GET_USERS_SUCCESS = 'sample/GET_USERS_SUCCESS'; //성공

export const getPost = createAction(GET_POST, id=>id);
export const getUsers = createAction(GET_USERS);

export const getPostSaga = createRequestSaga(GET_POST, api.getPost);
export const getUsersSaga = createRequestSaga(GET_USERS, api.getUsers);

export function* sampleSaga() {
    yield takeLatest(GET_POST, getPostSaga);
    yield takeLatest(GET_USERS, getUsersSaga);
}


//초기 상태 선언
//요청이 로딩 중 상태는 loading이라는 객체에서 관리 

const initialState = {
    post:null,
    users:null
};

const sample = handleActions(
    {
        [GET_POST_SUCCESS]: (state, action) => ({
            ...state,
            post:action.payload
        }),
        [GET_USERS_SUCCESS]: (state,action) => ({
            ...state,
            users:action.payload
        })
    },
    initialState
);

export default sample;