ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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

     

    express-async-handler

    Express Error Handler for Async Functions. Latest version: 1.2.0, last published: 3 years ago. Start using express-async-handler in your project by running `npm i express-async-handler`. There are 253 other projects in the npm registry using express-async-

    www.npmjs.com

    이 라이브러리를 받아서 사용해주고 있는데, 나는 아직 이걸 왜쓰는지 모르겠어서 설치하지 않았다.

    이름대로 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으로 선언해주고 있다.

    (참조 : https://velog.io/@padoling/JavaScript-%ED%99%94%EC%82%B4%ED%91%9C-%ED%95%A8%EC%88%98%EC%99%80-this-%EB%B0%94%EC%9D%B8%EB%94%A9)

     

    요약하자면 화살표함수를 사용하면 이 객체와 엮여있는 this를 못가져오기때문에, 이 객체와 엮여있는 this를 가져오기 위해 function을 사용하고 있는 것이다.  그리고 isModified는 몽구스의 함수이다. 

    (참조 : https://www.inflearn.com/community/questions/191348/ismodified-%EB%B6%80%EB%B6%84-%EC%A7%88%EB%AC%B8%EC%9D%84-%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4

    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

     

    GitHub - Wunhyeon/ChatApp-MERNStack

    Contribute to Wunhyeon/ChatApp-MERNStack development by creating an account on GitHub.

    github.com

     

    '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

    댓글

Designed by Tistory.