작업이 끝나기전에 화면이동이나 종료를 하면 오류가 발생할 수 있으므로
현재 진행중인 작업을 취소하는 클린업 함수를 추가할것
useLogout.js에 취소 여부를 담는 state를 생성한다.
const [isCancelled, setIsCancelled] = useState(false);
useEffect(() => {
setIsCancelled(false);
// 로그아웃 작업중 중간에 사라진다면
// useEffect의 return이 unmount될때의 작업(클린업)이 된다.
return () => setIsCancelled(true);
}, []);
return { logout, error, isPending };
};
그리고 위의 try catch도 바꿔준다.
const logout = async () => {
setError(null);
setIsPending(true);
try {
//파이어베이스 로그아웃
await fireauth.signOut();
//로그아웃 액션 디스패치
dispatch({ type: "LOGOUT" });
//작업이 취소되엇다면 state업데이트를 하지 않는다.
if (!isCancelled) {
setIsPending(false);
setError(null);
}
} catch (err) {
if (!isCancelled) {
console.log(err.message);
setError(err.message);
setIsPending(false);
}
}
};
useSignup.js도 동일하게 추가한다.
const [isCancelled, setIsCancelled] = useState(false);
const signup = async (email, password, displayName) => {
setError(null);
setIsPending(true);
try {
// 이메일,패스워드로 가입
const res = await fireauth.createUserWithEmailAndPassword(
email,
password
);
if (!res) {
throw new Error("가입중 오류가 발생했습니다.");
}
// 유저 프로파일에 이름을 업데이트
await res.user.updateProfile({ displayName: displayName });
// 유저 정보를 state에 저장한다.
dispatch({ type: "LOGIN", payload: res.user });
//작업이 취소되었다면 스테이트 업데이트를 하지 않는다.
if (!isCancelled) {
setError(null);
setIsPending(false);
}
} catch (err) {
if (!isCancelled) {
console.log(err.message);
setError(err.message);
setIsPending(false);
}
}
};
useEffect(() => {
setIsCancelled(false);
// 로그아웃 작업중 중간에 사라진다면
// useEffect의 return이 unmount될때의 작업(클린업)이 된다.
return () => setIsCancelled(true);
}, []);
return { signup, error, isPending };
};
오류가 나는지 테스트해봐야함
useLogin.js생성
logout.js 복사해서 사용함
import { useEffect, useState } from "react";
import { useAuthContext } from "./useAuthContext";
import { fireauth } from "../firebase/config";
export const useLogin = () => {
const [error, setError] = useState();
const [isPending, setIsPending] = useState(false);
const { dispatch } = useAuthContext();
const [isCancelled, setIsCancelled] = useState(false);
// 로그인 작업중 중간에 사라진다면
// useEffect의 return이 unmount될때의 작업(클린업)이 된다.
const login = async (email, password) => {
setError(null);
setIsPending(true);
try {
//파이어베이스 로그인
const res = await fireauth.signInWithEmailAndPassword(email, password);
//로그인 액션 디스패치
dispatch({ type: "LOGIN", payload: res.user });
//작업이 취소되었다면 state업데이트를 하지 않는다.
if (!isCancelled) {
setIsPending(false);
setError(null);
}
} catch (err) {
if (!isCancelled) {
console.log(err.message);
setError(err.message);
setIsPending(false);
}
}
};
useEffect(() => {
setIsCancelled(false);
// 로그인 작업중 중간에 사라진다면
// useEffect의 return이 unmount될때의 작업(클린업)이 된다.
return () => setIsCancelled(true);
}, []);
return { login, error, isPending };
};
Login.jsx에서 useLogin hook을 사용한다.
const Login = () => {
const [email, setEmail] = useState(""); //이메일
const [password, setPassword] = useState(""); //비밀번호
const { login, error, isPending } = useLogin(); //useLogin훅 가져오기
만들어 놓은 useLogin hook을 가져온다.
그리고 handleSubmit 시 저장한 email과 password를 사용해 login 함수를 실행한다.
const handleSubmit = (e) => {
e.preventDefault();
login(email, password);
};
{!isPending && <button className="btn">로그인</button>}
{isPending && (
<button className="btn " disabled>
로딩중...
</button>
)}
{error && <p>{error}</p>}
로딩중이 아닐 때만 로그인 출력하고 로딩 중일 경우 버튼은 클릭하지 못하도록 한다.
그리고 에러가 발생하면 에러를 출력한다.
테스트해봄
아까 가입해놨던 hong씨의 이메일로 로그인 시도를 했다.
올바르게 입력 시
로그인 하고나서 로그아웃 버튼 클릭 시
이메일 비밀번호 틀리게 입력했을 경우
로딩중일 때 취소하면 로그인 취소가 되는지 테스트 해볼려고 했는데
너무 빨라서 취소 확인이 어려움 ㅋ큐 ㅠㅠ
그리고 Navbar에 인증 된 유저일 경우 로그아웃만 보이게 해보자
Navbar.jsx
//CSS 모듈을 사용하면 CSS클래스가 다른 컴포넌트에도 적용되는것을 방지함
import { useAuthContext } from "../hooks/useAuthContext";
import { useLogout } from "../hooks/useLogout";
import styles from "./Navbar.module.css";
import { Link } from "react-router-dom";
const Navbar = () => {
//유저 정보 가져오기
const { user } = useAuthContext();
//로그아웃 메서드
const { logout } = useLogout();
return (
<nav className={styles.navbar}>
<ul>
<li className={styles.title}>
<Link to="/">myMoney</Link>
</li>
{!user && (
<>
<li>
<Link to="/login">로그인</Link>
</li>
<li>
<Link to="/signup">가입</Link>
</li>
</>
)}
{user && (
<>
<li>안녕하세요, {user.displayName}</li>
<li>
<button className="btn" onClick={logout}>
로그아웃
</button>
</li>
</>
)}
</ul>
</nav>
);
};
export default Navbar;
displayName은 아래와 같이 알 수 있다.
테스트한다.
로그인 상태
로그아웃 상태
파이어 베이스도 로그인 시 일정한 기간동안에
로그인 정보를 가지고 있음 (세션처럼)
그래서
현재는 새로고침을 할 경우에는 로그아웃이 되버리기 때문에
현재 로그인 되어있으면 가져오는걸로 수정해 보겠음.
AuthContext.jsx
// 인증 프로바이더에서 useReducer를 사용하고 스테이트와 디스패치를 제공
export const AuthContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
user: null,
authIsReady: false,
});
// console.log("AuthContext state: ", state);
authIsReady 추가하고 아래에 useState추가
// 인증 프로바이더에서 useReducer를 사용하고 스테이트와 디스패치를 제공
export const AuthContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
user: null,
authIsReady: false,
});
// console.log("AuthContext state: ", state);
useEffect(() => {
// 처음 시작시 유저를 확인해서 유저정보(없으면NULL)와 인증확인 액션 디스패치
const unsub = fireauth.onAuthStateChanged((user) => {
dispatch({ type: "AUTH_IS_READY", payload: user });
unsub();
});
}, []);
그리고 상단 리듀서 메소드에 dispatch type추가한다.
//리듀서 메소드
export const authReducer = (state, action) => {
switch (action.type) {
case "LOGIN":
return { ...state, user: action.payload };
case "LOGOUT":
return { ...state, user: null };
case "AUTH_IS_READY":
return { ...state, user: action.payload, authIsReady: true };
default:
return state;
}
};
로그인 페이지를 다시 새로고침해도 파이어베이스에 인증이 된 상태라면
계속해서 로그인 상태를 유지한다.
새로고침을 해도 그대로 유저 정보를 가져온다.
App.jsx
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import Home from "./pages/home/Home";
import Login from "./pages/login/Login";
import Signup from "./pages/signup/Signup";
import Navbar from "./Components/Navbar";
import { useAuthContext } from "./hooks/useAuthContext";
function App() {
//파이어 베이스 인증확인을 한 후 앱을 사용가능
const { authIsReady } = useAuthContext();
return (
<>
<div className="App">
{authIsReady && (
<BrowserRouter>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
</Routes>
</BrowserRouter>
)}
</div>
</>
);
}
export default App;