이후부터는 API 구현 전략과 그에 맞는 기능을 설명할 것이며, MVC패턴으로 프로젝트를 어떻게 구조화했는지 설명하고자 한다.
{
// 프로젝트 구성
"scripts": {
"start": "nodemon app",
},
"author": "Cupon Cafe",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.6",
"bcrypt": "^5.0.0",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-rate-limit": "^6.6.0",
"express-session": "^1.17.1",
"jsonwebtoken": "^8.5.1",
"morgan": "^1.10.0",
"multer": "^1.4.4",
"mysql2": "^2.2.5",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-kakao": "^1.0.1",
"passport-local": "^1.0.0",
"sequelize": "^6.3.5",
"sequelize-cli": "^6.2.0",
"socket.io": "^4.5.3",
"socketio-jwt": "^4.6.2"
},
"devDependencies": {
"nodemon": "^2.0.3"
}
}
백엔드는 기본적으로 MVC패턴(models, views, controllers)으로 이루어져있다.
이번 프로젝트 역시 MVC 패턴을 따르려 노력했으며, 다음은 프로젝트의 구조를 나타낸 것이다.
├── config
│ └── corsConfig.json # cors 설정 파일
├── controllers # 로직 폴더
│ ├── answer.js
│ ├── auth.js
│ ├── cafe.js
│ ├── find.js
│ ├── mail.js
│ ├── profile.js
│ ├── question.js
│ └── stamp.js
├── lib # 자체 제작한 라이브러리 모음 폴더
│ ├── error.js # error 처리용 유틸
│ └── util.js # 인증, 만료기한 모듈 모음
├── models # DB를 모델링하는 sequelize의 모델 함수용 폴더
│ ├── cafe.js
│ ├── index.js # sequelize를 이용한 DB설정 파일
│ ├── owner.js
│ ├── question.js
│ ├── question.js
│ └── stamp.js
├── routes # Router 폴더
│ ├── answer.js
│ ├── auth.js
│ ├── cafe.js
│ ├── customer.js
│ ├── find.js
│ ├── mail.js
│ ├── main.js
│ ├── profile.js
│ ├── question.js
│ └── stamp.js
├── .env # (개발용)환경설정 파일(직접 생성)
├── app.js # 앱 실행 메인 파일
├── package.json
└── socket.js # socket.io 실행 파일
models는 mysql로 이루어져 있으며, sequelize를 사용하여 DB를 구성하였다.
구성 자체는 크게 어렵지 않고, 연관관계 역시 앞전에 설명했기 때문에 넘어가도록 한다.
routes는 app.js에서 들어온 요청을 url로 분류한 후에 접근하는 파일들을 모아놓은 폴더이다. route라는 이름에 맞게 필요한 기능에 따라 restAPI를 사용하였고, 작동하는 기능은 controllers에서 수행하도록 하였다.
다음은 route에 대한 예시이다.
const express = require("express");
const {
signup,
signin,
checkEmail,
checkUserId,
} = require("../controllers/auth");
const router = express.Router();
// 회원가입 기능
router.post("/join", signup);
// 로그인 기능
router.post("/login", signin);
// 이메일에 대한 중복 체크
router.get("/check-email/:email", checkEmail);
// 아이디에 대한 중복 체크
router.get("/check-id/:userId", checkUserId);
module.exports = router;
controller는 기능을 수행하는 함수들을 꺼내서 사용하도록 폴더에 모아놓은 것이다. 대부분의 요청에 대한 처리를 이곳에서 하며, 알고리즘이나 계산이 필요할 함수들은 libs/util에 넣어두었다.
다음은 controllers에 대한 예시이다.
const { Owner } = require("../models");
const bcrypt = require("bcrypt");
const resCode = require("../libs/error");
exports.Ownerinfo = async (req, res, next) => {
try {
const myprofile = await Owner.findOne({ where: { id: req.decoded.id } });
if (!myprofile) {
const error = resCode.BAD_REQUEST_NO_USER;
console.error("ERROR RESPONSE -", error);
return res.status(error.code).json(error);
} else {
const response = JSON.parse(JSON.stringify(resCode.REQUEST_SUCCESS));
response.data = myprofile;
return res.status(response.code).json(response);
}
} catch (error) {
console.error("ERROR RESPONSE -", error);
error.statusCode = 500;
next(error);
}
};
exports.updateOwner = async (req, res, next) => {
try {
const { newPassword, currentPassword, ownerPhone } = req.body;
let passwordEncoding = null;
// password가 있으면 암호화 시켜야함
const owner = await Owner.findOne({ where: { id: req.decoded.id } });
// owner가 없을 때의 예외처리
if (!owner) {
const error = resCode.UNAUTHORIZED_ERROR;
console.error("ERROR RESPONSE -", error);
return res.status(error.code).json(error);
} else {
// 현재 패스워드와 새로운 패스워드가 모두 입력 되었을때 비밀번호 변경에 접근이 가능
if (newPassword && currentPassword) {
// 현재 비밀번호와 DB의 비밀번호를 크립토로 비교
if (!(await bcrypt.compare(currentPassword, owner.password))) {
const error = resCode.BAD_REQUEST_WRONG_DATA;
console.log("ERROR RESPONSE -", error);
return res.status(error.code).json(error);
}
// 비교 성공 시에 비밀번호를 암호화하여 넣음
passwordEncoding = await bcrypt.hash(newPassword, 12);
}
if (
!(await Owner.update(
{
password: passwordEncoding ? passwordEncoding : owner.password,
ownerPhone: ownerPhone ? ownerPhone : owner.ownerPhone,
},
{
where: { id: req.decoded.id },
}
))
) {
const error = resCode.BAD_REQUEST_WRONG_DATA;
console.error("ERROR RESPONSE -", error);
return res.status(error.code).json(error);
} else {
const response = resCode.REQUEST_SUCCESS;
return res.status(response.code).json(response);
}
}
} catch (error) {
console.error("ERROR RESPONSE -", error);
error.statusCode = 500;
next(error);
}
};
exports.withdrawOwner = async (req, res, next) => {
try {
const { email } = req.params;
// 암호화된 코드는 url에 담으면 안됨
const owner = await Owner.findOne({ where: { email, id: req.decoded.id } });
if (!owner) {
const error = resCode.FORBIDDEN_ERROR;
console.error("ERROR RESPONSE -", error);
return res.status(error.status).json(error);
} else {
if (!(await Owner.destroy({ where: { email, id: req.decoded.id } }))) {
const error = resCode.BAD_REQUEST_WRONG_DATA;
console.error("ERROR RESPONSE -", error);
return res.status(error.code).json(error);
} else {
const response = resCode.REQUEST_SUCCESS;
return res.status(response.code).json(response);
}
}
} catch (error) {
console.error("ERROR RESPONSE -", error);
error.statusCode = 500;
next(error);
}
};
이번 프로젝트를 진행하면서 백엔드의 구조화에 대해 많은 생각을 해보았다. 앞으로 설명할 response와 프로젝트, 코드의 구조화를 많이 생각해보는 계기가 되어 이후의 프로젝트에도 적용해볼 생각이다.
아직 수정이 필요한 프로젝트이기에 포스팅을 하면서 프로젝트를 조금 더 다듬을 생각이다.