관리 메뉴

거니의 velog

(8) 프론트엔드 프로젝트 : 시작 및 회원 인증 구현 8 본문

React_프론트엔드 프로젝트

(8) 프론트엔드 프로젝트 : 시작 및 회원 인증 구현 8

Unlimited00 2024. 2. 23. 12:48

(2) 로그인 상태를 보여 주고 유지하기

* 로그인 페이지에서 로그인에 성공하면 헤더 컴포넌트에서 로그인 중인 상태를 보여 주고, 새로고침을 해도 이 상태가 유지되도록 해 보자.

[1] 로그인 상태 보여주기

* 먼저 헤더 컴포넌트에 리덕스를 연결시켜 보자. containers 디렉터리에 common 디렉터리를 만들고, 그 안에 HeaderContainer 컴포넌트를 작성해 보자.

import React from 'react';
import { useSelector } from 'react-redux';
import Header from '../../components/common/Header';

const HeaderContainer = () => {
  const { user } = useSelector(({ user }) => ({ user: user.user }));
  return <Header user={user} />;
};

export default HeaderContainer;

* 헤더 컴포넌트에서 user 값이 주어질 경우 계정명과 로그아웃 버튼을 보여 주도록 수정하자.

[components/common/Header.js]

import React from 'react';
import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';
import { Link } from 'react-router-dom';

(...)

const UserInfo = styled.div`
  font-weight: 800;
  margin-right: 1rem;
`;

const Header = ({ user }) => {
  return (
    <>
      <HeaderBlock>
        <Wrapper>
          <Link to="/" className="logo">
            REACTERS
          </Link>
          {user ? (
            <div className="right">
              <UserInfo>{user.username}</UserInfo>
              <Button>로그아웃</Button>
            </div>
          ) : (
            <div className="right">
              <Button to="/login">로그인</Button>
            </div>
          )}
        </Wrapper>
      </HeaderBlock>
      <Spacer />
    </>
  );
};

export default Header;

* 다음으로 PostListPage에서 Header 컴포넌트를 HeaderContainer로 대체하자.

[pages/PostListPage.js]

import React from 'react';
import HeaderContainer from '../containers/common/HeaderContainer';

const PostListPage = () => {
  return (
    <div>
      <HeaderContainer />
      <div>안녕하세요.</div>
    </div>
  );
};

export default PostListPage;

* 여기까지 작업한 뒤 로그인 페이지에서 로그인해 보자. 헤더 컴포넌트가 다음과 같이 나타날 것이다.

로그인 상태

* 그런데 여기서 새로고침을 하면 상태가 초기화된다. 이를 유지시켜야만 한다.

[2] 로그인 상태 유지하기

* 로그인 상태를 유지하기 위해 브라우저에 내장되어 있는 localStorage를 사용할 것이다.

* LoginForm과 RegisterForm을 다음과 같이 수정해 주자.

[containers/auth/LoginForm.js]

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { changeField, initializeForm, login } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';
import { check } from '../../modules/user';

const LoginForm = ({ history }) => {
  (...)
  
  // 회원가입 성공/실패 처리
  // user 값이 잘 설정되었는지 확인
  
  useEffect(() => {
    if (user) {
      history.push('/');
      try {
        localStorage.setItem('user', JSON.stringify(user));
      } catch (e) {
        console.log('localStorage is not working');
      }
    }
  }, [history, user]);

  return (...);
};

export default withRouter(LoginForm);

* 회원가입 및 로그인을 하면 사용자 정보를 localStorage에 저장하도록 작업해 주었다. 페이지를 새로고침했을 때도 로그인 상태를 유지하려면, 리액트 앱이 브라우저에서 맨 처음 렌더링될 때 localStorage에서 값을 불러와 리덕스 스토어 안에 넣도록 구현해 주어야 한다.

* 이 작업은 App 컴포넌트에서 useEffect를 사용하여 처리하거나, App 컴포넌트를 클래스형 컴포넌트로 변환하여 componentDidMount 메서드를 만들고 그 안에서 처리해도 된다. 하지만 여기서는 프로젝트의 엔트리 파일인 index.js 에서 처리해 줄 것이다.

* 왜냐하면, componentDidMount와 useEffect는 컴포넌트가 한 번 렌더링된 이후에 실행되기 때문이다. 이 경우에는 사용자가 아주 짧은 깜박임 현상(로그인이 나타났다가 로그아웃이 나타나는 현상)을 경험할 수 있다. index.js에서 사용자 정보를 불러오도록 처리하고 컴포넌트를 렌더링하면 이러한 깜박임 현상이 발생하지 않는다.

* index.js를 다음과 같이 수정해 보자.

[src/index.js]

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import rootReducer, { rootSaga } from './modules';
import { tempSetUser, check } from './modules/user';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(sagaMiddleware)),
);

function loadUser() {
  try {
    const user = localStorage.getItem('user');
    if (!user) return; // 로그인 상태가 아니라면 아무것도 안함

    store.dispatch(tempSetUser(user));
    store.dispatch(check());
  } catch (e) {
    console.log('localStorage is not working');
  }
}

sagaMiddleware.run(rootSaga);
loadUser();

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById('root'),
);

* 위 코드를 작성할 때는 sagaMiddleware.run이 호출된 이후에 loadUser 함수를 호출하는 것이 중요하다. loadUser 함수를 먼저 호출하면 CHECK 액션을 디스패치했을 때 사가에서 이를 제대로 처리하지 않는다.

* 이제 로그인하고 나서 새로고침을 해 보자. 로그인 상태가 잘 유지되는가? 리덕스 개발자 도구를 통해 어떤 액션이 디스패치 되었는지, 리덕스 스토어는 어떤 상태를 가지고 있는지 확인해 보자.

로그인 상태 유지

* 현재 페이지가 새로고침될 때 localStorage에 사용자 정보가 들어 있다면 그 사용자 값을 리덕스 스토어에 넣는다. 그러고 나서 정말 사용자가 로그인 상태인지 CHECK 액션을 디스패치하여 검증하도록 했다.

* CHECK 액션이 디스패치되면 사가를 통해 /api/check API를 호출한다. 이 API는 성공할 수도 있고, 실패할 수도 있다. 만약 실패하면, 사용자 상태를 초기화해야 하고 localStorage에 들어 있는 값도 지워 주어야 한다.

[3] 로그인 검증 실패 시 정보 초기화

* 로그인 정보가 만료되었을 때를 대비하여 사용자 정보를 초기화하는 작업을 해 보겠다.

[modules/user.js]

import { createAction, handleActions } from 'redux-actions';
import { takeLatest } from 'redux-saga/effects';
import * as authAPI from '../lib/api/auth';
import createRequestSaga, {
  createRequestActionTypes,
} from '../lib/createRequestSaga';

const TEMP_SET_USER = 'user/TEMP_SET_USER'; // 새로고침 이후 임시 로그인 처리
// 회원 정보 확인
const [CHECK, CHECK_SUCCESS, CHECK_FAILURE] =
  createRequestActionTypes('user/CHECK');

export const tempSetUser = createAction(TEMP_SET_USER, (user) => user);
export const check = createAction(CHECK);

const checkSaga = createRequestSaga(CHECK, authAPI.check);

function checkFailureSaga() {
  try {
    localStorage.removeItem('user'); // localStorage 에서 user 제거
  } catch (e) {
    console.log('localStorage is not working');
  }
}

export function* userSaga() {
  yield takeLatest(CHECK, checkSaga);
  yield takeLatest(CHECK_FAILURE, checkFailureSaga);
}

const initialState = {
  user: null,
  checkError: null,
};

export default handleActions(
  (...)
  initialState,
);

* checkFailureSaga라는 함수를 만들고, CHECK_FAILURE 액션이 발생할 때 해당 함수가 호출되도록 설정했다. 이 함수에서는 localStorage 안에 있는 user 값을 초기화해 준다. 스토어 안의 user 값은 리듀서에서 CHECK_FAILURE 액션이 발생했을 때 user 값을 null 로 설정하도록 이미 처리했으니 신경 쓰지 않아도 된다. 또한, checkFailureSaga 함수에서는 yield를 사용하지 않으므로 function*을 사용하여 제너레이터 함수 형태로 만들어 주지 않아도 괜찮다.

* 이제 로그인 정보가 유효하지 않을 때 로그인 정보 초기화 후 새로고침이 잘 되는지 확인해 보자. 쿠키를 초기화하고 페이지를 새로고침해 보자.

쿠키 초기화

* 쿠키를 초기화할 때는 개발자 도구에서 Application 탭을 열고, Cookies > http://localhost:3000/ 을 선택한 다음 취소 아이콘을 누르면 된다.

* 쿠키를 초기화했다면 페이지를 새로고침해 보자. 로그인 정보가 잘 초기화 된다. 개발자 도구의 콘솔에서 console.log(localStorage.user); 를 입력해 보자. 초기화가 잘 되었다면 undefined가 나타나야 한다.

로그인 정보 초기화

* undefined가 잘 나타나는가?