
React-Native FE, NLP, DA, 기획 파트의 팀원들을 새롭게 맞이하고
2024년도 상반기 제품 상용화, 하반기 수익화를 위해 프로젝트를 재구성하였다.
추후 위 이미지 구조에 프로젝트를 진행하며 내용을 추가할 예정이다.
Github, Notion, Jira, Slack 등을 통해 협업을 진행할 예정이다.
현재 기획된 MVP 모델에서의 예상 API Chart이다.
OAuth, 일반 Auth를 구분하여 API를 구성하고 강의 데이터 구조가 확정되면 /lecture 라우트 하위에서 해당 데이터에 대한 처리를 진행할 예정이다.
또한 실시간 처리를 위한 Socket.io의 서버 기준 on, emit 처리는 위와 같다.
User 모델의 요구사항 명세서를 위와 같이 작성하였다.
강의 데이터를 저장할 Lecture 모델은 추후 작성할 예정이다.
/HEARUS-BACKEND
/controllers
로직을 구현하고 response를 보냄
/middlewares
HTTP req와 res 사이에서 단계별 동작을 수행
/models
Table Column들의 표현
/routers
특정 주소를 구현된 controller로 라우팅
app.js
API BE 서버를 구현하기 위해 프로젝트를 위와 같이 구성한다.
위 이미지와 같이 Hearus-Cluster를 구성하였다.
npm install mongodb bcrypt express-session mongoose
이후 MongoDB를 사용하기 위해 mongodb, bcrypt, mongoose 패키지를 설치한다.
const mongoose = require('mongoose');
// Connect MongoDB with mongoose
const uri = 'mongodb+srv://' +
process.env.MONGO_USERNAME + ':' +
process.env.MONGO_PASSWORD + '@' +
process.env.MONGO_HOST + '/' +
'test?retryWrites=true&w=majority';
mongoose.connect(uri, {
serverSelectionTimeoutMS: 5000
}).catch(err => console.log(err.reason))
.then(console.log("MongoDB Connected"));
위와 같이 환경변수로 USERNAME, PASSWORD, HOST를 connect시에 입력한다.
const mongoose = require('mongoose');
const userSchema = mongoose.Schema({
name: {
type: String,
maxlength: 50,
required: true,
},
email: {
// 중복 허용x, 사용자 로그인시 사용
type: String,
trim: true,
unique: 1,
required: true,
},
password: {
type: String,
minlength: 5,
required: true,
},
isOAuth: {
type: Boolean,
required: true,
},
OAuthType: {
type: String,
enum: ['kakao', 'google', 'naver'],
},
school: {
type: String,
required: true,
},
major: {
type: String,
required: true,
},
grade: {
// 1학년, 2학년, 3학년, 4학년, 휴학생, 졸업생, 대학원생
type: String,
enum: ['freshman', 'sophomore', 'junior', 'senior', 'leaveAbsense', 'graduate', 'postgraduate'],
required: true,
},
savedLectures: {
Array: {
type: String,
},
},
usePurpose: {
type: String,
enum: ['offline', 'online', 'conversation', 'else'],
required: true,
},
});
const User = mongoose.model("User", userSchema);
module.export = { User };
또한 이전 DB 요구사항 명세서를 위와 같이 mongoose를 활용하여 구현한다.
// controller/auth.js
const bycrpt = require('bcrypt');
const User = require('../models/user');
exports.signup = async (req, res, next) => {
// destructure req.body
const {
name, email, password, isOAuth, OAuthType,
school, major, grade, savedLectures, usePurpose,
} = req.body;
try {
const exUser = await User.findOne({ email: email });
if (exUser)
return res.status(409).json({ status: "fail", message: "User Already Exists" });
// hash password
const salt = await bycrpt.genSalt(10);
const hashedPW = await bycrpt.hash(password, salt);
// save newUser
const newUser = new User({
name, email, password: hashedPW, isOAuth, OAuthType,
school, major, grade, savedLectures, usePurpose,
});
await newUser.save();
return res.status(201).json({ status: "success", message: "Signup success" });
} catch (error) {
console.error(error);
return next(error);
}
}
auth의 controller에서 signup 함수를 위와 같이 구현하여 export한다.
// routes/auth.js
const express = require('express');
const { signup } = require('../controllers/auth');
const router = express.Router();
router.post('/signup', signup);
module.exports = router;
// app.js
const authRouter = require('./routes/auth');
...
app.use('/auth', authRouter);
이후 위와 같이 Router를 추가하여 준다.
위 이미지와 같이 JSON 형태로 Signup을 요청하면
MongoDB에 데이터가 정상적으로 들어가며 signup 기능이 구현된 것을 볼 수 있다.
보안을 강화하기 위해 위와 같이 AccessToken과 RefreshToken을 로그인시 발급한다.
이때 AccessToken이 1h이후 만료되면 만료시간이 더 긴 RefreshToken으로 재발급할 수 있도록 한다.
npm install jsonwebtoken
Token 방식의 Auth를 구현하기 위해 jsonwebtoken 패키지를 설치한다.
// controllers/auth.js
exports.login = async (req, res, next) => {
// Destructure req.body
const {
email, password, isOAuth, OAuthType,
} = req.body;
// OAuth
if (isOAuth)
return res.status(405).json({ status: "fail", message: "OAuth not Implemented" });
try {
// Find User
const exUser = await User.findOne({ email: email });
if (!exUser)
return res.status(401).json({ status: "fail", message: "Unknown User" });
// Match password
const matchPW = await bycrpt.compare(password, exUser.password);
if (!matchPW)
return res.status(401).json({ status: "fail", message: "Wrong Password" });
// Certify Tokens
const accessToken = jwt.sign({ userID: exUser._id }, process.env.JWT_ACCESS_SECRET, {
expiresIn: '1h',
});
const refreshToken = jwt.sign({ userID: exUser._id }, process.env.JWT_REFRESH_SECRET, {
expiresIn: '24h',
});
return res.status(201).json({
status: "success",
message: "Login Success",
accessToken: accessToken,
refreshToken: refreshToken,
});
} catch (error) {
console.error(error);
return next(error);
}
}
bycrpt를 통해 암호화된 비밀번호를 검증하고 이후 JWT를 발급하여 res로 전달한다.
// middlewares/jwtToken.js
exports.verifyAccessToken = (req, res, next) => {
const token = req.header('Authorization');
if (!token)
return res.status(401).json({ error: 'Access Denied' });
try {
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
req.userID = decoded.userID;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid Token' });
}
};
exports.verifyRefreshToken = (req, res, next) => {
const token = req.header('Authorization');
if (!token)
return res.status(401).json({ error: 'Access Denied' })
try {
const decoded = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
req.userID = decoded.userID;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid Token' });
}
}
AccessToken과 RefreshToken를 서로 다른 SecretKey로 verify하는 미들웨어를 작성한다.
// controllers/auth.js
exports.renewAccessToken = async (req, res, next) => {
try {
// Renew Access Token
const accessToken = jwt.sign({ userID: req.userID }, process.env.JWT_ACCESS_SECRET, {
expiresIn: '1h',
});
return res.status(201).json({
status: "success",
message: "Renew Access Token",
accessToken: accessToken,
});
} catch (error) {
console.error(error);
return next(error);
}
}
또한 만료된 AccessToken를 재발급할 수 있는 로직을 작성한다.
// routes/auth.js
const express = require('express');
const { verifyToken } = require('../middlewares/jwtToken');
const { signup, login, renewAccessToken } = require('../controllers/auth');
const router = express.Router();
router.post('/signup', signup);
router.post('/login', login);
router.post('/renewToken', verifyToken, renewAccessToken);
module.exports = router;
최종적으로 controller를 router를 통해 붙여주면 JWT를 이용한 Token Auth 로직 구현이 완료된다.

Auth와 Token을 테스트하기 위해 추후 구현될 lecture 구조를 구현하였다.
// routes/lecture.js
var express = require('express');
const { verifyAccessToken } = require('../middlewares/jwtToken');
const { renderLecture } = require('../controllers/lecture');
var router = express.Router();
router.get('/', verifyAccessToken, renderLecture);
module.exports = router;
// controllers/lecture.js
exports.renderLecture = (req, res) => {
res.locals.userID = req.userID;
res.render('lecture');
};
lecture controller는 token에서 decoded된 userID를 받아와서 view로 넘겨준다.
<!-- views/lecture.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
background-color: #121212;
margin: 0;
}
.background {
height: 100vh;
}
.card-container {
border-radius: 20px;
width: 30%;
height: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #1e1e1e;
}
.card-item {
margin: 15px;
}
.logo-img {
border-radius: 100px;
width: 150px;
}
.btn {
font-size: 20px;
border-radius: 10px;
background-color: #337ea9;
color: white;
}
.text {
font-size: 15px;
color: white;
}
</style>
</head>
<body>
<div class="background d-flex justify-content-center align-items-center vh-100">
<div class="card-container text-center">
<img src="/images/logo-red.png" alt="Logo" class="card-item logo-img mb-3" />
<p class="text">Protected Lecture API</p>
<p class="text">{{userID}}</p>
</div>
</div>
</body>
</html>
헤더에 토큰정보 없이 /lecture에 요청한 결과이다.
토큰과 함께 요청하면 userID를 뷰에서 받아오는 것을 볼 수 있다.
마지막으로 refreshToken을 통해 요청하면 요청이 거부되는 것을 볼 수 있다.
accessToken으로 요청하면 요청이 거부되며

refreshToken으로 요청하면 새로운 accessToken이 발급되는 것을 볼 수 있다.