사실 redux에 redux-persist를 이용하여 localstorage를 사용하는 방법은 파이널프로젝트에 사용하면서 방법은 알고 있었지만, sessionStorage와 localStorage를 같이 사용하는 방법을 찾다 다시 기록해보고자 한다.
리액스에서 순수 state를 사용하거나 redux를 통해 상태관리를 하다보면 새로고침시 state가 사라지게 되는 현상이 있다. 이럴 때 localStorage를 이용하여 새로고침해도 저장공간에 있는 데이터를 불러와 상태를 유지할 수 있도록 할 수 있다.
redux-persist를 사용하면 보다 쉽게 스토리지를 통한 관리를 할 수 있다.
먼저 리덕스가 적용된 프로젝트에 라이브러리를 설치하자. 나는 redux-toolkit을 사용하고 있기 때문에 redux-toolkit을 기준이로 사용해보고자 한다.
npm install redux-persist
먼저 작성한 store와 index.js를 보자면
store
import authSlice from "./reducers/authSlice";
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer : authSlice
})
export default store
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./store/store";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
);
위와 같은 아주 간단한 형태로 되어 있다.
redux-persist가 제공하는 persistReducer(persistConfig, reducer)를 통해 persistConfig가 추가된 reducer가 반환되도록 만들어 주면 된다.
먼저 persistConfig를 만들어보도록 하자.
const persistConfig = {
key: "root",
storage: storage,
whitelist: [],
};
config의 객체는 key, storage, whitelist, blacklist 등이 존재한다. key는 스토리지에서 사용되는 명칭이고, storage(localStorage, sessionStorage)는 필수로 입력해야하는 argument이며 나머지는 선택에 따라 넣으면 되겠다.
whitelist 는 스토리지를 통해 관리할 항목을 말하며 반대로 blacklist 는 스토리지로 관리하지 않을 항목을 나타낸다.
이를 토대로 만들어 두었던 store에 peristReducer를 적용해 새롭게 추가된 reducer를 만들어보자.
store
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import authSlice from "./reducers/authSlice";
import { persistReducer, persistStore } from "redux-persist";
import storage from "redux-persist/lib/storage";
import storageSession from 'redux-persist/lib/storage/session'
const persistConfig = {
key: "root",
// localStorage에 저장. sesstionStorage에 저장하려면 storageSession사용
storage: storage,
// 여러개의 reducer 중에 auth reducer만 localstorage에 저장.
whitelist: ["auth"],
// blacklist : 제외할 목록 저장
};
const rootReducer = combineReducers({auth : authSlice})
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
});
export persistor = persistStore(store);
persisrReducer(persistConfig, rootReducer)를 통해 리듀서를 persistReducer에 적용했다면, 적용한 리듀서와 persistStore를 이용하여 새로고침, 종료해도 지속될 store를 생성해주면 되겠다.
여기서 만들어준 store는 PersistGate를 통해 App에 적용되게 되는데, App컴포넌트를 PersisGate로 감싸 매핑하도록 해주면 된다.
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import {store, persistor} from "./store/store";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
</BrowserRouter>
);
허나 이렇게 실행할 경우 콘솔에 에러가 생기게 된다.
이 에러는 redux-toolkit에서 기본적으로 직렬화 할 경우 데이터의 유실이 발생을 방지하기위해 뜨는 에러이다. 직렬화 체크를 막음으로써 에러 로그가 나타나지 않도록 할 수 있으며 아래와 같이 store를 고치면 되겠다.
store
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import authSlice from "./reducers/authSlice";
import { persistReducer, persistStore, PURGE, PERSIST} from "redux-persist";
import storage from "redux-persist/lib/storage";
import storageSession from 'redux-persist/lib/storage/session'
const persistConfig = {
key: "root",
// localStorage에 저장. sesstionStorage에 저장하려면 storageSession사용
storage: storage,
// 여러개의 reducer 중에 auth reducer만 localstorage에 저장.
whitelist: ["auth"],
// blacklist : 제외할 목록 저장
};
const rootReducer = combineReducers({auth : authSlice})
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware(
{
serializableCheck: {
ignoredActions: [PERSIST, PURGE],
},
}
)
});
export const persistor = persistStore(store)
middleware는 기본적으로는 리덕스 미들웨어를 담는 배열이다. 사용할 모든 미들웨어를 배열에 담아서 명시적으로 작성할 수도 있는데 그렇지 않으면 getDefaultMiddleware를 호출하게 된다. 사용자 정의, 커스텀 미들웨어를 추가하면서 동시에 리덕스 기본 미들웨어를 사용할 때 유용한 방법이다.
리덕스 미들웨어는 dispatch(이하 디스패치)된 액션이 리듀서에 도달하기 전 중간 영역에서 사용자의 목적에 맞게 기능을 확장할 수 있도록 돕는다. 예를 들어 미들웨어로 redux-logger를 추가했다면 액션이 디스패치될 때마다 개발자 도구 콘솔에 로그가 찍히는 것을 생각해 볼 수 있다. 로그를 출력하는 과정이 중간에 추가된 것이다.
이외에도 개발 모드에서 일부 미들웨어는 특정한 역할을 기본적으로 수행한다. 상태의 변형(mutation)을 감지하거나 직렬화, 즉 데이터를 다른 데이터 구조로 맞추어 가공하는 행위가 불가능한 값(non-serializable value)을 사용하는 실수를 방지할 수 있도록 경고해준다.
여기서 직렬화가 불가능하다는 의미는 어떤 데이터를 직렬화 하는 과정에서 데이터를 유실할 수도 있다는 것이다.
// { it: 'has data' } 객체를 JSON 데이터 포맷으로 직렬화합니다.
const stringifiedObject =
JSON.stringify({ it: 'has data' }); // {"it":"has data"}
// 이것을 다시 역직렬화를 하면
// 처음 내용과 동일한 데이터를 담고 있음을 확인할 수 있습니다.
JSON.parse(stringifiedObject); // { it: 'has data' }
// 그러나 아래처럼 Set 자료형을 직렬화 한다면
// 그 과정에서 데이터가 유실됩니다.
const setObject = new Set([1, 2, 3]); // Set(3) {1, 2, 3}
const stringifiedSetObject =
JSON.stringify(setObject); // {}
// 역직렬화해도 데이터가 이미 유실된 상태이기 때문에
// 본래 값과 차이가 발생하여 앱이 기대하지 않은 방식으로 동작할 수 있습니다.
JSON.parse(stringifiedSetObject); // {}
이러한 이유로 직렬화가 불가능한 값을 액션이나 상태에서 사용하지 않는 것을 권장합니다. 직렬화가 불가능한 값들은 Promise, Symbol, Map/Set, function, class instance 등이 있다. 반면에 직렬화가 가능한 값들은 자바스크립트 원시 자료형에 속하는 string, number, null, undefined와 array, object literal(객체 리터럴) 방식으로 선언된 plain object가 있다.
reducer의 일부는 localStorage, sessionStorage를 사용하고 싶었다. 하여 npm의 redux-persist를 참고하여 코드를 짜면 되겠다.
Nested Persists를 보면 예시코드가 아주잘 나와 있는 것을 볼 수 있다.
import { combineReducers } from 'redux'
import { persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { authReducer, otherReducer } from './reducers'
const rootPersistConfig = {
key: 'root',
storage: storage,
blacklist: ['auth']
}
const authPersistConfig = {
key: 'auth',
storage: storage,
blacklist: ['somethingTemporary']
}
const rootReducer = combineReducers({
auth: persistReducer(authPersistConfig, authReducer),
other: otherReducer,
})
export default persistReducer(rootPersistConfig, rootReducer)
아주아주 간단하다. Config파일을 여러개를 만들고 rootReducer를 combineReducers로 묶을 때 배열의 형태로 만들어 같이 넣어주면 되겠다.
localstorage와 sessionstorage를 같이 사용하면서 위의 방법을 사용하여 적용했던 방법을 기록해둔다.
기존에는 rootReducer를 그저 combineReducer를 이용하여 묶어주고 내보내는 역할을 할 수 있도록 만들었는데 persistConfig와 persistReducer와 묶어 관리할 필요가 있어 해당 코드를 바꾸어 적용할 수 있도록 하였다.
기존 rootReducer
import { combineReducers } from "redux";
import loginSlice from "./loginSlice";
import mypageSlice from "./userInfoSlice";
const rootReducer = combineReducers({
login: loginSlice,
mypage: mypageSlice,
});
export default rootReducer;
기존 store
import { configureStore } from "@reduxjs/toolkit";
import { persistReducer, PERSIST, PURGE } from "redux-persist";
import storage from "redux-persist/lib/storage";
import rootReducer from "../reducers/rootReducer";
const persistConfig = {
key: "root",
version: 1,
storage,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [PERSIST, PURGE],
},
}),
});
export default store;
위와 같은 코드에서 login의 상태와 mypage의 상태를 따로따로 저장하고 싶은것이였다. login은 LocalStorage에 저장하고 mypage는 SessionStorage에 저장하여 나누려고 하였다.
하여, rootReducer에서 여러 reducer들을 하나로 combineReducer로 묶기 전에 persistReducer로 config와 slice를 묶어주어야 했던것이다.
아래와 같은 코드로 다시 config를 설정하고 각각의 storage를 설정하여 주었다.
바꾼 rootReducer
import { combineReducers } from 'redux'
import loginSlice from './loginSlice'
import myPageSlice from './mypageSlice'
import storage from 'redux-persist/lib/storage'
import storageSession from 'redux-persist/lib/storage/session'
import { persistReducer } from 'redux-persist'
const loginPersistConfig = {
key: 'authentication',
storage,
blacklist: ['orderProduct'],
}
const productPersistConfig = {
key: 'orderProduct',
storage: storageSession,
whitelist: ['orderProduct'],
}
const rootReducer = combineReducers({
login: persistReducer(loginPersistConfig, loginSlice),
mypage: persistReducer(productPersistConfig, myPageSlice),
})
export default rootReducer
바꾼 store
import { configureStore } from '@reduxjs/toolkit'
import { PERSIST, PURGE } from 'redux-persist'
import rootReducer from '../reducers/rootReducer'
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [PERSIST, PURGE],
},
}),
})
export default store
이전엔 store에 reducer : persistReduer로 config와 rootreducer를 같이 묶어 한번에 localStorage를 사용할 수 있도록 넣어 줬지만, rootReducer자체에 persistreducer를 사용하여 각각 넣어 줬으니 reducer에는 rootReducer만 들어가면 되겠다.
Reference
State를 local storage에 쉽게 저장하자. Redux Persist를 소개합니다.
Redux-toolkit
redux-persist npm
리덕스에 스토리지 적용하기