리덕스 미들웨어란
리듀서 함수 몸통에서는 부작용(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>
)
}
리덕스 로거 패키지 사용하기
리덕스 로거 패키지를 사용하기 위해 npm에서 명령어를 통해 리덕스 로거 패키지를 설치한다.
> npm i redux-logger
> npm i -D @types/redux-logger
useState의 logger import를 수정해 로거 결과를 다시 확인해본다.
import logger from '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>}}
'FrontEnd > React' 카테고리의 다른 글
[React] 트렐로 따라 만들기 (2) (0) | 2024.12.20 |
---|---|
[React] 트렐로 따라 만들기 (1) (1) | 2024.12.17 |
[React] 리듀서 활용하기 (0) | 2024.12.11 |
[React] 리덕스 기본 개념 이해하기 (2) | 2024.12.10 |
[React] useContext 훅 이해하기 (0) | 2024.12.10 |