Studying/JavaScript & Frameworks

[Node.js 떠먹여 주는 남자] 샘플 코드에 모델 추가와 MongoDB 연결하기

국장 지킴이 앨런 2022. 3. 9. 18:40
반응형

안녕하세요, 금방 돌아왔습니다 🤗. 무언가를 하면서 재미있다고 느끼게 된 것이 매우 오랜만이라 빨리 블로그에 글을 작성하고 싶다는 마음이 생기네요. 그나저나 오늘 대선 투표날인데 다들 투표는 하셨는지 모르겠군요.. 저는 복잡할 거 같아 사전 투표일에 미리 투표했는데, 다행이라고 생각했습니다. 컨디션이 좋지 않아 오늘은 점심을 먹은 것을 제외하고는 밖에 나가보지를 않았네요.. 😅

지난 포스트에서는 Node.js 샘플 코드에 컨트롤러와 서비스를 추가하는 코드를 작성해 보았습니다. 코드를 작성하기에 앞서 컨트롤러와 서비스는 무엇인지, 왜 써야 하는지도 알아보았는데요. 혹시 아직 컨트롤러와 서비스의 목적을 잘 모르시겠다면, 이 포스트를 먼저 보고 오시는 것을 추천 드립니다! 꼭 이 포스트가 아니어도 관계는 없지만서도..코드를 작성하여 기능을 구현하는 것도 물론 중요하지만, 어떤 기능을 어느 때에 사용하느냐도 못지 않게 중요하다는 것을 강조하고 싶습니다.

2022.03.08 - [Studying/Node.js & Express.js] - Node.js 부먹편 (2) - 컨트롤러, 서비스 를 이용한 샘플 코드 확장

 

Node.js 부먹편 (2) - 컨트롤러, 서비스 를 이용한 샘플 코드 확장

컨트롤러와 서비스는 무엇인가? 시작하기에 앞서 왜 컨트롤러와 서비스가 필요한 지 간단하게 짚고 넘어가도록 하겠습니다. 지난 포스트 2022.03.07 - [Studying/Node.js & Express.js] - Node.js 부먹편 - Node.j

cre8iveallen.tistory.com

그럼 본격적으로 Node.js 에서 모델은 어떠한 역할을 하는지에 관하여 간단하게 짚고 넘어가 보도록 하겠습니다.

모델은 무엇인가?

Models exist between your MongoDB database storage and the logic of the application. A good model defined on the Node.js server is based on “schema,” which describes the properties of the model. This write-up will demonstrate how to define models on the Node.js server.

출처: How to define Models on Node.js Server

 

 Node.js 에서 모델의 역할은 데이터베이스 컬렉션의 표현이라고 생각하시면 될 것 같습니다. 지난 2번의 Node.js 부먹편 포스트에서 저는 목 유저 데이터를 생성해 간단한 API CRUD 엔드포인트를 구현하였습니다. 각 유저는 id, name, gender, country, 그리고 job 프로퍼티가 존재했습니다. 이걸 데이터베이스 안에 유저 컬렉션이라고 가정해 보았을 때, 각 유저 도큐멘트는 id, name, gender, country, job 프로퍼티가 존재하게 되고, 모델 안에도 이 프로퍼티들이 타입과 함께 정의되게 됩니다.

 

최대한 쉽게 풀어서 설명해 보려 하였으나 말 주변이 부족한 관계로 바로 코드 작성 파트로 넘어가도록 하겠습니다 ㅠ.ㅠ

코드 작-성

코드 작성은 두 파트로 나누어 보았습니다. 첫째로 샘플 코드 안에 유저 모델을 정의하고, npm mongoose 패키지를 이용하여 실제로 데이터베이스와 Node.js 앱이 소통할 수 있도록 코드를 작성하여 보도록 하겠습니다.

 

User 모델 정의, db 파일 작성

유저 모델을 정의하기에 앞서, 프로젝트에 mongoose 패키지를 설치하여야 합니다. 터미널 창을 열고 아래의 커맨드를 입력합니다.

$ npm install mongoose --save # $ 사인은 커맨드 라인 환경임을 표현한 것입니다.

설치가 완료 되었으니, 본격적으로 유저 모델을 작성해 보도록 하겠습니다.

 

userMode.js

'use strict';
const mongoose = require('mongoose');

const UserSchema = mongoose.Schema({
  name: {
    type: String,
    required: true
  },
  age: {
    type: Number,
    required: true
  },
  gender: String,
  country: {
    type: String,
    required: true
  },
  job: String,
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  }
}, { timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } });

module.exports = mongoose.model('User', UserSchema);

위 모델 코드를 보시면, id 는 존재하지 않고 age, email 프로퍼티를 새로 추가하였습니다. MongoDB 에서는 새로운 도큐멘트가 생성될 때 기본적으로 _id 라는 스트링 타입의 유니크 프로퍼티가 생성됩니다. 그렇기 때문에 id 프로퍼티를 따로 추가하지 않았습니다.

자 이제, 모델을 작성하였으니 MongoDB 데이터베이스 연결하기 위한 코드를 작성해 보겠습니다.

 

db.js

'use strict';
const mongoose = require('mongoose');
const { DB: { DB_HOST, DB_PORT, DB_NAME } } = require('./api/config/config');
const connectionString = `mongodb://${DB_HOST}:${DB_PORT}/${DB_NAME}`;

mongoose.connection.once('open', () => {
  console.log(`DB connection opened on ${connectionString}`);
});

mongoose.connection.on('error', (err) => {
  console.log({ MongoDbConnectionError: err });
});

mongoose.connection.on('disconnected', () => {
  console.log('DB disconnected');
});

process.on('SIGINT', () => {
  mongoose.connection.close(() => {
    console.log('DB disconnected due to manual app termination.');
    process.exit(0);
  });
});

mongoose
  .connect(connectionString)
  .catch((err) => {
    console.log({ MongoConnectionError: err });
    process.exit(1);
  });

function startDatabase() {
  mongoose.connection;
}

module.exports = {
  startDatabase
};

코드는 간단합니다. mongoose 패키지를 이용하여 로컬호스트에 돌아가고 있는 MongoDB 에 연결하는 코드입니다. 연결이 되었을 시 콘솔 로그를 띄우고, 에러, 디스커넥션, 서버 강제 종료에 따른 로그도 디스플레이 해줍니다. connectionString 변수 선언에 보시면 ${DB_HOST} 등 config 파일에서 변수를 불러오는데 그 이유는, 후에 Docker 를 이용하여 Node.js 웹 서버를 이미지화 하고 버츄얼 머신 안에 컨테이너로 spin up 할 시에 환경 변수로 선언함으로서, 코드 자체를 하드코딩 하지 않고 더 편리하게 환경에 맞춰서 값을 바꿔줄 수 있기 때문입니다.

 

config.js

'use strict';
module.exports = {
  DB: {
    DB_HOST: process.env.DB_HOST || 'localhost',
    DB_PORT: process.env.DB_PORT || 27017,
    DB_NAME: process.env.DB_NAME || 'sample'
  },
  SERVER_PORT: process.env.SERVER_PORT || 8080
};

마지막으로, 이 프로젝트의 메인 파일인 app.js 파일에 db 파일을 임포트 해주고 startDatabase() 메소드를 호출해 줌으로서, 웹 서버가 가동될 때 데이터베이스 연결을 시도하게 됩니다.

 

app.js

'use strict';
const app = require('express')();
const bodyParser = require('body-parser');
const userRoute = require('./api/routes/userRoute');
const db = require('./db');
const { SERVER_PORT } = require('./api/config/config');


app.use(bodyParser.json({ strict: false }));
app.use('/users', userRoute);

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

이 코드를 실행시켜 보도록 하겠습니다.

다행히도 잘 연결되었다고 나오네요

제가 MongoDB 데이터베이스를 사용하여 개발을 할 때 사용하는 GUI 소프트웨어가 있습니다. 무료 버전이며, 나쁘지 않다고 꽤 유용하게 사용하고 있습니다.

https://robomongo.org/

 

Robo 3T | Free, open-source MongoDB GUI (formerly Robomongo)

Read by Dmitry Schetnikovich

robomongo.org

Robo 3T 이며 Studio 3T 는 프로 버전으로 알고 있습니다만,, 확실하지는 않습니다. 그럼 Robo 3T 를 이용하여 sample 데이터베이스로 접속이 가능한 지 시도해 보도록 하겠습니다.

Robo 3T 를 통해서도 sample 데이터베이스에 성공적으로 접속할 수 있었습니다. 데이터베이스 안에 보시면, Collections 가 보이고 그 안에 users 컬렉션이 보이네요.

 

여기까지 우리는 웹서버와 데이터베이스를 연결하는 부분까지 성공적으로 마칠 수 있었습니다. 다음 파트에서는 userService.js 파일을 업데이트 하여 실제로 데이터베이스와 웹 서버간의 인터랙션을 시도해 보도록 하겠습니다.

userService, userController 파일 업데이트

userService.js

'use strict';
const UserModel = require('../models/userModel');

module.exports = {
  getUsers: async () => {
    try {
      return await UserModel.find();
    } catch (e) {
      throw e;
    }
  },

  getUserById: async (userId) => {
    try {
      return await UserModel.findOne({ _id: userId });
    } catch (e) {
      throw e;
    }
  },

  createNewUser: async (userInfo) => {
    const newUser = new UserModel();
    for (const [key, value] of Object.entries(userInfo)) {
      newUser[key] = value;
    }
    try {
      return await newUser.save();
    } catch (e) {
      throw e;
    }
  },

  updateUserById: async (userId, userInfo) => {
    try {
      const updatedUser = await UserModel.findOne({ _id: userId });
      for (const [key, value] of Object.entries(userInfo)) {
        updatedUser[key] = value;
      }
      return await updatedUser.save();
    } catch (e) {
      throw e;
    }
  },

  deleteUserById: async (userId) => {
    try {
      return await UserModel.findOneAndDelete({ _id: userId });
    } catch(e) {
      throw e;
    }
  }
};

userController.js

'use strict';
const userService = require('../services/userService');
const { validateUserId } = require('../helper/helper');

module.exports = {
  getUsers: async (req, res, next) => {
    try {
      const users = await userService.getUsers();
      res.status(200).json(users);
    } catch (e) {
      res.status(500).json({ error: e.message });
    }
  },

  getUserById: async (req, res, next) => {
    validateUserId(req, res, next);
    try {
      const user = await userService.getUserById(req.params.id);
      res.status(200).json(user);
    } catch (e) {
      res.status(404).json({ error: e.message });
    }
  },

  createNewUser: async (req, res, next) => {
    if (
      !req.body.name || !req.body.age || !req.body.country || !req.body.email
    ) {
      res.status(400).json({
        message: 'Invalid request body. Required properties are: name, age, country, and email.'
      });
      next();
    }
    try {
      const newUser = await userService.createNewUser(req.body);
      res.status(201).json(newUser);
    } catch (e) {
      res.status(500).json({ error: e.message });
    }
  },

  updateUserById: async (req, res, next) => {
    validateUserId(req, res, next);
    try {
      const updatedUser = await userService.updateUserById(req.params.id, req.body);
      res.status(200).json(updatedUser);
    } catch (e) {
      res.status(404).json({ error: e.message });
    }
  },

  deleteUserById: async (req, res, next) => {
    validateUserId(req, res, next);
    try {
      const deletedUser = await userService.deleteUserById(req.params.id);
      res.status(200).json(deletedUser);
    } catch (e) {
      res.status(404).json({ error: e.message });
    }
  }
};

실제 MongoDB 데이터베이스를 연동 시킴으로서, 코드 자체에도 변화가 있었습니다. 가장 큰 변화는, 데이터베이스 오퍼레이션이 필요한 메소드를 async 메소드로 선언하여 db 오퍼레이션 실행이 끝나서 데이터를 받은 후 다음 코드를 진행시키도록 합니다. 또 한가지 중요한 점은, async - await 방식을 사용할 경우 에러 핸들링을 위해 try catch 문을 씌워주어야 한다는 것입니다.

 

그럼 위의 코드를 토대로 Postman 을 통해 실제로 테스트를 해보도록 하겠습니다.

새 유저 추가
모든 유저 받기
아이디를 소유한 유저 받기
특정 아이디를 가진 유저 업데이트
특정 아이디를 가진 유저 삭제
동일 e-mail 을 새로운 유저에 추가할 경우
Robo 3T 를 통해 확인한 유저 리스트

이렇게 모델을 추가하고, 데이터베이스를 연동하여 실제로  Node.js 로 요청을 보냈을 때 데이터베이스에 데이터가 저장이 되고, 그 데이터를 받아와 다시 클라이언트 사이드로 리턴하는 것까지 확인해 보았습니다. 시작 하기 전에는 금방 끝낼 수 있을 것 같았는데, 막상 해보니 저도 다시 한 번 공부 한다는 생각으로 꼼꼼하게 하려고 노력하다 보니 꽤 오랜 시간이 소요된 것 같네요..

 

하지만!

 

아직 끝나지 않았습니다. 기본 스트럭쳐라 하더라도 처음에 제대로 짚고 넘어가는 게 중요하다고 생각합니다. 왜냐하면 제가 그렇게 하지 않았기에 지금 많이 고생을 하고 있기 때문이죠. 물론 저도 아직 초보 개발자이고 배우는 단계이기에 빼먹은 부분이나 틀린 부분이 있을 거라고 생각합니다. 그럼에도 이렇게 블로그로 작성함으로서 다시 공부하는 기회도 갖고, 이 글을 보시게 될 다른 분들에게 조금이라도 아이디어를 제공할 수 있다면 그것만으로도 충분한 가치가 있다고 생각합니다.

Coming Up Next

  • 미들웨어 (Middleware) 를 통한 에러 핸들링
  • TDD (Test Driven Development)
  • async 와 Promise 의 차이
  • 등등

너무 많네요.. 그래도 알아두면 절대 손해 볼 것들은 아니기에 앞으로 올라올 포스트들도 기대해 주시면 감사하겠습니다 :) 그럼

 

반응형