👻RTK Query 미들웨어를 사용해야 했던 이유
솔로 프로젝트를 진행하던 중 약간의 문제가 있었습니다.
API 에 요청을 보내서 데이터를 받아오는데 받아오는 데이터에 일괄적으로 특정 프로퍼티를 추가시켜줘야했습니다.
API 데이터를 들어올때 처음부터 조작해줘야하니 모든 응답에 대해 거쳐가는 미들웨어를 만들어주고싶었습니다.
다만 미들웨어 로직을 redux thunk로는 한번 정도 해보았지만 RTK Query가 제공하는 미들웨어는
사용해본적이 없어서 시도해보았습니다.
우선은 이래저래 공식문서를 빠르게 읽으면서 사용법만 대충 익힌 뒤 사용을 했지만
이번 포스트에서는 미들웨어에 대해 차근차근 탐구해보도록 하겠습니다.
간단한 사용법만을 원하시는 분을 위해 먼저 사용법만 간략하게 소개해드리겠습니다.
1. configureStore의 middleware 프로퍼티에 getDefaultMiddleware 배열에 내가 원하는 미들웨어를 추가시켜준다.
2. 끝
코드로 보면 더욱 쉽습니다.
import { 나의 미들웨어, 나의 미들웨어 } from '미들웨어 저장해둔 경로'
const api = configureStore({
reducer : {
[나의API.reducerPath]: 나의API.reducer
},
middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat(나의 미들웨어, 나의 미들웨어2)
)
})
정말 쉽죠?
여러개를 합치고 싶은 경우에는 concat의 인자를 여러개 전달해주면 됩니다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/concat
또한 concat은 배열도 받기때문에 코드가 더러워지는게 싫다면
혹은 미들웨어가 너무 많다면
미들웨어로 이루어진 배열을 만들어둔뒤 그 배열을 export해서 concat으로 이어주면 됩니다.
다만 여기서 주의할 점은 타입스크립트 이용자는 타입을 어떻게 정해주어야할까?일 것 같습니다.
다행히도 @reduxjs/toolkit에서는 middleware에 대한 타입도 제공을 해주고있기때문에 이는 걱정을 안하셔도 됩니다.
import { Middleware } from '@reduxjs/toolkit';
const addBookmarkProperty: Middleware = () => (next) => (action) => {
if (action.type.endsWith('fulfilled')) {
action.payload = action.payload.map((data: CozApiInterface) => ({
...data,
bookmark: false,
}));
}
return next(action);
};
다음은 제 코드입니다.
만약 action의 type이 fulfilled(성공)인 경우에는 action의 payload에 map을 돌려서
bookmark 프로퍼티를 추가시키는 로직입니다.
또한 미들웨어의 타입은 reduxtoolkit에서 제공하는 미들웨어 타입을 사용합니다.
export interface Middleware<
DispatchExt = {},
S = any,
D extends Dispatch = Dispatch
> {
(api: MiddlewareAPI<D, S>): (
next: Dispatch<AnyAction>
) => (action: any) => any
}
미들웨어 인터페이스에 대한 정의는 다음과 같습니다.
제네릭을 통해 이것저것 타입을 정해줄 수 있긴한데 전 일단 제네릭은 전달하지 않고 사용했습니다.
공식문서의 미들웨어에 대한 설명이 정말 심플하네요..
configure store의 인수 객체에 전달할 수 있는 middleware 프로퍼티에 대한 설명입니다.
custom Redux middleware는 대충 여러가지 로직을 넣어둘 수 있다네요
다만 전 어떻게 사용해야할지를 찾아보고 싶은거니까 일단 패스하겠습니다.
간단하게는 그냥 평범한 미들웨어 형태의 함수를 직접 정의해서 사용하면 될 것 같습니다.
import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { pokemonApi } from './services/pokemon'
export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[pokemonApi.reducerPath]: pokemonApi.reducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(pokemonApi.middleware),
})
// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch)
공식문서의 configureStore에 대한 예제입니다.
middleware 부분을 보면 getdefaultMiddleware라는 인수를 받는데 호출하는 부분을 보면
함수라는 것을 예상할 수 있습니다.
그리고 함수의 호출값에 배열메서드 concat을 사용하는것을 보면 함수의 반환값은 배열이라는 것도 예상이 되네요
그리고 concat에 pokemonApi의 미들웨어를 합쳐주는 것을 보니 대충 그림은 그려집니다.
미들웨어 함수들이 잔뜩 들어있는 배열을 만들어두고
API 데이터가 성공적으로 받아와지면 배열의 순서대로 미들웨어함수들을 실행시켜서
데이터를 가공해주는 형식으로 돌아가겠군용
😎getDefaultMiddleware 알아보기
https://redux-toolkit.js.org/api/getDefaultMiddleware#included-default-middleware
위 링크를 참고했습니다.
getDefaultMiddleware는 미들웨어의 기본 목록을 포함하는 배열을 반환합니다.
기본적으로 configureStore가 제공하는 Redux Store 설정에 일부 미들웨어를 자동으로 추가해줍니다.
const store = configureStore({
reducer: rootReducer,
})
// Store has middleware added, because the middleware list was not customized
이렇게 우리는 configureStore에 middleware에 대한 로직을 작성해주지 않으면
알아서 configureStore가 기본 미들웨어를 추가해준다고 합니다.
다만 미들웨어 목록을 사용자가 지정해주려고하는 경우에는 다음과 같이 사용해 줄 수 있다고합니다.
제가 알고싶은 건 바로 이부분이네요!
const store = configureStore({
reducer: rootReducer,
middleware: [thunk, logger],
})
// Store specifically has the thunk and logger middleware applied
만약 미들웨어 목록을 커스터마이징하고 싶으면 이런형식으로 사용할 수 있습니다만
이렇게 사용하는 경우에는 리덕스툴킷쿼리가 제공하는 defaultMiddleware는 사용할 수 없어집니다.
뭘 제공하는지는 잘 모르겠지만 아무튼 기본으로 제공해준다는 건
필요한 기능일테니까.. 그런거겠죠? 따라서 기본 제공하는 것은 그대로 사용하면서
내 커스텀 미들웨어만 추가시켜주고 싶습니다.
바로 이럴때 getDefaultMiddleware가 유용하다는 거군요
import { configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import rootReducer from './reducer'
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
})
// Store has all of the default middleware added, _plus_ the logger middleware
아까 보았던 그 예제네요!
redux-logger 라이브러리에서 제공하는 logger 미들웨어를 추가시켜주는 로직입니다.
미들웨어를 받아오고
getDefaultMiddleware함수의 반환값에 concat 메서드를 통해 미들웨어함수를 붙여주는 모습입니다.
이때 그렇다면 배열 스프레드 연산자를 사용해도 되지 않나? 싶을 수 있습니다.
[...getDefaultMiddleware() , logger]
이런식으로 작성해도 되지 않냐라는 이야기로 보입니다만은
이런 경우 중요한 유형 정보를 잃을 수 있기 때문에
concat 메서드를 이용하는것을 권장한다고 합니다.
지식이 짧은 저로서는 왜 유형 정보를 잃을 수 있는지는 잘 모르겠지만
일단 concat을 이용해서 작성해야겠네요
😲 Default Middleware가 제공하는 미들웨어
앞서 getDefaultMiddleware가 무엇을 제공하는지 궁금했었습니다.
다행히 이것도 공식문서에서 설명을 해주네요
Immutability check middleware | 불변성을 체킹해주는 미들웨어입니다. |
Serializability check middleware | Serializability를 체킹해주는 미들웨어입니다. |
Action creator check middleware | action creator를 확인하는 미들웨어입니다. |
자세한 설명은 이어집니다.
1. Immutability check middleware
mutaion에 대한 state를 deeply하게 비교합니다. 만약 dispatch 중 mutation이 감지되면 에러를 발생시키고
상태 트리에서 mutation 값이 감지된 위치에 대한 키 경로를 표시해주는 미들웨어입니다.
(redux-immutable-state-invariant에서 fork되었다네요)
2. Serializability check middleware
RTK 에서 사용하기 위해 특별히 만든 커스텀 미들웨어라고 합니다.
컨셉자체는 immutable-state-invariant랑 비슷하지만 serializable하지 않은 value(직렬화가 불가능한 밸류)
예를 들어 불변 상태, 불변성과 개념은 비슷하지만 함수, Promise, Symbol 등등
non-plain-js-data values (일반적인 자바스크립트 데이터 밸류가 아닌 것들이라고 생각할 수 있을 것 같네요)
에 대하여 심층적으로 state tree(상태 트리)와 action을 검사하는 일을 수행합니다.
만약 직렬화할 수 없는 값이 감지되면 직렬화 할 수 없는 값이 감지된 위치의 키 경로와 함께
콘솔오류를 표시하는 미들웨어입니다.
3.Action creator check middleware
이것 역시 앞서본 시리얼 어쩌구 미들웨어처럼 RTK에서 사용하기 위해 특별히 제작한 미들웨어입니다.
action creator가 호출되지 않고 실수로 디스패치된 경우를 식별하고
해당 액션 유형으로 콘솔에 경고를 띄워줍니다.
4. Thunk
위 미들웨어에들에 추가로 Redux에 권장하는 미들웨어인 Redux-Thunk도 기본적으로 내장시켜줍니다.
전체적으로 읽어보면 알 수 있듯이 사용자의 실수, 잘못된 데이터 등에 대해
"너 이거 잘못 되었어!!"하고 알려주는 미들웨어들이 들어가있는 느낌이네요!
확실히 굳이 뺄 이유가 없는 미들웨어인 것 같습니다.
그래서 결론적으로 getDefaultMiddleware에는 총 네가지의 미들웨어가 포함되어있다는 걸 알수있습니다.
소개해드린 각 리듀서에 대해서는 더 자세한 설명을 공식문서에서 찾아보실 수 있습니다.
const middleware = [
actionCreatorInvariant,
immutableStateInvariant,
thunk,
serializableStateInvariant,
]
이렇게 되어있는 거군요
const middleware = [thunk]
또한 미들웨어의 반환값은 다음과 같습니다.
배열안에 thunk가 들어있다는 것일까요..? 아무튼 그런 느낌이네요
아님말고
🕊getDefaultMiddleware에 사용자정의 미들웨어를 추가하는 방법
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducer'
import { myCustomApiService } from './api'
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
thunk: {
extraArgument: myCustomApiService,
},
serializableCheck: false,
}),
})
getDefaultMiddleware를 그냥 호출만 해도 되기는 했지만
만약 getDefaultMiddleware에 무언가를 추가시켜주고싶다면 다음과 같이 할 수 있습니다.
getDefaultMiddleware의 인수로 객체를 전달하는데
thunk프로퍼티에 내가 추가하고싶은 미들웨어들을 넣어서 전달해주면 됩니다.
serializableCheck이라는 프로퍼티도 눈에띄는데 이것은 결과 배열에서 제외시킬지를 결정하는 설정입니다.
false를 전달하면 결과 배열(result array)에서 제외됩니다.
결과배열이 뭔지는 몰..?루?
🤡실제 프로젝트에 적용한 사례
createAPi.tsx
import { EndpointBuilder } from '@reduxjs/toolkit/dist/query/endpointDefinitions';
import {
BaseQueryFn,
FetchArgs,
FetchBaseQueryError,
FetchBaseQueryMeta,
createApi,
fetchBaseQuery,
} from '@reduxjs/toolkit/query/react';
export interface CozApiInterface {
id: number;
type: string;
title: string;
sub_title: string | null;
brand_name: string | null;
price: string;
discountPercentage: number | null;
image_url: string;
brand_image_url: string | null;
follower: string | null;
bookmark: boolean;
}
export const cozShoppingAPI = createApi({
baseQuery: fetchBaseQuery({
baseUrl: `http://cozshopping.codestates-seb.link/api/v1/`,
}),
endpoints: (
builder: EndpointBuilder<
BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError,
object,
FetchBaseQueryMeta
>,
never,
'api'
>,
) => ({
getProduct: builder.query({
query: (count?: string | number) => ({
url: count ? `/products?count=${count}` : `/products`,
}),
}),
}),
});
export const { useGetProductQuery } = cozShoppingAPI;
사실 타입스크립트에 대한 지식이 많은 편이 아니라
제 타입 설정이 최선이 아닐 것 같습니다.
간단한 api를 만들었습니다.
api에 대한 타입을 설정해주었고 endpoints의 builder인자에 대한 타입도 설정해주었습니다.
api 요청을 받아서 [{대충 객체}]를 받아오는 코드입니다.
import { configureStore } from '@reduxjs/toolkit';
import darkSlice from './darkSlice';
import { cozShoppingAPI, CozApiInterface } from './cozShoppingAPI';
import { setupListeners } from '@reduxjs/toolkit/query';
import { Middleware } from '@reduxjs/toolkit';
const addBookmarkProperty: Middleware = () => (next) => (action) => {
if (action.type.endsWith('fulfilled')) {
action.payload = action.payload.map((data: CozApiInterface) => ({
...data,
bookmark: false,
}));
}
return next(action);
};
const store = configureStore({
reducer: {
dark: darkSlice.reducer,
[cozShoppingAPI.reducerPath]: cozShoppingAPI.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
cozShoppingAPI.middleware,
addBookmarkProperty,
),
});
setupListeners(store.dispatch);
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
그리고 미들웨어를 설정해주었습니다.
편의상 한곳에서 정의했지만 다른곳에서 정의해도 될것같습니다.
GPT는 미들웨어함수를 작성할때는 불변성을 지키면서 작성하는 편이 좋다고하네요.
뭐.. 일단은 확실히 알고있는게 아니니까 좀 더 안전하게 작성을 하겠습니다.
아직 이해가 잘 되지 않은 부분이
cozShoppingAPI.middleware를 concat해주지않으면 에러가 발생한다는 것입니다.
그리고 cozShoppingAPI가 있다면 거기에 middleware를 추가시켜주는 방법도 있을듯한데
그 방법을 찾기가 어렵네요..
😍결론
읽다보니 RTK의 docs가 참 잘 구성되어있다는 생각이 들었습니다.
다만 양이 방대해서 제가 원하는 부분만 뜯어보기보다는 전체적으로 쭉 한번 읽어보는 시간을 가져야겠습니다.
😃 레퍼런스
https://redux-toolkit.js.org/api/getDefaultMiddleware#included-default-middleware
https://redux-toolkit.js.org/api/configureStore
'redux' 카테고리의 다른 글
조금 힘겨운 CreateAsyncThunk로 Toast UI 구성하기 (0) | 2023.05.16 |
---|---|
RTK Query 데이터에 local Storage를 사용하는 방법 (0) | 2023.05.15 |
updateQueryData RTKquery로 클라이언트에서만 쿼리 데이터를 조작하고 싶다면 (1) | 2023.05.14 |
예제와 함께 TS ReduxToolkit Query Slow start (1) | 2023.05.01 |
Redux와 Redux-toolkit (0) | 2023.04.15 |