코딩/Javascript

nodejs express mongodb 백엔드 서버 만들기

드리프트 2021. 6. 2. 20:10
728x170

 

안녕하세요?

 

최근에 nodejs + express + mongodb 를 이용해서 간단히 user 관리가 되는 백엔드(backend) 서버를 만들었었는데

 

공유하고자 블로그에 남깁니다.

 

일단 서버는 라즈베리파이 3로 만든 개인 NAS 서버에서 돌렸기 때문에 그 부분에 대해서도 알려드리도록 하겠습니다.

 

라즈베리파이로 개인 NAS 서버 만드는 방법은 제 예전 블로그 글이 있으니 링크 부분 참고하시기 바랍니다.

 

1편. 라즈베리파이로 NAS 서버 만들기

2편. 라즈베리파이로 NAS 서버 만들기- ftp

3편.라즈베리파이로 NAS 서버 만들기 - 토렌트 서버 (transmission)

 

일단, 기본적인 npm 세팅을 해야 겠죠?

mkdir mern-auth
cd mern-auth
npm init

프로젝트 이름을 mern-auth라고 지었습니다. 나중에 ReactJS 로 프로트엔드도 제작할 예정이라 MERN이라고 적었습니다.

 

참고로 MERN은 Mongodb, Expressjs, Reactjs, Nodejs 의 약자를 줄여서 부르는 호칭입니다.

 

Angular가 유행할 nodejs 초창기때는 MEAN stack이 유명했었는데 ReactJS가 대세가 되면서 MERN Stack Development가 유행하고 있는 추세죠.

 

package.json 파일을 열어보면 entry 포인트가 index.js라고 되어 있는데 우리는 entry 포인트를 server.js로 바꾸겠습니다.

 

그리고 관련된 nodejs 모듈을 설치하도록 하겠습니다.

 

npm i bcryptjs body-parser concurrently express is-empty jsonwebtoken mongoose passport passport-jwt validator

관련 패키지를 간단히 설명해 보자면,

 

bcryptjs : 유저의 패스워드를 DB에 저장하기 전에 패스워드 해시용 패키지

body-parser : express에서 request body 파싱용 미들웨어

concurrently : node dev 실행할 때 백엔드와 프로트엔드 동시에 실행하기 위한 패키지

express: nodejs에서 유명한 백엔드 구축용 프레임워크

is-empty  : validaotor를 사용할 때 유용한 헬퍼 펑션

jsonwebtoken : 인증을 위한 웹토큰

mongoose : MongoDB 사용을 위한 패키지

passport : 인증을 위한 유명한 passportjs

passport-jwt : passportjs 를 위한 JSON Web Token(JWT) 사용

validaotr : 유저의 인풋창 밸리데이터

 

그리고 서버 실행을 위해 nodemon을 설치하도록 합시다.

npm i -D nodemon

그리고 package.json 의 scripts 부분을 아래와 같이 수정합니다.

"scripts": {
    "start": "node server.js",
    "server": "nodemon server.js",
},

 

완성된 package.json 파일입니다.

 

{
  "name": "mern-auth",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
	  "start" : "node server.js",
	  "server" : "nodemon server.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "body-parser": "^1.19.0",
    "concurrently": "^6.2.0",
    "express": "^4.17.1",
    "is-empty": "^1.2.0",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^5.12.12",
    "passport": "^0.4.1",
    "passport-jwt": "^4.0.0",
    "validator": "^13.6.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.7"
  }
}

 

이제 MongoDB 연결을 위해 DB를 만들어야 합니다.

 

MongoDB 홈페이지에서 가입하면 꽁짜로 500MB의 DB를 사용할 수 있습니다.

 

우리는 User 정보만 저장할 예정이라 충분한 용량이겠죠.

 

사용방법은 가입하고 Sandbox를 만들고 database를 만들고 collection 을 만들면 됩니다.

 

database 네임은 mern-auth라고 한다면 아래와 같은 MongoDB URI를 얻을 수 있습니다.

 

mongodb+srv://<dbuser>:<password>@cluster0-fmwtw.mongodb.net/mern-auth

여기서 dbuser는 여러분의 아이디가 되겠고 password는 database를 만들때 지정한 패스워드입니다.

 

우리의 프로젝트는 서버단에서 실행되기 때문에 상기 정보를 파일로 저장해도 보안에 문제가 없습니다.

 

github에 올리때는 빼고 올리셔야 겠지만요.

 

그러면 상기 정보를 아래 파일에 저장하도록 하겠습니다.

 

mkdir config && cd config && touch keys.js

그리고 keys.js 파일에 MongoDB URI를 다음 형식으로 입력하면 됩니다.

module.exports = {
  mongoURI: "mongodb+srv://<dbuser>:<password>@cluster0-fmwtw.mongodb.net/mern-auth" 
};

 

 

이제 본격적으로 express 서버를 구축해 보도록 하겠습니다.

 

const express = require("express");
const mongoose = require("mongoose");
const bodyParser = require("body-parser");
const app = express();
// Bodyparser middleware
app.use(
  bodyParser.urlencoded({
    extended: false
  })
);
app.use(bodyParser.json());
// DB Config
const db = require("./config/keys").mongoURI;
// Connect to MongoDB
mongoose
  .connect(
    db,
    { useNewUrlParser: true }
  )
  .then(() => console.log("MongoDB successfully connected"))
  .catch(err => console.log(err));
const port = process.env.PORT || 5000; // process.env.port is Heroku's port if you choose to deploy the app there
app.listen(port, () => console.log(`Server up and running on port ${port} !`));

코드 내용은 간단합니다.

 

express 서버 구축 후 mongodb 접속해서 잘 되는지만 보는 코드입니다.

 

이제, 서버를 실행해 보도록 하겠습니다.

 

잘 실행되었고 MongoDB 접속도 성공했습니다.

 

이제 본격적으로 유저 생성, 삭제, 변경을 위한 로직을 만들도록 하겠습니다.

 

일단 User 처리를 위한 mongoose 스키마를 만들도록 하겠습니다.

 

mongoose 패키지는 스키마를 이용한 mongodb 미들웨어로 아주 유명합니다. 사용도 쉽고요.

 

models 폴더를 만들고 User.js 파일을 만듭니다.

 

mkdir models && cd models && touch User.js

그리고 User.js 파일에 다음과 같이 코드를 작성토록 합시다.

 

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
// Create Schema
const UserSchema = new Schema({
  name: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true
  },
  password: {
    type: String,
    required: true
  },
  date: {
    type: Date,
    default: Date.now
  }
});
module.exports = User = mongoose.model("users", UserSchema);

우리의 User 스키마는 간단합니다. name, email, password, date 항목이 있습니다. 각각 해당되는 이름이 그 역할을 하게 됩니다.

 

그리고 input form validator를 위해 관련 폴더와 register.js , login.js 파일을 만들겠습니다.

mkdir validation && cd validation && touch register.js login.js

일단 register.js 파일의 코드입니다.

const Validator = require("validator");
const isEmpty = require("is-empty");
module.exports = function validateRegisterInput(data) {
  let errors = {};
// Convert empty fields to an empty string so we can use validator functions
  data.name = !isEmpty(data.name) ? data.name : "";
  data.email = !isEmpty(data.email) ? data.email : "";
  data.password = !isEmpty(data.password) ? data.password : "";
  data.password2 = !isEmpty(data.password2) ? data.password2 : "";
// Name checks
  if (Validator.isEmpty(data.name)) {
    errors.name = "Name field is required";
  }
// Email checks
  if (Validator.isEmpty(data.email)) {
    errors.email = "Email field is required";
  } else if (!Validator.isEmail(data.email)) {
    errors.email = "Email is invalid";
  }
// Password checks
  if (Validator.isEmpty(data.password)) {
    errors.password = "Password field is required";
  }
if (Validator.isEmpty(data.password2)) {
    errors.password2 = "Confirm password field is required";
  }
if (!Validator.isLength(data.password, { min: 6, max: 30 })) {
    errors.password = "Password must be at least 6 characters";
  }
if (!Validator.equals(data.password, data.password2)) {
    errors.password2 = "Passwords must match";
  }
return {
    errors,
    isValid: isEmpty(errors)
  };
};

그리고, login.js 파일의 코드입니다.

const Validator = require("validator");
const isEmpty = require("is-empty");
module.exports = function validateLoginInput(data) {
  let errors = {};
// Convert empty fields to an empty string so we can use validator functions
  data.email = !isEmpty(data.email) ? data.email : "";
  data.password = !isEmpty(data.password) ? data.password : "";
// Email checks
  if (Validator.isEmpty(data.email)) {
    errors.email = "Email field is required";
  } else if (!Validator.isEmail(data.email)) {
    errors.email = "Email is invalid";
  }
// Password checks
  if (Validator.isEmpty(data.password)) {
    errors.password = "Password field is required";
  }
return {
    errors,
    isValid: isEmpty(errors)
  };
};

 

이제 express api 라우팅을 위한 본격적인 서버 구축에 들어가도록 하겠습니다.

 

일단 다음과 같이 폴더와 해당 파일을 만듭니다.

mkdir routes && cd routes && mkdir api && cd api && touch users.js

그리고 users.js의 코드는 다음과 같이 입력합시다.

const express = require("express");
const router = express.Router();
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const keys = require("../../config/keys");
// Load input validation
const validateRegisterInput = require("../../validation/register");
const validateLoginInput = require("../../validation/login");
// Load User model
const User = require("../../models/User");

 

이제 register 의 엔드포인트를 작성해 보도록 하겠습니다.

 

user.js 에 다음 코드를 추가합니다.

// @route POST api/users/register
// @desc Register user
// @access Public
router.post("/register", (req, res) => {
  // Form validation
const { errors, isValid } = validateRegisterInput(req.body);
// Check validation
  if (!isValid) {
    return res.status(400).json(errors);
  }
User.findOne({ email: req.body.email }).then(user => {
    if (user) {
      return res.status(400).json({ email: "Email already exists" });
    } else {
      const newUser = new User({
        name: req.body.name,
        email: req.body.email,
        password: req.body.password
      });
// Hash password before saving in database
      bcrypt.genSalt(10, (err, salt) => {
        bcrypt.hash(newUser.password, salt, (err, hash) => {
          if (err) throw err;
          newUser.password = hash;
          newUser
            .save()
            .then(user => res.json(user))
            .catch(err => console.log(err));
        });
      });
    }
  });
});

 

이제 passport 를 사용해 보도록 하겠습니다.

 

config 폴더에 passport.js 파일을 만듭니다.

cd config && touch passport.js

그리고 passsport.js 파일을 만들기 전에 keys.js 에 passport에서 사용하는 환경변수를 아래와 같이 저장하도록 합시다.

module.exports = {
  mongoURI: "YOUR_MONGOURI_HERE",
  secretOrKey: "secret"
};

secretOrKey 를 본인이 원하는 아무 문자로 넣으시면 됩니다.

 

이제 passport.js 파일을 다음과 같이 작성토록 합시다.

const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
const mongoose = require("mongoose");
const User = mongoose.model("users");
const keys = require("../config/keys");
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = keys.secretOrKey;
module.exports = passport => {
  passport.use(
    new JwtStrategy(opts, (jwt_payload, done) => {
      User.findById(jwt_payload.id)
        .then(user => {
          if (user) {
            return done(null, user);
          }
          return done(null, false);
        })
        .catch(err => console.log(err));
    })
  );
};

 

이제 express 서버에서 login 엔드포인트를 작성해 보도록 하겠습니다.

 

users.js에 아래 코드를 추가합니다.

// @route POST api/users/login
// @desc Login user and return JWT token
// @access Public
router.post("/login", (req, res) => {
  // Form validation
const { errors, isValid } = validateLoginInput(req.body);
// Check validation
  if (!isValid) {
    return res.status(400).json(errors);
  }
const email = req.body.email;
  const password = req.body.password;
// Find user by email
  User.findOne({ email }).then(user => {
    // Check if user exists
    if (!user) {
      return res.status(404).json({ emailnotfound: "Email not found" });
    }
// Check password
    bcrypt.compare(password, user.password).then(isMatch => {
      if (isMatch) {
        // User matched
        // Create JWT Payload
        const payload = {
          id: user.id,
          name: user.name
        };
// Sign token
        jwt.sign(
          payload,
          keys.secretOrKey,
          {
            expiresIn: 31556926 // 1 year in seconds
          },
          (err, token) => {
            res.json({
              success: true,
              token: "Bearer " + token
            });
          }
        );
      } else {
        return res
          .status(400)
          .json({ passwordincorrect: "Password incorrect" });
      }
    });
  });
});

 

그리고 users.js 끝에 아래와 같이 module export를 꼭 추가하도록 합니다.

module.exports = router;

 

그럼 이제, router를 express 메인 파일인 server.js에서 불러와서 사용토록 해 볼까요?

 

server.js를 다음과 같이 수정합니다.

const express = require("express");
const mongoose = require("mongoose");
const bodyParser = require("body-parser");
const passport = require("passport");
const users = require("./routes/api/users");
const app = express();
// Bodyparser middleware
app.use(
  bodyParser.urlencoded({
    extended: false
  })
);
app.use(bodyParser.json());
// DB Config
const db = require("./config/keys").mongoURI;
// Connect to MongoDB
mongoose
  .connect(
    db,
    { useNewUrlParser: true }
  )
  .then(() => console.log("MongoDB successfully connected"))
  .catch(err => console.log(err));
// Passport middleware
app.use(passport.initialize());
// Passport config
require("./config/passport")(passport);
// Routes
app.use("/api/users", users);
const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Server up and running on port ${port} !`));

 

이제, 기본적인 기능을 하는 코드는 완성되었습니다.

 

이제 Postman 어플을 이용해 테스트 해보도록 하겠습니다.

 

Postman을 열고, request 타입을 POST로 변경합니다.

 

그리고 요청 url을 "http://localhost:5000/api/users/register" 이렇게 지정합니다.

 

그리고 Body 탭으로 가서 x-www-form-urlencoded를 고른 다음, 해당 파라미터 즉, name, email, password, date 칸을 채웁니다.

 

마지막으로 send 버튼을 누르면 됩니다. 그러면 성공시 HTTP 상태 코드 200을 받으면 성공했다는 뜻이 됩니다.

 

그리고 해당 json을 보여줍니다.

 

login 엔드포인트 테스트도 같은 방식으로 하면 됩니다.

 

해당 url은 "http://localhost:5000/api/users/login"입니다.

 

똑같이 HTTP 상태코드 200을 받으면 성공했다는 뜻입니다.

 

지금까지 nodejs expressjs mongodb 를 이용해서 간단한 사용자 생성 Rest API 서버를 구축해 보았습니다.

 

 

 

참고로 자신의 서버를 라즈베리파이에서 실행할려고 하려면 다음과 같이 forever 패키지를 이용해서 서버 관리를 하면 좋습니다.

 

forever start -a -l forever.log -o out.log -e err.log server.js

forever 패키지는 npm 글로벌로 설치하시면 됩니다.

 

그리고 자신의 서버를 외부에서 접속할려고 하려면 라즈베리파이가 있는 포트를 외부에 오픈해야하고

 

또, DDNS 를 이용해서 서버 이름을 만들었다면 그 인증키를 server.js에서 처리해줘야 합니다.

 

DDNS 인증키는 라우터에서 받으시면 됩니다.

 

인증키는 보통 cert.pem과 key.pem으로 구성됩니다.

 

해당 파일을 server.js에서 취급하는 코드는 아래와 같습니다.

 

저는 외부에서 들어오는 포트를 제 공유기 라우터에서 2368로 지정했기 때문에 아래 코드의 PORT 부분이 2368로 되어 있습니다.

 

완성된 server.js 파일입니다.

const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const passport = require('passport');
const cors = require('cors');
const fs = require('fs');
const https = require('https');

const users = require('./routes/api/users');

const app = express();

// Certificate
const privateKey = fs.readFileSync('./cert_key/key.pem', 'utf8');
const certificate = fs.readFileSync('./cert_key/cert.pem', 'utf8');

const credentials = {
	key: privateKey,
	cert: certificate
}

// Bodyparser middleware
app.use(
  bodyParser.urlencoded({
    extended: false
  })
);
app.use(bodyParser.json());

app.use(cors());

const httpsServer = https.createServer(credentials, app);
// DB Config
const db = require('./config/keys').mongoURI;

// Connect to MongoDB
mongoose
  .connect(db, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('MongoDB successfully connected'))
  .catch(err => console.log(err));

// Passport middleware
app.use(passport.initialize());

// Passport config
require('./config/passport')(passport);

// Routes
app.use('/api/users', users);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.json({ error: err });
});

const port = process.env.PORT || 2368;

// https server
httpsServer.listen(2368, () => {
	console.log('HTTPS Server running on port 2368');
});

 

이상 블로그를 마치도록 하겠습니다.

 

다음에는 프로트엔드 부분을 ReactJS로 구축해 보도록 하겠습니다.

 

그리드형