-
6.유저인증 - JWT - 로그인, 회원가입등을 만들어보자. - 서버 ++ 에러핸들링PS 2024. 8. 1. 15:19
https://www.youtube.com/watch?v=nvjYCK9oDRU&list=PLKhlp2qtUcSZsGkxAdgnPcHioRr-4guZf&index=9
먼저 라우터를 만들어주자. app.get('/api',(req,res) => {}) 이렇게 하는 것보다는 라우터로 분기해주는 게 훨씬 깔끔하니깐.
음, 그리고 미리 양해를 구하지만 이 글은 어느정도 진행을 하고 나서 쓴 글이기 때문에 실제 작업 순서와는 약간 다를 수 있다.
그래서 코드에 나중에 만들어질 메서드 같은게 미리 들어있을 수 있다.
backend/routes/userRouter.js를 만들어준다.
// backend/routes/userRoutes.js const express = require("express"); const { registerUser } = require("../controller/userController"); const router = express.Router(); router.route("/").post(registerUser); // router.route('/login').get(() => {}).post() // router.post("/login", authUser); module.exports = router;
라우터에서 모든 코드를 처리하면 지저분해지니 컨트롤러도 만들어주자.
backend/controller/userController.js
// backend / controller / userController.js; const generateToken = require("../config/generateToken"); const User = require("../models/userModel"); const registerUser = async (req, res) => { console.log("req : ", req.body); const { name, email, password, pic } = req.body; if (!name || !email || !password) { res.status(400); throw new Error("Please Enter all the fields"); } const userExists = await User.findOne({ email }); if (userExists) { res.status(400); throw new Error("User already exists"); } const user = await User.create({ name, email, password, pic, }); if (user) { res.status(201).json({ _id: user._id, name: user.name, email: user.email, pic: user.pic, token: generateToken(user._id), }); } else { res.status(400); throw new Error("Failed to Create the User"); } }; module.exports = { registerUser };
현재 새로운 유저를 등록하는 메서드가 만들어져 있다.
간단히 설명해보면 먼저 유저가 요청한 request의 body에 필요한 정보들이 다 들어있나 검사하고, 없으면 뺀찌.
다음으로
const User = require("../models/userModel");이건 예전에 만들어준 스키마에서 유저모델을 가지고 온거다.
그러니깐 유저테이블을 어떻게 정의해줄건지(email : string 등등)를 정의해준건데,
몽구스에서 만든 이 모델은 레포지토리 역할도 한다. 디비의 유저 테이블에 접근해 인서트, 셀렉트 등등을 할수있다.
그래서 이걸 가져와서 findOne으로 이메일을 찾아 중복검사를 하고 중복이 안되있으면 다음으로 진행.
다음으로 create로 유저를 생성하는 걸 볼 수 있는데, DB에 인서트하는거다.
그리고 리스폰스가 잘 오면 만들어진 아이디와 여러 정보들을 리스폰스해주고 있다.
그런데 저기 token에서 보면 generateToken이라는 거에 user._id를 넣어서 반환해주고 있다.
generateToken을 만들자.
// backend/config/generateToken const jwt = require("jsonwebtoken"); const generateToken = (id) => { return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "30d", }); }; module.exports = generateToken;
여기서 보면 id로 JWT를 만들어서 반환해주는 걸 볼 수있다.
jwt 를 만들어주기 위해 jsonwebtoken 라이브러리를 설치해준다.
npm i jsonwebtoken
.env에 JWT_SECRET도 등록해준다.
유저 모델도 살짝 바꿔준다.
// backend/models/userModel.js const mongoose = require("mongoose"); const userSchema = mongoose.Schema( { name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, pic: { type: String, default: "https://icon-library.com/images/anonymous-avatar-icon/anonymous-avatar-icon-25.jpg", }, }, { timestamps: true } ); const User = mongoose.model("User", userSchema); module.exports = User;
많이 바뀐건 없고, email에 unique 값을 줘서 중복이 안되게끔 했고, pic에서 require를 빼줬다. 필수가 아니니깐.
이제 잘 되나 실험해보자. 아, 라우터 등록해준걸 빼먹었다.
backend/server.js
const express = require("express"); const { chats } = require("./data/data"); const dotenv = require("dotenv"); const connectDB = require("./config/db"); const userRoutes = require("./routes/userRoutes"); dotenv.config(); const app = express(); const PORT = process.env.PORT || 4000; connectDB(); app.use(express.json()); // body parser (request body) app.get("/", (req, res) => { res.send("API is Running"); }); app.use("/api/user", userRoutes);// userRouter등록 app.listen(PORT, () => { console.log("Server Started on PORT : ", PORT); });
별다른건 없고, userRouter등록해줬다.
그리고
app.use(express.json()); // body parser (request body)
이거는 리퀘스트로 들어온 바디를 json형식으로 바꿔주는거다. 이거 없으면 req.body를 찾지를 못한다.
이제 포스트맨으로 요청을 보내보자.
잘 되는걸 볼 수 있다. 전에는 몰랐던 게 하나 있는데, 포스트맨에서 변수를 만들어서 쓸 수 있다.
뉴 클릭
변수클릭
변수등록, save
오른쪽 위에서 방금 변수만든 환경파일 선택
중괄호 하나치면 자동완성으로 나온다. 신기방기
아, 그리고 동영상에서는 컨트롤러에서
https://www.npmjs.com/package/express-async-handler
이 라이브러리를 받아서 사용해주고 있는데, 나는 아직 이걸 왜쓰는지 모르겠어서 설치하지 않았다.
이름대로 async를 쓰기위해서라고 한다면, 이거 안쓰고도 쓸 수 있는데...
나중에 필요하면 그때 쓰겠다.
다음으로 회원가입
회원가입으로 들어가기 전에 비밀번호 암호화를 해주겠다.
npm i bcryptjs
// backend/models/userModel.js const bcrypt = require("bcryptjs"); userSchema.pre("save", async function (next) { if (!this.isModified) { next(); } const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); console.log("password : ", this.password); }); 추가해주자!!!
위에서 pre가 뭐냐면, 말그대로 전에 라는 뜻으로 대충 유추해볼 수 있듯이 save전에 실행하는 거다.
트리거랑 같다고 보면 된다.
그러니깐 위에서는 user 테이블에 데이터를 인서트하기 전에 실행되는 거라고 보면 되겠지.
다음으로, function 선언식을 쓰고 있는 것을 볼 수 있다. 나는 웬만하면 function이라고 선언하는 것 보다는 화살표함수 () => {}
를 더 선호하는 편인데, 여기서는 객체와 연결된 this를 사용하기 위해 function으로 선언해주고 있다.
요약하자면 화살표함수를 사용하면 이 객체와 엮여있는 this를 못가져오기때문에, 이 객체와 엮여있는 this를 가져오기 위해 function을 사용하고 있는 것이다. 그리고 isModified는 몽구스의 함수이다.
https://mongoosejs.com/docs/api/document.html#Document.prototype.isModified() )
앞에 !가 붙어있으므로, 아무것도 변환된게 없다면 next를 실행하도록 해주는데, next가 뭔지 모르겠다. 콜백으로 함수를 뭔가 자동으로 넘겨주는 게 있나 싶었는데, 콘솔 찍어보면 anonymous라고 나와서 그것도 아닌거 같고. 그냥 넣어준건가
어쨌든, 여기서는 회원가입이므로 아이디, 이메일이 추가되는 등의 변화가 있으므로 조건문에 걸리지않고 넘어간다.
아래에서 bcrypt로 password를 암호화해주고 저장한다.
(bcrypt 암호화 관련 참조 : https://www.inflearn.com/community/questions/328877/bcrypt-hash-%ED%95%A8%EC%88%98-%EC%82%AC%EC%9A%A9-%EC%8B%9C-salt-%EA%B4%80%EB%A0%A8 )
디비에 접속해 확인해보면 암호화된 password를 볼 수 있다.
이제 로그인을 만들어주자.
userController에 추가
const authUser = async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (user && (await user.matchPassword(password))) { // if (user && (await user.matchPassword(password, user))) { res.json({ _id: user._id, name: user.name, email: user.email, pic: user.pic, token: generateToken(user._id), }); } }; module.exports = { registerUser, authUser };
userModel에 추가
userSchema.methods.matchPassword = async function (enterPassword) { console.log("enterPassword : ", enterPassword); console.log("this.password : ", this.password); return await bcrypt.compare(enterPassword, this.password); };
먼저 userModel에서 userSchema에 matchPassword라는 함수를 추가해주고있다. 이름에서 유추할 수 있듯이 비밀번호를 대조해보는 함수인데, 사용자 입력으로 들어온 비밀번호를 암호화한다음(해시) 디비에 저장되어있는 암호화된 비밀번호와 대조해주는 함수이다. 여기서 맞으면 로그인이 된다.
다음으로 쓸 수있게 라우터에 등록해주자.
userRouter
router.post("/login", authUser);
이제 포스트맨에 가서 로그인을 실험해보자!
이때, 처음만든 디비에 패스워드가 암호화가 안된 걸로 실험하는 게 아니라
새로 아이디를 생성해서 비밀번호가 암호화된 아이디를 가지고 실험해보자.
로그인이 잘 되는 걸 볼 수 있다.
그리고 위에서 용도를 모르겠다고 한 express-async-handler의 기능 하나를 알게됬다.
이걸 씌어 놓으면 에러가 나도 서버가 멈추지 않는다.
이건 매우 중요한 거라서 다 적용해주기로 했다.
npm i express-async-handler
// backend / controller / userController.js; const asyncHandler = require("express-async-handler"); const generateToken = require("../config/generateToken"); const User = require("../models/userModel"); const registerUser = asyncHandler(async (req, res) => { console.log("req : ", req.body); const { name, email, password, pic } = req.body; if (!name || !email || !password) { res.status(400); throw new Error("Please Enter all the fields"); } const userExists = await User.findOne({ email }); if (userExists) { res.status(400); throw new Error("User already exists"); } const user = await User.create({ name, email, password, pic, }); if (user) { res.status(201).json({ _id: user._id, name: user.name, email: user.email, pic: user.pic, token: generateToken(user._id), }); } else { res.status(400); throw new Error("Failed to Create the User"); } }); const authUser = asyncHandler(async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (user && (await user.matchPassword(password))) { // if (user && (await user.matchPassword(password, user))) { res.json({ _id: user._id, name: user.name, email: user.email, pic: user.pic, token: generateToken(user._id), }); } }); module.exports = { registerUser, authUser };
그리고 추가로 미들웨어로 에러 핸들링을 좀 해주자.
// backend/middleware/errorMiddleware.js const notFound = (req, res, next) => { const error = new Error(`Not found - ${req.originalUrl}`); res.status(404); next(error); }; const errorHandler = (err, req, res, next) => { const statusCode = res.statusCode === 200 ? 500 : res.statusCode; res.status(statusCode); res.json({ message: err.message, stack: process.env.NODE_ENV === "production" ? null : err.stack, }); }; module.exports = { notFound, errorHandler };
위의 메서드는 NotFound 에러, 그러니깐 없는 url로 들어올 경우를 처리해주기 위함이고,
아래의 에러핸들러는 범용적으로 쓰이는 에러 핸들러이다.
그러니깐 가장 마지막에 불리는 에러핸들러다. 코든안에서 보면 res.json으로 응답해주고 있는 걸 볼 수있다.
이 앞가지 에러가 나면 그냥 throw만 해줬었는데, 최종적으로 여기서 응답을 해주는 것이다.
이제 만든 에러 미들웨어를 쓸수있도록 미들웨어 등록을 해주자.
backend/server.js
// backend/server.js const express = require("express"); const { chats } = require("./data/data"); const dotenv = require("dotenv"); const connectDB = require("./config/db"); const userRoutes = require("./routes/userRoutes"); const { notFound, errorHandler } = require("./middleware/errorMiddleware"); dotenv.config(); const app = express(); const PORT = process.env.PORT || 4000; connectDB(); app.use(express.json()); // body parser (request body) app.get("/", (req, res) => { res.send("API is Running"); }); app.use("/api/user", userRoutes); app.use(notFound); // 위 라우터에 걸리지 않는 url은 여기 걸린다. app.use(errorHandler); // 위에서부터 내려오는 에러들을 처리해주도록. app.listen(PORT, () => { console.log("Server Started on PORT : ", PORT); });
위 코드를 보면 notFound에러를 가장 마지막바로 위에서 거르고, 최종적으로 errorHandler로 에러처리를 해주고 있음을 볼 수 있다.
글이 생각보다 너무 길어져서 다음편에 이어서 쓰겠다.
github : https://github.com/Wunhyeon/ChatApp-MERNStack/tree/6.Login/SignUpAPI
'PS' 카테고리의 다른 글
BOJ 4485 - 녹색 옷 입은 애가 젤다지? (0) 2024.08.04 BOJ 5972 택배 배송 (0) 2024.08.02 BOJ 14719 빗물 (0) 2024.08.01 BOJ 20437 - 문자열 게임2 (0) 2024.07.31 BOJ 15989 - 1, 2, 3 더하기 4 (0) 2024.07.30