React 새로고침시 로그인 유지하기(JWT, Cookie, Redux)

yunbiyomi·2024년 2월 13일






지난 번에 토큰을 쿠키에 저장하는 부분까지 구현해 보았다.
이번에는 로그인 기능뿐만 아니라 사용자가 다른 페이지로 이동하거나 페이지를 새로고침해도 로그인 상태를 유지할 수 있도록 구현해보려고 한다.

작년에 팀 프로젝트를 할 때 상태 관리 라이브러리로 Recoil을 사용해본 경험이 있어
이번 싸커 마켓에서는 Redux를 활용해 구현할 것이다.





📌 Redux

🌐 공식 문서 : https://ko.redux.js.org/introduction/getting-started

Redux는 JavaScript 애플리케이션의 상태 관리를 위한 예측 가능하고 중앙 집중식인 상태 컨테이너 라이브러리이다.

애플리케이션의 상태를 예측 가능하고 효율적으로 관리할 수 있도록 도와주는 강력한 도구이며,
상태의 중앙 집중식 관리와 단방향 데이터 흐름을 통해 애플리케이션의 복잡성을 줄이고 유지 보수성을 향상시켜준다.



🔷 핵심 개념

◼️ Single Source of Truth (단일 출처의 진리)

  • Redux 애플리케이션에서는 상태가 단일 Store에 저장됨

  • 이 Store에 있는 상태가 애플리케이션의 전역 상태를 나타내며, 여러 컴포넌트가 이 상태에 접근 가능

  • 애플리케이션의 상태를 중앙 집중식으로 관리하고, 상태의 일관성과 예측 가능성 보장


◼️ States are Read-Only (상태는 읽기 전용)

  • Redux에서는 상태를 직접 수정하는 것이 아니라 Action을 통해 상태를 변경함

  • 상태를 변경하기 위해선 Action을 발생시켜야 하며, 이 Action은 Reducer에 의해 처리됨

  • 상태의 불변성을 유지하고, 상태 변경을 추적하기 쉽게함


◼️ Changes are Mace with Pure Functions(변경은 순수 함수로 이루어저야 한다)

  • Redux에서 상태를 변경하는 로직은 순수 함수로 작성되어야함

  • 순수 함수란 이전 상태와 Action을 받아서 새로운 상태를 반환하는 함수

  • 순수 함수는 외부의 상태를 변경하지 않으며, 동일한 입력에 대해서 항상 동일한 출력 반환

  • 예측 가능하고 테스트 가능한 상태 관리를 제공



🔷 주요 용어

◼️ Store

Redux의 상태를 단일 Store에 저장하는 객체로
애플리케이션의 전역 상태를 보유하고 있으며, 상태를 읽고 업데이트 하는 메서드가 포함되어 있다.

◼️ Action

Redux 애플리케이션에서 상태를 변경하기 위해 발생하는 이벤트로
일반적으로 JavaScript 객체로 표현되며, 반드시 type 필드를 가져야한다.

◼️ Reducer

이전 상태와 Action을 받아 새로운 상태를 반환하는 순수 함수로
애플리케이션의 상태를 변경하는 로직을 포함하고 있으며, Store에 등록된다.
Redux에서는 여러 개의 Reducer를 조합하여 하나의 root Reducer를 만들어야한다.

◼️ Dispatch

Store에 Action을 보내는 메서드로
Action을 Dispatch하면 Store는 해당 Action을 받아서 Reducer에 전달하고, 상태를 업데이트한다.

◼️ Subscribe

Store의 상태 변경을 감지하고 이에 반응하는 콜백 함수를 등록하는 메서드로
Subscribe를 통해 등록된 콜백 함수는 상태가 변경될 때마다 실행된다.





📌 Redux 구현

  1. Redux 설정하기
  2. 상태(State) 정의하기
  3. 액션(Action) 정의하기
  4. 리듀서(Reducer) 정의하기
  5. Redux Store 생성하기
  6. Component와 Redux 연결하기

다음과 같은 순서대로 구현하였다.



🔷 설치

먼저 Redux를 설치해준다.

npm install redux



🔷 State 정의

isLogIn, token, memberType과 같은 사용자 인증 상태를 저장할 Redux 상태를 정의해준다.

const initialState = {
  isLogIn: false,
  token: null,
  memberType: ''
}

const authReducer = ( state=initialState, action ) => {
  switch(action.type) {
    case LOGIN:
      return {
        ...state,
        isLogIn: true,
        token: action.payload.token,
        memberType: action.payload.memberType
      }
    case LOGOUT: 
      return {
        ...state,
        isLogIn: false,
        token: null,
        memberType: ''
      }
    default:
      return state;
  }
}

export default authReducer



🔷 Action 정의

login Action과 logout Action을 정의해주고 각 Action의 알맞은 Type 작성해준다.


export const LOGIN = 'LOGIN';
export const LOGOUT = 'LOGOUT';

export function login(token, memberType) {
  return {
    type: LOGIN,
    payload: {
      token,
      memberType
    }
  }
}

export function logout() {
  return {
    type: LOGOUT
  }
}



🔷 Reducer 정의

Redux에서 Store에는 하나의 Reducer만 연결 가능하기 때문에 combineReducers 함수를 이용해서 rootReducer에 Reducer를 통합해주었다.

지금은 하나의 Reducer만 존재하지만Reducer를 추가로 생성하게되면 rootReducer에 추가해주면된다.


const rootReducer = combineReducers({
  auth: authReducer
})

export default rootReducer



🔷 Store 생성

Redux Store를 생성하고 초기 상태를 rootReducer의 반환 값으로 설정해준다.

store.subscribe(()=>{}) 이부분은 store.subscribe 메서드를 사용하여 Store의 상태 변경을 구독해준다.
Store의 상태 변경이 제대로 이루어지고 있는지 확인하기위해 잠시 넣어주었다.


const store = createStore(rootReducer);

store.subscribe(() => {
  console.log('Updated state: ', store.getState());
})

export default store;



🔷 Component와 Redux 연결

생성된 Redux Store를 전역으로 제공하기 위해 애플리케이션의 최상위 Component에서 Provider Component를 활용한다.
이를 통해 하위 Component들이 Redux 상태에 쉽게 액세스 할 수 있다.


// index.js

const container = document.getElementById("root");
const root = createRoot(container);
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store={store}>
        <App />
      </Provider>
    </BrowserRouter>
  </React.StrictMode>
);



🔷 로그인

useDispatch 훅을 사용하여 Dispatch 함수를 가져오고,
이를 통해 적절한 Redux Action을 Dispatch 해주면 된다.

아래 코드의 경우 로그인 기능이므로 token 값과 memberType 값을 반환하여 login Action을 Dispatch 해주었다.

/// Login
const dispatch = useDispatch();

const handleLogIn = async () => {
  const formData = {
        username: userName,
        password: password,
        login_type: memberType,
  }

  try {
        /// ...	
        const token = response.data.token;
        /// ...
          dispatch(login(token, memberType));
        }
        navigate(-1);
      } 
	catch (error) {
       /...
      }
}



🔷 로그아웃

로그아웃 또한 logout Action을 Dispatch 해주었다.

/// Logout
const dispatch = useDispatch();

const handleLogOut = async () => {
    try {
      const response = await axios.post(`/accounts/logout/`);
      console.log(response);
      removeCookie('token', { path: "/"});
      dispatch(logout());
      alert(response.data.detail);
      navigate('/');
      window.location.reload();
    } catch (error) {
      console.error('로그아웃 오류: ', error.response.data);
    }
 }





📌 로그인 유지 구현

이제 이번 글의 핵심인 로그인 유지를 구현할 차례이다.

이전 글인 'JWT 기반 인증 로그인 구현하기'에서 token을 Cookie에 저장하는 것까지 확인 할 수 있다.

로그인 유지는 Cookie에 저장된 token의 유무를 확인하여 처리된다.

만약 token이 존재한다면, getCookie 함수를 이용해 token 값과 memberType값 을 가져온 후 login Action을 Dispatch 해주면 된다.
이러한 과정은 useEffect를 사용하여 Dispatch 값이 변경될 때마다 실행되도록 설정해준다.

위에 로그아웃 코드를 보면
로그아웃 시 removeCookie 함수를 이용해 Cookie에 저장된 token 값을 지워준다.

그래서 한번 로그인 한 상태에서는 로그아웃을 할 때까지 Cookie에 token 값이 존재하기 때문에
페이지를 새로 고침하거나 다른 페이지로 이동하더라도 로그인 상태가 유지된다.


/// App.js
const dispatch = useDispatch();

useEffect(() => {
  const token = getCookie('token');
  const memberType = getCookie('memberType');

  if(token)
    dispatch(login(token, memberType));
}, [dispatch])






참고
https://hanamon.kr/redux%EB%9E%80-%EB%A6%AC%EB%8D%95%EC%8A%A4-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC/

profile
새로움을 두려워 하지 않는 도전하는 프론트엔드 개발자👩‍💻

0개의 댓글