오늘은 PrivateRoute / ProtectedRoute 의 구현 방법을 정리해 보려고 합니다.
PrivateRoute / ProtectedRoute 는 사용자 인증 상태를 확인하여 인증된 사용자만 특정 경로에 접근할 수 있도록 하는 것입니다. 이를 위해 리액트 라우터를 사용하여 인증 로직을 추가할 수 있습니다.
1. React Router 설치
먼저, 리액트 라우터가 설치되어 있는지 확인합니다. 설치되어 있지 않다면 아래 명령어를 사용하여 설치합니다.
npm install react-router-dom
2. Auth 설정
인증 상태 관리는 Context API, Redux, Zustand 무엇이 되었든 user 정보나 isLoggedIn 등의 상태를 받을 수 있으면 됩니다. 여기서는 Zustand 와 Tanstack Query를 사용하여 api 를 구성하고 useAuth 훅을 만들었다고 가정해 보겠습니다.
// useAuth.tsx
import api from "@/api/api";
import { AuthData } from "@/types/d";
import { useAuthStore } from "@/zustand/auth.store";
import { useMutation } from "@tanstack/react-query";
import { useShallow } from "zustand/react/shallow";
interface ChangeProfileParams {
accessToken: string | null;
data: AuthData;
}
function useAuth() {
const { user, isLoggedIn, logOut } = useAuthStore(
useShallow((state) => ({
user: state.user,
isLoggedIn: state.isLoggedIn,
logOut: state.logOut,
}))
);
const { mutateAsync: signUp } = useMutation({
mutationFn: (data: AuthData) => api.auth.signUp(data),
});
const { mutateAsync: logIn } = useMutation({
mutationFn: (data: AuthData) => api.auth.logIn(data),
});
const { mutateAsync: getUser } = useMutation({
mutationFn: (accessToken: string | null) =>
api.auth.getUser(accessToken),
});
const { mutateAsync: changeProfile } = useMutation({
mutationFn: ({ accessToken, data }: ChangeProfileParams) =>
api.auth.changeProfile(accessToken, data),
});
return {
user,
isLoggedIn,
signUp,
logIn,
getUser,
changeProfile,
logOut
};
}
export default useAuth;
3. PrivateRoute 컴포넌트 구현
PrivateRoute
컴포넌트를 구현하여 인증 상태를 확인합니다.
// ProtectedRoute
import useAuth from "@/hooks/useAuth";
import { Navigate, Outlet } from "react-router-dom";
const ProtectedRoute: React.FC = () => {
const { isLoggedIn } = useAuth();
if (!isLoggedIn) {
// 유저 정보가 없다면 홈으로! 혹은 로그인페이지로 가게 할 수 있음
return <Navigate to="/" replace={true} />;
}
// 유저 정보가 있다면 자식 컴포넌트를 보여줌
return <Outlet />;
};
export default ProtectedRoute;
4. router.tsx 설정
App.js
에서 PrivateRoute
를 사용하여 특정 경로를 보호합니다.
// router.tsx
import Detail from "@/components/Detail";
import DefaultLayout from "@/components/Layouts/DefaultLayout";
import SignIn from "@/components/SignIn";
import SignUp from "@/components/SignUp";
import LedgerPage from "@/components/pages/LedgerPage";
import MyPage from "@/components/pages/MyPage";
import { createBrowserRouter } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
const router = createBrowserRouter([
{
element: <DefaultLayout />,
children: [
{
path: "/",
element: <SignIn />,
},
{
path: "/sign-up",
element: <SignUp />,
},
{
element: <ProtectedRoute />,
children: [
{
path: "/ledger",
element: <LedgerPage />,
},
{
path: "/mypage",
element: <MyPage />,
},
{
path: "detail/:id",
element: <Detail />,
},
],
},
],
},
]);
export default router;
5. LoginPage 구현
로그인 페이지에서 사용자 인증을 처리하고, 인증이 완료되면 navigate 시킵니다.
// SignIn.tsx
import useAuth from "@/hooks/useAuth";
import { ChangeEvent, useEffect, useId, useState } from "react";
import { useNavigate } from "react-router-dom";
import Swal from "sweetalert2";
function SignIn() {
const navigate = useNavigate();
const idId = useId();
const passwordId = useId();
const { isLoggedIn, logIn } = useAuth();
const [input, setInput] = useState({
id: "",
password: "",
});
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const id = e.target.id;
const value = e.target.value;
switch (id) {
case idId:
setInput({ ...input, id: value });
break;
case passwordId:
setInput({ ...input, password: value });
break;
}
};
const handleLogInClick = async () => {
const data = {
id: input.id,
password: input.password,
};
try {
const result = await logIn(data);
if (result.success) {
Swal.fire({
title: "로그인 성공",
text: `${result.userId}님 환영합니다`,
icon: "success",
});
navigate("/ledger");
}
} catch (error) {
Swal.fire({
title: "로그인 에러",
text: `에러가 발생했습니다! ${error}`,
icon: "error",
});
}
};
const handleSignUpClick = () => {
navigate("/sign-up");
};
useEffect(() => {
if (isLoggedIn) {
navigate("/ledger");
}
}, [isLoggedIn, navigate]);
return (
<div className="grid grid-cols-1 gap-y-6">
<h1 className="text-2xl font-semibold text-center">로그인</h1>
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-1.5 items-start">
<label htmlFor={idId} className="text-sm font-medium">
{"아이디"}
</label>
<input
id={idId}
className="border px-4 py-2.5 rounded-md w-80"
type="text"
value={input.id}
onChange={handleInputChange}
/>
</div>
<div className="flex flex-col gap-y-1.5 items-start">
<label htmlFor={passwordId} className="text-sm font-medium">
{"비밀번호"}
</label>
<input
id={passwordId}
className="border px-4 py-2.5 rounded-md w-80"
type="password"
value={input.password}
onChange={handleInputChange}
/>
</div>
</div>
<button
onClick={handleLogInClick}
className="bg-black text-white py-3 text-[15px] rounded-md font-medium hover:bg-black/80 transition active:bg-black/70"
>
로그인
</button>
<button
onClick={handleSignUpClick}
className="bg-black text-white py-3 text-[15px] rounded-md font-medium hover:bg-black/80 transition active:bg-black/70"
>
회원가입
</button>
</div>
);
}
export default SignIn;
'react' 카테고리의 다른 글
[240623 WIL] 네이버지도 Step by Step 2 (0) | 2024.06.23 |
---|---|
[240622 WIL 네이버지도 api Step by Step 1 (0) | 2024.06.22 |
[240610 TIL] Dom Purify, html-react-parser (0) | 2024.06.10 |
[240607 TIL] Quill base64 이미지 처리 (1) | 2024.06.07 |
[240603 TIL] useRef, debounce (0) | 2024.06.03 |