13편의 코드를 그대로 키워나가면 곧 한 파일 500줄. 라우트 10개, 인증 검사, 로깅, 에러 처리 — 다 한 파일에 박힌다. 유지보수 불가능.
Express 의 해답은 두 가지 — Router(라우트 모듈화)와 미들웨어 체인(공통 로직 분리). 둘이 어우러져 실전 백엔드의 골격이 된다. 이번 편이 핵심.
1. 미들웨어의 정체 — (req, res, next) 함수
Express 의 모든 것은 결국 (req, res, next) => { ... } 함수. 라우트 핸들러도 미들웨어, express.json() 도 미들웨어. 둘의 유일한 차이는 next() 를 부르는가.
// 미들웨어 1번 — 로거
function logger(req, res, next) {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next(); // ← 다음 미들웨어로 넘김
}
// 미들웨어 2번 — 인증
function requireAuth(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: '토큰 없음' });
req.user = { id: verifyToken(token) };
next();
}
// 라우트 핸들러 (next 안 부르고 응답으로 끝)
function getMe(req, res) {
res.json({ user: req.user });
}
app.use(logger); // 전역 적용
app.get('/api/me', requireAuth, getMe); // 이 라우트만 인증
핵심 — next() 를 부르면 다음 함수로, 안 부르고 res.send/json 하면 응답으로 종료. 잊고 둘 다 안 하면 요청이 영원히 hang. 흔한 사고.
미들웨어 체인 = 양파 — app.use(A); app.use(B); app.get('/x', C) 면 /x 요청은 A → B → C 순서로 통과. C 가 응답하면 끝. 어떤 미들웨어가 응답하면 그 뒤는 실행 안 됨. 이 모델이 Koa·Hono 등 후속 프레임워크에도 그대로 차용.
2. 미들웨어 3가지 위치
| 적용 방법 | 범위 | 예 |
app.use(mw) | 모든 요청 | logger·cors·express.json |
app.use(경로, mw) | 경로 prefix 매칭만 | app.use('/admin', requireAuth) |
app.get(경로, mw1, mw2, handler) | 그 라우트 한정 | app.post('/posts', requireAuth, validate, createPost) |
같은 인증 미들웨어를 /admin/* 전체에 걸고, 특정 라우트엔 추가 검증을 끼우는 식의 조합이 실전 백엔드의 기본 패턴.
3. Router — 라우트 모듈화
한 파일에 라우트 100개 박지 말고, 리소스별 파일 분리.
// routes/posts.js
import { Router } from 'express';
const router = Router();
router.get('/', listPosts);
router.get('/:id', getPost);
router.post('/', requireAuth, createPost);
router.delete('/:id', requireAuth, requireOwner, deletePost);
export default router;
// server.js
import express from 'express';
import postsRouter from './routes/posts.js';
import usersRouter from './routes/users.js';
const app = express();
app.use(express.json());
app.use('/api/posts', postsRouter);
app.use('/api/users', usersRouter);
app.listen(3000);
이렇게 분리하면 routes/posts.js 안의 router.get('/') 가 실제 URL /api/posts 가 된다. 라우터 자체도 미들웨어 — Express 의 일관된 디자인 덕분.
4. 자주 만드는 커스텀 미들웨어 4가지
① 요청 로거
function logger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const ms = Date.now() - start;
console.log(`${req.method} ${req.url} ${res.statusCode} ${ms}ms`);
});
next();
}
app.use(logger);
② CORS — 크로스 도메인 허용
// 직접 (특정 origin 만 허용)
app.use((req, res, next) => {
const allowedOrigins = ['https://junai.ai', 'http://localhost:3000'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
if (req.method === 'OPTIONS') return res.status(204).end();
next();
});
// 또는 cors npm 패키지로 한 줄
// app.use(cors({ origin: allowedOrigins }));
③ JWT 인증
import jwt from 'jsonwebtoken';
function requireAuth(req, res, next) {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) {
return res.status(401).json({ error: '토큰 없음' });
}
try {
req.user = jwt.verify(auth.slice(7), process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: '잘못된 토큰' });
}
}
④ 에러 핸들러 — 인자 4개
// 가장 마지막에 등록. 인자가 4개라는 게 신호.
app.use((err, req, res, next) => {
console.error('[error]', err);
res.status(err.status || 500).json({
error: err.message || '서버 오류',
});
});
// 핸들러에서 throw 하면 자동으로 이리 옴 (async 면 next(err) 호출 필요)
app.get('/api/danger', async (req, res, next) => {
try {
const data = await fetchSomething();
res.json(data);
} catch (err) {
next(err);
}
});
async 미들웨어 함정 — Express 4 까지는 async 핸들러의 throw 를 자동으로 못 잡았다. next(err) 직접 호출 필요. Express 5 (2024 stable) 부터 async throw 자동 캐치. 어느 버전인지 확인. 5 가 아니면 express-async-errors 패키지 추가.
5. 미들웨어 순서 — 자주 깜빡하는 함정
등록 순서 = 실행 순서. 잘못 두면 안 도는 게 흔한 사고.
// ❌ 잘못된 순서
app.get('/api/me', getMe);
app.use(express.json()); // 이미 라우트 등록 뒤
app.use(requireAuth); // /api/me 엔 적용 안 됨
// ✅ 정답
app.use(express.json()); // 1. 본문 파싱 먼저
app.use(logger); // 2. 모든 요청 로깅
app.use('/api', requireAuth); // 3. /api/* 인증
app.get('/api/me', getMe); // 4. 라우트
app.use((err, req, res, next) => …); // 5. 에러 핸들러 (제일 끝)
표준 순서 — 본문 파싱 → 로깅 → CORS → 인증 → 라우트 → 에러 핸들러. 외워둘 가치 있다.
요약 — 14편 좌표
여기까지 정리. Express 의 모든 것은 (req, res, next) 함수 — 미들웨어. next() 부르면 다음, 응답하면 끝. app.use(전역) · 경로 prefix · 라우트 인라인 3가지 위치. Router 로 리소스별 라우트 분리. 자주 만드는 커스텀 4종 — 로거·CORS·JWT 인증·에러 핸들러(인자 4개). 등록 순서가 곧 실행 순서. 다음 편에서 JSON API 의 REST 설계를 묶는다.
다음 편 예고 — JSON API 와 REST
RESTful CRUD 설계와 응답 규약. 15편.