리덕스 toolkit과 리덕스 persist를 사용한 로그인 유지
- 로그인 api를 통해 서버로부터 받아온 회원 정보를 저장하고자 함.
- 새로고침 해도 로그인이 풀리지 않도록 함.
Redux-Toolkit의 전체적인 구조는 위와 같다.
여러가지 Slice가 하나의 Store에 존재하며 한꺼번에 관리된다.
이때 Slice는 기존 리덕스의 액션, 리듀서와 비슷하다.
Slice에는 기본 state값과 액션에 따라 state를 변화시키는 리듀서가 한번에 존재한다.
전체적인 흐름
- createSlice로 state의 초기값들과 action creator와 reducer를 생성해준다.
- 모든 Slice들을 combineReducers으로 결합시켜서 하나로 모아준다.
- 위에서 combineReducers으로 모아준 reducer를 최종적으로 configureStore에서 반환 후 사용한다.
선언한 slice의 name에 따라서 액션 생성자, 액션 타입, 리듀서를 자동으로 생성해줍니다.
따라서 별도로 createAction이나 createReducer를 사용하지 않아도 됩니다.
slice를 생성할 때 액션 타입은 name이 앞에 붙은 형태(counter/setCounter, todo/setTodo)로 생성됩니다.
생성된 액션 타입을 가진 액션이 디스패치되면 리듀서가 실행됩니다.
import { createSlice } from "@reduxjs/toolkit";
// state의 초기값 (유저 정보)
const initialState = {
nickname: "",
email: "",
};
// userSlice라는 이름으로 유저 Slice 생성
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action) => { // 액션 타입 1
state.nickname = action.payload.nickname;
state.email = action.payload.email;
},
setUserName: (state, action) => { // 액션 타입 2
state.nickname = action.payload.nickname;
},
},
});
// actions
//dispatch로 액션을 전달해 상태를 어떻게 변화시킬지를 결정함
export const { setUser, setUserName } = userSlice.actions;
//reducer
export default userSlice.reducer;
리덕스 라이브러리의의 createStore 함수를 추상화한 것입니다.
기존의 번거로웠던 리덕스 설정을 간편하게 할 수 있도록 해주고 설정시 디폴트로 redux-thunk 와 DevTools 를 제공합니다.
: 리덕스 스토어의 rootReducer를 설정. combineReducers 함수를 사용하여 여러가지 slice reducer들을 병합한 rootReducer를 설정 가능.
단일 함수로 설정한 경우엔 스토어의 rootReducer로 사용됨.
slice reducer로 설정한 경우엔 자동으로 combineReducers에 전달하여 rootReducer를 생성함
: redux-logger와 같은 리덕스 미들웨어를 설정. 미들웨어를 설정한 경우엔 자동으로 applyMiddleware에 전달한다. 미들웨어를 설정하지 않은 경우엔 getDefaultMiddleware를 호출.
: Redux DevTools 사용 여부 설정. (기본값은 true)
: 리덕스 스토어의 초기값 설정.
: 사용자 정의 미들웨어를 설정. 콜백 함수로 설정하면 미들웨어 적용 순서를 정의 가능.
...
const reducers = combineReducers({
user: userReducer, // 리듀서 여러개 합치기
});
...
export const store = configureStore({
reducer: reducers,
devTools: process.env.NODE_ENV !== "production",
middleware: [thunk],
});
새로고침해도 state 값이 사라지지 않도록, localstorage에 reducer를 저장한다.
- localStorage에 저장하고 싶다면
import storage from 'redux-persist/lib/storage
- session Storage에 저장하고 싶으면
import storageSession from 'redux-persist/lib/storage/session
- persisConfig = { } : 새로운 persist의 선언
- key : reducer의 어느 지점에서부터 데이터를 저장할 것인지 지정
- storage : 웹의 localStorage에 저장한다는 뜻
persistReducer(persisConfig, reducer) : persisConfig가 추가된 reducer 반환
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
const persistConfig = {
key: "root",
storage,// localStorage에 저장
};
const persistedReducer = persistReducer(persistConfig, reducers);
persistStore() : 새로 고침, 종료해도 지속될 store를 생성 (로컬 스토리지에 저장하고 관리할 store)
persist store가 redux에 저장될 때까지 앱 UI 렌더링이 지연됩니다.
- loading : store를 불러오는 과정(로딩) 중에 보여줄 컴포넌트
- persistor : 로컬스토리지에 저장할 스토어
<PersistGate loading={null} persistor={persistor}>
// 생략
import { Provider } from "react-redux";
import { store } from "./store";
import { PersistGate } from "redux-persist/integration/react";
import { persistStore } from "redux-persist";
// 스토어를 export 해줘야한다. (안그럼 PersistGate가 store를 못 읽는다)
export let persistor = persistStore(store);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<React.StrictMode>
<App />
</React.StrictMode>
</PersistGate>
</Provider>,
);
로그아웃을 하면 store를 purge하는 작업을 거쳐야 한다.
persistConfig에서 localStorage에 저장한 state들을 전부 초기화 시켜주는 과정이다.
import { PURGE } from "redux-persist";
...
extraReducers: builder => {
builder.addCase(PURGE, () => initialState);
},
const purge = async () => {
location.reload();
await persistor.purge(); // persistStore의 데이터 전부 날림
store에서 내가 원하는 state만 뽑아내는 함수
액션 발생 함수
- 리듀서 안에 반드시 객체 형태로 값을 넣어야한다.
useDispatch(setUser({nickname : "~~" }))
import { useDispatch, useSelector } from "react-redux";
import { setUser } from '~~'; //리듀서
function App() {
...
// store에서 원하는 state의 현재 값 뽑아 오기
const { nickname, email } = useAppSelector(state => state.user);
const dispatch = useDispatch();
...
const changeUserInfo = () => {
// 유저 정보 업데이트
dispatch(setUser({nickname:"바뀐 이름", email:"바뀐 이메일" }));
};
return (
<>
<div>
<h1>{nickname}</h1>
<h1>{email}</h1>
<div>
<button onClick={()=>changeUserInfo()}>업데이트</button>
</div>
</div>
</>
);
};
export default App;
// 리듀서
import { createSlice } from "@reduxjs/toolkit";
import { PURGE } from "redux-persist";
const name = "UserSlice";
const initialState = {
id: "",
password: "",
username: "",
eyes: 3,
};
export const userSlice = createSlice({
name: name,
initialState,
reducers: {
setUser: (state, action) => {
state.id = action.payload.id;
state.password = action.payload.password;
state.username = action.payload.username;
state.eyes = action.payload.eyes;
},
setEye: (state, action) => {
state.eyes = action.payload.eyes;
},
initUser: state => {
state.id = initialState.id;
state.password = initialState.password;
state.username = initialState.username;
state.eyes = initialState.eyes;
},
},
extraReducers: builder => {
builder.addCase(PURGE, () => initialState);
},
});
export const { setUser, initUser, setEye } = userSlice.actions;
export default userSlice.reducer;
// 스토어
import { configureStore } from "@reduxjs/toolkit";
import { useDispatch, useSelector } from "react-redux";
import storage from "redux-persist/lib/storage";
import { combineReducers } from "redux";
import { persistReducer } from "redux-persist";
import thunk from "redux-thunk";
import userReducer from "./features/userSlice";
const reducers = combineReducers({
user: userReducer,
});
const persistConfig = {
key: "root",
storage,
};
const persistedReducer = persistReducer(persistConfig, reducers);
export const store = configureStore({
reducer: persistedReducer,
devTools: process.env.NODE_ENV !== "production",
middleware: [thunk],
});
export const useAppDispatch = () => useDispatch();
export const useAppSelector = useSelector;
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import { store } from "./store";
import { PersistGate } from "redux-persist/integration/react";
import { persistStore } from "redux-persist";
export let persistor = persistStore(store);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<React.StrictMode>
<App />
</React.StrictMode>
</PersistGate>
</Provider>,
);
import React from "react";
import { useAppDispatch, useAppSelector } from "../store"; // index
import { initUser } from "../store/features/userSlice";
import { persistor } from "../";
import { setUser } from "../store/features/userSlice";
const Authpage = () => {
const { id, username, eyes, password } = useAppSelector(state => state.user);
const dispatch = useAppDispatch();
const setUserInfo = () => {
dispatch(
setUser({
id: 11,
username: "마루",
password: "1234",
eyes: 0,
}),
);
};
// 로그아웃에 사용
const purge = async () => {
location.reload();
await persistor.purge(); // persistStore의 데이터 전부 날림
};
return (
<div>
<h3>
{id}, {username},{eyes},{password}
</h3>
<button onClick={() => setUserInfo()}>변경</button>
<button onClick={async () => purge()}>로그아웃</button>
</div>
);
};
export default Authpage;