Backend Developer Guide Series
BackendDevGuide0002
Express.js 완전 정복 A-Z
비전공자도 현업에서 바로 쓸 수 있는 Express.js 완전 가이드 — 입문부터 실전까지
예상 학습시간
30~40시간
난이도
입문 ~ 중급
선수 지식
Node.js 기초
목표
현업 투입 가능
📋 학습 목차
01. Express.js란 무엇인가?
02. 설치와 프로젝트 구조
03. 라우팅 완전 정복
04. 미들웨어 완전 정복
05. 요청/응답 처리 심화
06. MVC 패턴 & 구조 설계
07. REST API 설계 원칙
08. 유효성 검사 & 에러 처리
09. 인증(JWT) 완전 구현
10. 보안 & 성능 최적화
11. 실전 프로젝트: 블로그 API
12. 테스트 코드 작성법
13. 현업/면접 Q&A
14. 백엔드 학습 로드맵
01 Express.js란 무엇인가?
💡 핵심 한 줄 요약: Express.js는 Node.js를 위한 미니멀하고 유연한 웹 프레임워크로, 웹 서버와 API를 빠르게 만들 수 있게 해주는 도구입니다.
🍽️ 음식점 비유로 이해하기
Node.js가 원재료(식재료)라면, Express.js는 주방 설비(가스레인지, 오븐, 도마 등)입니다.
원재료만으로도 요리를 만들 수 있지만, 주방 설비가 있으면 훨씬 빠르고 효율적으로 만들 수 있죠!
🛣️
라우팅
URL에 따라 요청을 알맞은 함수로 연결 (주문을 맞는 직원에게 배분)
🔧
미들웨어
요청 처리 전/후에 공통 작업 수행 (주방 위생검사, 재료 손질)
⚡
간결한 코드
순수 Node.js보다 훨씬 적은 코드로 같은 기능 구현 가능
📊 Express.js vs 순수 Node.js 비교
| 비교 항목 | 순수 Node.js http | Express.js |
|---|---|---|
| 라우팅 | if/else로 직접 분기 처리 | app.get/post/put/delete 메서드 제공 |
| 미들웨어 | 직접 구현해야 함 | app.use()로 체이닝 가능 |
| JSON 파싱 | 수동으로 스트림 처리 | express.json() 한 줄로 완료 |
| 코드 양 | 라우터 하나에 30~50줄 | 5~10줄로 간결하게 |
| 에러 처리 | try-catch 수동 관리 | 4개 인자 에러 핸들러 패턴 |
🏢 Express.js를 사용하는 기업들
Netflix Twitter/X IBM 카카오 쿠팡 라인 당근마켓
02 설치와 프로젝트 구조
⚠️ 선수 조건: Node.js 18.x 이상, npm 9.x 이상이 설치되어 있어야 합니다. 미설치 시 BackendDevGuide0001을 먼저 완료하세요!
🔧 프로젝트 초기 세팅
# 1. 프로젝트 폴더 생성
mkdir express-masterclass && cd express-masterclass
# 2. npm 초기화 (package.json 생성)
npm init -y
# 3. Express 설치
npm install express
# 4. 개발용 도구들 설치
npm install -D nodemon # 파일 변경 시 자동 재시작
# 5. 필수 미들웨어들 설치
npm install dotenv cors helmet morgan
# 6. 버전 확인
node -e "console.log(require('./node_modules/express/package.json').version)"
📁 현업 표준 프로젝트 구조
💡 처음에는 app.js 하나에 다 쓰고 싶겠지만, 현업에서는 아래 구조로 관리합니다. 이 구조를 처음부터 익혀두세요!
express-masterclass/
├── src/
│ ├── routes/ # URL 경로 정의
│ │ ├── index.js # 루트 라우터 (모든 라우터 통합)
│ │ ├── auth.routes.js
│ │ ├── user.routes.js
│ │ └── post.routes.js
│ ├── controllers/ # 요청/응답 처리
│ │ ├── auth.controller.js
│ │ ├── user.controller.js
│ │ └── post.controller.js
│ ├── services/ # 핵심 비즈니스 로직 (DB 접근 등)
│ │ ├── auth.service.js
│ │ └── user.service.js
│ ├── middleware/ # 미들웨어 함수들
│ │ ├── auth.middleware.js
│ │ ├── validate.middleware.js
│ │ └── error.middleware.js
│ ├── models/ # 데이터 모델 (DB 스키마)
│ ├── utils/ # 유틸리티 함수들
│ │ ├── ApiError.js # 커스텀 에러 클래스
│ │ └── catchAsync.js
│ └── app.js # Express 앱 설정
├── .env # ⚠️ 절대 Git에 올리지 않음!
├── .env.example # 환경변수 예시 (팀원 공유용)
├── .gitignore
├── server.js # 서버 시작점
└── package.json
🤔 왜 app.js와 server.js를 분리하나요?
테스트 코드 작성 시 서버를 실제로 시작하지 않고 Express 앱만 임포트해서 테스트할 수 있기 때문입니다. Jest 같은 테스트 프레임워크에서 require('./src/app')만 불러와 포트 충돌 없이 테스트할 수 있습니다.
📝 기본 설정 파일들
src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const routes = require('./routes');
const errorHandler = require('./middleware/error.middleware');
const app = express();
// ====== 보안 미들웨어 ======
app.use(helmet()); // 보안 HTTP 헤더 설정
app.use(cors({
origin: process.env.CLIENT_URL || 'http://localhost:3001',
credentials: true, // 쿠키 허용
}));
// ====== 요청 파싱 미들웨어 ======
app.use(express.json({ limit: '10mb' })); // JSON body 파싱
app.use(express.urlencoded({ extended: true })); // form data 파싱
// ====== 로깅 미들웨어 ======
if (process.env.NODE_ENV !== 'test') {
app.use(morgan('dev')); // GET /api/users 200 15ms
}
// ====== 라우터 연결 ======
app.use('/api', routes);
// ====== 에러 핸들러 (반드시 마지막에!) ======
app.use(errorHandler);
module.exports = app;
03 라우팅 완전 정복
📌 라우팅이란? 클라이언트의 요청(URL + HTTP 메서드)을 어떤 함수가 처리할지 연결(매핑)하는 것입니다. 마치 콜센터에서 "A 문의는 1번 상담사, B 문의는 2번 상담사"로 연결하는 것과 같습니다.
📊 HTTP 메서드와 CRUD 매핑
| HTTP 메서드 | CRUD 동작 | Express 코드 | URL 예시 | 응답 코드 |
|---|---|---|---|---|
| GET | Read (조회) | app.get() |
/api/users | 200 |
| POST | Create (생성) | app.post() |
/api/users | 201 |
| PUT | Update (전체 수정) | app.put() |
/api/users/:id | 200 |
| PATCH | Update (부분 수정) | app.patch() |
/api/users/:id | 200 |
| DELETE | Delete (삭제) | app.delete() |
/api/users/:id | 204 |
🔑 라우터 파라미터 3가지
const express = require('express');
const router = express.Router();
// 1. Route Parameters (:id) — URL 경로값
// GET /api/users/42
router.get('/:id', (req, res) => {
const userId = req.params.id; // '42' (문자열!)
const numId = Number(userId); // 42 (숫자로 변환)
res.json({ userId: numId });
});
// 2. Query String (?page=1) — 쿼리값
// GET /api/users?page=2&limit=10&search=홍길동
router.get('/', (req, res) => {
const {
page = 1, // 기본값 1
limit = 10, // 기본값 10
search = '', // 기본값 빈 문자열
sort = 'createdAt', // 정렬 기준
} = req.query;
res.json({ page: Number(page), limit: Number(limit), search, sort });
});
// 3. Request Body — POST/PUT/PATCH 데이터
// POST /api/users Body: {"name":"홍길동","email":"hong@e.com"}
router.post('/', (req, res) => {
const { name, email, password } = req.body; // express.json() 필요!
if (!name || !email) {
return res.status(400).json({ error: 'name과 email은 필수입니다' });
}
res.status(201).json({ message: '생성 완료', user: { name, email } });
});
💡 router.route() 체이닝으로 코드 줄이기
// ❌ 중복 경로 반복 (나쁜 방법)
router.get('/:id', getUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);
// ✅ 체이닝으로 묶기 (좋은 방법)
router.route('/')
.get(getAllUsers)
.post(createUser);
router.route('/:id')
.get(getUser)
.put(updateUser)
.patch(patchUser)
.delete(deleteUser);
04 미들웨어 완전 정복
📌 미들웨어란? 요청(Request)이 들어오고 응답(Response)이 나가기 전 사이에서 실행되는 함수입니다. 공항 보안 검색대처럼 탑승 전 반드시 통과해야 하는 관문들입니다!
미들웨어는 (req, res, next) 세 가지 인자를 받으며, 다음 미들웨어로 넘기려면 next()를 호출해야 합니다.
미들웨어는 (req, res, next) 세 가지 인자를 받으며, 다음 미들웨어로 넘기려면 next()를 호출해야 합니다.
🏗️ 미들웨어 실행 흐름
클라이언트
요청
요청
→
helmet()
→
cors()
→
morgan()
→
express.json()
→
authenticate
→
라우터
핸들러
핸들러
→
응답
전송
전송
📋 미들웨어 5가지 종류
| 종류 | 사용 방식 | 예시 |
|---|---|---|
| 애플리케이션 미들웨어 | app.use(fn) |
전역 로깅, CORS |
| 라우터 미들웨어 | router.use(fn) |
특정 라우터 인증 |
| 에러 처리 미들웨어 | app.use((err,req,res,next)) |
전역 에러 처리 (4인자) |
| 내장 미들웨어 | express.json() |
Body 파싱, static 파일 |
| 서드파티 미들웨어 | npm install 후 use() | morgan, helmet, cors |
💻 JWT 인증 미들웨어 직접 만들기 (현업 필수!)
// middleware/auth.middleware.js
const jwt = require('jsonwebtoken');
/**
* JWT 인증 미들웨어
* Authorization: Bearer <token> 헤더에서 토큰 검증
*/
const authenticate = (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: '인증 토큰이 필요합니다',
});
}
const token = authHeader.split(' ')[1]; // 'Bearer ' 이후 토큰만 추출
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // ⭐ 다음 핸들러에서 req.user로 접근 가능!
next(); // ⭐ 다음 미들웨어/라우터로 이동
} catch (err) {
res.status(401).json({
success: false,
message: '유효하지 않은 토큰입니다',
});
}
};
/**
* 권한 체크 미들웨어 (RBAC: Role-Based Access Control)
* authenticate 이후에만 사용!
*/
const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: '접근 권한이 없습니다',
});
}
next();
};
};
// 사용 예시:
// router.delete('/:id', authenticate, authorize('ADMIN'), deleteUser);
module.exports = { authenticate, authorize };
⚡ 자주 쓰는 서드파티 미들웨어 정리
| 패키지명 | 역할 | 중요도 |
|---|---|---|
morgan |
HTTP 요청 로깅 | 권장 |
helmet |
보안 헤더 설정 (XSS, CSRF 등 방어) | 필수 |
cors |
CORS 허용 설정 | 필수 |
express-rate-limit |
API 요청 제한 (DDoS 방어) | 필수 |
compression |
응답 gzip 압축 (트래픽 ~70% 절감) | 권장 |
multer |
파일 업로드 처리 | 필요시 |
05 요청/응답 처리 심화
📥 요청(Request) 객체 완전 분석
app.get('/debug', (req, res) => {
console.log(req.method); // 'GET'
console.log(req.url); // '/debug?test=1'
console.log(req.path); // '/debug' (query 제외)
console.log(req.params); // { id: '42' } — URL 파라미터
console.log(req.query); // { test: '1' } — 쿼리스트링
console.log(req.body); // { name: '홍길동' } — Body 데이터
console.log(req.headers); // 모든 요청 헤더
console.log(req.cookies); // 쿠키 (cookie-parser 필요)
console.log(req.ip); // 클라이언트 IP 주소
console.log(req.user); // 인증 미들웨어에서 설정한 사용자
console.log(req.get('Content-Type')); // 특정 헤더 값 조회
console.log(req.is('application/json')); // Content-Type 확인
});
📤 응답(Response) 메서드 완전 정리
// 1. JSON 응답 (API에서 가장 많이 사용)
res.status(200).json({ success: true, data: user });
// 2. 파일 다운로드
res.download('/path/to/file.pdf', 'report.pdf');
// 3. 리다이렉트
res.redirect(301, 'https://newdomain.com'); // 영구 이동
res.redirect('/login'); // 임시 이동 (302)
// 4. 쿠키 설정 (httpOnly로 XSS 방어)
res.cookie('refreshToken', token, {
httpOnly: true, // JS 접근 차단 (XSS 방어)
secure: true, // HTTPS에서만 전송
sameSite: 'strict', // CSRF 방어
maxAge: 7 * 24 * 60 * 60 * 1000, // 7일
});
// 5. 상태 코드만 전송
res.sendStatus(204); // 204 No Content (DELETE 성공 시)
📊 HTTP 상태 코드 완전 정리 (현업 필수 암기!)
| 범주 | 코드 | 의미 | 사용 상황 |
|---|---|---|---|
| 2xx 성공 | 200 OK | 요청 성공 | GET, PUT, PATCH 성공 |
| 201 Created | 리소스 생성됨 | POST 생성 성공 | |
| 204 No Content | 응답 본문 없음 | DELETE 성공 | |
| 4xx 클라이언트 오류 | 400 Bad Request | 잘못된 요청 | 입력값 오류, 형식 불일치 |
| 401 Unauthorized | 인증 필요 | 로그인 안 한 상태 | |
| 403 Forbidden | 권한 없음 | 로그인은 했지만 권한 없음 | |
| 404 Not Found | 리소스 없음 | 존재하지 않는 데이터 요청 | |
| 422 Unprocessable | 유효성 검사 실패 | Joi/Zod 검증 실패 | |
| 5xx 서버 오류 | 500 Internal Server Error | 서버 코드 오류 | 처리 안 된 예외 발생 |
| 502 Bad Gateway | 게이트웨이 오류 | 프록시/로드밸런서 문제 | |
| 503 Service Unavailable | 서버 불가 | 서버 점검/과부하 |
🎯 표준 API 응답 형식 (현업 표준)
// utils/response.js — 일관된 응답 형식
const successResponse = (res, data, message = '성공', statusCode = 200) => {
return res.status(statusCode).json({
success: true,
message,
data,
timestamp: new Date().toISOString(),
});
};
const paginatedResponse = (res, data, total, page, limit) => {
return res.status(200).json({
success: true,
data,
pagination: {
total,
page: Number(page),
limit: Number(limit),
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
},
});
};
module.exports = { successResponse, paginatedResponse };
06 MVC 패턴 & 구조 설계
📌 MVC란? Model-View-Controller의 약자입니다. 코드를 역할에 따라 3가지로 분리하는 설계 패턴입니다. 백엔드 API에서는 View 대신 Response(JSON 응답)로 대체합니다.
🏗️ MVC 역할 분리
| 레이어 | 역할 | 파일 위치 | 담당 업무 |
|---|---|---|---|
| Routes | URL 연결 | routes/ | URL-메서드-컨트롤러 연결만 담당 |
| Controller | 요청/응답 처리 | controllers/ | req에서 데이터 추출 → Service 호출 → res 반환 |
| Service | 비즈니스 로직 | services/ | 실제 로직 처리 (DB 조회, 계산, 검증) |
| Model | 데이터 구조 | models/ | DB 스키마 정의 (Prisma, Mongoose 등) |
💻 MVC 패턴 실전 코드
routes/user.routes.js — URL 연결만!
const express = require('express');
const router = express.Router();
const userController = require('../controllers/user.controller');
const { authenticate } = require('../middleware/auth.middleware');
router.route('/')
.get(userController.getAllUsers)
.post(authenticate, userController.createUser);
router.route('/:id')
.get(userController.getUserById)
.put(authenticate, userController.updateUser)
.delete(authenticate, userController.deleteUser);
module.exports = router;
controllers/user.controller.js — req/res 처리
const userService = require('../services/user.service');
const catchAsync = require('../utils/catchAsync');
// ✅ catchAsync 래퍼로 try-catch 없이 에러 처리
const getAllUsers = catchAsync(async (req, res) => {
const { page = 1, limit = 10, search } = req.query;
const result = await userService.findAllUsers({ page, limit, search });
res.json({ success: true, ...result });
});
const getUserById = catchAsync(async (req, res) => {
const user = await userService.findUserById(req.params.id);
res.json({ success: true, data: user });
});
const createUser = catchAsync(async (req, res) => {
const user = await userService.createUser(req.body);
res.status(201).json({ success: true, data: user });
});
module.exports = { getAllUsers, getUserById, createUser };
services/user.service.js — 비즈니스 로직
const bcrypt = require('bcryptjs');
const prisma = require('../db/prisma');
const { ApiError } = require('../utils/ApiError');
const findAllUsers = async ({ page, limit, search }) => {
const skip = (page - 1) * limit;
const where = search
? { OR: [
{ name: { contains: search } },
{ email: { contains: search } },
]}
: {};
// Promise.all로 병렬 처리 (성능 최적화!)
const [users, total] = await Promise.all([
prisma.user.findMany({
where, skip: Number(skip), take: Number(limit),
select: { id: true, name: true, email: true, createdAt: true },
}),
prisma.user.count({ where }),
]);
return { data: users, total, page: Number(page), totalPages: Math.ceil(total / limit) };
};
const findUserById = async (id) => {
const user = await prisma.user.findUnique({
where: { id: Number(id) },
select: { id: true, name: true, email: true },
});
if (!user) throw new ApiError(404, '사용자를 찾을 수 없습니다');
return user;
};
module.exports = { findAllUsers, findUserById };
07 REST API 설계 원칙
📌 REST란? REpresentational State Transfer의 약자입니다. HTTP를 잘 활용하기 위한 아키텍처 스타일로, REST를 따르면 API 사용자가 문서 없이도 URL만 보고 어떤 동작인지 예측할 수 있습니다.
📐 REST URL 설계 원칙 (좋은 예 vs 나쁜 예)
| 원칙 | ❌ 나쁜 예 | ✅ 좋은 예 |
|---|---|---|
| 명사 사용 | /getUsers, /createUser | /users |
| 복수형 사용 | /user/1 | /users/1 |
| 소문자 + 하이픈 | /BlogPosts, /blog_posts | /blog-posts |
| 버전 관리 | /users (버전 없음) | /api/v1/users |
| 계층 관계 | /getPostsByUser?userId=1 | /users/1/posts |
| 동작은 메서드로 | /deleteUser/1 | DELETE /users/1 |
📋 게시판 REST API 설계 완성 예시
| HTTP 메서드 | URL | 동작 | 인증 | 상태코드 |
|---|---|---|---|---|
| GET | /api/v1/posts | 게시글 목록 (페이지네이션) | 불필요 | 200 |
| GET | /api/v1/posts/:id | 특정 게시글 조회 | 불필요 | 200, 404 |
| POST | /api/v1/posts | 게시글 생성 | 필요 | 201, 401 |
| PUT | /api/v1/posts/:id | 게시글 전체 수정 | 필요 (작성자) | 200, 403, 404 |
| DELETE | /api/v1/posts/:id | 게시글 삭제 | 필요 (작성자) | 204, 403, 404 |
| GET | /api/v1/posts/:id/comments | 특정 게시글의 댓글 | 불필요 | 200 |
| POST | /api/v1/posts/:id/likes | 게시글 좋아요 | 필요 | 201, 409 |
08 유효성 검사 & 에러 처리
🛡️ Joi로 유효성 검사 (실전 패턴)
# 설치
npm install joi
// middleware/validate.middleware.js
const Joi = require('joi');
const schemas = {
register: Joi.object({
name: Joi.string().min(2).max(50).required()
.messages({ 'any.required': '이름은 필수입니다' }),
email: Joi.string().email().required()
.messages({ 'string.email': '유효한 이메일 형식이 아닙니다' }),
password: Joi.string()
.pattern(new RegExp('^(?=.*[a-z])(?=.*[0-9]).{8,}$'))
.required()
.messages({ 'string.pattern.base': '비밀번호는 영문+숫자 8자 이상' }),
}),
createPost: Joi.object({
title: Joi.string().min(1).max(200).required(),
content: Joi.string().min(10).required(),
tags: Joi.array().items(Joi.string()).max(5),
}),
};
const validate = (schemaName) => (req, res, next) => {
const { error, value } = schemas[schemaName].validate(req.body, { abortEarly: false });
if (error) {
const errors = error.details.map(d => d.message);
return res.status(422).json({ success: false, errors });
}
req.body = value; // 정제된 데이터로 교체
next();
};
module.exports = { validate };
💥 글로벌 에러 핸들러 (완전 버전)
// utils/ApiError.js
class ApiError extends Error {
constructor(statusCode, message, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
}
}
// utils/catchAsync.js — try-catch 없이 async 에러 처리!
const catchAsync = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// middleware/error.middleware.js — 반드시 4개 인자!
const errorHandler = (err, req, res, next) => {
let { statusCode = 500, message } = err;
// Prisma 에러 처리
if (err.code === 'P2002') { statusCode = 409; message = '이미 존재하는 데이터'; }
if (err.code === 'P2025') { statusCode = 404; message = '데이터를 찾을 수 없습니다'; }
// JWT 에러 처리
if (err.name === 'JsonWebTokenError') { statusCode = 401; message = '유효하지 않은 토큰'; }
if (err.name === 'TokenExpiredError') { statusCode = 401; message = '토큰이 만료되었습니다'; }
const isDev = process.env.NODE_ENV === 'development';
res.status(statusCode).json({
success: false,
message: message || '서버 내부 오류',
...isDev && { stack: err.stack }, // 개발 환경에서만 스택 노출
});
};
module.exports = errorHandler;
09 인증(JWT) 완전 구현
📌 JWT란? JSON Web Token의 약자입니다. 로그인 성공 시 서버가 발급하는 전자 신분증으로, 이후 요청마다 이 토큰을 헤더에 보내면 서버가 신원을 확인합니다.
🔐 JWT 구조 이해
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJ1c2VySWQiOjEsInJvbGUiOiJVU0VSIn0 . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header 알고리즘 정보
Payload 사용자 데이터 (암호화 아님! 인코딩)
Signature 위변조 방지 서명
🔑 회원가입 & 로그인 완전 구현
# 필요 패키지
npm install jsonwebtoken bcryptjs
// services/auth.service.js
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const register = async ({ name, email, password }) => {
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) throw new ApiError(409, '이미 사용 중인 이메일입니다');
// ⭐ 비밀번호는 절대 평문 저장 금지! bcrypt로 해시화
const hashedPw = await bcrypt.hash(password, 12); // saltRounds: 12 권장
const user = await prisma.user.create({
data: { name, email, password: hashedPw },
select: { id: true, name: true, email: true }, // 비밀번호 제외!
});
return user;
};
const login = async ({ email, password }) => {
const user = await prisma.user.findUnique({ where: { email } });
// ⭐ 보안 팁: 이메일 없어도 같은 메시지 사용 (계정 존재 여부 노출 방지)
if (!user) throw new ApiError(401, '이메일 또는 비밀번호가 올바르지 않습니다');
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) throw new ApiError(401, '이메일 또는 비밀번호가 올바르지 않습니다');
// Access Token (짧은 수명) + Refresh Token (긴 수명) 패턴
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' } // ⭐ Access Token은 짧게!
);
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken, user: { id: user.id, name: user.name } };
};
🔒 JWT 보안 체크리스트
- Access Token 수명은 짧게 (15분~1시간)
- Refresh Token은 httpOnly 쿠키에 저장 (JS 접근 불가, XSS 방어)
- JWT_SECRET은 32자 이상의 랜덤 문자열 사용
- 민감 정보(비밀번호 등)는 Payload에 절대 넣지 않기
10 보안 & 성능 최적화
🔒 보안 필수 체크리스트
| 보안 항목 | 방법 | 패키지 | 중요도 |
|---|---|---|---|
| SQL Injection 방지 | ORM 사용 | Prisma, Sequelize | 필수 |
| XSS 방지 | 입력값 새니타이징 | xss, helmet | 필수 |
| Rate Limiting | IP당 요청 수 제한 | express-rate-limit | 필수 |
| 보안 헤더 | HTTP 보안 헤더 자동 설정 | helmet | 필수 |
| 환경변수 보호 | .env 파일 gitignore | dotenv | 필수 |
| bcrypt 해시화 | 비밀번호 평문 저장 금지 | bcryptjs | 필수 |
⚡ Rate Limiting 실전 설정
const rateLimit = require('express-rate-limit');
// 전역 API Rate Limit
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // 100번까지
standardHeaders: true,
message: { error: '너무 많은 요청입니다. 잠시 후 다시 시도해주세요.' },
});
// 로그인 전용 (더 엄격하게)
const loginLimiter = rateLimit({
windowMs: 60 * 1000, // 1분
max: 5, // 5번까지
skipSuccessfulRequests: true, // 성공한 요청은 카운트 안함
});
app.use('/api', globalLimiter);
app.use('/api/v1/auth/login', loginLimiter);
🚀 성능 최적화 핵심 기법
// 1. 응답 압축 (gzip) - 네트워크 트래픽 ~70% 감소
const compression = require('compression');
app.use(compression());
// 2. N+1 문제 방지 (DB 쿼리 최적화)
// ❌ N+1 문제 (사용자 10명이면 쿼리 11번!)
const posts = await prisma.post.findMany();
for (const post of posts) {
post.author = await prisma.user.findUnique({ where: { id: post.authorId } });
}
// ✅ include로 한 번에 조인 (쿼리 1번!)
const posts = await prisma.post.findMany({
include: { author: { select: { name: true, email: true } } },
});
// 3. 병렬 처리로 응답 속도 향상
// ❌ 순차 처리 (총 600ms)
const user = await getUser(); // 200ms
const posts = await getPosts(); // 200ms
const stats = await getStats(); // 200ms
// ✅ 병렬 처리 (총 200ms — 3배 빠름!)
const [user, posts, stats] = await Promise.all([
getUser(), getPosts(), getStats()
]);
11 실전 프로젝트: 블로그 API 완전 구현
🎯 무엇을 만드나요?
회원가입/로그인 + 게시글 CRUD + 댓글 + 좋아요 기능을 갖춘 완전한 블로그 REST API를 구현합니다. 이것을 완성하면 어떤 백엔드 프로젝트도 만들 수 있습니다!
🗄️ Prisma 스키마 설계
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
role Role @default(USER)
posts Post[]
comments Comment[]
likes Like[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String @db.Text
published Boolean @default(false)
viewCount Int @default(0)
tags String[]
author User @relation(fields: [authorId], references: [id])
authorId Int
comments Comment[]
likes Like[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Comment {
id Int @id @default(autoincrement())
content String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId Int
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}
model Like {
userId Int
postId Int
user User @relation(fields: [userId], references: [id])
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@id([userId, postId]) // 복합 기본키 (중복 좋아요 방지!)
}
enum Role { USER ADMIN }
📮 Postman API 테스트 시나리오 (전체 흐름)
# ============ 1. 회원가입 ============
POST http://localhost:3000/api/v1/auth/register
Content-Type: application/json
{
"name": "홍길동",
"email": "hong@example.com",
"password": "password123"
}
# 응답: 201 { "success": true, "data": { "id": 1, "name": "홍길동", "email": "..." } }
# ============ 2. 로그인 → 토큰 저장 ============
POST http://localhost:3000/api/v1/auth/login
# 응답의 accessToken을 복사해서 다음 요청에 사용!
# ============ 3. 게시글 작성 (토큰 필요) ============
POST http://localhost:3000/api/v1/posts
Authorization: Bearer <accessToken>
{
"title": "첫 번째 게시글",
"content": "Express.js 너무 재밌다!",
"tags": ["express", "nodejs"]
}
# ============ 4. 게시글 목록 (페이지네이션) ============
GET http://localhost:3000/api/v1/posts?page=1&limit=5&search=Express
# ============ 5. 댓글 작성 ============
POST http://localhost:3000/api/v1/posts/1/comments
Authorization: Bearer <accessToken>
{ "content": "좋은 글이네요!" }
# ============ 6. 좋아요 ============
POST http://localhost:3000/api/v1/posts/1/likes
Authorization: Bearer <accessToken>
📂 파일 업로드 구현 (Multer)
npm install multer
const multer = require('multer');
const storage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
cb(null, Date.now() + '-' + file.originalname);
},
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB 제한
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) cb(null, true);
else cb(new Error('이미지 파일만 업로드 가능합니다'));
},
});
router.post('/upload', authenticate, upload.single('image'), (req, res) => {
res.json({
filename: req.file.filename,
url: '/uploads/' + req.file.filename,
});
});
12 테스트 코드 작성법
📌 왜 테스트 코드를 작성하나요?
코드를 수정할 때마다 Postman으로 모든 API를 테스트하는 건 불가능합니다. 테스트 코드를 작성하면 버튼 하나로 전체 API를 자동 검증할 수 있습니다. 현업에서는 PR(Pull Request) 시 테스트가 통과해야만 머지할 수 있게 설정합니다.
코드를 수정할 때마다 Postman으로 모든 API를 테스트하는 건 불가능합니다. 테스트 코드를 작성하면 버튼 하나로 전체 API를 자동 검증할 수 있습니다. 현업에서는 PR(Pull Request) 시 테스트가 통과해야만 머지할 수 있게 설정합니다.
# 설치
npm install -D jest supertest
// __tests__/auth.test.js
const request = require('supertest');
const app = require('../src/app');
describe('Auth API', () => {
const testUser = {
name: '테스트 유저',
email: 'test@example.com',
password: 'password123',
};
describe('POST /api/v1/auth/register', () => {
it('201 - 회원가입 성공', async () => {
const res = await request(app)
.post('/api/v1/auth/register')
.send(testUser);
expect(res.status).toBe(201);
expect(res.body.success).toBe(true);
expect(res.body.data.email).toBe(testUser.email);
expect(res.body.data.password).toBeUndefined(); // 비밀번호 노출 방지 확인!
});
it('422 - 유효성 검사 실패', async () => {
const res = await request(app)
.post('/api/v1/auth/register')
.send({ ...testUser, email: 'not-an-email' });
expect(res.status).toBe(422);
});
it('409 - 중복 이메일', async () => {
await request(app).post('/api/v1/auth/register').send(testUser);
const res = await request(app).post('/api/v1/auth/register').send(testUser);
expect(res.status).toBe(409);
});
});
describe('POST /api/v1/auth/login', () => {
it('200 - 로그인 성공, 토큰 반환', async () => {
const res = await request(app)
.post('/api/v1/auth/login')
.send({ email: testUser.email, password: testUser.password });
expect(res.status).toBe(200);
expect(res.body.data).toHaveProperty('accessToken');
});
});
});
13 현업/면접 단골 Q&A
Q1. Express 미들웨어란 무엇이고 어떤 순서로 실행되나요?
A: 미들웨어는 요청과 응답 사이에서 실행되는 함수로, (req, res, next) 세 인자를 받습니다. app.use()로 등록한 순서대로 위에서 아래로 실행됩니다.
next()를 호출해야 다음 미들웨어로 넘어가고, 호출하지 않으면 요청이 그 미들웨어에서 멈춥니다. 에러 처리 미들웨어는 4개 인자(err, req, res, next)를 가지며 일반 미들웨어 이후에 배치해야 합니다.Q2. REST API 설계 시 URL 구조는 어떻게 정하나요?
A: 리소스는 명사+복수형(/users, /posts), HTTP 메서드(GET/POST/PUT/DELETE)로 동작을 표현합니다. 계층 관계는 /users/:id/posts처럼 중첩하여 표현합니다. 소문자와 하이픈 사용(/blog-posts), 버전은 /api/v1/ 형식으로 관리합니다. URL에 동사를 쓰는 것은 피합니다 (/getUsers 대신 GET /users).
Q3. MVC 패턴에서 Controller와 Service의 차이는?
A: Controller는 req/res 처리를 담당합니다. 요청에서 데이터를 추출하고, 서비스를 호출하며, 응답을 반환합니다. Service는 실제 비즈니스 로직을 담당합니다. DB 조회, 데이터 가공, 외부 API 호출 등. 이렇게 분리하면 Service를 다른 곳에서도 재사용할 수 있고, 테스트하기도 쉬워집니다.
Q4. JWT 인증에서 Access Token과 Refresh Token을 나누는 이유는?
A: Access Token이 탈취되어도 피해를 최소화하기 위해서입니다. Access Token은 수명을 짧게(15분~1시간), Refresh Token은 길게(7~30일) 설정합니다. Access Token이 만료되면 클라이언트는 Refresh Token으로 새 Access Token을 발급받습니다. Refresh Token은 httpOnly 쿠키에 저장하여 JS에서 접근하지 못하게 보호합니다.
Q5. CORS 오류가 발생하는 이유와 해결 방법은?
A: 브라우저의 동일 출처 정책(Same-Origin Policy) 때문입니다. 프론트(localhost:3000)에서 다른 포트의 API(localhost:5000)를 호출하면 CORS 오류가 발생합니다. 서버에서 cors 미들웨어로 허용할 출처를 명시하거나, origin: '*'로 모든 출처를 허용할 수 있습니다. 프로덕션에서는 실제 프론트엔드 도메인만 허용해야 합니다.
Q6. N+1 문제가 무엇이고 어떻게 해결하나요?
A: 목록 조회(1번 쿼리) 후 각 항목의 관련 데이터를 별도로 조회(N번 쿼리)해서 총 N+1번의 쿼리가 발생하는 문제입니다. Prisma에서는 include를 사용해 JOIN으로 한 번에 가져오거나, Promise.all로 병렬 처리하여 해결합니다. N+1 문제는 실제 서비스에서 성능에 큰 영향을 미칩니다.
14 백엔드 개발자 학습 로드맵
| 단계 | 학습 내용 | 예상 기간 | 연계 가이드 | 상태 |
|---|---|---|---|---|
| 0단계 | 백엔드 개요 + JS 기초 (ES6+) | 2~3주 | Guide 0000 | 완료 |
| 1단계 | Node.js 기초 + 비동기 프로그래밍 | 2~3주 | Guide 0001 | 완료 |
| 2단계 | Express.js + REST API + MVC + JWT | 3~4주 | Guide 0002 | 현재 |
| 3단계 | 데이터베이스 완전 정복 (PostgreSQL + Prisma + MongoDB) | 3~4주 | Guide 0003 | 예정 |
| 4단계 | 인증/보안 심화 (JWT, OAuth2, HTTPS) | 2~3주 | Guide 0004 | 예정 |
| 5단계 | 테스트 심화 (Jest, Supertest, TDD) | 2~3주 | Guide 0005 | 예정 |
| 6단계 | TypeScript + NestJS (현업 표준 프레임워크) | 4~5주 | Guide 0006 | 예정 |
| 7단계 | 배포/인프라 (Docker, AWS, CI/CD) | 4~5주 | Guide 0007 | 예정 |
🎉
수고하셨습니다!
Express.js의 핵심 개념부터 실전 블로그 API 구현까지 완주하셨습니다!
라우팅, 미들웨어, MVC 패턴, REST API 설계, JWT 인증, 보안, 테스트까지
이 내용을 완전히 이해했다면 주니어 백엔드 개발자로 취업할 수 있는 수준입니다!
✅ Express.js 완료 체크리스트
☐ 라우팅 (Router, params, query, body) 이해
☐ 미들웨어 직접 작성 (인증, 로깅, 에러 처리)
☐ MVC 패턴으로 프로젝트 구조화
☐ REST API 설계 원칙 준수
☐ JWT 인증 (회원가입, 로그인, 토큰 검증)
☐ Joi 유효성 검사 + 글로벌 에러 핸들러
☐ Rate Limiting + Helmet 보안 설정
☐ 블로그 API 완성 (회원가입 + CRUD + 댓글 + 좋아요)
☐ Jest + Supertest 테스트 코드 작성
☐ 미들웨어 직접 작성 (인증, 로깅, 에러 처리)
☐ MVC 패턴으로 프로젝트 구조화
☐ REST API 설계 원칙 준수
☐ JWT 인증 (회원가입, 로그인, 토큰 검증)
☐ Joi 유효성 검사 + 글로벌 에러 핸들러
☐ Rate Limiting + Helmet 보안 설정
☐ 블로그 API 완성 (회원가입 + CRUD + 댓글 + 좋아요)
☐ Jest + Supertest 테스트 코드 작성
📚 다음 단계: BackendDevGuide0003 — 데이터베이스 완전 정복
PostgreSQL + Prisma ORM + MongoDB + 쿼리 최적화
PostgreSQL + Prisma ORM + MongoDB + 쿼리 최적화
반응형