Node.js 교재 · 14편 · Express 심화

Express 라우팅·미들웨어 — Router와 체인

13편이 단순 입문이면 이번이 진짜 시작. Express 의 정체는 미들웨어 체인.

Express 미들웨어 체인과 라우터 분리 컨셉 일러스트

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편.

© 2026 주나이테크(주) @JUNAITECH