본문 바로가기
웹 프로그래밍/👨‍👨‍👧‍👧소셜 가계부

[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 개인정보, 토큰 저장 방식 변경1 (LocalStorage → redux-persist)

by 청량리 물냉면 2024. 3. 31.
반응형

기존의 프로젝트에서, 사용자의 토큰값과 토큰 유효기간, 사용자 정보를 브라우저에 저장하기 위한 저장소로 Local Storage를 택했다.

그러나 이런 식으로 개인정보를 저장하면 보안에 취약하다는 말을 듣고 다른 방식으로 유저 데이터를 저장할 수 있는 방법을 찾아 구현해 보기로 했다. 

 

 

기존 프로젝트의 구조 살펴보기

우선 현재 소셜 가계부 프로젝트의 구조는 다음과 같다. 

//Auth.jsx

  const handleAuthSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);

    let responseData;
    try {
      if (isLoginMode) {
        responseData = await loginAPI(form);
      } else {
        responseData = await signupAPI(form);
      }

      dispatch(loginSuccess());
      dispatch(setUserInfo(responseData.userInfo));
      dispatch(setToken(responseData.token));

      //만료시간 3시간
      const tokenExpirationDate = tokenExpiration || new Date(new Date().getTime() + 3 * 1000 * 60 * 60);
      dispatch(setTokenExpiration(tokenExpirationDate));

      localStorage.setItem(
        "userData",
        JSON.stringify({
          userInfo: responseData.userInfo,
          token: responseData.token,
          tokenExpiration: tokenExpirationDate.toISOString(),
        }),
      );
      setIsLoading(false);
      setErrorMsg(null);
      nav("/", { replace: true });
    } catch (err) {
      setIsLoading(false);
      setErrorMsg(err.message || "회원가입 진행 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
    }
  };

`Auth`페이지의 `handleAuthSubmit` 함수에서는 로그인 및 회원가입 API를 호출하고, `responseData` 변수에 반환된 값을 저장한다. 반환된 유저 데이터에는 추후 프로젝트 여기저기서 사용될 유저 정보가 담겨 있다.

반환된 유저 데이터는 `setItem` 함수를 이용해 로컬 스토리지에 저장하였다.

 

//AppRouter.tsx

  useEffect(() => {
    const storedData = JSON.parse(localStorage.getItem("userData") || "{}");
    if (storedData && storedData.token && new Date(storedData.tokenExpiration) > new Date()) {
      dispatch(loginSuccess());
      dispatch(setUserInfo(storedData.userInfo));
      dispatch(setToken(storedData.token));
      dispatch(setTokenExpiration(storedData.tokenExpiration));
    }

    setIsLoading(false);
  }, [logoutHandler, dispatch]);

이후 `AppRouter.tsx`에서 Local Storage의 정보를 사용한다.

`Auth` 페이지에서 저장한 Local Storage의 내용이 존재하는지, 존재한다면 내부에 토큰 데이터 있는지, 토큰 유효기간이 지나지 않았는지 확인하고 모든 조건이 충족하면 Local Storage의 내용을 리덕스 user 스토어에 저장해 주었다. 

 

//Sidebar.jsx

  const handleLogout = useCallback(() => {
    dispatch(logout());
    localStorage.removeItem("userData");
    alert("로그아웃되었습니다!");
    nav("/", { replace: true });
  }, [dispatch, nav]);

로그아웃 시 Local Storage에 저장되어 있던 userData를 삭제하고 모든 store data를 초기화시키는 코드이다.

 

그런데 여기까지 찬찬히 기존 코드를 다시 살펴보다 보니, 굳이 Local Storage에 따로 데이터를 저장해야 하는지에 대한 의문이 들었다. 어차피 상태관리 store에도 유저 데이터가 저장되고 있기 때문이다.

그렇다면 지금처럼 데이터를 Local Storage, 리덕스에 각각 따로 저장하는 대신 리덕스 하나에만 저장한 뒤 그 리덕스 정보를 브라우저에 저장하는 게 낫겠다는 생각이 들었다.

 

 

리덕스 데이터를 어떻게 브라우저에 저장할까?

리덕스를 이용보고자 하는 결정을 내린 뒤, 리덕스 데이터를 브라우저에 저장하는 방법 + 보안 방법에 대해 서칭하기 시작했다. 그러다 아래 포스팅을 발견했다.

 

Redux 실제 서비스에서는 이렇게 쓴다

Redux 시리즈 1편

velog.io

블로그에서 리덕스를 사용하는 방법 중 `persist`와 `crpytoJS`에 대한 설명이 눈길을 끌었다. 

 

🤔 persist ??

리덕스의 정보는 새로 고침이나 브라우저 탭을 닫으면 소실된다.

하지만 `persist`를 사용하면 로컬 스토리지처럼 브라우저 내부에 정보를 저장할 수 있다.

 

🤔 cryptoJS ??

`cryptoJS`는 리덕스에 토큰 및 사용자 정보와 같은 민감 정보를 저장할 때 정보를 암호화해주는 라이브러리라고 한다.

 

😀 결정!

persist, cryptoJS 라는 개념은 처음 접해 보는데, 기존 데이터 방식을 대체하는 방법으로는 딱이라고 여겨졌다.

특히 `persist`가 여전히 Local Storage를 이용하는 방식이기 때문에 데이터 보안을 위해서는 추가적인 장치가 꼭 필요한데 `cryptoJS`로 데이터를 암호화함으로써 이 부분을 해결할 수 있다니 매우 좋다.

그래서 해당 라이브러리들을 사용하여 브라우저에 리덕스 데이터를 저장+암호화까지 진행해 보기로 했다.

 

 

구현 시작

redux-persist

우선 라이브러리를 설치해 주었다.

npm install redux-persist
 

redux-persist

persist and rehydrate redux stores. Latest version: 6.0.0, last published: 5 years ago. Start using redux-persist in your project by running `npm i redux-persist`. There are 1274 other projects in the npm registry using redux-persist.

www.npmjs.com

 

react-redux 사용법, redux, react, react16, state management, flux, store, reducer, dispatch, action

react-redux 사용법, redux, react, react16, state management, flux, store, reducer, dispatch, action

kyounghwan01.github.io

 

위 매뉴얼과 블로그를 따라서 차근차근 코드를 변경해 나갔다.

가장 먼저 변경할 부분은 모든 슬라이스 리듀서를 통합하여 사용할 수 있게 해주는 `rootReducer` 파일이다.

//rootReducer.js

import { combineReducers } from "redux";
import { persistReducer } from "redux-persist";  //추가
import storage from "redux-persist/lib/storage"; //추가

import userReducer from "./user";
import transactionReducer from "./transactions";
import transactionAnalyticsReducer from "./transactionAnalytics";

 //추가
const persistConfig = {
  key: "root",
  whitelist: ["user"],
  storage,
};

const rootReducer = combineReducers({
  user: userReducer,
  transactions: transactionReducer,
  transactionAnalytics: transactionAnalyticsReducer,
});

//기존
//export default rootReducer;
export default persistReducer(persistConfig, rootReducer);

`key`는 Local Storage에 리덕스 데이터가 저장될 때의 키값이다.

`witelist`는 Local Storage에 저장하고자 하는 데이터이다. 나는 유저 데이터만 브라우저에 저장하기를 원하기 때문에 `user`를 넣었다. (아래 `rootReducer` 내부에서 통합되고 있는 리듀서의 이름이다)

반대로 `blaklist` 속성도 있다고 한다. `witelist` 속성과 달리 절대 로컬 스토리지에 저장하지 않을 데이터를 지정하는 속성이라고 한다. 

`storage`는 내가 Local Storage에 데이터를 저장하기 원하기 때문에 입력해 준 속성이다.

 

//index.js

import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import { createStore } from "redux";
import rootReducer from "./modules/rootReducer";
import { persistStore } from "redux-persist"; //추가
import { PersistGate } from "redux-persist/integration/react"; //추가

const store = createStore(rootReducer);
const persistor = persistStore(store); //추가

createRoot(document.getElementById("root")).render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}> //추가
      {console.log(store.getState())}
      <App />
    </PersistGate>
  </Provider>,
);

store를 생성해 전역으로 전달하던 최상위 파일 `index.js`도 바꾸어준다.

기존 `<Provider>` 태그 아래에 `<PersistGate>` 태그를 이용해 persistor 역시 모든 컴포넌트에 전달해 준다. 

 

Local Storage 관련 코드 삭제해보기 & 실행

//Auth.jsx

  const handleAuthSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);

    let responseData;
    try {
      if (isLoginMode) {
        responseData = await loginAPI(form);
      } else {
        responseData = await signupAPI(form);
      }

      dispatch(loginSuccess());
      dispatch(setUserInfo(responseData.userInfo));
      dispatch(setToken(responseData.token));

      //만료시간 3시간
      const tokenExpirationDate = tokenExpiration || new Date(new Date().getTime() + 3 * 1000 * 60 * 60);
      dispatch(setTokenExpiration(tokenExpirationDate.toISOString()));

      // localStorage.setItem(
      //   "userData",
      //   JSON.stringify({
      //     userInfo: responseData.userInfo,
      //     token: responseData.token,
      //     tokenExpiration: tokenExpirationDate.toISOString(),
      //   }),
      // );
      
      setIsLoading(false);
      setErrorMsg(null);
      nav("/", { replace: true });
    } catch (err) {
      setIsLoading(false);
      setErrorMsg(err.message || "회원가입 진행 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
    }
  };

Local Storage 사용하는 부분을 통째로 주석처리 했다.

 

//AppRouter.tsx

  useEffect(() => {
    //기존
    //const storedData = JSON.parse(localStorage.getItem("userData") || "{}");
    const storedData = JSON.parse(localStorage.getItem("root") || "{}");
    if (storedData && storedData.token && new Date(storedData.tokenExpiration) > new Date()) {
      dispatch(loginSuccess());
      dispatch(setUserInfo(storedData.userInfo));
      dispatch(setToken(storedData.token));
      dispatch(setTokenExpiration(storedData.tokenExpiration));
    }

    setIsLoading(false);
  }, [logoutHandler, dispatch]);

여기도 마찬가지.

기존과 같은 방법(API로 가져온 데이터를 바로 Local Storage에 저장)을 사용하는 대신, 리덕스 데이터를 Local Storage에 저장해 보았다.

실제 로그인이 잘 되며 Local Storage에 유저의 리덕스 정보도 잘 저장되고 있음을 확인할 수 있다.

 

 

로그아웃

찾아보니 `purge`라는 함수로 Local Storage에 저장된 리덕스 값을 삭제할 수 있는 모양이다

 

How to remove value from localStorage in redux-persist?

I have index.js with persist configuration: import {configureStore, combineReducers} from "@reduxjs/toolkit" import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE,

stackoverflow.com

 

redux-persist와 persistor.purge()로 로그아웃 구현하기 (redux-toolkit )

redux-persist로 먼저 새로고침하더라도 redux의 상태들이 초기화되지 않도록 하는 과정을 먼저 거쳐야 한다. npm으로 설치하기rootReducer.jsstore.js주의: 발생할 수 있는 에러when using a middleware builder func

velog.io

 

기존에 로그아웃 코드를 작성해 놓았던 `Sidebar` 컴포넌트로 가서 `purge`함수를 작성 후 실제 로그아웃을 실행해 봤다.

//Sidebar.jsx

import { persistor } from "..";

function Sidebar() {
  ...

  //추가
  const purge = async () => {
    await persistor.purge();
  };

  const handleLogout = useCallback(async () => {
    dispatch(logout());
    await setTimeout(() => purge(), 200); //추가
    alert("로그아웃되었습니다!");
    nav("/", { replace: true });
  }, [dispatch, nav]);

 

 

 

자동 로그아웃

//AppRouter.tsx

  const logoutHandler = useCallback(async () => {
    dispatch(logout());
    await setTimeout(() => purge(), 200);
  }, [dispatch]);

자동 로그아웃까지 잘 동작한다.

아래 gif의 경우 토큰 만료 시간을 3초로 잡고 실험해 본 결과물이다.

 

Local Storage에 저장된 리덕스 데이터를 암호화하는 일은 다음 시간에...

반응형