[React JS] 5. ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
1. ํ์๊ฐ์ ํ์ด์ง UI ์์ฑํ๊ธฐ
RegisterPage.js
import React from "react";
const RegisterPage = () => {
return (
<section className="flex flex-col justify-center mt-20 max-w-[400px] m-auto">
<div className="p-6 bg-white rounded-md shadow">
<h1 className="text-3xl front-semibold text-center">ํ์๊ฐ์
</h1>
<form className="mt-6">
<div className="mb-2">
<label
htmlFor="email"
className="text-sm font-semibold text-gray-800"
>
Email
</label>
<input
type="email"
id="email"
className="w-full px-4 py-2 mt-2 bg-white border rounded-md"
/>
</div>
<div className="mb-2">
<label
htmlFor="name"
className="text-sm font-semibold text-gray-800"
>
Name
</label>
<input
type="text"
id="name"
className="w-full px-4 py-2 mt-2 bg-white border rounded-md"
/>
</div>
<div className="mb-2">
<label
htmlFor="password"
className="text-sm font-semibold text-gray-800"
>
Password
</label>
<input
type="password"
id="password"
className="w-full px-4 py-2 mt-2 bg-white border rounded-md"
/>
</div>
<div className="mt-6">
<button
type="submit"
className="w-full bg-black text-white px-4 py-2 rounded-md hover:bg-gray-700 duration-200"
>
ํ์๊ฐ์
</button>
</div>
<p className="mt-8 text-xs font-light text-center text-gray-700">
์์ด๋๊ฐ ์๋ค๋ฉด{" "}
<a href="/login" className="font-medium hover:underline">
๋ก๊ทธ์ธ
</a>
</p>
</form>
</div>
</section>
);
};
export default RegisterPage;
2. React-Hook-Form
์ ํจ์ฑ ๊ฒ์ฌ
- ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด ๋ง๋ค์ด๋ ๋์ง๋ง react-hook-form ์ฌ์ฉํ๋ฉด ํธ๋ฆฌ + re-๋๋๋ง ์ต์ํ ๊ฐ๋ฅ (์ ์ ์ฝ๋, ๋์ ์ฑ๋ฅ)
React-Hook-Form
npm i react-hook-form |
- useForm() ์ฌ์ฉ
import React from "react";
import { useForm } from "react-hook-form";
const RegisterPage = () => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm( {mode: 'onChange'});
const onSubmit = ({ email, password, name }) => {
reset();
};
const userEmail = {
required: "ํ์ ํ๋์
๋๋ค.",
};
const userName = {
required: "ํ์ ํ๋์
๋๋ค.",
};
const userPassword = {
required: "ํ์ ํ๋์
๋๋ค.",
minLength: {
value: 6,
message: "์ต์ 6์์
๋๋ค.",
},
};
return (
<section className="flex flex-col justify-center mt-20 max-w-[400px] m-auto">
<div className="p-6 bg-white rounded-md shadow">
<h1 className="text-3xl front-semibold text-center">ํ์๊ฐ์
</h1>
<form className="mt-6" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-2">
<label
htmlFor="email"
className="text-sm font-semibold text-gray-800"
>
Email
</label>
<input
type="email"
id="email"
className="w-full px-4 py-2 mt-2 bg-white border rounded-md"
{...register("email", userEmail)}
/>
{errors?.email && (
<div>
<span className="text-red-500">{errors.email.message}</span>
</div>
)}
</div>
export default RegisterPage;
3. Axios Instance
Axios- ๋ธ๋ผ์ฐ์ , Node.js๋ฅผ ์ํ Promise API๋ฅผ ํ์ฉํ๋ HTTP ๋น๋๊ธฐ ํต์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ- ๋ฐฑ์๋ - ํ๋ก ํธ์๋ ํต์ ์ฝ๊ฒํ๊ธฐ ์ํด Ajax์ ํจ๊ป ์ฌ์ฉ= fetch(๋ด์ฅ)์ด ์๋ Axios ์ฌ์ฉํ๋ค๋ ๋ง
npm i axios |
Axios ์ธ์คํด์คํ
- ์ธ์คํด์คํ ํ๋ ์ด์ : ์ค๋ณต๋ ์์ฒญ ํธํ๊ฒ ๊ฐ๋ฅ
- ex) localhost:4000 ์ base URL๋ก ๋ฑ๋ฑ
* ํ ํฐ์ interceptor์ ๋ฃ์ด์ค์ผ๋จ (์์ฒญ ๋ณด๋ผ๋๋ง๋ค ํ์ธํด์ผํ๊ธฐ ๋๋ฌธ - ๋์ค์)
utils > axios ํด๋
import axios from "axios";
const axiosInstance = axios.create({
baseURL: import.meta.env.PROD ? "" : "http://localhost:4000",
});
export default axiosInstance
(4000๋ฒ ์๋ฒ, ๊ฐ๋ฐํ๊ฒฝ)
4. ํ์๊ฐ์ ๊ธฐ๋ฅ ์์ฑํ๊ธฐ
Redux ์ด์ฉํด์ ๋ฐฑ์๋๋ก ๋ฐ์ดํฐ ๋ณด๋ด๊ธฐ- ๋ฐ์์จ ๋ฐ์ดํฐ๋ฅผ redux store์ ๋ฃ์ด์ค ๊ฒ - action์ dispatchํ๋ฉด action์ด reducer ์ ๋ฌ๋๊ธฐ ์ ์ middleware์ด์ฉํด์ ์์ฒญ ๋ณด๋ด๊ณ ๋ฐฑ์๋ ์ฒ๋ฆฌ(์ ๋ณด์ ์ฅ)ํ๊ณ ์๋ต์ฃผ๋ฉด response data = action์ payload๋ฅผ ์ฒ๋ฆฌ
RegisterPage.js
const dispatch = useDispatch();
const onSubmit = ({ email, password, name }) => {
const body = {
email,
password,
name,
image:
"https://search.pstatic.net/sunny/?src=https%3A%2F%2Fi.pinimg.com%2F736x%2Fdf%2F77%2F7a%2Fdf777a668034cb80e6d0d0fa1a1d83cb.jpg&type=ofullfill340_600_png",
};
dispatch(registerUser(body));
reset();
};
userSlice.js
const userSlice = createSlice({
name: "user",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(registerUser.pending, (state) => {
state.isLoading = true;
})
.addCase(registerUser.fulfilled, (state) => {
state.isLoading = false;
})
.addCase(registerUser.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
})
},
});
thunkFunctions.js (store)
import { createAsyncThunk } from "@reduxjs/toolkit";
import axiosInstance from "../utils/axios";
export const registerUser = createAsyncThunk(
"user/registerUser",
async (body, thunkAPI) => {
try {
const response = await axiosInstance.post("/users/register", body);
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
);
5. react toast ์ด์ฉํ๊ธฐ
์ค์น
npm i react-toastify |
- ์ผ์ ์๊ฐ๋์ alert ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ
App.jsx
<ToastContainer
position="bottom-right"
theme="light"
pauseOnHover
autoClose={1500}
/>
userSlice.js
const userSlice = createSlice({
name: "user",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(registerUser.pending, (state) => {
state.isLoading = true;
})
.addCase(registerUser.fulfilled, (state) => {
state.isLoading = false;
toast.info("ํ์๊ฐ์
์ฑ๊ณต");
})
.addCase(registerUser.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
toast.error(action.payload);
});
},
});
6. register route ์์ฑํ๊ธฐ
index.js
app.use("/users", require("./routes/users"));
routes/users.js
const express = require("express");
const { User } = require("../models/User");
const router = express.Router();
router.post("/register", async (req, res, next) => {
try {
const user = new User(req.body);
await user.save();
return res.sendStatus(200);
} catch (error) {
next(error);
}
});
router.post("/users/login", (req, res) => {});
router.post("/users/auth", (req, res) => {});
module.exports = router;
7. ๋น๋ฐ๋ฒํธ ์ํธํํ๊ธฐ
bcryptjs ๋ชจ๋ ์ฌ์ฉ
- ์๋ฐฉํฅ (์ํธ ๋ ธ์ถ๋๋ฉด X)
- ๋จ๋ฐฉํฅ (hash, ๋ณตํธํ ๋ถ๊ฐ)
๋ฌธ์ ์
-> ๋ ์ธ๋ณด์ฐ ํ ์ด๋ธ์ ํตํด ์ฐพ์ ์ ์์ (์ง์ ๋ผ์๋ ๊ฒ)
ํด๊ฒฐ
-> salt
-> ๋๋ค๊ฐ์ ๋ฃ์ด์ฃผ๋ ๊ฒ
-> salt + ๋น๋ฐ๋ฒํธ -> ๋ณตํธํ -> ํด์ํ๋ ์ํธ
User.js
userSchema.pre("save", async function (next) {
let user = this;
if (user.isModified("password")) {
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(user.password, salt);
user.password = hash;
}
});
8. ๋ก๊ทธ์ธํ์ด์ง ์์ฑํ๊ธฐ (Json Web Token)
ํ๋ก ํธ : ํ์ด์ง ์ถ๊ฐ -> thunkFunctions.js ์ถ๊ฐ -> userSlice.js ์ถ๊ฐ
๋ฐฑ์๋ : ๋ผ์ฐํฐ ์ถ๊ฐ
* ์ธ์ฆ ์ ์ฐจ๊ฐ ํ์ํ ์ด์
- HTTP๋ stateless (์ํ๋ฅผ ์ ์งํ์ง ์์)
- ๋๋ฌด ๋ง์ ๊ฒ์ ๋ฃ์ด ๋ณด๋ด๋ฉด ์ฑ๋ฅ์ด ์์ข๊ธฐ ๋๋ฌธ์
- ์ธ์ฆ์ ์ฐจ ๊ธฐ๋ณธ ํ๋ฆ
1) ์๋ฒ์ ์์ฒญ ๋ณด๋
2) ์๋ฒ์์ ์ ์ ์ ๋ณด ํฌํจํ๋ ํ ํฐ ์์ฑ
3) response header์ ํ ํฐ ์ฌ๋ ค์ ๋ณด๋
4) ํด๋ผ์ด์ธํธ์์ ์ฟ ํค/๋ก์ปฌ/๋ฉ๋ชจ๋ฆฌ์ ํ ํฐ ์ ์ฅ
5) ์ดํ ์์ฒญ ๋ณด๋ผ ๋ ํ ํฐ์ ๊ฐ์ด ๋ณด๋ด์ค
-> ์๋ฒ์์๋ ํ ํฐ ๋ณตํธํํด์ ์ ์ ์ ๋ณด ์ ์ ์์
* JWT๋? (JSON Web Token) ๋ชจ๋
- ๋น์ฌ์๊ฐ ์ ๋ณด๋ฅผ JSON ๊ฐ์ฒด๋ก ์์ ํ๊ฒ ์ ์กํ๊ธฐ ์ํ ์ปดํฉํธํ๊ณ ๋ ๋ฆฝ์ ์ธ ๋ฐฉ์์ ์ ์ํ๋ ๊ฐ๋ฐฉํ ํ์ค (RFC 7519)
- ๋์งํธ ์๋ช ์ด ๋์ด์๊ธฐ ๋๋ฌธ์ ํ์ธํ๊ณ ์ ๋ขฐ๊ฐ๋ฅ
- ๊ตฌ์กฐ (header / payload / verify signature)
1) ํ ํฐ ์ ํจ ํ๋จ
2) ์ ์ ์ ๋ณด ํ๋จ
๋น๊ต : headers(ํด๋ผ์ด์ธํธ) + payload(ํด๋ผ์ด์ธํธ) + secrete text(์๋ฒ)
* login router
users.js
router.post("/login", async (req, res, next) => {
// req.body / password / emali
try {
// ์กด์ฌํ๋ ์ ์ ์ธ์ง ์ฒดํฌ
const user = await User.findOne({ email: req.body.email });
if (!user) {
return res.status(400).send("Auth failed, email not found");
}
// ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅธ ๊ฒ์ธ์ง ์ฒดํฌ
const isMatch = await user.comparePassword(req.body.password);
if (!isMatch) {
return res.status(400).send("Wrong Password");
}
const payload = {
userId: user._id.toHexString(),
};
// token์ ์์ฑ
const accessToken = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: "1h",
});
return res.json({ user, accessToken });
} catch (error) {
next(error);
}
});
User.js
userSchema.methods.comparePassword = async function (plainPassword) {
let user = this;
const match = await bcrypt.compare(plainPassword, user.password);
return match;
};
9. ํ์ด์ง ์ด๋ํ ๋๋ง๋ค ์ธ์ฆ์ด ๋์ด์๋์ง ์ฒดํฌํ๊ธฐ
ํ๋ก ํธ
App.jsx
function App() {
const dispatch = useDispatch();
const isAuth = useSelector((state) => state.user?.isAuth);
const { pathname } = useLocation();
useEffect(() => {
if (isAuth) {
dispatch(authUser());
}
}, [isAuth, pathname, dispatch]);
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<LandingPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
</Route>
</Routes>
);
}
thunkfunciton.js
export const authUser = createAsyncThunk(
"user/authUser",
async (_, thunkAPI) => {
try {
const response = await axiosInstance.get("/users/auth");
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
);
axios.js
axiosInstance.interceptors.request.use(
function (config) {
config.headers.Authorization =
"Bearer " + localStorage.getItem("accessToken");
return config;
},
function (error) {
return Promise.reject(error);
}
);
- ์์ฒญ ๋ณด๋ด์ง๊ธฐ ์ ์ ๋ญ ํ๊ณ ์ถ์๋ ์ฌ๊ธฐ์!
๋ฐฑ์๋
users.js
router.get("/auth", auth, async (req, res, next) => {
return res.json({
id: req.user._id,
email: req.user.email,
name: req.user.name,
role: req.user.role,
image: req.user.image,
});
});
๋ฏธ๋ค์จ์ด auth.js
const jwt = require("jsonwebtoken");
const { User } = require("../models/User");
let auth = async (req, res, next) => {
// ํ ํฐ์ request headers์์ ๊ฐ์ ธ์ค๊ธฐ
const authHeader = req.headers["authorization"];
// Bearer asdfja;sdfjasdlfjsdaf.asdfasf.adsfdasdfasf
const token = authHeader && authHeader.split(" ")[1];
if (token === null) return res.sendStatus(401);
try {
// ํ ํฐ์ด ์ ํจํ ํ ํฐ์ธ์ง ํ์ธ
const decode = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findOne({ _id: decode.userId });
if (!user) {
return res.status(400).send("์๋ ์ ์ ์
๋๋ค.");
}
req.user = user;
next();
} catch (error) {
next(error);
}
};
module.exports = { auth };
ํ๋ก ํธ (์๋ต)
userslice.js
.addCase(authUser.pending, (state) => {
state.isLoading = true;
})
.addCase(authUser.fulfilled, (state, action) => {
state.isLoading = false;
state.userData = action.payload;
state.isAuth = true;
})
.addCase(authUser.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
state.isAuth = false;
localStorage.removeItem("accessToken");
});
10. NotAuthRoutes, ProtectedRoutes
auth์ ๋ฐ๋ผ ์ ๊ทผ ๋ง์์ฃผ๋ ๋ฐฉ๋ฒ
NotAuthRotues : ๋ก๊ทธ์ธ ์๋ ์ฌ๋์ด ๋ก๊ทธ์ธ ํ ํ์ด์ง์ ๋ค์ด๊ฐ๋ ค๋ ๊ฑธ ๋ง์์ฃผ๋ ๊ฒ
ProtectedRoutes : ๋ก๊ทธ์ธ ๋ ์ฌ๋์ด ๋ก๊ทธ์ธ ์ ํ ํ์ด์ง์ ๋ค์ด๊ฐ๋ ค๋ ๊ฑธ ๋ง์์ฃผ๋ ๊ฒ
ProtectedRoutes.js
import React from "react";
import { Navigate, Outlet } from "react-router-dom";
const ProtectedRoutes = ({ isAuth }) => {
return isAuth ? <Outlet /> : <Navigate to={"/lgoin"} />;
};
export default ProtectedRoutes;
App.jsx
<Route element={<ProtectedRoutes isAuth={isAuth} />}>
<Route path="/protected" element={<ProtectedPage />} />
</Route>
- Outlet ์ด ProtectedPage๊ฐ ๋๋ ๊ฒ
11. Navbar & NavItem (logout)
NavBar
import React, { useState } from "react";
import { Link } from "react-router-dom";
import NavItem from "./Sections/NavItem";
const Navbar = () => {
const [menu, setMenu] = useState(false);
const handleMenu = () => {
setMenu(!menu);
};
return (
<section className="relative z-10 text-white bg-gray-900">
{/* z index๋ static ์๋๊ณ relative๋ง ๋ผ์ */}
<div className="w-full">
<div className="flex items-center justify-between mx-5 sm:mx-10 lg:mx-20">
{/* logo */}
<div className="flex items-center text-2xl h-14">
<Link to="/">logo</Link>
</div>
{/* menu button */}
<div className="text-2xl sm:hidden">
<button onClick={handleMenu}>{menu ? "-" : "+"}</button>
</div>
{/* big screen nav-items */}
<div className="hidden sm:block">
{/* ์ฌ์ด์ฆ๊ฐ sm๋ณด๋ค ํด๋ block */}
<NavItem />
</div>
</div>
{/* mobile nav-items */}
<div className="block sm:hidden">{menu && <NavItem />}</div>
</div>
</section>
);
};
export default Navbar;
NavItem
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useNavigate } from "react-router-dom";
import { logoutUser } from "../../../store/thunkFunctions";
const routes = [
{ to: "/login", name: "๋ก๊ทธ์ธ", auth: false },
{ to: "/register", name: "ํ์๊ฐ์
", auth: false },
{ to: "", name: "๋ก๊ทธ์์", auth: true },
];
const NavItem = ({ mobile }) => {
const isAuth = useSelector((state) => state.user?.isAuth);
const dispatch = useDispatch();
const navigate = useNavigate();
const handleLogout = () => {
dispatch(logoutUser()).then(() => {
navigate("/login");
});
};
return (
<ul
className={`text-md justify-center w-full flex gap-4 ${
mobile && "flex-col bg-gray-900 h-full"
} items-center`}
>
{routes.map(({ to, name, auth }) => {
if (isAuth !== auth) return null;
if (name === "๋ก๊ทธ์์") {
return (
<li
key={name}
className="py-2 text-center border-b-4 cursor-pointer"
>
<Link onClick={handleLogout}>{name}</Link>
</li>
);
} else {
return (
<li
key={name}
className="py-2 text-center border-b-4 cursor-pointer"
>
<Link to={to}>{name}</Link>
</li>
);
}
})}
</ul>
);
};
export default NavItem;
thunkfunction.js
export const logoutUser = createAsyncThunk(
"user/logoutUser",
async (_, thunkAPI) => {
try {
const response = await axiosInstance.post("/users/logout");
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
);
user.js
router.post("/logout", auth, async (req, res, next) => {
try {
return res.sendStatus(200);
} catch (error) {
next(error);
}
});
userslice.js
.addCase(logoutUser.pending, (state) => {
state.isLoading = true;
})
.addCase(logoutUser.fulfilled, (state, action) => {
state.isLoading = false;
state.userData = initialState.userData;
state.isAuth = false;
localStorage.removeItem("accessToken");
})
.addCase(logoutUser.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
toast.error(action.payload);
});
12. token ๋ง๋ฃ ์ฒ๋ฆฌ
axios.js
axiosInstance.interceptors.response.use(
function (response) {
return response;
},
function (error) {
if (error.response.data === "jwt expired") {
window.location.reload();
}
return Promise.reject(error);
}
);
๋ฆฌํ๋ ์ ํ ํฐ์ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ง์ฝ ํ ํฐ์ด ๋ง๋ฃ๋ ๊ฒฝ์ฐ axios ์์ฒญ์์ ํ์ธํด์ ํ ํฐ ๋ง๋ฃ๋ ๊ฒฝ์ฐ reloadํ์ฌ
ํ๋ก ํธ App.jsx์ if(isAuth) dispatch ๋ถ๋ถ์์ -> userslice.js์ authUser rejected์์ accessToken ์ง์ธ ์ ์๋๋ก ํด์ค
'๐จโ๐ป Web Development > React JS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[React JS] 7. ์ํ ๋ํ ์ผ ํ์ด์ง (0) | 2023.09.11 |
---|---|
[React JS] 6. ์ํ ์์ฑ, ๋ฉ์ธ ํ์ด์ง ์์ฑํ๊ธฐ (0) | 2023.08.17 |
[React JS] 4. ๋ฐฑ์๋ ๊ธฐ๋ณธ๊ตฌ์กฐ ์์ฑํ๊ธฐ (0) | 2023.07.15 |
[React JS] [์ฐธ์กฐ] Redux (0) | 2023.06.22 |
[React JS] 3. ํ๋ก ํธ์๋ ๊ธฐ๋ณธ๊ตฌ์กฐ ์์ฑํ๊ธฐ (0) | 2023.06.18 |
์ต๊ทผ๋๊ธ