단의 개발 블로그

리덕스 미들웨어 본문

Web/React

리덕스 미들웨어

danso 2024. 11. 5. 15:51

리액트 애플리케이션에서 API 서버를 연동할 때 API 요청에 대한 상태도 관리해야 한다. 요청 시작 시 로딩, 성공 실패에 따른 각각 응답 상태 관리를 해야한다는 뜻이다. 미들웨어를 사용하여 효율적이고 편하게 상태 관리를 할 수 있다. 아래 패키지를 추가한다.

yarn add redux react-redux redux-actions

 

 

리덕스 생성

import {createAction, handleActions} from "redux-actions";

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

export const increase = createAction(INCREASE)
export const decrease = createAction(DECREASE)

const initialState = 0;

const counter = handleActions(
    {
        [INCREASE]: state => state + 1,
        [DECREASE]: state => state - 1
    },
    initialState
);
export default counter;
const Counter = ({onIncrease, onDecrease, number}) => {
    return (
        <>
            <h1>{number}</h1>
            <button onClick={onIncrease}>+1</button>
            <button onClick={onDecrease}>-1</button>
        </>
    )
}

export default Counter
import Counter from "../components/Counter";
import {connect} from "react-redux";
import {decrease, increase} from "../modules/counter";

const CounterContainer = ({number, increase, decrease}) => {
    return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
    )
}

export default connect(
    state => ({
        number: state.counter
    }),
    {
        increase,
        decrease
    }
)(CounterContainer);
import {combineReducers} from "redux";
import counter from "./counter";

const rootReducer = combineReducers({
    counter
})

export default rootReducer
import './App.css';
import CounterContainer from "./containers/CounterContainer";

function App() {
  return (
    <>
      <CounterContainer/>
    </>
  );
}

export default App;
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {createStore} from "redux";
import rootReducer from "./modules";
import {Provider} from "react-redux";

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

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

미들웨어

액션을 디스패치 했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업을 실행한다. 미들웨어는 액션과 리듀서 사이의 중간에서 지정된 행동을 수행하는 중간자이다. 전달받은 액션을 콘솔에 기록하거나, 전달받은 액션 정보를 기반으로 액션을 아예 취소하거나, 다른 종류의 액션을 추가로 디스패치 할 수 있다.

보통 다른 개발자가 이미 만들어 놓은 미들웨어를 주로 사용하는데, 이해하려면 직접 만들어서 사용해 보는게 이해하는데 좋다. 상황에 따라 다른 개발자가 만든 미들웨어를 쓰더라도 커스터마이징해서 사용할 수도 있기 때문이다. 미들웨어의 기본 구조는 다음과 같다.

const loggerMiddleware = (store) => next => action => {
    
}
export default loggerMiddleware

미들웨어는 함수를 반환하는 함수다. 파라미터로 store는 리덕스 스토어 인스턴스, action은 디스패치된 액션을 가르킨다. next는 함수형태로 store.dispatch와 비슷한 역할을 한다. next(action)을 호출하면 다음 미들웨어에게 역할을 넘겨주고, 다음 미들웨어가 없다면 리듀서에게 액션을 넘겨주게 된다. 내부에서 store.dispatch를 사용하면 첫번째 미들웨어부터 다시 처리한다. next를 사용하지 않으면 액션이 리듀서에 전달되지 않고 무시된다.

const loggerMiddleware = (store) => next => action => {
    console.group(action && action.type);
    console.log('이전 상태', store.getState());
    console.log('액션', action);
    next(action);
    console.log('다음 상태', store.getState());
    console.groupEnd();

}
export default loggerMiddleware
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));

logging 하는 미들웨어는 다른 개발자가 만들어 둔 라이브러리가 있다. yarn add redux-logger를 설치하고 적용시킨다.

const store = createStore(rootReducer, applyMiddleware(logger));

 

비동기 처리 미들웨어

비동기 처리 하는 미들웨어는 다양하다. 여기서는 아래 두가지 미들웨어를 다룬다.

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

redux-thunk

리덕스를 만든 댄 아브라모프가 만들었으며 공식 메뉴얼이 존재한다. Thunk는 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미한다. yarn add redux-thunk를 설치한다. 그리고 적용 시킨다.

const store = createStore(rootReducer, applyMiddleware(logger, thunk ));

리덕스를 수정한다. 모듈과 컨테이너를 수정한다.

export const increaseAsync = () => dispatch => {
    setTimeout(() => {
        dispatch(increase());
    }, 1000);
};

export const decreaseAsync = () => dispatch => {
    setTimeout(() => {
        dispatch(decrease());
    }, 1000)
}
const CounterContainer = ({number, increaseAsync, decreseAsync}) => {
    return (
        <Counter number={number} onIncrease={increaseAsync} onDecrease={decreseAsync} />
    )
}

export default connect(
    state => ({
        number: state.counter
    }),
    {
        increaseAsync,
        decreaseAsync
    }
)(CounterContainer);

 

웹 요청 비동기 작업

가짜 api를 사용해서 비동기 요청 처리를 실습한다. yarn add axios 설치 후 해당 파일을 만든다.

import axios from "axios";

export const getPost = id => axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
export const getUsers = id => axios.get(`https://jsonplaceholder.typicode.com/users`);

모듈을 생성한다. counter에서 작업한것과 동일하게 루트 리듀서에 추가한다.

import * as api from "../lib/api";
import {handleActions} from "redux-actions";

const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';
const GET_POST_FAILURE = 'sample/GET_POST_FAILURE';

const GET_USERS = 'sample/GET_USERS';
const GET_USERS_SUCCESS = 'sample/GET_USERS_SUCCESS';
const GET_USERS_FAILURE = 'sample/GET_USERS_FAILURE';

export const getPost = id => async dispatch => {
    dispatch({type: GET_POST});
    try{
        const res = await api.getPost(id);
        dispatch({
            type: GET_POST_SUCCESS,
            payload: res.data,
        });
    } catch (error) {
        dispatch({type: GET_POST_FAILURE, payload: error, error:true});
        throw error;
    }
}

export const getUsers = () => async dispatch => {
    dispatch({type: GET_USERS});
    try{
        const res = await api.getUsers();
        dispatch({type: GET_USERS_SUCCESS, payload: res.data});
    }catch(error){
        dispatch({type: GET_USERS_FAILURE, payload: error, error:true});
        throw error;
    }
}

const initialState = {
    loading: {
        GET_POST: false,
        GET_USERS: false,
    },
    post: null,
    users: null
};

const sample = handleActions({
    [GET_POST]: state => ({
        ...state,
        loading: {
            ...state.loading,
            GET_POST: true
        }
    }),
    [GET_POST_SUCCESS]: (state,action) => ({
        ...state,
        loading: {
            ...state.loading,
            GET_POST: false
        },
        post: action.payload,
    }),
    [GET_POST_FAILURE]: (state, action) => ({
        ...state,
        loading: {
            ...state.loading,
            GET_POST: false
        }
    }),
    [GET_USERS]: state => ({
        ...state,
        loading: {
            ...state.loading,
            GET_USERS: true
        }
    }),
    [GET_USERS_SUCCESS]: (state, action) => ({
        ...state,
        loading: {
            ...state.loading,
            GET_USERS: false
        },
        users: action.payload,
    }),
    [GET_USERS_FAILURE]: (state, action) => ({
        ...state,
        loading: {
            ...state.loading,
            GET_USERS: false
        }
    })
}, initialState)

export default sample;

sample 관련 리듀서를 작성한다.

const Sample = ({loadingPost, loadingUsers, post, users}) => {
    return (
        <>
            <section>
                <h1>포스트</h1>
                {loadingPost && '로딩중'}
                {!loadingPost && post && (
                <div>
                    <h3>{post.title}</h3>
                    <h3>{post.body}</h3>
                </div>
                )}
            </section>
            <hr />
            <section>
                <h1>사용자 목록</h1>
                {loadingUsers && '로딩중 ..'}
                {!loadingUsers && users && (
                    <ul>
                        {users.map(user => (
                            <li key={user.id}>
                                {user.name} ({user.email})
                            </li>
                        ))}
                    </ul>
                )}
            </section>
        </>
    )
}

export default Sample;
import {getUsers, getPost} from "../modules/sample";
import {post} from "axios";
import {useEffect} from "react";
import Sample from "../components/Sample";
import {connect} from "react-redux";

const SampleContainer = ({
    getPost,
    getUsers,
    post,
    users,
    loadingPost,
    loadingUsers
}) => {
    useEffect(() => {
        getPost(1);
        getUsers(1);
    }, [getPost, getUsers]);
    return (
        <Sample
            post={post}
            users={users}
            loadingUsers={loadingUsers}
            loadingPost={loadingPost}
            />
    )
}

export default connect(({sample}) => ({
    post: sample.post,
    users: sample.users,
    loadingPost: sample.loadingPost.GET_POST,
    loadingUsers: sample.loadingUsers.GET_USERS
}), {
    getPost,
    getUsers
})(SampleContainer)

thunk 함수를 공통화로 빼놓으면 유용하다.

export default function createRequestThunk(type, request) {
    const SUCCESS = `${type}_SUCCESS`;
    const FAILURE = `${type}_FAILURE`;
    return params => async dispatch => {
        dispatch({type});
        try {
            const res = await request(params);
            dispatch({type: SUCCESS, payload: res.data});
        } catch (error) {
            dispatch({type: FAILURE, payload: error, error:true});
            throw error;
        }
    }
}
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);

 

 

redus-saga

thunk는 함수 형태의 액션을 디스패치하여 미들웨어에서 해당 함수에 스토어의 dispatch와 getState를 파라미터로 놓어 사용한다. 대부분의 경우에는 해당 기능으로 충분히 구현 가능하지만 redux-saga는 까다로운 상황에서 유용하다.

  • 기존 요청을 취소 처리해야 할 때(불필요한 중복 요청 방지)
  • 특정 액션이 발생했을 때 다른 액션을 발생시키거나, API 요청 등 리덕스와 관계 없는 코드를 실행할 때
  • 웹 소켓을 사용할 때
  • API 요청 시 재 요청해야할 때

사용방법은 다음과 같다.

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);
    yield put(increase());
}

function* decreaseSaga() {
    yield delay(1000);
    yield put(decrease());
}
export function* counterSaga() {
    yield takeEvery(INCREASE_ASYNC, increaseSaga);
    yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}

루트 사가도 추가한다.

export function* rootSaga() {
    yield all([counterSaga()]);
}
const logger = createLogger()
const store = createStore(rootReducer, applyMiddleware(logger, thunk, sagaMiddleware ));
sagaMiddleware.run(rootSaga)
root.render(

리덕스 모듈을 수정한다.

function* getPostSaga(action) {
    yield put(startLoading(GET_POST));
    try {
        const post = yield call(api.getPost, action.payload);
        yield put ({
            type: GET_POST_SUCCESS,
            payload: post.data
        });
    } catch (error) {
        yield put({
            type: GET_POST_FAILURE,
            payload: error,
            error: true
        })
    }
    yield put(startLoading(GET_POST));
}

function* getUserSaga() {
    yield put(startLoading(GET_USERS));
    try {
        const users = yield call(api.getUsers);
        yield put ({
            type: GET_USERS_SUCCESS,
            payload: users.data
        });
    } catch (e) {
        yield put({
            type: GET_USERS_FAILURE,
            payload: e,
            error: true
        });
    }
    yield put(startLoading(GET_USERS));
}
export function* sampleSaga() {
    yield takeLatest(GET_POST, getPost);
    yield takeLatest(GET_USERS, getPost);
}

마찬가지로 saga또한 공통 코드로 변환이 가능하다.

import {call, put} from 'redux-saga/effects';
import {finishLoading, startLoading} 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 res = yield call(request, action.payload);
            yield put({
                type: SUCCESS,
                payload: res.data,
            })
        }catch (e) {
            yield put({
                type: FAILURE,
                payload: e,
                error: true
            })
        }
        yield put(finishLoading(type));
    }
}

delay, put, call외에도 다른 기능이 있다. 

  • select : 스토어의 상태를 조회
  • throttle : 실행되는 주기를 제한

더 많은 기능들은 공식 문서를 참고하여 작업하면 된다.

'Web > React' 카테고리의 다른 글

SSR & CSR, Hydrate  (1) 2024.11.08
코드 스플리팅  (0) 2024.11.05
리덕스  (0) 2024.11.04
라우팅  (0) 2024.11.03
Hooks  (0) 2024.11.01