Javascript/React

리덕스 미들웨어

kyoulho 2024. 7. 29. 21:21

리듀서 함수 몸통에서는 사이드 이펙트를 일으키는 코드를 사용할 수 없다. 그런데 이 점은 리덕스 기능을 사용하는 컴포넌트를 복잡하게 만든다. 리덕스 미들웨어는 리듀서 앞 단에서 부작용이 있는 코드들을 실행하여 얻은 결과를 리듀서 쪽으로 넘겨주는 역할을 한다.

dispatch(액션) -> 미들웨어 -> 리듀서 -> 리덕스 저장소

 

리덕스 미들웨어 타입

// 리덕스 미들웨어 타입
interface Middleware<_DispatchExt = {}, S = any, D extends Dispatch = Dispatch> {
    (api: MiddlewareAPI<D, S>): (next: (action: unknown) => unknown) => (action: unknown) => unknown;
}

// 리덕스 미들웨어 API 타입
interface MiddlewareAPI<D extends Dispatch = Dispatch, S = any> {
    dispatch: D;
    getState(): S;
}

// Dispatch 타입
interface Dispatch<A extends Action = UnknownAction> {
    <T extends A>(action: T, ...extraArgs: any[]): T;
}

// 리덕스 미들웨어 구현
export function someMiddleware<S = any>({dispatch: Dispatch, getState}: {getState: () => S}) {
      return (next: Dispatch) => (action: Action) => {
          return next(action)
      }
}

리덕스 미들웨어는 2차 고차 함수이다. 여기서 Dispatch는 useDispatch 훅으로 얻을 수 있는 dispatch()  함수의 타입과 같다.

그리고 리덕스 미들웨어는 항상 action을 매개변수로 받는 함수를 반환해야 하며 미들웨어는 몸통에서 next 함수를 호출해 다음 미들웨어나 리듀서에 액션을 전달해야 한다.

즉, 미들웨어가 next 함수를 호출해서 반환된 액션은 각각의 미들웨어를 거쳐 최종 리듀서까지 전달되고, 다시 역으로 미들웨어들을 거치며 돌아온다. 이러한 구조를 통해 리듀서에서 액션을 처리하기 전후로 추가 로직을 넣을 수 있다.

 

로거 미들웨어 예제

상태 값을 로깅하는 예제이다. redux-logger라는 패키지가 이미 존재한다. (redux-logger, @types/redux-logger)

src/store/logger.ts

import {Action, Dispatch} from "redux";

export default function logger<S = any>({getState}: { getState: () => S }) {
    return (next: Dispatch) => (action: Action) => {
        // getState() 함수는 현재 리덕스 저장소에 담긴 모든 상탯값을 가져온다.
        console.log('state before next', getState())
        console.log('action', action)
        const returnedAction = next(action)
        console.log('state after next', getState())
        return returnedAction
    }
}

src/store/useStore.ts

import {configureStore} from "@reduxjs/toolkit";
import {rootReducer} from "./rootReducer";
import {useMemo} from "react";
import logger from './logger'

const useLogger = process.env.NODE_ENV !== 'production'

const initializeStore = () => {
    const middleware: any[] = []
    if(useLogger){
        middleware.push(logger)
    }

    return configureStore({
        reducer: rootReducer,
        middleware: getDefaultMiddleware => getDefaultMiddleware().concat(middleware)
    })
}

export const useStore = () => useMemo(() => initializeStore(), []);

loggerTest.tsx

import {useDispatch} from "react-redux";
import {useEffect} from "react";
import {Title} from "../components";

export default function LoggerTest() {
    const dispatch = useDispatch()
    useEffect(() => {
        dispatch({type: 'hello', payload: 'world'})
    }, [dispatch])

    return (
        <section className={'mt-4'}>
           <Title>LoggerTest</Title>
            <div className={"mt-4"}/>
        </section>
    )
}

 

redux-thunk

썽크는 action의 타입이 함수이면 action을 함수로서 호출해 주는 기능을 추가한 미들웨어이다. (redux-thunk, @types/redux-thunk)

이에 따라 썽크 미들웨어를 장착하면 dispatch 함수를 매개변수로 수신하는 함수 형태로 액션 생성기를 만들 수 있다.

리듀서는 순수 함수여야 하지만 리덕스 미들웨어는 순수 함수일 필요가 없다. 사실상 미들웨어는 부작용이 있는 코드를 마치 리듀서에서 동작하는 것처럼 만들어 주는 역할을 한다.

export function thunkMiddleware<S = any>({dispatch: Dispatch, getState}: {getState: () => S}) {
      return (next: Dispatch) => (action: Action) => {
          if(typeof action === 'function')
              return action(dispatch, getState)
          return next(action)
      }
}

// 액션을 대신하는 함수
const functionAction = (dispatch: Dispatch) => {
    dispatch(someAction)
}

// dispatch
const doSomething = useCallback(() => {
    dispatch<any>(functionAction)
})

 

로딩 UI 예제

src/store/useStore.ts

import {configureStore} from "@reduxjs/toolkit";
import {rootReducer} from "./rootReducer";
import {useMemo} from "react";
import logger from 'redux-logger'
import {thunk} from 'redux-thunk'

const useLogger = process.env.NODE_ENV !== 'production'

const initializeStore = () => {
    const middleware: any[] = [thunk]
    if(useLogger){
        middleware.push(logger)
    }

    return configureStore({
        reducer: rootReducer,
        middleware: getDefaultMiddleware => getDefaultMiddleware().concat(middleware)
    })
}

export const useStore = () => useMemo(() => initializeStore(), []);

src/store/loading/types.ts

import type {Action} from "redux";

export type State = boolean

export type SetLoadingAction = Action<'@loading/setLoadingAction'> & {
    payload: State
}

export type Actions = SetLoadingAction

src/store/loading/actions.ts

import type * as T from './types'

export const setLoading = (payload: T.State): T.SetLoadingAction => ({
    type: '@loading/setLoadingAction',
    payload
})

src/store/loading/reducers.ts

import type * as T from './types'

const initialState: T.State = false

export const reducer = (state: T.State = initialState, action: T.Actions) => {
    switch (action.type) {
        case "@loading/setLoadingAction":
            return action.payload
    }
    return state
}

src/store/loading/doTimedLoading.ts

import { Dispatch } from "redux";
import { setLoading } from "./actions";

// doTimedLoading 미들웨어 액션 생성자 함수를 정의
// 기본적으로 3초 동안 로딩 상태를 유지하도록 설정되어 있다
export const doTimedLoading =
    (duration: number = 3 * 1000) =>
        (dispatch: Dispatch) => {
            dispatch(setLoading(true)); // 로딩 상태를 true로 설정하는 액션을 디스패치

            const timerId = setTimeout(() => { // 지정된 지속 시간(duration) 후에 실행되는 타이머를 설정한다.
                clearTimeout(timerId); // 타이머를 정리한다.
                dispatch(setLoading(false)); // 로딩 상태를 false로 설정하는 액션을 디스패치
            }, duration); // duration 후에 타이머가 작동한다.
        };

LoadingTest.tsx

import {useDispatch, useSelector} from "react-redux";
import type {AppState} from "../store";
import * as L from '../store/loading'
import {useCallback} from "react";
import {Title} from "../components";
import {Button} from "../theme/daisyui";

export default function LoadingTest() {
    const dispatch = useDispatch();
    const loading = useSelector<AppState, L.State>(({loading}) => loading);

    const doTimedLoading = useCallback(() => {
        dispatch<any>(L.doTimedLoading(1000))
    }, [dispatch])

    return (
        <section className={'mt-4'}>
            <Title>LoadingTest</Title>
            <div className={"mt-4"}>
                <div className={"flex justify-center mt-4"}>
                    <Button
                        className={"btn-sm btn-primary"}
                        onClick={doTimedLoading}
                        disabled={loading}>
                        DO TIMED LOADING
                    </Button>
                </div>
                {loading && (
                    <div className={"flex items-center justify-center"}>
                        <Button className={"btn-circle loading"}/>
                    </div>
                )}
            </div>
        </section>
    )
}

 

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

React Beautiful DnD 라이브러리  (0) 2024.07.31
React DnD 라이브러리  (0) 2024.07.31
리듀서 활용하기  (0) 2024.07.22
리덕스 기본 개념 이해하기  (2) 2024.07.21
useContext  (0) 2024.07.21