| marp | true |
|---|---|
| theme | default |
| paginate | true |
| backgroundColor | |
| backgroundImage | url('https://marp.app/assets/hero-background.svg') |
- 프로젝트 구조 개요
- 라우터(Router): 경로 관리와 요청 분배
- 미들웨어(Middleware): 요청 처리의 중간 단계
- 컨트롤러(Controller): 비즈니스 로직 처리
- 모델(Model): 데이터 구조 정의
- 유틸리티(Utility): 헬퍼 기능
- 전체 요청 처리 흐름
Express.js 백엔드는 MVC 아키텍처 패턴을 기반으로 구조화되어 있습니다.
server/
├── config/ # 설정 파일
│ ├── db.js # MongoDB 연결 설정
│ └── swagger.js # API 문서화 설정
├── controllers/ # 비즈니스 로직
│ ├── authController.js # 인증 관련 로직
│ ├── postController.js # 게시글 관련 로직
│ ├── commentController.js # 댓글 관련 로직
│ ├── userController.js # 사용자 관리 로직
│ └── aiController.js # AI 응답 생성 로직
├── middleware/ # 미들웨어
│ ├── auth.js # 인증 미들웨어
│ └── errorHandler.js # 오류 처리 미들웨어
├── models/ # 데이터 모델
│ ├── User.js # 사용자 모델
│ ├── Post.js # 게시글 모델
│ └── Comment.js # 댓글 모델
├── routes/ # 라우터
│ ├── authRoutes.js # 인증 관련 경로
│ ├── postRoutes.js # 게시글 관련 경로
│ ├── commentRoutes.js # 댓글 관련 경로
│ ├── userRoutes.js # 사용자 관리 경로
│ └── aiRoutes.js # AI 기능 경로
├── utils/ # 유틸리티 함수
│ ├── fileUpload.js # 파일 업로드 기능
│ └── openai.js # OpenAI API 통신
├── app.js # Express 앱 설정
└── server.js # 서버 시작 파일
const express = require('express');
const cors = require('cors');
const app = express();
// 전역 미들웨어 등록
app.use(express.json());
app.use(cors({ origin: "http://localhost:5173", credentials: true }));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// 라우터 등록
app.use('/auth', authRoutes);
app.use('/users', userRoutes);
app.use('/posts', postRoutes);
app.use('/comments', commentRoutes);
// 오류 처리 미들웨어
app.use(errorHandler);
module.exports = app;const app = require('./app');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
// 환경 변수 로드
dotenv.config();
const PORT = process.env.PORT || 5000;
// 서버 시작 함수
const startServer = () => {
app.listen(PORT, () => {
console.log(`서버가 ${PORT} 포트에서 실행 중입니다`);
});
};
// MongoDB 연결 후 서버 시작
mongoose.connect(process.env.MONGO_URI)
.then(() => {
console.log('MongoDB에 연결되었습니다');
startServer();
})
.catch(err => {
console.error('MongoDB 연결 오류:', err.message);
process.exit(1);
});- URL 경로와 HTTP 메서드를 기반으로 요청을 분류하는 역할
- 각 경로에 적절한 컨트롤러 함수를 연결
- 필요한 미들웨어를 경로별로 적용 가능
라우터는 경로 정의(URI + HTTP 메서드) → 미들웨어 적용 → 컨트롤러 실행 순서로 작동.
클라이언트 요청 → 라우터 → (미들웨어) → 컨트롤러 → 응답
실생활 비유: 우체국의 우편물 분류 시스템
- 우편물(요청)에 적힌 주소(URL)에 따라
- 적절한 담당자(컨트롤러)에게 전달
// routes/authRoutes.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { protect } = require('../middleware/auth');
// 회원가입 - POST /auth/register
router.post('/register', authController.register);
// 로그인 - POST /auth/login
router.post('/login', authController.login);
// 로그아웃 - GET /auth/logout
router.get('/logout', authController.logout);
// 사용자 정보 - GET /auth/me (인증 필요)
router.get('/me', protect, authController.getMe);
module.exports = router;// routes/postRoutes.js
const router = express.Router();
const { protect, checkOwnership } = require('../middleware/auth');
const Post = require('../models/Post');
// 모든 게시글 조회 및 게시글 생성
router.route('/')
.get(postController.getPosts)
.post(protect, upload.array('files', 3), postController.createPost);
// 특정 게시글 조회, 수정, 삭제
router.route('/:id')
.get(postController.getPost)
.put(protect, checkOwnership(Post), postController.updatePost)
.delete(protect, checkOwnership(Post), postController.deletePost);
// 게시글 좋아요/좋아요 취소
router.put('/:id/like', protect, postController.toggleLike);app.js에서 라우터를 기본 경로와 함께 등록.
// app.js
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes');
const postRoutes = require('./routes/postRoutes');
const commentRoutes = require('./routes/commentRoutes');
// API 라우트 등록
app.use('/auth', authRoutes);
app.use('/users', userRoutes);
app.use('/posts', postRoutes);
app.use('/comments', commentRoutes);app.use('/auth', authRoutes) 의미:
- 클라이언트가'/auth'로 시작하는 경로에 요청을 보낼 경우, 해당 요청은 authRoutes에서 정의된 라우터가 처리.
- 예: '/auth/login', '/auth/register' 등
- 요청과 응답 사이에서 실행되는 함수들
- 요청 객체(req), 응답 객체(res), 다음 미들웨어 호출 함수(next)를 인자로 받음
next()를 호출하면 다음 미들웨어로 제어권이 넘어감
미들웨어는 요청 전처리 → 응답 생성 또는 다음 미들웨어 호출의 순서로 작동.
요청 → 미들웨어1 → 미들웨어2 → ... → 라우트 핸들러 → 응답
실생활 비유: 공장의 조립 라인
- 각 작업대(미들웨어)는 특정 작업만 수행
- 작업이 끝나면 다음 작업대로 제품을 전달
-
애플리케이션 레벨 미들웨어:
app.use()app.use(express.json()); app.use(cors());
-
라우터 레벨 미들웨어: 특정 라우터에만 적용
router.use(loggerMiddleware);
-
경로 레벨 미들웨어: 특정 경로에만 적용
router.get('/profile', authMiddleware, profileController);
-
오류 처리 미들웨어: 4개의 인자를 가짐
app.use((err, req, res, next) => { /* 오류 처리 */ });
// middleware/auth.js
exports.protect = async (req, res, next) => {
let token;
// 헤더에서 Authorization 토큰 확인
if (req.headers.authorization?.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
// 토큰이 없으면 접근 거부
if (!token) {
return res.status(401).json({
success: false,
message: '이 리소스에 접근하려면 로그인이 필요합니다'
});
}
try {
// 토큰 검증 및 사용자 정보 가져오기
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
if (!req.user) {
return res.status(401).json({
success: false,
message: '사용자를 찾을 수 없습니다'
});
}
next(); // 다음 미들웨어로 진행
} catch (err) {
return res.status(401).json({
success: false,
message: '인증에 실패했습니다'
});
}
};// middleware/auth.js
exports.checkOwnership = (model) => async (req, res, next) => {
try {
const resource = await model.findById(req.params.id);
if (!resource) {
return res.status(404).json({
success: false,
message: '리소스를 찾을 수 없습니다'
});
}
// 관리자는 모든 리소스에 접근 가능
if (req.user.role === 'admin') {
req.resource = resource;
return next();
}
// 소유자 확인
const ownerId = resource.author || resource.user;
if (ownerId.toString() !== req.user.id) {
return res.status(403).json({
success: false,
message: '이 리소스에 대한 권한이 없습니다'
});
}
// 리소스를 req 객체에 추가 (컨트롤러에서 사용)
req.resource = resource;
next();
} catch (err) {
next(err);
}
};// middleware/errorHandler.js
exports.errorHandler = (err, req, res, next) => {
console.error(err);
let error = { ...err };
error.message = err.message;
// Mongoose 잘못된 ObjectId
if (err.name === 'CastError') {
error = { message: '리소스를 찾을 수 없습니다', statusCode: 404 };
}
// Mongoose 중복 키 에러
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
error = { message: `${field} 필드의 값이 이미 사용 중입니다`, statusCode: 400 };
}
// Mongoose 유효성 검사 실패
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = { message, statusCode: 400 };
}
// 응답 반환
res.status(error.statusCode || 500).json({
success: false,
message: error.message || '서버 에러',
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
});
};에러 처리 미들웨어와 함께 비동기 함수의 오류를 캐치하는 유틸리티.
// middleware/errorHandler.js
exports.asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);사용 예시:
exports.getPost = asyncHandler(async (req, res) => {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
success: false,
message: '게시글을 찾을 수 없습니다'
});
}
res.status(200).json({
success: true,
data: post
});
});- 비즈니스 로직을 처리하는 함수들
- 요청에서 데이터를 추출하여 처리
- 데이터베이스 작업 수행
- 클라이언트에게 응답 반환
컨트롤러는 요청 데이터 추출 → 비즈니스 로직 처리 → 응답 반환의 순서로 작동.
실생활 비유: 레스토랑 주방장
- 주문(요청)을 받아서
- 음식(데이터)을 준비하고
- 손님(클라이언트)에게 음식(응답)을 제공
// controllers/authController.js
exports.login = asyncHandler(async (req, res) => {
const { email, password } = req.body;
// 1. 요청 데이터 검증
if (!email || !password) {
return res.status(400).json({
success: false,
message: '이메일과 비밀번호를 입력해주세요'
});
}
// 2. 사용자 조회
const user = await User.findOne({ email }).select('+password');
// 3. 사용자 존재 여부 확인
if (!user) {
return res.status(401).json({
success: false,
message: '유효하지 않은 인증 정보입니다'
});
}
// 4. 비밀번호 확인
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return res.status(401).json({
success: false,
message: '유효하지 않은 인증 정보입니다'
});
}
// 5. JWT 토큰 생성 및 응답
sendTokenResponse(user, 200, res);
});// controllers/postController.js
exports.getPost = asyncHandler(async (req, res) => {
// 게시글 ID로 조회
const post = await Post.findById(req.params.id)
.populate({
path: 'author',
select: 'username nickname profileImage'
});
// 게시글 존재 확인
if (!post) {
return res.status(404).json({
success: false,
message: '게시글을 찾을 수 없습니다'
});
}
// 댓글 정보 조회
if (post.comments && post.comments.length > 0) {
await post.populate({
path: 'comments',
populate: {
path: 'author',
select: 'username nickname profileImage'
}
});
}
// 조회수 증가
post.viewCount += 1;
await post.save({ validateBeforeSave: false });
// 응답 반환
res.status(200).json({
success: true,
data: post
});
});// controllers/postController.js
exports.createPost = asyncHandler(async (req, res) => {
const { title, content, categories, tags } = req.body;
// 카테고리와 태그 처리
const categoriesArray = Array.isArray(categories)
? categories
: categories.split(',').map(c => c.trim());
const tagsArray = Array.isArray(tags)
? tags
: tags.split(',').map(t => t.trim());
// 첨부파일 처리
const attachments = req.files ? req.files.map(file => ({
filename: file.filename,
originalname: file.originalname,
path: `/uploads/post-attachments/${file.filename}`,
mimetype: file.mimetype,
size: file.size
})) : [];
// 게시글 생성
const post = await Post.create({
title,
content,
author: req.user.id,
categories: categoriesArray,
tags: tagsArray,
attachments
});
// 응답
res.status(201).json({
success: true,
data: post
});
});- MongoDB 스키마를 정의하는 파일
- 데이터 구조, 유효성 검사 규칙 정의
- 데이터 관련 메서드 구현
- Mongoose를 사용하여 MongoDB와 상호작용
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: [true, '사용자 이름은 필수입니다'],
unique: true,
trim: true,
minlength: [3, '사용자 이름은 최소 3자 이상이어야 합니다'],
maxlength: [20, '사용자 이름은 최대 20자까지 가능합니다'],
},
email: {
type: String,
required: [true, '이메일은 필수입니다'],
unique: true,
match: [
/^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/,
'유효한 이메일 주소를 입력해주세요',
],
},
password: {
type: String,
required: [true, '비밀번호는 필수입니다'],
minlength: [6, '비밀번호는 최소 6자 이상이어야 합니다'],
select: false, // 기본적으로 조회 결과에 포함되지 않음
},
// 기타 필드...
}, { timestamps: true });// models/User.js (계속)
// 비밀번호 해싱 미들웨어
UserSchema.pre('save', async function (next) {
if (!this.isModified('password')) {
return next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// 비밀번호 확인 메서드
UserSchema.methods.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
// JWT 토큰 생성 메서드
UserSchema.methods.getSignedJwtToken = function () {
return jwt.sign(
{ id: this._id, role: this.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
};
module.exports = mongoose.model('User', UserSchema);// models/Post.js
const mongoose = require('mongoose');
const PostSchema = new mongoose.Schema({
title: {
type: String,
required: [true, '제목은 필수입니다'],
trim: true,
maxlength: [100, '제목은 최대 100자까지 가능합니다'],
},
content: {
type: String,
required: [true, '내용은 필수입니다'],
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
categories: {
type: [String],
default: ['기타'],
enum: ['수학', '물리학', '화학', '생물학', '컴퓨터공학', '전자공학', '기계공학', '경영학', '경제학', '심리학', '사회학', '기타']
},
// 기타 필드...
}, { timestamps: true, toJSON: { virtuals: true } });
// 가상 필드: 좋아요 수
PostSchema.virtual('likeCount').get(function () {
return this.likes.length;
});프로젝트에서 재사용 가능한 다양한 헬퍼 함수들을 제공.
- 파일 업로드 유틸리티 (multer 설정)
- OpenAI API 연동 유틸리티
- JWT 토큰 관리
- 에러 처리 유틸리티
// utils/fileUpload.js
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
// 안전한 파일명 생성
const generateSafeFilename = (originalname) => {
const extension = path.extname(originalname);
const timestamp = Date.now();
const randomString = crypto.randomBytes(8).toString('hex');
return `${path.basename(originalname, extension).substring(0, 50)}_${timestamp}_${randomString}${extension}`;
};
// 프로필 이미지 저장 설정
const profileImageStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.join(__dirname, '..', 'uploads/profile-images'));
},
filename: (req, file, cb) => {
const safeFilename = generateSafeFilename(file.originalname);
cb(null, `profile_${req.user.id}_${safeFilename}`);
}
});
// Multer 설정 내보내기
const upload = {
profileImage: multer({
storage: profileImageStorage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: imageFilter
}),
// 기타 업로드 설정...
};// utils/openai.js
class OpenAIClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseURL = 'https://api.openai.com/v1';
}
async generateResponse(content, title, category, tags = []) {
try {
// 시스템 및 사용자 프롬프트 생성
const systemPrompt = this.createSystemPrompt(category);
const userPrompt = this.createUserPrompt(content, title, category, tags);
const response = await axios.post(
`${this.baseURL}/chat/completions`,
{
model: 'gpt-4',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
],
max_tokens: 500,
temperature: 0.7,
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
}
}
);
return response.data.choices[0].message.content;
} catch (error) {
console.error('OpenAI API 오류:', error.message);
throw new Error('AI 응답 생성 실패');
}
}
// 기타 메서드...
}- 클라이언트에서 요청 전송 (
POST /posts/123/comments) - Express 앱이 요청을 받아 전역 미들웨어 적용 (JSON 파싱, CORS 등)
- 라우터가 경로에 따라 적합한 핸들러로 요청 전달 (
commentRoutes.js) - 인증 미들웨어가 JWT 토큰을 검증하고 사용자 정보 추가
- 컨트롤러가 비즈니스 로직 처리 (
commentController.createComment) - 모델을 통해 데이터베이스 작업 수행 (
Comment.create()) - 컨트롤러가 클라이언트에 응답 반환 (
res.status(201).json({...})) - 클라이언트가 응답 수신
- 라우터(Router): 요청을 적절한 핸들러로 전달하는 분배기
- 미들웨어(Middleware): 요청 처리 과정의 중간 단계 함수들
- 컨트롤러(Controller): 비즈니스 로직을 처리하는 함수들
- 모델(Model): 데이터 구조와 유효성 검사 규칙 정의
- 유틸리티(Utility): 공통 기능을 제공하는 헬퍼 함수들
각 구성 요소는 단일 책임 원칙에 따라 명확한 역할을 수행하며, 함께 작동하여 완전한 백엔드 시스템을 구성합니다.
백엔드 개발 및 Express.js에 대한 질문이 있으신가요?
감사합니다!
