Guider/Backend/BackendDevGuide0002
Backend#02

BackendDevGuide0002

Express.js 완전 정복

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()를 호출해야 합니다.

🏗️ 미들웨어 실행 흐름

클라이언트
요청
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) 시 테스트가 통과해야만 머지할 수 있게 설정합니다.
# 설치
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 테스트 코드 작성
📚 다음 단계: BackendDevGuide0003 — 데이터베이스 완전 정복
PostgreSQL + Prisma ORM + MongoDB + 쿼리 최적화
반응형