본문 바로가기
프로젝트/2024

로그인 기능 작업을 하면서

by dev__log 2024. 2. 21.

UI 작업

로그인과 회원가입은 login.jsx 에 구현되어 있으며, isJoin state로 회원가입 상태인지 관리하여, true 일 경우 회원 가입 상태로 보고 회원가입 폼이 나오도록 만들었다. 

 

[login.jsx]

import React, { useRef, useState } from "react";
import { InputName, InputStyle, LoginWrap, PageTitle, BtnBlackBg, BtnBlackText, ColorError, BtnArea } from "./LoginStyles";
import api from "../../axios/api";
import { useDispatch } from "react-redux";
import { setIsLogin, setUser } from "../../redux/modules/authSlice";
import { useNavigate } from "react-router-dom";

export default function LoginForm() {
  const [isJoin, setIsJoin] = useState(false);
  const dispatch = useDispatch();
  const navigate = useNavigate();

  //생략

  return (
    <LoginWrap>
      <PageTitle>{isJoin ? "회원가입" : "로그인"}</PageTitle>
      <form onSubmit={submitHandler}>
        <InputName htmlFor="id">아이디</InputName>
        <InputStyle type="text" id="id" name="id" ref={idRef} minLength="4" maxLength="10" placeholder="아이디 (4~10글자)" />
        <InputName htmlFor="password">비밀번호</InputName>
        <InputStyle type="password" id="password" name="password" ref={passwordRef} minLength="4" maxLength="15" autoComplete="off" placeholder="비밀번호 (4~15글자)" />
        {isJoin ? (
          <>
            <InputName htmlFor="passwordConfirm">비밀번호 확인</InputName>
            <InputStyle type="password" id="passwordConfirm" name="passwordConfirm" ref={passwordConfirmRef} minLength="4" maxLength="15" autoComplete="off" placeholder="비밀번호 (4~15글자)" />
            <InputName htmlFor="nickname">닉네임</InputName>
            <InputStyle type="text" id="nickname" name="nickname" ref={nicknameRef} minLength="1" maxLength="10" placeholder="닉네임 (1~10글자)" />
          </>
        ) : (
          <></>
        )}

        <BtnArea>
          <BtnBlackBg>{isJoin ? "회원가입" : "로그인"}</BtnBlackBg>
        </BtnArea>
      </form>
      <BtnBlackText onClick={toggleState}>{isJoin ? "로그인" : "회원가입"}</BtnBlackText>
    </LoginWrap>
  );
}

 

 

로그인과 회원가입을 같은 컴포넌트에서 하기 때문에 form onSubmit 시 실행되는 함수에서도 isJoin으로 현재 로그인하는지, 회원가입을 하는지에 따라 기능 작동이 각각 될 수 있도록 하였다.

//로그인 및 회원가입
  const submitHandler = (e) => {
    e.preventDefault();
    const id = e.target.id.value;
    const password = e.target.password.value;
    const passwordConfirm = isJoin ? e.target.passwordConfirm.value : "";
    const nickname = isJoin ? e.target.nickname.value : "";
    
    if (!id) {
      alert("아이디를 입력해주세요.");
      return idRef.current.focus();
    }
    if (!password) {
      alert("비밀번호를 입력해주세요.");
      return passwordRef.current.focus();
    }
    if (isJoin && !passwordConfirm) {
      alert("비밀번호 확인을 입력해주세요.");
      return passwordConfirmRef.current.focus();
    }
    if (isJoin && passwordConfirm !== password) {
      alert("비밀번호가 다릅니다.");
      return passwordConfirmRef.current.focus();
    }
    if (isJoin && !nickname) {
      alert("닉네임을 입력해주세요.");
      return nicknameRef.current.focus();
    }

    //isJoin state 에 따른 회원가입/로그인 로직 분기
    if (isJoin) { //회원가입의 경우
      const newObj = {
        id,
        password,
        nickname
      };

      handleRegister(newObj);
    } else { //로그인의 경우
      const memberObj = {
        id,
        password
      };

      handleLogin(memberObj);
    }
  };

 

 

로그인

post로 보낸 요청이 성공하여 로그인이 정상적으로 되면 쿠키를 설정하고, setUser와 setIsLogin 리듀서를 실행한다.

//쿠키 설정
  const setCookie = (token, minutes) => {
    const date = new Date();
    date.setTime(date.getTime() + minutes * 60 * 1000);
    const expires = "; expires=" + date.toUTCString();

    document.cookie = "accessToken=" + (token || "") + expires + "; path=/";
  };

  //로그인
  const handleLogin = async (memberObj) => {
    try {
      const response = await api.post("/login", memberObj, { widthCredentials: true });
      const accessToken = response.data.accessToken;
      setCookie(accessToken, 60);
      dispatch(setUser(response.data));
      dispatch(setIsLogin());
      navigate("/");
    } catch (error) {
      // alert("에러가 발생했습니다.");
      console.error(error);
    }
  };

 

 

setIsLogin 은 로그인 상태인지를 관리하고, 

setUser는 로그인한 유저의 정보를 state와 localStorage에 저장한다.

그런데 지금은 다 localStorage에 저장한 유저 정보만 사용하고 있어서 user 데이터는 삭제해야 될 것 같다.

 

authSlice.js

const initialState = {
  isLogin: false,
  user: {
    userId: "",
    avatar: "",
    nickname: "",
    accessToken: ""
  }
};

const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    setIsLogin: (state) => {
      state.isLogin = !state.isLogin;
    },
    setUser: (state, action) => {
      const { userId, avatar, nickname, accessToken } = action.payload;
      //localStorage 저장
      localStorage.setItem("accessToken", accessToken);
      localStorage.setItem("userId", userId);
      localStorage.setItem("avatar", avatar);
      localStorage.setItem("nickname", nickname);
      //state 저장
      state.user = {
        userId,
        avatar,
        nickname,
        accessToken
      };
    }
  }
});

 

 

쿠키가 만료되면 로그아웃

제일 고민스러웠던 부분은 토큰이 유효시간이 지나면 로그아웃이 되어야 하는데, 이걸 어떻게 처리할지에 대한 것이었다. 

지금은 useEffect에서 쿠키 값이 변경될 때마다 실행하는데, 'accessToken'이라는 이름을 가진 쿠키가 없으면 isLogin state를 false로 바꾸고, localStorage에 저장했던 유저 정보를 삭제하여 로그아웃 처리를 하고 있다. 

구현한 소스는 다음과 같다.

 

header.jsx의 일부분

  useEffect(() => {
    if (!ACCESS_TOKEN) {
      if (isLogin) {
        dispatch(setIsLogin());
      }

      localStorage.removeItem("accessToken");
      localStorage.removeItem("userId");
      localStorage.removeItem("avatar");
      localStorage.removeItem("nickname");
    }
  }, [ACCESS_TOKEN]);

 

로그인이 풀려 세션의 값을 삭제하면 헤더의 로그인 페이지로 이동하는 버튼이 다시 보이게 된다.

 

 

이제 개인과제 해설강의를 들으면서 잘못구현한 부분을 수정해야겠다.