express 에서의 MVC패턴에 대하여

bshunter·2023년 7월 21일
0

웹 애플리케이션 개발 시, 코드 구조를 깔끔하게 구성하고 모듈화하는 것은 반드시 필요한 과정입니다.
Express.js를 사용하여 웹 애플리케이션 개발하는 경우 이를 구현하기 위해 많은 좋은 방법이 있습니다.
이 글에서는 Express에서 모델-뷰-컨트롤러(MVC) 패턴을 적용하여 API 서버를 구축하는 방법에 대해 소개하고자 합니다.

모듈화의 필요성

초보 개발자분들은 모듈화와 디자인 패턴에 대한 필요성을 잘 느끼지 못하실 수 있습니다.
그러나 작은 프로젝트라도 실제 서비스하는 코드를 직접 작성 해보신다면,
며칠만 지나도 해당 코드를 유지 보수 하는 데에 엄청난 어려움을 느끼게 됩니다.

MVC 패턴이란?

MVC 패턴이란 Model-View-Controller(MVC) 패턴은 개발할 애플리케이션을 세 가지 역할(컴포넌트)로 구분하는 소프트웨어 아키텍처 디자인 패턴입니다.

Model: 데이터베이스와 통신하며 서버에서 사용되는 데이터 구조를 정의합니다.

View: 사용자 인터페이스를 생성하고 보여주는 코드를 정의합니다.(html,ejs등)
이 글에서는 API 서버를 구축하므로 View는 생략합니다.

Controller: 사용자의 요청을 처리하고 알맞은 데이터를 보여주기 위해 Model 과 View 사이에서 데이터를 이동하며 로직을 처리하는 역할을 합니다.

무슨 말인지 저도 모르겠습니다. 아래 예제를 보며 알아봅시다

먼저 간단한 CRUD 코드를 보시겠습니다.

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');

const app = express();

// DB , 스키마 설정
const MemoSchema = new mongoose.Schema({
  content: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

const Memo = mongoose.model('Memo', MemoSchema);

// 미들웨어 설정
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

// 데이터베이스 연결
mongoose.connect('mongodb://localhost/memo-app', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

// 메모 작성
app.post('/memo', async (req, res) => {
  try {
    const memo = await Memo.create(req.body);
    res.status(201).json(memo);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// 모든 메모 조회
app.get('/memo', async (req, res) => {
  try {
    const memos = await Memo.find().sort({ createdAt: -1 });
    res.status(200).json(memos);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

//메모 조회
app.get('/memo/:id', async (req, res) => {
  try {
    const memo = await Memo.findById(req.params.id);
    if (!memo) {
      return res.status(404).json({ error: 'Memo not found' });
    }
    res.status(200).json(memo);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

//메모 수정
app.put('/memo/:id', async (req, res) => {
  try {
    const memo = await Memo.findByIdAndUpdate(req.params.id, req.body, { new: true });
    if (!memo) {
      return res.status(404).json({ error: 'Memo not found' });
    }
    res.status(200).json(memo);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

//메모 삭제
app.delete('/memo/:id', async (req, res) => {
  try {
    const memo = await Memo.findByIdAndRemove(req.params.id);
    if (!memo) {
      return res.status(404).json({ error: 'Memo not found' });
    }
    res.status(200).json({ message: 'Memo deleted successfully' });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// 서버 시작
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

여러분과 제가 자주 작성하는 app.js에 전부 때려넣은 코드 입니다.
로직은 아래와 같습니다.

  1. DB와 스키마를 정의합니다.
  2. 미들웨어를 설정하고 데이터베이스와 서버를 연결합니다.
  3. 각 요청에 따라 데이터베이스와 통신하여 각기 다른 응답을 전송합니다.

DB관련 로직과 스키마 정의한 로직을 Model로 나누고,
응답시 전송할 ejs,html 파일들을 View 로 나누고,
요청에 따른 응답 로직을 Controllers로 나누어 관리하는 디자인 패턴이 바로 MVC 패턴입니다.
(위 코드에서는 View 파일이 없는 API서버이기 때문에 View 모듈은 생략합니다.)

이제 app.js를 위 방식으로 하나 하나 나누어 보겠습니다.

먼저 폴더 구조 입니다.

app.js는 놔두고 controllers,models,routes 폴더를 만들어 줍니다.

.
├── app.js
├── package.json
├── controllers
│   ├── user.controller.js
│   └── ...
├── models
│   ├── user.model.js
│   └── ...
└── routes
    ├── user.route.js
    └── ...
  1. DB 스키마 정의 부분을 model로 나눕니다.
const mongoose = require('mongoose');

const memoSchema = new mongoose.Schema({
  content: {
    type: String,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

module.exports = mongoose.model('Memo', memoSchema);
  1. 응답하는 코드 들을 controllers로 나눕니다. (app.get 등의 안에 있던 코드)
    응답을 위해 DB가 필요하기 때문에 위의 model을 가져옵니다.
const Memo = require('../models/Memo');

exports.createMemo = async (req, res) => {
  try {
    const memo = await Memo.create(req.body);
    res.status(201).json(memo);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
};

exports.getMemoList = async (req, res) => {
  try {
    const memos = await Memo.find().sort({ createdAt: -1 });
    res.status(200).json(memos);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
};

exports.getMemo = async (req, res) => {
  try {
    const memo = await Memo.findById(req.params.id);
    if (!memo) {
      return res.status(404).json({ error: 'Memo not found' });
    }
    res.status(200).json(memo);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
};

exports.updateMemo = async (req, res) => {
  try {
    const memo = await Memo.findByIdAndUpdate(req.params.id, req.body, { new: true });
    if (!memo) {
      return res.status(404).json({ error: 'Memo not found' });
    }
    res.status(200).json(memo);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
};

exports.deleteMemo = async (req, res) => {
  try {
    const memo = await Memo.findByIdAndRemove(req.params.id);
    if (!memo) {
      return res.status(404).json({ error: 'Memo not found' });
    }
    res.status(200).json({ message: 'Memo deleted successfully' });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
};
  1. 라우터를 사용해 라우트를 나누고 요청이 오면 위 controllers에 있는 매서드를 사용하여 요청을 처리합니다.
const express = require('express');
const memoController = require('../controllers/memoController');
const router = express.Router();

router.post('/', memoController.createMemo);
router.get('/', memoController.getMemoList);
router.get('/:id', memoController.getMemo);
router.put('/:id', memoController.updateMemo);
router.delete('/:id', memoController.deleteMemo);

module.exports = router;

마지막으로 app.js에서는 미들웨어들을 넣어주고 요청은 라우트로 넘겨줍니다.

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const memoRoutes = require('./routes/memo');

const app = express();

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

mongoose.connect('mongodb://localhost/memo-app', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

app.use('/memo', memoRoutes);

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

마치며

디자인 패턴을 처음 접하는 분들은 개념이 머릿속에 잘 정리되지 않을 수 있습니다.
그럴때는 위 예시처럼 간단한 서버코드를 한곳에 작성 하신 후
블로그 글의 카테고리를 나눈다는 느낌으로 하나씩 분리를 해보세요.

또한, MVC패턴을 포함한 다른 모든 디자인 패턴들은 가이드라인 일 뿐 정답이 아닙니다.
정해진 규칙을 강박적으로 맞추기보다, 본인의 코드에 맞춘 규칙을 만드는 과정에서 유명한 디자인 패턴으로부터 아이디어와 도움을 얻어 가며 학습한다면, 좋은 디자인 패턴을 가진 코드를 작성 할 수 있을것이라 생각합니다.

1개의 댓글

comment-user-thumbnail
2023년 7월 21일

글 잘 봤습니다, 많은 도움이 되었습니다.

답글 달기