본문으로 건너뛰기
Advertisement

Express.js 실전

Express.js란?

Express.js는 Node.js에서 가장 많이 사용되는 웹 프레임워크입니다. http 모듈의 복잡한 부분을 추상화하고, 라우팅·미들웨어·에러 처리를 간결하게 작성할 수 있게 합니다.

2010년 출시 후 15년 이상 Node.js 생태계의 표준으로 자리를 지켜왔습니다. 주간 다운로드 수 3,000만 이상으로 Fastify, Koa, Hono 등 후발 주자와의 비교에서도 여전히 압도적입니다.


설치 및 기본 구조

mkdir express-app && cd express-app
npm init -y
npm install express
npm install --save-dev nodemon
// app.js
const express = require('express');
const app = express();

// 미들웨어 등록
app.use(express.json()); // JSON 바디 파싱
app.use(express.urlencoded({ extended: true })); // HTML 폼 파싱

// 라우트 정의
app.get('/', (req, res) => {
res.json({ message: 'Express 서버 동작 중!' });
});

// 서버 시작
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`서버 실행: http://localhost:${PORT}`);
});

module.exports = app; // 테스트를 위해 export
// package.json scripts 추가
{
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
}
}

라우터

기본 라우트 메서드

const express = require('express');
const app = express();
app.use(express.json());

// HTTP 메서드별 라우트
app.get('/users', (req, res) => {
res.json({ users: [] });
});

app.post('/users', (req, res) => {
const user = req.body;
res.status(201).json({ created: user });
});

app.put('/users/:id', (req, res) => {
const { id } = req.params;
res.json({ updated: id, data: req.body });
});

app.patch('/users/:id', (req, res) => {
const { id } = req.params;
res.json({ patched: id });
});

app.delete('/users/:id', (req, res) => {
const { id } = req.params;
res.status(204).send(); // 내용 없음
});

// 모든 HTTP 메서드 처리
app.all('/any', (req, res) => {
res.json({ method: req.method });
});

Router()로 모듈화

// routes/users.js
const express = require('express');
const router = express.Router();

// 가짜 데이터베이스
let users = [
{ id: 1, name: '김철수', email: 'kim@example.com' },
{ id: 2, name: '이영희', email: 'lee@example.com' },
];
let nextId = 3;

// GET /users
router.get('/', (req, res) => {
const { page = 1, limit = 10, search } = req.query;
let result = users;

if (search) {
result = users.filter(u =>
u.name.includes(search) || u.email.includes(search)
);
}

const start = (page - 1) * limit;
const paged = result.slice(start, start + Number(limit));

res.json({
total: result.length,
page: Number(page),
limit: Number(limit),
data: paged,
});
});

// GET /users/:id
router.get('/:id', (req, res) => {
const id = Number(req.params.id);
const user = users.find(u => u.id === id);

if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
res.json(user);
});

// POST /users
router.post('/', (req, res) => {
const { name, email } = req.body;

if (!name || !email) {
return res.status(400).json({ error: 'name과 email은 필수입니다' });
}

const user = { id: nextId++, name, email };
users.push(user);
res.status(201).json(user);
});

// PUT /users/:id
router.put('/:id', (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex(u => u.id === id);

if (index === -1) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}

users[index] = { id, ...req.body };
res.json(users[index]);
});

// DELETE /users/:id
router.delete('/:id', (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex(u => u.id === id);

if (index === -1) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}

users.splice(index, 1);
res.status(204).send();
});

module.exports = router;
// app.js에서 라우터 연결
const usersRouter = require('./routes/users');
app.use('/users', usersRouter);
// /users → router.get('/')
// /users/1 → router.get('/:id')

미들웨어

미들웨어는 (req, res, next) 시그니처를 가진 함수입니다. Express의 핵심입니다.

전역 미들웨어

const express = require('express');
const app = express();

// 요청 로거 (전역)
app.use((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((req, res, next) => {
req.user = null; // 기본값

const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.slice(7);
// 실제로는 JWT 검증
if (token === 'valid-token') {
req.user = { id: 1, name: '관리자' };
}
}
next();
});

라우터별 미들웨어

// 특정 라우트에만 적용
function requireAuth(req, res, next) {
if (!req.user) {
return res.status(401).json({ error: '인증이 필요합니다' });
}
next();
}

function requireAdmin(req, res, next) {
if (req.user?.role !== 'admin') {
return res.status(403).json({ error: '관리자 권한이 필요합니다' });
}
next();
}

// 단일 라우트에 여러 미들웨어
app.get('/profile', requireAuth, (req, res) => {
res.json(req.user);
});

// 관리자 전용 라우트
app.delete('/users/:id', requireAuth, requireAdmin, (req, res) => {
res.json({ deleted: req.params.id });
});

// Router에 미들웨어 적용
const adminRouter = express.Router();
adminRouter.use(requireAuth, requireAdmin); // 이 라우터의 모든 라우트에 적용
adminRouter.get('/stats', (req, res) => res.json({ stats: {} }));
app.use('/admin', adminRouter);

에러 핸들러 미들웨어

에러 핸들러는 매개변수가 4개 (err, req, res, next)입니다. 반드시 마지막에 등록합니다.

// 커스텀 에러 클래스
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
this.name = 'AppError';
}
}

// 비동기 래퍼 (async 함수의 에러를 next로 전달)
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}

// 라우트에서 에러 던지기
app.get('/error-test', asyncHandler(async (req, res) => {
throw new AppError('테스트 에러입니다', 400);
}));

// 404 처리 (모든 라우트 뒤에)
app.use((req, res, next) => {
next(new AppError(`${req.url} 를 찾을 수 없습니다`, 404));
});

// 글로벌 에러 핸들러 (매개변수 4개!)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || '서버 오류가 발생했습니다';

// 개발 환경에서만 스택 트레이스 노출
const response = {
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
};

console.error(`[ERROR] ${statusCode}: ${message}`);
res.status(statusCode).json(response);
});

요청 객체 (req)

app.get('/users/:id/posts/:postId', (req, res) => {
// 라우트 파라미터
console.log(req.params); // { id: '1', postId: '42' }

// 쿼리 스트링
// URL: /users/1/posts/42?sort=date&order=desc&tags[]=js&tags[]=node
console.log(req.query); // { sort: 'date', order: 'desc', tags: ['js', 'node'] }

// 요청 바디 (express.json() 미들웨어 필요)
console.log(req.body); // { title: '제목', content: '내용' }

// 헤더
console.log(req.headers);
console.log(req.get('Authorization')); // 헤더 값 가져오기

// 기타 유용한 속성
console.log(req.method); // GET
console.log(req.path); // /users/1/posts/42
console.log(req.hostname); // localhost
console.log(req.ip); // 127.0.0.1
console.log(req.protocol); // http 또는 https
console.log(req.secure); // https 여부
console.log(req.xhr); // XMLHttpRequest 여부

res.json({ params: req.params });
});

응답 객체 (res)

app.get('/response-demo', (req, res) => {
// JSON 응답 (Content-Type: application/json 자동 설정)
res.json({ message: '성공' });

// 상태 코드 체이닝
res.status(201).json({ created: true });

// 문자열 응답
res.send('텍스트 응답');

// HTML 응답
res.send('<h1>HTML 응답</h1>');

// 파일 다운로드
res.download('./file.pdf', '다운로드.pdf');

// 파일 전송 (브라우저에서 표시)
res.sendFile('/absolute/path/to/file.html');

// 리디렉션
res.redirect('/new-url');
res.redirect(301, '/permanent-url'); // 영구 리디렉션

// 헤더 설정
res.set('X-Custom', 'value');
res.set({
'X-Custom-1': 'value1',
'X-Custom-2': 'value2',
});

// 상태 코드만
res.sendStatus(404); // "Not Found" 텍스트 포함
res.status(204).end(); // 빈 응답
});

CORS 설정

npm install cors
const express = require('express');
const cors = require('cors');
const app = express();

// 모든 도메인 허용 (개발용)
app.use(cors());

// 특정 도메인만 허용 (프로덕션)
const corsOptions = {
origin: [
'https://myapp.com',
'https://www.myapp.com',
'http://localhost:3000', // 개발 환경
],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // 쿠키 포함 요청 허용
maxAge: 86400, // preflight 캐시 시간(초)
};
app.use(cors(corsOptions));

// 특정 라우트에만 적용
app.get('/public', cors(), (req, res) => {
res.json({ public: true });
});

정적 파일 서빙

const path = require('path');

// /public 폴더의 파일을 정적으로 서빙
// http://localhost:3000/images/logo.png → public/images/logo.png
app.use(express.static(path.join(__dirname, 'public')));

// 가상 경로 접두사
// http://localhost:3000/static/style.css → public/style.css
app.use('/static', express.static(path.join(__dirname, 'public')));

// 캐시 설정
app.use(express.static('public', {
maxAge: '1d', // 1일 캐시
etag: true,
lastModified: true,
index: 'index.html', // 디렉토리 기본 파일
}));

실전: RESTful API 서버

// server.js — 완전한 RESTful API 예제
const express = require('express');
const cors = require('cors');

const app = express();

// 미들웨어
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 요청 로거
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
next();
});

// 가짜 DB
const db = {
users: new Map([
[1, { id: 1, name: '김철수', email: 'kim@example.com', role: 'admin' }],
[2, { id: 2, name: '이영희', email: 'lee@example.com', role: 'user' }],
]),
nextId: 3,
};

// 헬스체크
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});

// Users CRUD
app.get('/api/users', (req, res) => {
const users = Array.from(db.users.values());
res.json({ count: users.length, data: users });
});

app.get('/api/users/:id', (req, res) => {
const user = db.users.get(Number(req.params.id));
if (!user) return res.status(404).json({ error: '사용자 없음' });
res.json(user);
});

app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'name, email 필수' });
}
const user = { id: db.nextId++, name, email, role: 'user' };
db.users.set(user.id, user);
res.status(201).json(user);
});

app.put('/api/users/:id', (req, res) => {
const id = Number(req.params.id);
if (!db.users.has(id)) return res.status(404).json({ error: '사용자 없음' });
const updated = { ...db.users.get(id), ...req.body, id };
db.users.set(id, updated);
res.json(updated);
});

app.delete('/api/users/:id', (req, res) => {
const id = Number(req.params.id);
if (!db.users.has(id)) return res.status(404).json({ error: '사용자 없음' });
db.users.delete(id);
res.status(204).send();
});

// 404 처리
app.use((req, res) => {
res.status(404).json({ error: `${req.method} ${req.url} 없음` });
});

// 에러 핸들러
app.use((err, req, res, next) => {
console.error(err);
res.status(err.statusCode || 500).json({ error: err.message });
});

app.listen(3000, () => {
console.log('RESTful API 서버: http://localhost:3000');
console.log('API 목록:');
console.log(' GET /api/users');
console.log(' GET /api/users/:id');
console.log(' POST /api/users');
console.log(' PUT /api/users/:id');
console.log(' DELETE /api/users/:id');
});

고수 팁

요청 유효성 검사 (express-validator)

npm install express-validator
const { body, param, validationResult } = require('express-validator');

function validate(validations) {
return async (req, res, next) => {
await Promise.all(validations.map(v => v.run(req)));
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
}

app.post('/users',
validate([
body('name').trim().notEmpty().withMessage('이름은 필수입니다'),
body('email').isEmail().withMessage('올바른 이메일 형식이 아닙니다'),
body('age').optional().isInt({ min: 1, max: 150 }).withMessage('나이는 1-150 사이'),
]),
(req, res) => {
res.status(201).json({ created: req.body });
}
);

Rate Limiting

npm install express-rate-limit
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // 최대 100 요청
message: { error: '요청 횟수 초과, 15분 후 재시도' },
standardHeaders: true,
legacyHeaders: false,
});

app.use('/api', limiter); // API 라우트에만 적용
Advertisement