마이 블로그 프로젝트 03 - 로그인/로그아웃.

이유승·2023년 7월 17일
0
post-custom-banner

회원가입 기능과 마찬가지로 외부에서 볼 일이 없는 기능이다. 여기도 마찬가지로 개발 능력 증진을 위한 연습의 일환으로 로그인 기능을 구현하였다.



1. UI와 기본 기능.

Login.js
export default function Login() {

  // useLogin 커스텀 훅으로부터 로그인 기능과 상태를 가져옴
  const { error, isPending, login, anonymousLogin } = useLogin();
  const [userData, setUserData] = useState({
    id: '',
    pwd: ''
  });

  const loginEvent = (event) => {
      event.preventDefault();
      login(userData.id, userData.pwd);
  };

  // eslint-disable-next-line
  const anonymousLoginEvent = (event) => {
      event.preventDefault();
      anonymousLogin();
  };

  // 입력 필드 값 변경 시 호출되는 함수
  const onChangeEvent = (event) => {
      setUserData({
         	...userData,
          	[event.target.name]: event.target.value
      })
  };

  // 페이지 타이틀 설정  		
  useEffect(() => {
  		const titleElement = document.getElementsByTagName("title")[0];
  		titleElement.innerHTML = '로그인 페이지';
  }, []);

  return (
      <div className={styles.login}>
          <form className={styles.loginform} onSubmit={loginEvent}>
              <p>로그인</p>
              <div className={styles.input}>
                  <label htmlFor='id'>email : </label>
                  <input name='id' type='text' required placeholder='이메일을 입력해주세요' value={userData.id} onChange={onChangeEvent} />
              </div>
              <div className={styles.input}>
                  <label htmlFor='pwd'>password : </label>
                  <input name='pwd' type='password' required placeholder='비밀번호를 입력해주세요' value={userData.pwd} onChange={onChangeEvent} />
              </div>
              <div className={styles.loginbutton}>
                  {!isPending && <button type='submit' className={styles.submitbutton}>로그인</button>}
                  {/* {!isPending && <button className={styles.submitbutton2} onClick={anonymousLoginEvent}>비로그인 접속하기</button>} */}
                  {isPending && <><br /><strong>로그인이 진행중입니다...</strong></>}
                  {error && <><br /><strong>로그인 에러가 발생하였습니다...</strong></>}
              </div>
          </form>
      </div>
  );
};

UI의 기본적인 틀은 회원가입 기능과 거의 동일하다. 기능 구조는 완전 동일하고 세세한 UI만 다를 뿐이다.

사용자가 아이디와 비밀번호를 입력하고 버튼을 클릭할 경우, loginEvent 함수를 호출하고 이 함수에서는 백엔드에 해당하는 login 함수를 사용자의 입력값을 인자로 넣어 호출하여준다.

  • 익명 로그인, 비회원 기능 구현을 위해서 파이어베이스에서 제공하는 anonymousLogin 함수를 사용하여 익명 로그인 기능을 구현하였다. 그런데 이게 실제로는 비회원이 아니라 '익명 회원'이라는 또 다른 계정을 생성하여 로그인하는 방식인데다가 매번 새로운 회원을 생성하는 방식이라 사용을 중단하고 비회원 사용은 그냥 로그인을 할 필요가 아예 없도록 구현하였다.

코드 평가.

평가 방법, 개인적인 코드 리뷰 및 Chat GPT 사용.

-> 보안 취약. 로그인 쪽과 동일한 문제.
-> UI 개선점. 기능의 정상동작 여부를 UI에 출력하는 수단이 alert()인데, 간편하기는 하지만 시각적 측면에서 다소 보기 안 좋을 수 있음. 개선 필요. (모달 창 또는 에러 페이지를 추가하는게 좋을지도 모르겠다.)
-> 에러 처리 보강 필요. 현재 회원 기능 UI의 에러 처리는 문제가 발생했을 때, 에러가 발생했다는 메시지만 띄워주고 있음. 발생한 에러에 대한 상세 정보를 다룰 곳이 필요함. (백엔드에서는 error 변수에 에러 메시지가 저장되도록 하고있으나 파이어베이스에서 반환하는 에러 메시지가 영문이었기에 사용자에게 불편함을 줄 수도 있다고 판단하여 에러 발생 여부만 체크하도록 하였다.)



2. 로그인 기능.

useLogin.js
export const useLogin = () => {
    const [error, setError] = useState(null);
    const [isPending, setIsPending] = useState(false);
    const { dispatch } = useAuthContext();
    const navigate = useNavigate();

const login = (email, password) => {
    setError(null);
    setIsPending(true);
    setPersistence(appAuth, browserLocalPersistence)
        .then(() => {
            signInWithEmailAndPassword(appAuth, email, password)
            .then((userCredential) => {
                const user = userCredential.user;
                dispatch({ type: 'login', payload: user });
                setError(null);
                setIsPending(false);
                if (!user) {
                    throw new Error('로그인에 실패했습니다.');
                }
                alert('방문해주셔서 감사합니다.');
                navigate('/main', { replace: true });
            })
            .catch((error) => {
                setError(error.message);
                setIsPending(false);
                console.log(error.message);
                alert('에러가 발생하였습니다.');
                window.location.replace('/');
            });
        })
        .catch((error) => {
            const errorCode = error.code;
            const errorMessage = error.message;
            console.log(errorCode, errorMessage);
        });
};

const anonymousLogin = () => {
    setError(null);
    setIsPending(true);
    setPersistence(appAuth, browserSessionPersistence)
        .then(() => {
            signInAnonymously(appAuth)
            .then(() => {
                setError(null);
                setIsPending(false);
                alert('방문해주셔서 감사합니다.');
                navigate('/main', { replace: true });
            })
            .catch((error) => {
                setError(error.message);
                setIsPending(false);
                console.log(error.message);
                alert('에러가 발생하였습니다.');
                window.location.replace('/');
            });
        })
        .catch((error) => {
            const errorCode = error.code;
            const errorMessage = error.message;
            console.log(errorCode, errorMessage);
        });
};

return { error, isPending, login, anonymousLogin };

로그인 기능은 useLogin Hook을 직접 만들어 구현하였다. 다른 곳에서 사용되거나 하지 않기 때문에 재사용성을 위해 굳이 별도로 분리할 필요성은 낮지만, 코드 모듈화에 대한 경험을 쌓을 겸 그리고 코드의 유지보수를 간편하게 하기 위해서 따로 분리하여 구현하였다.

useState를 이용하여 로딩 중, 에러 발생의 상태를 다룰 플래그 변수를 관리하고 있다. error는 오류 메시지를 저장하고, isPending은 로그인 작업이 진행 중인지 여부를 나타낸다.

  • 플래그 변수의 중요성.
    -> 어떤 기능이 동작하였을 때, 기능이 즉각적으로 완료되는 것은 드문 일이고 에러가 발생할 가능성도 존재한다. 플래그 변수로 이런 상황들을 관리해주지 않으면 기능이 아직 동작 중인데 다른 기능을 호출하여 에러가 발생할 수도 있고, 에러가 발생하였는데도 이것이 무슨 문제가 생긴건지, 아니면 그냥 통신에 조금 딜레이가 걸렸을 뿐인지 구분할 수가 없다.

기능의 동작 과정에서 발생하는 상태 변화 등은 Context API로 관리한다. 정상적으로 회원가입 절차가 마쳐질 경우 useAuthContext Hook에서 dispatch를 가져와서 필요한 플래그 변수와 데이터를 갱신한다.

로그인 기능이 호출되면, 파이어베이스에서 signInWithEmailAndPassword 함수를 사용하여 로그인 작업을 수행한다. 사용자 인증 상태 지속성을 위해 setPersistence 함수를 먼저 호출한 다음 signInWithEmailAndPassword 함수를 호출하게 된다.

기능 동작 중 에러가 발생할 경우 해당 에러 메시지를 setError 함수를 통해 저장하고, isPending 상태를 false로 변경한 뒤 동작을 마친다. 성공 시 환영 메시지를 띄우고, 페이지를 /main으로 이동시키며, 실패 시 실패 메시지를 띄우고 프로젝트의 처음 페이지로 이동시킨다.

  • appAuth?
    -> 파이어베이스와 내 프로젝트를 연결시키는 역할을 하는 객체. 이게 존재하지 않으면 파이어베이스 서비스와 통신을 주고받을 수가 없다.

코드 평가.

평가 방법, 개인적인 코드 리뷰 및 Chat GPT 사용.

-> 에러 처리 보강 필요. 현재 회원 기능 UI의 에러 처리는 문제가 발생했을 때, 에러가 발생했다는 메시지만 띄워주고 있음. 발생한 에러에 대한 상세 정보를 다룰 곳이 필요함. (백엔드에서는 error 변수에 에러 메시지가 저장되도록 하고있으나 파이어베이스에서 반환하는 에러 메시지가 영문이었기에 사용자에게 불편함을 줄 수도 있다고 판단하여 에러 발생 여부만 체크하도록 하였다.)



3. 로그아웃 기능.

import { useState } from 'react'
import { appAuth } from '../configs/firebase'
import { useAuthContext } from './useAuthContext'
import { signOut } from 'firebase/auth';

export const useLogout = () => {
    const [error, setError] = useState(null);
    const [isPending, setIsPending] = useState(false);
    const { dispatch } = useAuthContext();

const logout = () => {
    setError(null);
    setIsPending(true);
    signOut(appAuth).then(() => {
        dispatch({ type: 'logout' });
        setError(null);
        setIsPending(false);
        alert('로그아웃되었습니다. 방문해주셔서 감사합니다.');
    }).catch((err) => {
        setError(err.message);
        setIsPending(false);
        console.log(err.message);
    });
};

return { error, isPending, logout };

로그아웃은 그다지 복잡한 기능이 아니다. 더구나 파이어베이스에서 기능을 간편하게 제공해주고 있고, 기본적인 구성은 로그인 쪽과 동일하다.

profile
프론트엔드 개발자를 준비하고 있습니다.
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 7월 17일

정말 좋은 정보 감사합니다!

답글 달기
comment-user-thumbnail
2023년 7월 17일

글이 잘 정리되어 있네요. 감사합니다.

답글 달기