본문 바로가기

FrontEnd/React

[React] 리덕스 미들웨어 이해하기

리덕스 미들웨어란

 리듀서 함수 몸통에서는 부작용(side effect)을 일으키는 코드를 사용할 수 없다.   그렇기 때문에 리덕스 미들웨어는 리듀서 앞 단에서 부작용이 있는 코드(순수함수X)들을 실행해 얻은 결과를 리듀서 쪽으로 넘겨주는 역할을 한다.  

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

 

 

 리덕스 미들웨어는 2차 고차함수이다.

import {Action, Dispatch} from 'redux'
// {dispatch: Dispatch, getState} 구조분해로 object값을 얻어와 분해한다.
// const obj = { dispatch: () => console.log("dispatch"), getState: () => console.log("getState") };
// const { dispatch, getState } = obj; // obj에서 두 속성을 분리
// {getState: () => S} : getState에 대해서만 함수로 타입을 선언
export function someMiddleware<S= any>({dispatch: Dispatch, getState}: {getState: () => S}) {
    // 리덕스 미들웨어는 항상 다음처럼 action을 매개변수로 받는 함수를 반환
    return (next: Dispatch) => (action: Action) => {
        
        // 미들웨어는 next가 반환한 액션을 반환해줘야 액션이 리듀서로 유입될 수 있다.
        const returnValue = next(action)
        return returnValue  
    }
}

 

 

 고차함수의 매개변수 타입중 Dispatch는 useDispatch 훅으로 얻을 수 있는 dispatch() 함수의 타입과 같다.

export interface Dispatch<A extends Action = AnyAction> {
    <T extends A>(action: T): T
}

 

 

 리덕스 미들웨어는 항상 action을 매개변수로 받는 함수를 반환해야 하며 next(Dispatch)를 통해 반환값을 다시 반환해 다른 미들웨어도 동작할 수 있게 해야한다. 

(next: Dispatch) => (action: Action) => {
    retrun next(action)
}

 

로거 미들웨어 만들기

  이 코드는 리덕스 저장소에 유입되는 액션과 리듀서들이 유입한 액션을 결합해 새로운 앱 상태를 콘솔 창에 출력하는 기능을 구했다.  미들웨어는 반드시 next() 함수 호출로 얻은 반환값을 다시 반환해야 한다.

import {Action, Dispatch} from 'redux'

// getState() : 리덕스 저장소에 담긴 모든 상태값을 얻어온다.
export default function logger<S = any>({getState}: {getState: () => S}) {
  return (next: Dispatch) => (action: Action) => {
    
    console.log('state before next', getState())
    console.log('action', action)
    const returnedAction = next(action)  // next() 함수로 호출로 얻은 반환 값을 다시 반환해야 한다.
    console.log('state after next', getState())
    return returnedAction   // next()함수로 얻은 반환값인 returnedAction
  }
}

 

 

 미들웨어 설정하기

  기존 설정했던 useStore.ts에 미들웨어 부분에 logger를 설정하기 위해 코드를 수정한다.

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

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

const initializeStore = () => {
  const middleware: any[] = []
  if (useLogger) {
    middleware.push(logger)
  }
  const store = configureStore({
    reducer: rootReducer,
    middleware: getDefaultMiddleware => getDefaultMiddleware().concat(middleware)
  })
  return store
}

export function useState() {
  const store = useMemo(() => initializeStore(), [])
  return store
}

 

 

 미들웨어 테스트하기

  미들웨어를 테스트하기 위해 dispatch를 추가해 해당 action을 수행해본다.  action이 리덕스에서 처리될 경우 logger를 타는 것을 확인할 수 있다.

import {useEffect} from 'react'
import {useDispatch} from 'react-redux'
import {Title} from '../components'
export default function LoggerTest() {
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch({type: 'hello', payload: 'world'})
  })
  return (
    <section className="mt-4">
      <Title>LoggerTest</Title>
      <div className="mt-4"></div>
    </section>
  )
}

 

실행결과 개발자 도구에 action정보가 정상적으로 표시됨을 알 수 있다.

 

 

 리덕스 로거 패키지 사용하기

  리덕스 로거 패키지를 사용하기 위해 npm에서 명령어를 통해 리덕스 로거 패키지를 설치한다.

> npm i redux-logger
> npm i -D @types/redux-logger

 

 

  useState의 logger import를 수정해 로거 결과를 다시 확인해본다.

import logger from 'redux-logger'

 

실행결과로 redux logger를 통해 좀더 확연하게 표시되는 로그를 확인할 수 있다.

 

 

 

 

썽크 미들웨어 알아보기

redux-thunk 미들웨어는 비동기 작업을 처리하는데 사용하는 미들웨어로 비동기 작업을 다루는 미들웨어 중 가장 대표적인 미들웨어이다.   썽크 미들웨어는 주로 연산 결과가 필요할 때까지 연산을 지연시키는 용도로 사용된다

> npm i redux-thunk
> npm i-D @type/redux-thunk

 

 

 리덕스 미들웨어는 2차 고차함수로 선언되어있고, 썽크의 경우 action의 타입이 함수면 action을 함수로서 호출해주는 기능이 추가된 미들웨어이다.

import {Action, Dispatch} from 'redux'

export function someMiddleware<S= any>({dispatch: Disaptch, getState}: {getState: () => S}) {
    // 리덕스 미들웨어는 항상 다음처럼 action을 매개변수로 받는 함수를 반환
    return (next: Dispatch) => (action: Action) => {
	// 썽크는 action의 타입이 함수면 action을 함수로서 호출해주는 기능이 추가됨
        if(typeof action === 'function') {
            retrun action(dispatch, getState)
        }
        return next(action)
    }
}

 

 

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

const functionAction = (dispatch: Dispatch) => {
    dispatch(someAction)
}

 

 

 로딩 UI 구현하기

  loading을 구현함에 있어 필요한 속성은 boolean타입만으로도 충분하므로 loading types의 State의 타입을 boolean으로 설정한다.

import type {Action} from 'redux'

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

 

  SetLoadingAction 타입 액션을 생성하는 setLoading 액션 생성기를 구현한다.

import type * as T from './types'

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

 

 SetLoadingAction 타입 액션에 대한 리듀서를 구현한다.

import * as T from './types'

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

 

 

 하단의 썽크 미들웨어로 처리하면 컴포넌트마다 발생하는 코드의 중복을 막을 수 있다.  만약 doTimedLoading 함수를 성크액션으로 구성하지 않으면 로딩이 필요한 컴포넌트마다 기능을 추가해야 하는 불편함이 있을 것이다.

import {Dispatch} from 'redux'
import {setLoading} from './actions'

export const doTimedLoading = (duration: number = 3 * 1000) =>
  (dispatch: Dispatch) => {
    dispatch(setLoading(true))
    const timerId = setTimeout(() => {
      clearTimeout(timerId)
      dispatch(setLoading(false))
    }, duration)
  }

 

 

 위의 doTimedloading의 결과를 확인하고자 하는 코드를 추가한다.

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

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 btn-circle loading"></Button>
          </div>
        )}
      </div>
    </section>
  )
}

 

실행결과로 로딩이 정상적으로 실행되는 결과를 확인할 수 있다.

 

 

 

 오류 메시지 구현하기

  오류 메시지를 출력하는 errorMessage 멤버 상태를 구현한다.  Error는 자바스크립트 엔진이 기본으로 제공하는 타입이다.  리액트 개발에서 Error 타입 객체는 Promise 타입 객체를 처리하는 코드와 try~catch 구문을 사용하는 코드에서 흔히 만날 수 있다. 


 보통 리액트 개발에서 Error 객체는 Error | null 타입 상태로 구현한다.   다만 리덕스 상태로는 Error 타입은 이처럼 null값일 수 있는 형태로 구현하는 것은 바람직하지 않다.

const [error, setError] = useState<Error | null>(null)
{error && <p>{error.message}</p>}

 

 

  왜냐하면 UI관점의 Error타입은 message 속성에 담긴 오류 메시지인 meesage 속성만을 요하므로 message의 길이가 0보다 큰지 판단하는 코드로 구성함이 옳다.

const errorMessage = useSelector(state => state.errorMessage)
{{errorMessage.length && <p>{errorMessage}</p>}}