인증 처리하기+PrivateRoute+로그아웃React+REST API 게시판 구현/FE - React2021. 10. 4. 15:12
Table of Contents
jwt-token 유효성 검증 & 해독 유틸리티 클래스 구현
- 인증이 필요한 게시판 추가, 수정, 삭제 & 댓글 추가, 수정, 삭제 기능을 화면을 사용할 때 사용자가 로그인을 해서 해당 기능을 사용할 때, 서버에서 리턴된 jwt 토큰이 유효한지 검증하는 부분이 필요하다.
- 또한, jwt 토큰을 해독해서 사용자의 ID를 알아내서 게시판과 댓글 추가시에 DB에 반영해야 한다.
- src폴더 아래 utils 폴더를 만들고 jwtUtils.ts 파일을 만들자.
utils/jwtUtils.ts
import jwtDecode from "jwt-decode";
export class jwtUtils {
// 토큰 유효성 검사
static isAuth(token) {
if (!token) {
return false;
}
const decoded = jwtDecode(token);
if (decoded.exp > new Date().getTime() / 1000) {
return true;
} else {
return false;
}
}
// 토큰에서 유저 id 가져오기
static getId(token) {
const decoded = jwtDecode(token)
return decoded.jti;
}
}
Private Route 구현
- 인증된 사용자만 통과시킬 수 있는 컴포넌트를 생성하자.
- src 폴더 아래 routes 폴더를 만들고 PrivateRoute.js 파일을 만들자.
routes/PrivateRoutes.js
- 만약 인증된 사용자만 사용할 수 있는 페이지를 사용하려고 할 때, 로그인을 하지 않은 상태이거나 jwt-token이 유효하지 않으면 로그인 화면으로 가서 로그인에 성공하면 다시 원래 화면으로 돌아올 수 있게 리다이렉트하게 할 수 있게 react-router-dom의 Navigate 컴포넌트의 url에 쿼리 파라미터로 기존에 사용하고자하는 페이지의 url을 넘겨주자.
- 만약 로그인이 된 상태라면 기존에 이용하고자 하는 페이지를 Route를 이용해 렌더링한다.
import React from "react";
import {Navigate} from "react-router-dom";
import {jwtUtils} from "../utils/jwtUtils";
import {useSelector} from "react-redux";
const PrivateRoute = (props) => {
// 넘어오는 props를 파악하는게 중요.
// path, component ....
const token = useSelector((state) => state.Auth.token);
const {component: RouteComponent, path} = props;
// redirectUrl은 로그인이 성공후 돌아갈 화면이다.
if (!jwtUtils.isAuth(token)) {
alert("로그인이 필요한 페이지입니다");
return <Navigate to={`/login?redirectUrl=${path}`}/>;
}
return <RouteComponent/>;
};
export default PrivateRoute;
pages/login/Login.js
- 로그인 페이지에 로그인 성공 시 원래 사용하고자 하는 페이지로 리다이렉트할 수 있는 기능을 추가하자.
import axios from "axios";
import {toast, ToastContainer} from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import {Formik, ErrorMessage} from "formik";
import * as Yup from "yup";
import {Button, TextField} from "@mui/material";
import {useNavigate, useSearchParams} from "react-router-dom";
// SignUp 컴포넌트 scss 이용
import "../sign-up/signUp.scss";
import {useDispatch} from "react-redux";
import {setToken} from "../../redux/reducers/AuthReducer";
const Login = () => {
const navigate = useNavigate();
// 쿼리 파라미터 받아오기
const [searchParams, setSearchParams] = useSearchParams();
const dispatch = useDispatch();
const validationSchema = Yup.object().shape({
email: Yup.string()
.email("올바른 이메일 형식이 아닙니다!")
.required("이메일을 입력하세요!"),
password: Yup.string()
.required("패스워드를 입력하세요!")
});
const submit = async (values) => {
const {email, password} = values;
try {
const {data} = await axios.post("/api/auth/signin", {
email,
password,
});
dispatch(setToken(data.jwt));
const redirectUrl = searchParams.get("redirectUrl");
toast.success(<h3>로그인 성공😎</h3>, {
position: "top-center",
autoClose: 2000
});
// redirectUrl이 쿼리스트링으로 존재하면
// 원래가고자 했던 페이지로 돌아가기
setTimeout(()=> {
if (redirectUrl) {
navigate(redirectUrl);
} else {
navigate("/");
}
}, 2000);
} catch (e) {
// 서버에서 받은 에러 메시지 출력
toast.error(e.response.data.message + "😭", {
position: "top-center",
});
}
};
return (
<Formik
initialValues={{
email: "",
password: "",
}}
validationSchema={validationSchema}
onSubmit={submit}
>
{({values, handleSubmit, handleChange}) => (
<div className="signup-wrapper">
<ToastContainer/>
<form onSubmit={handleSubmit} autoComplete="off">
<div className="input-forms">
<div className="input-forms-item">
<div className="input-label">이메일</div>
<TextField
value={values.email}
name="email"
variant="outlined"
onChange={handleChange}
/>
<div className="error-message">
<ErrorMessage name="email"/>
</div>
</div>
<div className="input-forms-item">
<div className="input-label">비밀번호</div>
<TextField
value={values.password}
name="password"
variant="outlined"
type="password"
onChange={handleChange}
/>
<div className="error-message">
<ErrorMessage name="password"/>
</div>
</div>
<Button
color="primary"
variant="contained"
fullWidth
type="submit"
>
로그인
</Button>
</div>
</form>
</div>
)}
</Formik>
);
};
export default Login;
인증 로직 적용하기 + 로그아웃 구현
- Header 컴포넌트에 인증 로직을 적용해보자!!
- useEffect 훅에서 jwt-token을 가져와서 유효성을 검증하고 isAuth 상태를 정의한다.
- 이 상태를 가지고 로그인이 된 상태(isAuth가 true)라면 게시판, 글쓰기, 내 게시물, 로그아웃 버튼이 보이게 하자
- 또한, 로그아웃된 상태라면 게시판, 글쓰기, 로그인, 회원가입 버튼만 보이게 하자!
- 로그아웃 버튼을 클릭했을 때 redux의 상태를 빈 값으로 만들어서 jwt-token에 대한 정보를 없애고 홈 화면으로 돌아가게 한다.
- 추가로 아래 주석으로 인증이 필요한 화면에 PrivateRoute를 사용하는 예시를 넣어놨다!
Header 컴포넌트 수정하기
components/Header.js
- 로그인 상태에 따라 메뉴가 다르게 보이도록 구현한다.
- 토큰을 비우는 dispatch를 비동기로 처리한 이유는 redux 상태 변화하는 함수가 마지막에 처리되어서 만약 지금 PrivateRoute에 있다면 다시 로그인 페이지로 리다이렉트 되기 때문이다!
import "./header.scss";
import {Link, useNavigate} from "react-router-dom";
import {useDispatch, useSelector} from "react-redux";
import {jwtUtils} from "../utils/jwtUtils";
import {useEffect, useState} from "react";
import {setToken} from "../redux/reducers/AuthReducer";
const Header = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const token = useSelector(state => state.Auth.token);
const [isAuth, setIsAuth] = useState(false);
useEffect(() => {
if (jwtUtils.isAuth(token)) {
setIsAuth(true);
} else {
setIsAuth(false);
}
}, [token]);
// 비동기로 처리!
const logout = async () => {
await dispatch(setToken(""));
alert("로그아웃 되었습니다😎");
navigate("/");
};
return (
<div className="header-wrapper">
<div className="header-title">
<Link to="/">
<span>Duckgugong</span>
</Link>
</div>
<div className="header-menu">
<Link to="/board-list">게시판</Link>
<Link to="/add-board">글쓰기</Link>
{isAuth ? (
<>
<Link to="/myboard-list">내 게시물</Link>
<Link to="#" onClick={logout}>로그아웃</Link>
</>
) : (
<>
<Link to="/login">로그인</Link>
<Link to="/sign-up">회원가입</Link>
</>
)}
</div>
</div>
);
};
export default Header;
PrivateRoute 적용 예
App.js
import React from "react";
import {Routes, Route} from "react-router-dom";
import Header from "./components/Header";
import Home from "./pages/home/Home";
import SignUp from "./pages/sign-up/SignUp";
import Login from "./pages/login/Login";
import PrivateRoute from "./routes/PrivateRoute";
const App = () => {
return (
<React.Fragment>
<Header/>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/sign-up" element={<SignUp/>}/>
<Route path="/login" element={<Login/>}/>
{/* 인증이 필요한 컴포넌트는 아래처럼 PrivateRoute 컴포넌트 사용!*/}
<Route
path="/add-board"
element={
<PrivateRoute path="/add-board" component={AddMagazine}/>
}
/>
</Routes>
</React.Fragment>
)
}
export default App
'React+REST API 게시판 구현 > FE - React' 카테고리의 다른 글
게시물 등록하기 페이지 구현 (0) | 2021.10.11 |
---|---|
axios interceptor 구현하기 (0) | 2021.10.04 |
로그인 구현 (redux-persist) (0) | 2021.10.02 |
회원가입 구현하기 (0) | 2021.09.29 |
초반 프로젝트 설정하기 (0) | 2021.09.26 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!