MERN Stack Boiler-Plate만들기: 서버편

onezerokang·2021년 3월 7일
1

MERN stack boiler plate

목록 보기
1/2


저는 코딩 초보입니다. 제 글을 보고 별로 얻으실 것이 없다는 것을 미리 말씀드립니다. 단지 제가 공부한 내용을 잊어버릴 때마다 보기 위해 작성해둔 것입니다.

0. 서버 폴더 구조

핵심: routes, middleware, db>model, config

0. package.json

 {
  "name": "name",
  "version": "1.0.0",
  "description": "description",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "backend": "nodemon server/index.js",
    "frontend": "npm run start --prefix client",
    "dev": "concurrently \"npm run backend\" \"npm run start --prefix client\""
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "cookie-parser": "^1.4.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "fluent-ffmpeg": "^2.1.2",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^5.11.14",
    "morgan": "^1.10.0",
    "multer": "^1.4.2",
    "react-dropzone": "^11.3.0",
    "react-redux": "^7.2.2",
    "redux": "^4.0.5",
    "redux-promise": "^0.6.0",
    "redux-thunk": "^2.3.0"
  },
  "devDependencies": {
    "concurrently": "^5.3.0",
    "nodemon": "^2.0.7"
  }
}

1. index.js 작성하기

server의 중심이 될 index.js파일을 작성한다. 이 파일에 대부분의 미들웨어를 연결하고, 라우터를 연결할 예정이다. 현재는 user router만 연결했으며 4번에 작성할 것이다.

const express = require('express');
const mongodb = require('./db/mongoose');
const morgan = require('morgan');
const dotenv = require('dotenv');
const cookieParser = require('cookie-parser');

const port = process.env.PORT || 5000;
const app = express();

dotenv.config();
app.use(morgan("dev"):
app.use(express.urlencoded({extended: false});
app.use(express.json());
app.use(cookieParser());
mongodb();

app.use("/api/users", require("./routes/users"));

app.get("/", (req, res) => res.send("hello 0 to 100"));

app.listen(port, () => console.log(`App is listening on port ${port}`));

2. MongoDB 연결하기

Mongoose를 활용해서 MongoDB를 연결하겠다.

const mongoose = require("mongoose");
const config = require("../config/key");

const connect = () => {
  if (process.env.NODE_ENV !== "production") {
    mongoose.set("debug", true);
  }
  mongoose.connect(
    config.mongoURI,
    {
      dbName: "0to100",
      useNewUrlParser: true,
      useCreateIndex: true,
      useUnifiedTopology: true,
    },
    (error) => {
      if (error) {
        console.log("mongodb error", error);
      } else {
        console.log("mongodb connected...");
      }
    }
  );
};

mongoose.connection.on("error", (error) => {
  console.error("mongodb error", error);
});

mongoose.connection.on("disconnected", () => {
  console.error("mongodb is disconnected. Retry the connection");
});

module.exports = connect;

3. User Schema 작성하기

mongodb와 ODM인 mongoose를 사용할 예정이기에 유저 정보를 담을 User Schema를 작성해야 한다. User Schema를 작성하는 이유는 잘못된 데이터 타입의 정보가 들어오는 것을 필터링해주는 효과가 있다.

나중에 사용할 statics과 methods를 미리 만들어준다. methods는 document에서 사용하고 statics는 Model에서 직접사용할 때 사용한다.

const mongoose = require("mongoose");
const { Schema } = mongoose;
const { model } = mongoose;
const bcrypt = require("bcryptjs");
const saltRounds = 10;
const jwt = require("jsonwebtoken");

const userSchema = Schema({
  name: {
    type: String,
    required: true,
    maxlength: 18,
    minlength: 2,
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
  },
  password: {
    type: String,
    required: true,
    minlength: 8,
  },
  nickname: {
    type: String,
    required: true,
    maxlength: 18,
    minlength: 2,
  },
  role: {
    type: Number,
    default: 0,
  },
  image: String,
  token: String,
  tokenExp: Number,
});

userSchema.pre("save", async function (next) {
  const user = this;
  if (user.isModified("password")) {
    try {
      const salt = await bcrypt.genSalt(saltRounds);
      const hash = await bcrypt.hash(user.password, salt);
      user.password = hash;
      next();
    } catch (error) {
      next(error);
    }
  } else {
    next();
  }
});

userSchema.methods.comparePassword = function (plainPassword) {
  const user = this;
  return new Promise(async (resolve, reject) => {
    try {
      const result = await bcrypt.compare(plainPassword, user.password);
      resolve(result);
    } catch (error) {
      reject(error);
    }
  });
};

userSchema.methods.generateToken = function () {
  let user = this;
  return new Promise(async (resolve, reject) => {
    try {
      const token = await jwt.sign(
        user._id.toHexString(),
        `${process.env.SECRET_TOKEN}`
      );
      user.token = token;
      user = await user.save();
      resolve(user);
    } catch (error) {
      reject(error);
    }
  });
};

userSchema.statics.findByToken = function (token) {
  let user = this;
  return new Promise(async (resolve, reject) => {
    try {
      const decode = jwt.verify(token, `${process.env.SECRET_TOKEN}`);
      user = await user.findOne({ _id: decode, token: token });
      resolve(user);
    } catch (error) {
      reject(error);
    }
  });
};

const User = model("User", userSchema);
module.exports = User;

주의사항: userSchema.pre('save')에서 user.isModified를 하지 않으면 save작업을 할 때마다 새로운 암호화를 하기 때문에 재 로그인 할 수 없다.(이거 문제점 찾느라 몇시간 썼는지 ㅋㅋ..)

4.유저 라우터 만들기

회원가입 라우터, 로그인 라우터, 인증 라우터, 로그아웃 라우터를 만든다.

  • 회원가입라우터는 request에서 들어온 정보를 바탕으로 새 User document를 만드는 원리다.

  • 로그인 라우터는 request에서 들어온 정보를 바탕으로 email이 존재하는지 확인한다. 이후 비밀번호를 bcrypt.compare 메서드로 조회한다. 두개가 해당하는 document가 있다면 그 _id를 바탕으로 Jsonwebtoken을 만들고 이를 쿠키에 저장하는 방식이다.

  • 인증라우터는 auth미들웨어가 먼저 동작한다. auth 미들웨어에서 쿠키에서 토큰을 가져와 토큰을 복호화 한다. 이후 토큰값과 유저의 _id값이 갖다면 user의 정보를 return 한다.

  • 로그아웃 라우터는 auth 미들웨어로 현재 로그인을 했는지 확인하다. 이후 findOneAndUpdate 메서드를 사용해서 토큰의 값을 빈문자열("")로 변경시켜준다.

const express = require("express");
const User = require("../db/model/User");
const auth = require("../middleware/auth");

const router = express.Router();

router.post("/register", async (req, res) => {
  try {
    const newUser = new User(req.body);
    await newUser.save();
    return res.json({ success: true });
  } catch (error) {
    return res.json({ success: false, error });
  }
});

router.post("/login", async (req, res) => {
  try {
    let user = await User.findOne({ email: req.body.email });
    const result = await user.comparePassword(req.body.password);
    if (!result) {
      return res.json({ success: false, message: "비밀번호를 틀렸습니다" });
    }
    user = await user.generateToken();
    return res
      .cookie("x_auth", user.token)
      .status(200)
      .json({ success: true, userId: user._id });
  } catch (error) {
    return res.json({ success: false, error });
  }
});

router.get("/auth", auth, (req, res) => {
  return res.json({
    _id: req.user._id,
    name: req.user.name,
    email: req.user.email,
    nickname: req.user.nickname,
    image: req.user.image,
    role: req.user.role,
    isAdmin: req.user.role ? true : false,
    token: req.user.token,
    tokenExp: req.user.tokenExp,
    isAuth: true,
  });
});

router.get("/logout", auth, async (req, res) => {
  try {
    await User.findOneAndUpdate({ _id: req.user._id }, { token: "" });
    return res.status(200).json({ success: true });
  } catch (error) {
    return res.json({ success: false });
  }
});

module.exports = router;

5. auth 미들웨어 만들기

지금 로그인을 했는지 안했는지 검증할 auth 미들웨어를 만들어야 한다. 이후 앞단에서 auth hoc가 auth로 request를 보내 로그인 여부를 확인하고 유저정보를 가져오게 될 것이다.

const User = require("../db/model/User");

const auth = async (req, res, next) => {
  const token = req.cookies.x_auth;
  try {
    let user = await User.findByToken(token);
    if (!user) {
      return res.json({ isAuth: false });
    }
    req.user = user;
    next();
  } catch (error) {
    res.json({ isAuth: false });
    throw error;
  }
};

module.exports = auth;
profile
블로그 이전 했습니다: https://techpedia.tistory.com

0개의 댓글