로그인, 로그아웃을 구현하는 중 로그인 시에는 access token을 redux store에 저장하여 전역상태로 관리하고 로그아웃 시에는 저장되어있는 access token을 null로 변경하기 위한 작업이 필요했습니다.
refresh token은 http only 옵션으로 프론트에서 직접 관리할 수 없고, axios interceptor를 활용하여 통신시 request에 access token을 항상 전달하고 response로 refresh token으로 인해 갱신되는 access token을 다시 redux store에 저장하는 작업을 진행했습니다.
우선, redux store에 상태를 저장하고 새로고침을 한다면 저장된 상태는 사라지게 되나 새로고침을 해도 저장된 상태를 사라지지 않기 위해서는 redux-persist
라이브러리가 필요합니다.
redux-toolkit, redux-persist 공식 문서에도 사용하는 방법이 나와있습니다. 이를 조합하여 사용한 코드는 아래와 같습니다.
// index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { createHashRouter, RouterProvider } from "react-router-dom";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { persistor, store } from "./app/store";
...
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RouterProvider router={router} />
</PersistGate>
</Provider>
);
// .store/accessTokenSlice.ts
import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import { PURGE } from "redux-persist";
export type accessToken = {
accessTokenValue: string | null;
};
const initialState: accessToken = {
accessTokenValue: null,
};
export const accessTokenSlice = createSlice({
name: "access token",
initialState,
reducers: {
getAccessToken: (state, action: PayloadAction<string | null>) => {
state.accessTokenValue = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(PURGE, (state) => {
return initialState;
});
},
});
export const { getAccessToken } = accessTokenSlice.actions;
export default accessTokenSlice.reducer;
// ./app/store.ts
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import {
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
persistStore,
} from "redux-persist";
import storage from "redux-persist/lib/storage";
import accessTokenReducer from "../store/api/accessTokenSlice";
const accessTokenPersistConfig = {
key: "root",
storage
};
const persistedAcceessTokenReducer = persistReducer(
accessTokenPersistConfig,
accessTokenReducer
);
export const store = configureStore({
reducer: {
accessTokenValue: persistedAcceessTokenReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
})
});
setupListeners(store.dispatch);
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Logout.tsx
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { persistor, RootState } from "../../../app/store";
import { useLogoutMutation } from "../../../services/user"; // RTK-query 를 사용하여 로그아웃을 구현하였습니다.
const Logout = () => {
const navigate = useNavigate();
const [logout] = useLogoutMutation();
const signOut = async () => {
try {
const result = await logout(null).unwrap();
await persistor.purge();
navigate("/");
} catch (error) {
// 에러처리
}
};
return (
<button type="button" onClick={signOut}>Logout</button>
...
)
export default Logout;
reference
redux-persist 공식문서
redux toolkit 공식문서
https://velog.io/@s_yeah/redux-persist%EC%99%80-persistor.purge%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-redux-toolkit