티스토리 뷰

✅ 이전 포스팅에서 Redux의 기초에 대해서 알아봤었습니다. 이번 포스팅 글에서는 Redux의 미들웨어로 많이 사용되는 Redux-Saga에 대해서 알아보겠습니다. Redux에 잘 모르는 상태에서 처음으로 이 글을 접하셨다면 아래 포스팅 글을 먼저 읽고 오시는 것을 추천드리겠습니다! + 부가적으로 generator함수와 yield에 관해서도 읽는 것을 추천드립니다!

 

React - 상태관리 Redux 기초

✅ 사내에서 redux로 상태 관리를 하여 개발한 메뉴가 있는데, 당시 개발 기간이 짧아 정확하게 이해하지 못하고 구현하느라 개념잡기가 어려웠습니다. 따라서 이번 포스팅으로 공부해보고자 글

ji-musclecode.tistory.com

 

JS - generator 함수와 yield

회사에서 redux-saga 상태 관리 라이브러리로 개발한 프로젝트가 있는데 generator 함수와 yield 키워드를 사용해서 개발을 했었습니다. 당시에는 개발 기한이 짧아 잘 이해하지 못하고 사용했었는데,

ji-musclecode.tistory.com

 

( 글을 읽기 전 redux-saga 기본 용어에 대해 읽어보시면 이해하는데 도움이 될 것 같아서 더보기란에 짧게 정리하였습니다. )

 

더보기

* delay : 설정된 시간 이후에 resolve 하는 Promise 객체를 리턴합니다. 

 

* put : 특정 Action을 Dispatch 합니다.

 

* fork : Generator 함수를 실행합니다. ( call 과는 다릅니다. )

 

* takeEvery : 들어오는 모든 Action에 대해 특정 작업을 처리합니다.

 

* takeLatest : 기존에 진행 중이던 작업이 있다면 취소 처리하고, 가장 마지막으로 실행된 작업만 수행합니다.

 

* takeLeading : 첫 번째 이벤트만 실행하고, 그 후로는 무시합니다.

 

* call : 함수의 첫 번째 파라미터는 함수, 나머지 파라미터는 해당 함수에 넣을 인수입니다. Saga 함수 안의 로직이 동기 처리되도록 도와줍니다.

 

* all : Generator 함수를 배열 형태로 넣어주면, 해당 함수들이 병행적으로 동시에 실행되고, 전부 resolve 될 때 까지 기다립니다.

 

* throttle : - 초를 주기로 request를 1번만 실행합니다.

 

* debounce : -초 이내에 request를 1버만 실행합니다. (중복 호출 함수들 중 마지막)

[ 알아두어야 할 점 ]

- call은 동기함수호출 (api가 리턴할 때까지 기다립니다.), fork은 비동기 함수 호출 (안 기다리고 다음 로직으로 이동합니다.)


📌 Redux - Saga

  • redux-saga는 redux middleware 라이브러리 중 하나로, Action과 Reducer 사이에서 흐름을 제어합니다.
  • Action을 모니터링 하다가 Action이 발생하면 Reducer가 Action을 처리하기 전에 다양한 작업을 할 수 있습니다.
     
    • 기존 요청 취소, 불필요한 중복 요청 방지가 가능합니다.
    • 비동기 작업을 처리하는데 효과적입니다. 
    • 특정 Action이 발생했을 때 이에 따라 다른 Action이 Dispatch 되게 하거나, JS 코드를 실행할 수 있습니다.

 

📌 이제 코드로 살펴보겠습니다.

// npm
npm install redux-saga
npm install axios // => api 호출을 위해

// yarn
yarn add redux-saga
yarn add axios // => api 호출을 위해

// api 호출은 https://jsonplaceholder.typicode.com/ 사이트에서 기본으로 제공해주는 api를 사용했습니다.

1.actions, reducers, sagas 폴더를 만들고 위처럼 파일들을 만들어줍니다. 앞으로 나올 예제 확인을 위해서는 API, Components 폴더들 또한 만들어 줍니다.

 

더보기를 누르시면 소스코드를 확인하실 수 있습니다. (play1.js의 코드는 3.그럼 이제 사용해봅시다 쪽에 있습니다.)

 

더보기
// playAction.js

export const PostsRequest = () => {
    return {
        type: "getPosts",
    }
}

// getPosts 라는 action을 반환합니다.
// API.js

import axios from "axios";

const API_END_POINT = 'https://jsonplaceholder.typicode.com';

// GET
export const callSelectAPI = (type) => {
    let url = '';
    
    if (type === 'getPosts') {
        url = API_END_POINT + '/posts';
    }

    return axios.get(url).then((res) => {
        return res
    }).catch(e => ({ e }));
}

// axios를 이용한 API 호출 예제입니다. (method : GET)
// reducers - index.js

export const initalSatte = {
    keyData: []
};

const reducer = (state = initalSatte, action) => {
    switch (action.type) {
        case "AXIOS_GET":
            return {
                ...state,
                keyData: action.payload.response
            }
        default:
            return state;
    }
};

// reducer 만들기
export default reducer;
// PlaySaga.js

import { callSelectAPI } from "../API/API"
import { takeLatest, call, put } from 'redux-saga/effects'
import * as PlaySaga from './PlaySaga'

function groupBy(arr, key) {
    return arr.reduce((result, cur) => {
        (result[cur[key]] = result[cur[key]] || []).push(cur);
        return result
    }, {});
}

export function* getPostData() {

    try {
        let response = yield call(callSelectAPI, 'getPosts');

        let keyData = [];
        if (200 == response.status && response.data.length > 0) {
            keyData = Object.keys(groupBy(response.data, 'userId'));
        }

        yield put(Object.assign({}, {
            type: "AXIOS_GET",
            payload: {
                response: keyData
            },
        }));

    } catch {

    } finally {

    }

}

export default [
    takeLatest('getPosts', PlaySaga.getPostData),
];

/*
[ yield call ]
함수의 첫 번째 파라미터는 함수, 나머지 파라미터는 해당 함수에 넣을 인수입니다.
주어진 함수를 실행하게 되는 것입니다.

[ yield put ]
특정 액션을 dispatch하도록 합니다.

[ takeLatest ]
기존에 진행 중이던 작업이 있다면 취소 처리하고 가장 마지막으로 실행된 작업만 수행하도록 처리해줍니다.
즉 'getPosts' 액션에 대해서 기존에 진행 중이던 작업이 있다면 취소 처리하고, 가장 마지막으로 실행된 작업에 대해서만 Saga함수를 실행한다
*/
// rootSaga.js

import { all } from 'redux-saga/effects';
import PlaySaga from './PlaySaga';

export default function* rootSaga() {
    yield all([...PlaySaga]);
}

// all은 배열 안에 있는 것들을 한번에 실행해줍니다.

// all함수를 사용해서 제너레이터 함수를 배열의 형태로 인자로 넣어주면,
// 제너레이터 함수들이 병행적으로 동시에 실행되고,
// 전부 resolve될때까지 기다립니다. Promise.all과 비슷한 개념입니다.

 

2. 위에서 만든 Reudx-Saga를 적용시킵니다.

// 최상단의 index.js 

// 1. react와 redux 사용을 위한 세팅
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import './index.css';
import App from './App';
import reducer from './reducers'

// 2. redux - saga를 위한 세팅
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas/rootSaga';

// 3. sagaMiddleware 선언(미들웨어로 사용)
const sagaMiddleware = createSagaMiddleware();

// 4. redux의 store 생성, 리듀서와 미들웨어 사용
const store = createStore(reducer, applyMiddleware(sagaMiddleware));

// 5. 항상 store 보다 아래에서 코드가 작성되어야 한다.rootSaga를 인자로 둔다.
sagaMiddleware.run(rootSaga);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
기존 Redux의 store를 선언할 때와 비교해보면 createSagaMiddleware로 미들웨어를 만들어 applyMiddleware로 적용시키고 rootSaga를 넣어서 해당 Saga를 적용할 것이라고 알려주는 것이 추가됩니다.

 

3. 그럼 이제 redux-saga 를 사용해봅시다!

/* App.js 하위 컴포넌트를 정의하고 렌더링해봅시다.
App.js ex))

    const App = () => {
        return (
            <div class="App">
                <body className="App-body">
                    <Play1 />
                </body>
            </div>
        );
    };
    
*/

// App.js 하위 컴포넌트
import React, { useState, useEffect } from "react";
import { PostsRequest } from "../actions/playAction";
import { connect } from "react-redux";

const Play1 = (props) => {

    useEffect(() => {

        props.PostsRequest();

    }, []);

    return (
        <React.Fragment>
            <section>
                {
                    props.keyData.map((dr) => {
                        return <p>{dr}</p>
                    })
                }
            </section>
        </React.Fragment>
    )
}

let mapStateToProps = (state) => {
    return {
        keyData: state.keyDataa
    }
}

let mapDispatchToProps = (dispatch) => {
    return {
        PostsRequest: () => dispatch(PostsRequest()),
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Play1);
기존 Redux를 적용시켰을 때와 비교해보면 별 다른 차이점이 없습니다. 하지만 내부 데이터 흐름이 다릅니다. 결과 확인 후 자세히 보겠습니다. 

 

4. 결과 확인!

 

5. 마지막으로 데이터 흐름을 확인해보겠습니다.

useEffect 쪽 디버거
playAction 쪽 디버거
PlaySaga 쪽 디버거
API 쪽 디버거
PlaySaga쪽 디버거
reducers - index.js 쪽 디버거
콘솔 결과

  • 디버거와 콘솔을 찍으면서 확인해봅시다.

    • mapDispatchToProps에서 정의했던 PostsRequest 함수를 호출하면
    • PostsRequest 액션 함수가 호출됩니다. PostsRequest함수는 "getPosts"이라는 tpye을 리턴하여 액션이 발생하고
    • redux-saga에서는 이를 감지하고 액션에 해당하는 동작 getPostData 함수를 호출합니다.
    • getPostData 함수는 yield call을 만나 함수의 리턴이 있을 때까지 기다립니다.
    • 즉 callSelectAPI 함수를 호출하고  callSelectAPI 함수는 axios.get()을 통해 받아온 데이터를 리턴하면 다음 동작을 수행합니다.
    • yield put을 만나 새로운 액션에 대해 Dispatch 합니다.
    • reducer에 액션에 대한 state 변경이 있으므로 이를 수행합니다.
    • 변경된 state는 mapStateToProps를 통해서 컴포넌트에 전달되고 우리는 최종적으로 4번의 결과 화면이 렌더링 된 것을 확인할 수 있습니다.

 


❓ saga 미들웨어가 존재하기 때문에 reducer가 아니라 먼저 rootSaga를 검사하여 type이 일치하는 함수를 호출하는 것으로 알고 있는데 콘솔 결과창 중간 2번 다음에 왜 6번이 왜 찍혔나요? (즉 rootSaga 보다 reducer를 왜 먼저 검사했느냐)

=> saga 미들웨어가 존재한다고해서 rootSaga부터 검사하는 것이 아닌 항상 reducer를 처음으로 검사하고 그다음으로 rootSaga를 검사합니다. 따라서 로직이 꼬이지 않도록 타입을 잘 정리해 둘 필요가 있습니다.

 

❓ 근데 제너레이터 함수는 yield를 만나면 멈추고 .next() 함수로 재개하는 거 아녔나요?
=> 맞습니다. 하지만 action을 통해 제너레이터 함수를 호출하면 함수 내 yield 부분들을 자동으로 차례대로 호출해줍니다.

 

 

 

🔗 참고한 글

 

redux-saga 사용법, react-redux 사용법, redux, react, react16, state management, flux, store, reducer, dispatch, action

redux-saga 사용법, react-redux 사용법, redux, react, react16, state management, flux, store, reducer, dispatch, action

kyounghwan01.github.io

댓글