Express.js 완벽 가이드 | Node.js 웹 프레임워크
이 글의 핵심
Express.js 완벽 가이드에 대한 실전 가이드입니다. Node.js 웹 프레임워크 등을 예제와 함께 상세히 설명합니다.
들어가며
Express.js란?
Express.js는 Node.js를 위한 빠르고 간결한 웹 프레임워크입니다.
요청이 들어오면 주소(경로)와 HTTP 메서드에 따라 교통 표지판·우편 집배처럼 “이 편지는 이 핸들러로”라고 연결해 줍니다. 그 사이사이에 미들웨어를 끼우는데, 이는 검문소나 공장의 필터처럼 로그인·JSON 파싱·CORS 같은 공통 작업을 통과시키는 단계라고 이해하시면 됩니다.
특징:
- ✅ 간결한 API: 최소한의 코드로 서버 구축
- ✅ 미들웨어: 요청 처리 파이프라인
- ✅ 라우팅: URL 패턴 매칭
- ✅ 템플릿 엔진: EJS, Pug 등 지원
- ✅ 풍부한 생태계: 수천 개의 미들웨어
사용 사례:
- REST API 서버
- 웹 애플리케이션
- 마이크로서비스
- 프록시 서버
1. 설치 및 기본 서버
설치
# 프로젝트 초기화
npm init -y
# Express 설치
npm install express
# 개발 도구 설치
npm install --save-dev nodemon
Hello World
// app.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello, Express!');
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`서버 실행 중: http://localhost:${PORT}`);
});
실행:
node app.js
# 또는
nodemon app.js
기본 구조
const express = require('express');
const app = express();
// 미들웨어
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 라우트
app.get('/', (req, res) => {
res.send('홈 페이지');
});
app.get('/about', (req, res) => {
res.send('소개 페이지');
});
// 404 처리
app.use((req, res) => {
res.status(404).send('페이지를 찾을 수 없습니다');
});
// 에러 처리
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('서버 에러');
});
// 서버 시작
app.listen(3000, () => {
console.log('서버 실행 중: http://localhost:3000');
});
2. 라우팅 (Routing)
app.get('/users', …)처럼 경로 + 메서드가 맞을 때만 실행되는 함수를 붙입니다. 우편물 주소가 동네·번지·호수까지 정확히 맞아야 배달되듯, 클라이언트가 GET /api/users/42처럼 요청하면 :id에 42가 들어가 해당 핸들러 한 곳으로만 전달됩니다.
HTTP 메서드
const express = require('express');
const app = express();
// GET: 데이터 조회
app.get('/users', (req, res) => {
res.json({ users: ['홍길동', '김철수'] });
});
// POST: 데이터 생성
app.post('/users', (req, res) => {
const user = req.body;
res.status(201).json({ message: '사용자 생성됨', user });
});
// PUT: 데이터 전체 수정
app.put('/users/:id', (req, res) => {
const { id } = req.params;
const user = req.body;
res.json({ message: `사용자 ${id} 수정됨`, user });
});
// PATCH: 데이터 일부 수정
app.patch('/users/:id', (req, res) => {
const { id } = req.params;
const updates = req.body;
res.json({ message: `사용자 ${id} 일부 수정됨`, updates });
});
// DELETE: 데이터 삭제
app.delete('/users/:id', (req, res) => {
const { id } = req.params;
res.json({ message: `사용자 ${id} 삭제됨` });
});
// ALL: 모든 메서드
app.all('/secret', (req, res) => {
res.send('비밀 페이지');
});
경로 매개변수 (Route Parameters)
// 단일 매개변수
app.get('/users/:id', (req, res) => {
const { id } = req.params;
res.send(`사용자 ID: ${id}`);
});
// GET /users/123 → id: "123"
// 여러 매개변수
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.json({ userId, postId });
});
// GET /users/123/posts/456 → { userId: "123", postId: "456" }
// 정규식 패턴
app.get('/files/:filename(\\d+\\.txt)', (req, res) => {
const { filename } = req.params;
res.send(`파일: ${filename}`);
});
// GET /files/123.txt ✅
// GET /files/abc.txt ❌
// 선택적 매개변수
app.get('/users/:id?', (req, res) => {
if (req.params.id) {
res.send(`사용자 ID: ${req.params.id}`);
} else {
res.send('모든 사용자');
}
});
쿼리 문자열 (Query String)
app.get('/search', (req, res) => {
const { q, page, limit } = req.query;
res.json({
query: q,
page: parseInt(page) || 1,
limit: parseInt(limit) || 10
});
});
// GET /search?q=nodejs&page=2&limit=20
// { query: "nodejs", page: 2, limit: 20 }
라우터 분리
// routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ users: [] });
});
router.get('/:id', (req, res) => {
res.json({ id: req.params.id });
});
router.post('/', (req, res) => {
res.status(201).json({ message: '생성됨' });
});
module.exports = router;
// app.js
const express = require('express');
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');
const app = express();
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);
app.listen(3000);
3. 미들웨어 (Middleware)
미들웨어란?
미들웨어는 요청과 응답 사이에서 실행되는 함수입니다. 들어온 요청이 라우트 핸들러에 닿기 전에 검문·신분 확인·본문(JSON) 해석 같은 공통 절차를 거치게 할 때 씁니다. next()를 호출해야만 다음 검문소로 통과하고, 호출하지 않으면 그 자리에서 응답으로 끝납니다.
function myMiddleware(req, res, next) {
console.log('미들웨어 실행');
next(); // 다음 미들웨어로 이동
}
app.use(myMiddleware);
실행 흐름:
요청 → 미들웨어1 → 미들웨어2 → 라우트 핸들러 → 응답
애플리케이션 레벨 미들웨어
const express = require('express');
const app = express();
// 모든 요청에 실행
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
console.log('시간:', new Date().toISOString());
next();
});
// 특정 경로에만 실행
app.use('/api', (req, res, next) => {
console.log('API 요청');
next();
});
// 여러 미들웨어 체이닝
app.get('/users',
(req, res, next) => {
console.log('미들웨어 1');
next();
},
(req, res, next) => {
console.log('미들웨어 2');
next();
},
(req, res) => {
res.send('사용자 목록');
}
);
내장 미들웨어
// JSON 파싱
app.use(express.json());
// URL-encoded 파싱
app.use(express.urlencoded({ extended: true }));
// 정적 파일 서빙
app.use(express.static('public'));
// public/style.css → http://localhost:3000/style.css
// 여러 정적 폴더
app.use('/static', express.static('public'));
app.use('/uploads', express.static('uploads'));
서드파티 미들웨어
npm install cors morgan helmet compression
const cors = require('cors');
const morgan = require('morgan');
const helmet = require('helmet');
const compression = require('compression');
// CORS (Cross-Origin Resource Sharing)
app.use(cors());
// HTTP 요청 로깅
app.use(morgan('dev'));
// GET /users 200 15.234 ms - 123
// 보안 헤더
app.use(helmet());
// 응답 압축
app.use(compression());
커스텀 미들웨어
로깅 미들웨어:
function logger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
});
next();
}
app.use(logger);
인증 미들웨어:
function authenticate(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: '인증 토큰이 필요합니다' });
}
try {
// 토큰 검증 (예: JWT)
const user = verifyToken(token);
req.user = user;
next();
} catch (err) {
res.status(401).json({ error: '유효하지 않은 토큰' });
}
}
// 보호된 라우트
app.get('/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
권한 확인 미들웨어:
function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '인증 필요' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: '권한 없음' });
}
next();
};
}
// 사용
app.delete('/users/:id',
authenticate,
authorize('admin'),
(req, res) => {
res.json({ message: '사용자 삭제됨' });
}
);
4. REST API 구축
CRUD 구현
const express = require('express');
const app = express();
app.use(express.json());
// 임시 데이터베이스
let users = [
{ id: 1, name: '홍길동', email: '[email protected]' },
{ id: 2, name: '김철수', email: '[email protected]' }
];
let nextId = 3;
// CREATE: 사용자 생성
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
// 유효성 검사
if (!name || !email) {
return res.status(400).json({ error: '이름과 이메일이 필요합니다' });
}
const user = { id: nextId++, name, email };
users.push(user);
res.status(201).json(user);
});
// READ: 모든 사용자 조회
app.get('/api/users', (req, res) => {
const { page = 1, limit = 10 } = req.query;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
const paginatedUsers = users.slice(startIndex, endIndex);
res.json({
users: paginatedUsers,
total: users.length,
page: parseInt(page),
totalPages: Math.ceil(users.length / limit)
});
});
// READ: 특정 사용자 조회
app.get('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
res.json(user);
});
// UPDATE: 사용자 수정
app.put('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const { name, email } = req.body;
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
users[userIndex] = { id, name, email };
res.json(users[userIndex]);
});
// DELETE: 사용자 삭제
app.delete('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
users.splice(userIndex, 1);
res.status(204).send(); // No Content
});
app.listen(3000);
HTTP 상태 코드
| 코드 | 의미 | 사용 예 |
|---|---|---|
| 200 | OK | 성공 |
| 201 | Created | 리소스 생성 |
| 204 | No Content | 삭제 성공 |
| 400 | Bad Request | 잘못된 요청 |
| 401 | Unauthorized | 인증 필요 |
| 403 | Forbidden | 권한 없음 |
| 404 | Not Found | 리소스 없음 |
| 500 | Internal Server Error | 서버 에러 |
5. 요청과 응답
Request 객체
app.get('/demo', (req, res) => {
// URL 매개변수
console.log(req.params);
// 쿼리 문자열
console.log(req.query);
// 요청 본문
console.log(req.body);
// 헤더
console.log(req.headers);
console.log(req.get('User-Agent'));
// HTTP 메서드
console.log(req.method);
// URL
console.log(req.url);
console.log(req.path);
console.log(req.originalUrl);
// IP 주소
console.log(req.ip);
// 쿠키 (cookie-parser 필요)
console.log(req.cookies);
res.send('완료');
});
Response 객체
app.get('/response-demo', (req, res) => {
// 텍스트 응답
res.send('Hello');
// JSON 응답
res.json({ message: 'Success' });
// 상태 코드 + JSON
res.status(201).json({ created: true });
// 파일 전송
res.sendFile('/path/to/file.pdf');
// 파일 다운로드
res.download('/path/to/file.pdf', 'custom-name.pdf');
// 리다이렉트
res.redirect('/new-url');
res.redirect(301, '/permanent-url');
// 헤더 설정
res.set('Content-Type', 'text/html');
res.set({
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
});
// 쿠키 설정
res.cookie('name', 'value', { maxAge: 900000, httpOnly: true });
// 쿠키 삭제
res.clearCookie('name');
// 렌더링 (템플릿 엔진)
res.render('index', { title: '홈' });
});
6. 미들웨어 심화
에러 처리 미들웨어
// 비동기 에러 래퍼
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// 사용
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new Error('사용자를 찾을 수 없습니다');
}
res.json(user);
}));
// 에러 처리 미들웨어 (맨 마지막에 배치)
app.use((err, req, res, next) => {
console.error(err.stack);
const statusCode = err.statusCode || 500;
const message = err.message || '서버 에러';
res.status(statusCode).json({
error: {
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
});
커스텀 에러 클래스
// errors.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(message = '리소스를 찾을 수 없습니다') {
super(message, 404);
}
}
class ValidationError extends AppError {
constructor(message = '유효하지 않은 입력') {
super(message, 400);
}
}
module.exports = { AppError, NotFoundError, ValidationError };
// 사용
const { NotFoundError, ValidationError } = require('./errors');
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('사용자를 찾을 수 없습니다');
}
res.json(user);
} catch (err) {
next(err);
}
});
7. 실전 예제
예제 1: 블로그 API
// app.js
const express = require('express');
const app = express();
app.use(express.json());
// 데이터
let posts = [
{ id: 1, title: '첫 글', content: '내용', author: '홍길동', createdAt: new Date() }
];
let nextId = 2;
// 모든 글 조회
app.get('/api/posts', (req, res) => {
const { author, sort = 'desc' } = req.query;
let result = [...posts];
// 필터링
if (author) {
result = result.filter(p => p.author === author);
}
// 정렬
result.sort((a, b) => {
return sort === 'asc'
? a.createdAt - b.createdAt
: b.createdAt - a.createdAt;
});
res.json({ posts: result, total: result.length });
});
// 특정 글 조회
app.get('/api/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: '글을 찾을 수 없습니다' });
}
res.json(post);
});
// 글 작성
app.post('/api/posts', (req, res) => {
const { title, content, author } = req.body;
if (!title || !content || !author) {
return res.status(400).json({
error: '제목, 내용, 작성자가 필요합니다'
});
}
const post = {
id: nextId++,
title,
content,
author,
createdAt: new Date()
};
posts.push(post);
res.status(201).json(post);
});
// 글 수정
app.put('/api/posts/:id', (req, res) => {
const id = parseInt(req.params.id);
const { title, content } = req.body;
const postIndex = posts.findIndex(p => p.id === id);
if (postIndex === -1) {
return res.status(404).json({ error: '글을 찾을 수 없습니다' });
}
posts[postIndex] = {
...posts[postIndex],
title,
content,
updatedAt: new Date()
};
res.json(posts[postIndex]);
});
// 글 삭제
app.delete('/api/posts/:id', (req, res) => {
const id = parseInt(req.params.id);
const postIndex = posts.findIndex(p => p.id === id);
if (postIndex === -1) {
return res.status(404).json({ error: '글을 찾을 수 없습니다' });
}
posts.splice(postIndex, 1);
res.status(204).send();
});
app.listen(3000, () => {
console.log('블로그 API 서버 실행 중: http://localhost:3000');
});
예제 2: 파일 업로드
npm install multer
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// 저장 설정
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
// 파일 필터
const fileFilter = (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
cb(null, true);
} else {
cb(new Error('이미지 파일만 업로드 가능합니다'));
}
};
const upload = multer({
storage,
fileFilter,
limits: { fileSize: 5 * 1024 * 1024 } // 5MB
});
// 단일 파일 업로드
app.post('/upload', upload.single('image'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: '파일이 필요합니다' });
}
res.json({
message: '업로드 성공',
file: {
filename: req.file.filename,
size: req.file.size,
path: req.file.path
}
});
});
// 여러 파일 업로드
app.post('/upload-multiple', upload.array('images', 5), (req, res) => {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: '파일이 필요합니다' });
}
res.json({
message: `${req.files.length}개 파일 업로드 성공`,
files: req.files.map(f => ({
filename: f.filename,
size: f.size
}))
});
});
// 에러 처리
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: '파일 크기가 너무 큽니다' });
}
}
res.status(500).json({ error: err.message });
});
app.listen(3000);
예제 3: 인증 시스템
npm install bcrypt jsonwebtoken
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const JWT_SECRET = 'your-secret-key';
const users = [];
// 회원가입
app.post('/auth/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// 유효성 검사
if (!email || !password || !name) {
return res.status(400).json({ error: '모든 필드가 필요합니다' });
}
// 중복 확인
if (users.find(u => u.email === email)) {
return res.status(400).json({ error: '이미 존재하는 이메일입니다' });
}
// 비밀번호 해싱
const hashedPassword = await bcrypt.hash(password, 10);
const user = {
id: users.length + 1,
email,
password: hashedPassword,
name,
createdAt: new Date()
};
users.push(user);
// 비밀번호 제외하고 응답
const { password: _, ...userWithoutPassword } = user;
res.status(201).json(userWithoutPassword);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 로그인
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// 사용자 찾기
const user = users.find(u => u.email === email);
if (!user) {
return res.status(401).json({ error: '이메일 또는 비밀번호가 잘못되었습니다' });
}
// 비밀번호 확인
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: '이메일 또는 비밀번호가 잘못되었습니다' });
}
// JWT 토큰 생성
const token = jwt.sign(
{ id: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token, user: { id: user.id, email: user.email, name: user.name } });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 인증 미들웨어
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '인증 토큰이 필요합니다' });
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: '유효하지 않은 토큰' });
}
}
// 보호된 라우트
app.get('/auth/profile', authenticate, (req, res) => {
const user = users.find(u => u.id === req.user.id);
if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
const { password, ...userWithoutPassword } = user;
res.json(userWithoutPassword);
});
app.listen(3000);
8. 템플릿 엔진 (EJS)
설치 및 설정
npm install ejs
const express = require('express');
const app = express();
// 뷰 엔진 설정
app.set('view engine', 'ejs');
app.set('views', './views');
app.get('/', (req, res) => {
res.render('index', {
title: '홈 페이지',
message: 'Express + EJS'
});
});
app.listen(3000);
EJS 템플릿
<!-- views/index.ejs -->
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1><%= message %></h1>
<% if (users && users.length > 0) { %>
<ul>
<% users.forEach(user => { %>
<li><%= user.name %> (<%= user.email %>)</li>
<% }); %>
</ul>
<% } else { %>
<p>사용자가 없습니다.</p>
<% } %>
</body>
</html>
app.get('/users', (req, res) => {
const users = [
{ name: '홍길동', email: '[email protected]' },
{ name: '김철수', email: '[email protected]' }
];
res.render('index', {
title: '사용자 목록',
message: '등록된 사용자',
users
});
});
9. 보안
기본 보안 설정
npm install helmet cors express-rate-limit
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const app = express();
// Helmet: 보안 헤더 설정
app.use(helmet());
// CORS 설정
app.use(cors({
origin: 'https://yourdomain.com',
credentials: true
}));
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // 최대 100개 요청
message: '너무 많은 요청이 발생했습니다'
});
app.use('/api/', limiter);
// XSS 방지
app.use(express.json({ limit: '10kb' }));
// SQL Injection 방지 (파라미터 검증)
app.get('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: '유효하지 않은 ID' });
}
// ...
});
app.listen(3000);
입력 검증
npm install express-validator
const { body, validationResult } = require('express-validator');
app.post('/api/users',
// 검증 규칙
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('name').trim().notEmpty(),
// 핸들러
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 유효한 데이터 처리
res.json({ message: '사용자 생성됨' });
}
);
10. 프로덕션 배포
환경 설정
// config.js
module.exports = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
isDevelopment: process.env.NODE_ENV === 'development',
isProduction: process.env.NODE_ENV === 'production'
};
프로덕션 설정
const express = require('express');
const config = require('./config');
const app = express();
// 프로덕션 전용 설정
if (config.isProduction) {
// 신뢰할 수 있는 프록시 설정
app.set('trust proxy', 1);
// 압축
const compression = require('compression');
app.use(compression());
// 로깅
const morgan = require('morgan');
app.use(morgan('combined'));
}
// 개발 전용 설정
if (config.isDevelopment) {
const morgan = require('morgan');
app.use(morgan('dev'));
}
app.listen(config.port);
PM2로 배포
# PM2 설치
npm install -g pm2
# 앱 시작
pm2 start app.js --name "my-app"
# 클러스터 모드 (멀티 코어 활용)
pm2 start app.js -i max
# 상태 확인
pm2 status
pm2 logs
pm2 monit
# 재시작
pm2 restart my-app
# 중지
pm2 stop my-app
# 삭제
pm2 delete my-app
# 부팅 시 자동 시작
pm2 startup
pm2 save
Nginx 리버스 프록시
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
11. 자주 발생하는 문제
문제 1: Cannot set headers after they are sent
원인: 응답을 두 번 보냄
// ❌ 잘못된 코드
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
res.status(404).json({ error: '없음' });
}
res.json(user); // 에러! (404 응답 후 또 응답)
});
// ✅ return 사용
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '없음' });
}
res.json(user);
});
문제 2: 미들웨어 순서
// ❌ 잘못된 순서
app.get('/users', (req, res) => {
res.json({ users: [] });
});
app.use(express.json()); // 너무 늦음!
// ✅ 올바른 순서
app.use(express.json()); // 먼저
app.get('/users', (req, res) => {
console.log(req.body); // 파싱됨
res.json({ users: [] });
});
문제 3: next() 호출 누락
// ❌ next() 없음
app.use((req, res, next) => {
console.log('미들웨어');
// next()를 호출하지 않으면 여기서 멈춤
});
// ✅ next() 호출
app.use((req, res, next) => {
console.log('미들웨어');
next(); // 다음으로 진행
});
12. 실전 팁
프로젝트 구조
my-express-app/
├── src/
│ ├── config/
│ │ └── database.js
│ ├── controllers/
│ │ ├── userController.js
│ │ └── postController.js
│ ├── middlewares/
│ │ ├── auth.js
│ │ └── errorHandler.js
│ ├── models/
│ │ ├── User.js
│ │ └── Post.js
│ ├── routes/
│ │ ├── userRoutes.js
│ │ └── postRoutes.js
│ ├── utils/
│ │ └── logger.js
│ └── app.js
├── public/
├── views/
├── .env
├── .gitignore
├── package.json
└── server.js
컨트롤러 패턴
// controllers/userController.js
const User = require('../models/User');
exports.getAllUsers = async (req, res, next) => {
try {
const users = await User.find();
res.json({ users });
} catch (err) {
next(err);
}
};
exports.getUserById = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: '사용자 없음' });
}
res.json(user);
} catch (err) {
next(err);
}
};
exports.createUser = async (req, res, next) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (err) {
next(err);
}
};
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.post('/', userController.createUser);
module.exports = router;
// app.js
const userRoutes = require('./routes/userRoutes');
app.use('/api/users', userRoutes);
API 버전 관리
// v1/routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ version: 'v1', users: [] });
});
module.exports = router;
// v2/routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ version: 'v2', users: [], meta: {} });
});
module.exports = router;
// app.js
const v1Users = require('./v1/routes/users');
const v2Users = require('./v2/routes/users');
app.use('/api/v1/users', v1Users);
app.use('/api/v2/users', v2Users);
정리
핵심 요약
- Express.js: Node.js 웹 프레임워크
- 라우팅: HTTP 메서드 + URL 패턴
- 미들웨어: 요청 처리 파이프라인
- REST API: CRUD 작업 구현
- 에러 처리: 에러 미들웨어, 비동기 래퍼
- 보안: Helmet, CORS, Rate Limiting
- 배포: PM2, Nginx
Express vs 다른 프레임워크
| 프레임워크 | 특징 | 사용 시기 |
|---|---|---|
| Express | 간결, 생태계 큼 | 일반적인 웹 앱 |
| Fastify | 빠름, 스키마 검증 | 고성능 API |
| Koa | 최신 문법, 가벼움 | 모던 프로젝트 |
| NestJS | TypeScript, 구조화 | 대규모 엔터프라이즈 |
다음 단계
- Node.js 파일 시스템
- Node.js 데이터베이스 연동
- Node.js 인증과 보안
추천 학습 자료
공식 문서:
미들웨어:
관련 글
- TypeScript 실전 프로젝트 | REST API 서버 만들기
- C++ 초경량 HTTP 웹 프레임워크 바닥부터 만들기 [#48-2]
- Node.js 시작하기 | 설치, 설정, Hello World
- Flask 기초 | Python 웹 프레임워크 시작하기
- Python REST API | Flask/Django로 API 서버 만들기