redux toolkit을 사용해 전역 상태를 관리하고 있는 프로젝트에서 새로고침을 해도 전역 상태가 유지되게 하기 위해 웹 브라우저의 스토리지에 직접 상태를 저장하고 제거하는 로직을 걷어내고, redux persist 패키지를 사용해 리팩토링을 했다.
1. store.ts 파일
Before
const store = configureStore({
reducer: {
isLogin: isLogin.reducer,
userInfo: userInfo.reducer,
tokens: tokens.reducer
}
});
export default store;
After
const rootReducer = combineReducers({
isLogin: isLogin.reducer,
userInfo: userInfo.reducer,
tokens: tokens.reducer
});
const persistConfig = {
key: 'root',
storage
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = configureStore({
reducer: persistedReducer
});
const persistor = persistStore(store);
export default () => {
return { store, persistor };
};
핵심은 store를 생성하는 configureStore에 기존의 reducer 묶음 대신 persistedReducer를 넣어주는 것이다. 이를 위해 combinedReducers로 slice reducer를 rootReducer로 통해 묶어주고, redux-persist의 persistReducer에 config, reducer 인자를 넣어 기본 세팅을 해준다.
persistConfig에서 whitelist와 blacklist로 redux-persist를 사용할 reducer와 사용하지 않을 reducer를 선정할 수도 있다. default로 whitelist에 모든 리듀서가 다 포함된다.
const persistConfig = {
key: 'root',
storage,
whitelist: ['isLogin', 'userInfo', 'tokens']
};
또한 persistConfig의 key는 브라우저 스토리지의 key가 된다. key name에는 아래 캡처 이미지에서 보는 것처럼 persist: 가 기본으로 붙는다.
마지막으로 persistStore를 통해 새로고침을 해도 지속될 store를 생성하고, store와 persistor를 내보낸다.
2. index.tsx 파일
Before
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
);
After
import { PersistGate } from 'redux-persist/integration/react';
import createStore from './store/store';
const { store, persistor } = createStore();
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RouterProvider router={router} />
</PersistGate>
</Provider>
);
루트 컴포넌트를 PersistGate로 감싼다. PersistGate는 전역 스토어를 전달하기 위한 redux의 Provider처럼 persistor를 전달하는 역할을 한다. 이렇게 하면 유지되는 상태를 가져와서 redux에 저장될 때까지 앱의 UI 렌더링이 지연된다. 이때 loading prop은 null일 수도 있고, 리액트 인스턴스(예: loading={<Loading />})일 수도 있다.
📍 리팩토링의 이점
기존에 브라우저의 스토리지에 직접 접근해서 setItem, getItem, removeItem 등으로 스토리지의 key-value를 직접 제어하던 코드가 곳곳에 분산되어 있었는데, 이 코드들을 싹 걷어내고 한 곳에서 관리할 수 있게 되었다.
redux-persist로 리팩토링하는 과정에서 콘솔창에 에러가 하나 떴다.
A non-serializable value was detected in an action, in the path: `register`.
action에 비직렬화 값이 전달되었다는 메시지인데, 무슨 얘기인고 하니.. RTK 공홈에 관련 항목이 있길래 찾아왔다.
One of the core usage principles for Redux is that you should not put non-serializable values in state or actions.
However, like most rules, there are exceptions. There may be occasions when you have to deal with actions that need to accept non-serializable data. This should be done very rarely and only if necessary, and these non-serializable payloads shouldn't ever make it into your application state through a reducer.
The serializability dev check middleware will automatically warn anytime it detects non-serializable values in your actions or state. We encourage you to leave this middleware active to help avoid accidentally making mistakes. However, if you do need to turnoff those warnings, you can customize the middleware by configuring it to ignore specific action types, or fields in actions and state.
[...]
If using Redux-Persist, you should specifically ignore all the action types it dispatches:
import { configureStore } from '@reduxjs/toolkit' import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, } from 'redux-persist' import storage from 'redux-persist/lib/storage' import { PersistGate } from 'redux-persist/integration/react' import App from './App' import rootReducer from './reducers' const persistConfig = { key: 'root', version: 1, storage, } const persistedReducer = persistReducer(persistConfig, rootReducer) const store = configureStore({ reducer: persistedReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], }, }), }) let persistor = persistStore(store) ReactDOM.render( <Provider store={store}> <PersistGate loading={null} persistor={persistor}> <App /> </PersistGate> </Provider>, document.getElementById('root') )
Redux를 사용할 때는 직렬화할 수 없는 값을 상태나 액션에 넣지 않아야 한다. 하지만 redux-persist를 사용하는 경우 이러한 핵심 원칙에 예외가 발생하기 때문에 오류 메시지에 나온 register를 포함해 dispatch하는 모든 액션 유형을 무시해야 한다.
여기서 직렬화를 이해하기 위해서 redux의 3가지 원칙 중 첫 번째 원칙인 진실은 하나의 근원으로부터(single source of truth)를 돌아가봤다.
애플리케이션의 모든 상태는 하나의 저장소 안에 하나의 객체 트리 구조로 저장됩니다.
이를 통해 범용적인 애플리케이션(universal application, 하나의 코드 베이스로 다양한 환경에서 실행 가능한 코드)을 만들기 쉽게 만들 수 있습니다. 서버로부터 가져온 상태는 시리얼라이즈되거나(serialized) 수화되어(hydrated) 전달되며 클라이언트에서 추가적인 코딩 없이도 사용할 수 있습니다.
직렬화와 역직렬화의 예시는 localStorage나 sessionStorage에 값을 저장할 때 object를 string으로 변환하는 JSON.stringify와 반대로 다시 값을 꺼내쓸 때 사용하는 메소드인 JSON.parse를 들 수 있다.
직렬화할 수 없는 값(예: function, promise 등)을 스토어에 저장하는 것은 불가능한 것은 아니지만 이 경우 redux-dev tools에 상태 표시가 안 되고, 위와 같은 콘솔 경고 메시지가 뜬다. redux는 스토어의 일관성 유지, 복원 기능, 시간 여행 디버깅 등을 위해 직렬화할 수 없는 항목을 상태나 액션에 넣지 않는 것을 권장하지만 이런 기능이 작동하지 않아도 괜찮다면 스토어에 넣는 것을 전적으로 제한하지는 않는 것이다.
📚 참고자료
GitHub - rt2zz/redux-persist: persist and rehydrate a redux store
persist and rehydrate a redux store. Contribute to rt2zz/redux-persist development by creating an account on GitHub.
github.com
Redux-persist
지난 포스트에서 Redux 예제를 사용하며 localStorage에 state를 저장해 사용하는 방법을 다뤘다. Redux persist 라이브러리를 통해 Web Storage에 state를 저장할 수 있다는 것을 알게 되어 직접 적용해보고 간
mieumje.tistory.com
Usage Guide | Redux Toolkit
redux-toolkit.js.org
Redux Toolkit - A non-serializable value was detected in an action, in the path: `type` 오류 해결
리덕스 강의 과제를 따라하다가 아래와 같은 오류와 직면했다. 오류 내용을 천천히 읽어 보면 action에 직렬화가 불가능한 값을 전달했다는 뜻으로 해석할 수 있다. 여기서 직렬화란 redux에서 값
guiyomi.tistory.com
'배워서 남 주자' 카테고리의 다른 글
프로그래머스 Lv.1 | 키패드 누르기 (JavaScript) (0) | 2023.06.08 |
---|---|
[기능 구현 챌린지] Social media dashboard (0) | 2023.06.06 |
Math.floor()와 parseInt()의 차이 (2) | 2023.05.23 |
프로그래머스 Lv.1 | 기사단원의 무기 (JavaScript) (0) | 2023.05.21 |
[styled-components] css 스타일 코드를 분리해서 작성하기 (0) | 2023.05.20 |