언어/Next.js

[Next 고캠핑] 로그인 상태관리, 로그아웃

홍시_코딩기록 2024. 8. 14. 23:58

로그인 페이지/ 로그인 시 전화면으로 이동/ 로그아웃시

 

 

로그인, 회원가입 화면

page > login

  
  const [formError, setFormError] = useState<boolean>(false); // 로그인, 회원가입 에러
  const [tabLogin, setTabLogin] = useState<boolean>(true); // 로그인, 회원가입 탭
  const router = useRouter();
  const { isLogin } = useAuth();

  useEffect(() => {
    // 로그인된 상태에서 /login 들어오면 홈으로
    if (isLogin) {
      void router.replace("/");
    } else {
      // active 클래스 1초 뒤 삭제
      const timer = setTimeout(() => {
        setFormError(false);
      }, 1000);
      return () => {
        clearTimeout(timer);
      };
    }
  }, [formError, isLogin]);

 (isLogin 하단 context API 코드 참고)

- 로그인, 회원가입에서 setFormError가 넘어오면 클래스 active가 추가된다.

 스타일을 위해서 1초뒤 삭제했다.

- 로그인을 한 상태에서 로그인페이지에 다시 올 수 도 있으니 그때는 / 홈으로 이

 

 

로그인 컴포넌트

componentes > formLogin.tsx

  const emailInput = useRef<HTMLInputElement>(null);
  const pwdInput = useRef<HTMLInputElement>(null);
  const router = useRouter();

  async function login(email: string, password: string) {
    try {
      const userLogin = await signInWithEmailAndPassword(auth, email, password);
      setFormError(false);
      router.back();
      return userLogin;
    } catch (error) {
      console.log(error);
      setFormError(true);
      return null;
    }
  }

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const email = emailInput.current?.value ?? "";
    const password = pwdInput.current?.value ?? "";
    await login(email, password);
  };

- 로그인을 하면 이전 화면으로 이동

- onSubmit 여기서도 email, password는 ref로 input 값을 받아왔다.

 

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup

- 원래 onSubmit에 router.back()을 했는데 오류가 나왔었다.

 찾아보니 컴포넌트가 이미 언마운트되었는데도 상태를 업데이트하려고 하면 나온다고 한다.

 그래서 페이지 이동을 login에서 진행되게 바꿔주었다.

 

 

https://kyounghwan01.github.io/blog/React/cant-perform-a-React-state-update-on-an-unmounted-component/#%E1%84%87%E1%85%A1%E1%86%AF%E1%84%89%E1%85%A2%E1%86%BC-%E1%84%8B%E1%85%B5%E1%84%8B%E1%85%B2

 

로그아웃

export default function LayoutHeader({ className }: { className?: string }) {
  const { isLogin } = useAuth();
  const { currentModal, openModal, closeModal } = useModal();

  // 로그아웃 클릭 이벤트
  const onClickLogout = async () => {
    try {
      await signOut(auth);
      openModal("logout");
    } catch (error) {
      console.log(error);
    }
  };

헤더에 있는 로그아웃 버튼

- 버튼을 클릭하면 로그아웃이 되고 모달을 띄운다.

 

💡로그아웃 header.tsx

더보기
export default function LayoutHeader({ className }: { className?: string }) {
  const isMobile = useIsMobile();
  const [menuOpen, setMenuOpen] = useState<boolean>(false);
  const { isLogin } = useAuth();
  const { currentModal, openModal, closeModal } = useModal();

  // 로그아웃 클릭 이벤트
  const onClickLogout = async () => {
    try {
      await signOut(auth);
      openModal("logout");
    } catch (error) {
      console.log(error);
    }
  };

  //  모바일 메뉴 토글 클릭이벤트
  const onClickMenu = () => {
    setMenuOpen((prev) => !prev);
  };

  return (
    <>
      <Header className={className}>
        <div className="header_wrap">
          <Link href="/" passHref>
            <Logo>
              <span className="sr_only">요즘캠핑</span>
            </Logo>
          </Link>
          {!isMobile ? (
            <Menu>
              <li>
                <Link href="/campingReview" passHref>
                  요즘 캠핑 후기
                </Link>
              </li>
              <li>
                <Link href={!isLogin ? "/login" : "/myCamping"} passHref>
                  내 캠핑장
                </Link>
              </li>
              <li className="login">
                {!isLogin ? (
                  <Link href="/login" passHref>
                    로그인
                  </Link>
                ) : (
                  <button onClick={onClickLogout}>로그아웃</button>
                )}
              </li>
            </Menu>
          ) : (
            <MobileHeader>
              <li className="login">
                {!isLogin ? (
                  <Link href="/login" passHref>
                    로그인
                  </Link>
                ) : (
                  <button onClick={onClickLogout}>로그아웃</button>
                )}
              </li>
              <li>
                <MobileMenuBtn onClick={onClickMenu}>
                  <IoMenu />
                </MobileMenuBtn>
              </li>
            </MobileHeader>
          )}
          {/* 모바일메뉴 */}
          {isMobile && (
            <MobileMenuModal menuOpen={menuOpen} onClick={onClickMenu} />
          )}
          {/* 후기, 내 캠핑장 alert */}
          {currentModal === "logout" && (
            <Modal
              currentModal={currentModal}
              hide={closeModal}
              message="로그아웃 되었습니다!"
            />
          )}
        </div>
      </Header>
    </>
  );
}

 

💡전체코드  formLogin.tsx

더보기
export default function FormLogin({
  formError,
  setFormError,
}: {
  formError: boolean;
  setFormError: (value: boolean) => void;
}) {
  const emailInput = useRef<HTMLInputElement>(null);
  const pwdInput = useRef<HTMLInputElement>(null);
  const router = useRouter();

  async function login(email: string, password: string) {
    try {
      const userLogin = await signInWithEmailAndPassword(auth, email, password);
      setFormError(false);
      router.back();
      return userLogin;
    } catch (error) {
      console.log(error);
      setFormError(true);
      return null;
    }
  }

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const email = emailInput.current?.value ?? "";
    const password = pwdInput.current?.value ?? "";
    await login(email, password);
  };
  return (
    <Form onSubmit={onSubmit}>
      <p>Email</p>
      <input
        type="email"
        ref={emailInput}
        name="email"
        placeholder="test@email.com"
      />
      <p className="pwd">Password</p>
      <input
        type="password"
        ref={pwdInput}
        name="password"
        placeholder="test1234"
      />
      {formError && <p className="error">이메일, 비밀번호를 확인해주세요.</p>}
      <input className="btn" type="submit" value="Login" />
    </Form>
  );
}


💡 전체 코드 useAuth

- 나는 우선 유저정보는 필요없고 로그인 상태만 필요해서 login이 됐는지 안됐는지만 확인하는 용도로 context를 만들었다.

더보기
import { auth } from "@/firebase/firebase";
import { onAuthStateChanged } from "firebase/auth";
import { createContext, useContext, useEffect, useState } from "react";

interface AuthContextType {
  isLogin: boolean;
}

export const AuthContext = createContext<AuthContextType>({
  isLogin: false,
});

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
  const [isLogin, setIsLogin] = useState<boolean>(false);

  useEffect(() => {
    onAuthStateChanged(auth, (user) => {
      if (user) {
        setIsLogin(true);
      } else {
        setIsLogin(false);
      }
    });
  }, [auth]);
  return (
    <AuthContext.Provider value={{ isLogin }}>{children}</AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("context가 없을 경우");
  }
  return context;
};

💡 전체 코드 login

더보기
export default function index() {
  const [formError, setFormError] = useState<boolean>(false);
  const [tabLogin, setTabLogin] = useState<boolean>(true);
  const router = useRouter();
  const { isLogin } = useAuth();

  useEffect(() => {
    // 로그인된 상태에서 /login 들어오면 홈으로
    if (isLogin) {
      void router.replace("/");
    } else {
      // active 클래스 1초 뒤 삭제
      const timer = setTimeout(() => {
        setFormError(false);
      }, 1000);
      return () => {
        clearTimeout(timer);
      };
    }
  }, [formError, isLogin]);

  if (isLogin) {
    return null;
  }
  return (
    <Wrap>
      <BoxWrap>
        <LoginBox className={formError ? "active" : ""}>
          <div className="left">
            <h2>
              Welcome
              <br />
              Daily Camping!
            </h2>
            <img src="/image/login_camping.png" alt="캠핑 이미지" />
          </div>
          <div className="right">
            <Link href="/" passHref>
              <a className="link_home">
                홈 화면으로 돌아가기
                <IoIosArrowForward />
              </a>
            </Link>
            {/* 로그인/ 회원가입 탭 */}
            {tabLogin ? (
              <div className="form">
                <h2>Login</h2>
                <p className="info">아래 테스트 이메일로 로그인 가능합니다:)</p>
                <FormLogin />
              </div>
            ) : (
              <div className="form">
                <h2>Sign Up</h2>
                <p className="info">회원가입 입니다:)</p>
                <FormJoin setFormError={setFormError} />
              </div>
            )}
            <Button
              onClick={() => {
                setTabLogin((prev) => !prev);
              }}
              className="tab"
            >
              {tabLogin ? "Sign Up" : "Login"}
              <IoIosArrowForward />
            </Button>
          </div>
        </LoginBox>
      </BoxWrap>
    </Wrap>
  );
}

 

 

 

참고 블로그

https://velog.io/@jian09/Firebase-Auth-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC

https://velog.io/@konveloper/Firebase-React-%EC%9D%B4%EB%A9%94%EC%9D%BC-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84

https://velog.io/@minz-cha/React-Firebase%EB%A5%BC-%ED%86%B5%ED%95%B4-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84