React-10 로그인 상태 관리 (23/03/09)

nazzzo·2023년 3월 9일
1

로그인 상태 관리


let isLogin : false -> true

리액트에서 말하는 상태(state)란 캐시나 메모리와 같은 역할을 하는 자바스크립트 변수입니다
하지만 자바스크립트 변수는 브라우저를 새로고침하면 초기화된다는 문제가 있는데요


그래서 오늘 포스팅의 관건은 로그인 상태(true)를 새로고침해도 유지되도록 만드는 것
이를 위해서는 로그인 상태를 로컬 스토리지에 저장해야 합니다


  • 로컬 스토리지를 사용하는 이유?
    쿠키와 달리 브라우저의 데이터가 서버에 전송되지 않는다는 측면에서 보안성이 더 높습니다
    쿠키는 응답 결과인 토큰을 저장하는 용도로, 로컬 스토리지는 로그인 상태변수 저장하는 용도로 복합적으로 사용합니다

+) 토큰을 쿠키에 저장하는 로직은 백엔드에서 처리하는 것이 좋습니다
(프론트에서 document.cookie를 사용하는 방법도 가능합니다만
리액트에서는 document 객체를 잘 사용하지 않기도 하고, 또 보안적인 이유도 크다고 합니다)


↓ 토큰 저장 예제 코드입니다

res.setHeader('Set-Cookie', `token=${token}; expires=${date}; path='/'`);
res.status(200).send()


// express에서 cookie-parser를 사용중이라면 
res.cookie("token", token, { maxAge : 600_000, path : "/" });

+) 요청시 CORS 처리에도 주의할 것!

request.post(
      "/auths",
      {
        userid: userid.value,
        userpw: userpw.value,
      },
      { withCredentials: true }
    );



1. 로그인



  1. 로컬 스토리지에서 데이터 존재여부를 먼저 확인합니다
const storageState = localStorage.getItem("state")
console.log(storageState) // null

  1. localStorage.getItem : 데이터가 없으면 기존의 initialState를 사용하기
    (isLogin: false)
  const storageState = localStorage.getItem("state");
  console.log(storageState);

  const initialState = {
    isLogin: false,
    user: {},
  };

  // 로컬스토리지의 데이터 타입은 스트링
  const initial = !storageState ? initialState : JSON.parse(storageState);

  const [state, dispatch] = useReducer(rootReducer, initial);

이제 로컬스토리지에 데이터가 존재한다면 앞으로는 로컬스토리지에 저장된 상태를 기본 상태로 사용하게 됩니다


  1. localStorage.setItem :dispach 함수 수정하기
  const [state, dispatch] = useReducer(rootReducer, initial);

  const globalState = {
    state, 
    dispatch: (action) => {
      dispatch(action);
      // 로컬 스토리지에 관련된 코드 작성
        // setItem을 할 때는 key, value 형태로 입력해야 합니다
      localStorage.setItem('state', JSON.stringify(rootReducer(state, action)));
    }
  }

  return (
    <Context.Provider value={globalState}>{children}</Context.Provider>
  );
};

{isLogin: true}인 객체를 로컬스토리지에 저장합니다



2. 로그아웃


[Logout.jsx]

import { useStore } from "../store"
import { useEffect } from "react"
import { useNavigate } from "react-router-dom"

export const Logout = () => {
    const { dispatch } = useStore()
    const navigate = useNavigate()

    useEffect(()=> {
        dispatch({ type: "LOGOUT"})
        document.cookie = "token=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;";
        navigate("/")
    }, [])
}

[reduce.jsx]

export const rootReducer = (state, action) => {
  switch (action.type) {
    case "LOGIN":
      return { ...state, isLogin: action.payload };
    case "LOGOUT":
      return { ...state, isLogin: false };
    default:
      return state;
  }
};

쿠키 삭제는 백엔드 요청 없이 프론트 측에서 document.cookie를 사용하기도 한다고 하네요



3. 커스텀 훅


로컬스토리지의 상태 저장 로직을 커스텀 훅으로 만들어서 사용합니다



[usePersistedState.jsx]

import { useState, useEffect } from "react";

// localstorage의 key와 value(상태)
export const usePersistedState = (key, initialState) => {
  const [state, setState] = useState(() => {
    const storagedState = localStorage.getItem(key);
    return !storagedState ? initialState : JSON.parse(storagedState);
  });

  // state가 바뀔 때 실행할 코드
  useEffect(() => {
    localStorage.setItem("state", JSON.stringify(state));
  }, [key, state]);

  return [state, setState];
};

[store/index.jsx]

import { createContext, useContext, useReducer } from "react";
import { rootReducer } from "./reducer";
import { usePersistedState } from "../hooks/usePersistedState";

export const Context = createContext();
export const useStore = () => useContext(Context);

export const StoreProvider = ({ children }) => {
  // const storageState = localStorage.getItem("state");

  const initialState = {
    isLogin: false,
    user: {},
  };

  // const initial = !storageState ? initialState : JSON.parse(storageState);
  const [state, dispatch] = useReducer(rootReducer, initialState);
  const [persistedState, setPersistedState] = usePersistedState("state", state)

  const globalState = {
    state : persistedState,
    dispatch: (action) => {
      dispatch(action);
      // localStorage.setItem('state', JSON.stringify(rootReducer(state, action)));
      setPersistedState(rootReducer(persistedState, action))
    },
  };

  return <Context.Provider value={globalState}>{children}</Context.Provider>;
};




  • useStateusePersistedState를 구분해서 globalState에서 동기화하는 이유_

이유는 두 상태관리 함수의 용도가 다르기 때문입니다
useState는 컴포넌트의 상태를 관리하는 데 사용되는 일반적인 상태관리 함수이며,
usePersistedState는 localStorage에 데이터를 저장하는 용도로 사용합니다
(새로고침으로 컴포넌트가 리렌더링될 때에도 상태를 유지하기 위해서)

그리고 서로 다른 상태인 statepersistedStateuseEffect를 이용해서 동기화하는 구조입니다
state가 업데이트될 때마다 localStorage에 데이터를 저장하고,
key가 변경될 때마다 localStorage에서 데이터를 가져와서 state를 초기화합니다


주의할 점!
reducer 함수는 업데이트된 상태 객체를 리턴하는 함수이지, 상태를 바꾸는 함수가 아닙니다
실제로 상태를 바꾸는 역할은 그것을 전달받은 dispatch(=setState)가 담당합니다


이런 식으로 상태를 분리하고 동기화하는 로직은 리액트에서 종종 사용되기 때문에 이유를 납득해야 합니다 ㅠㅠ



0개의 댓글