Express 기초 — 라우팅, 미들웨어, 요청-응답 사이클
HTTP 요청이 Express 서버에 들어오면, 응답이 나가기까지 내부에서 어떤 일이 벌어질까요?
Express를 쓰면서 app.get()과 app.use()를 매일 작성하지만, 요청이 미들웨어를 거쳐 응답으로 나가는 전체 흐름을 한 번에 그려보면 동작이 훨씬 명확해집니다.
개념 정의
Express 는 Node.js 위에서 동작하는 미니멀한 웹 프레임워크입니다. 핵심은 미들웨어 함수의 체인 으로, HTTP 요청이 들어오면 등록된 미들웨어를 순서대로 통과시키며 최종 응답을 만듭니다.
요청-응답 사이클
Express의 모든 처리는 하나의 흐름으로 설명됩니다.
HTTP 요청 → [미들웨어 1] → [미들웨어 2] → [라우트 핸들러] → HTTP 응답
│ │ │
└── next() ─────└── next() ──────└── res.send()
이 흐름에서 핵심은 next() 함수입니다. 미들웨어가 next()를 호출하면 다음 미들웨어로 넘어가고, res.send()를 호출하면 응답을 보내고 체인이 끝납니다. 어느 쪽도 하지 않으면 요청이 영원히 대기합니다.
라우팅 — 요청을 올바른 핸들러로 연결
라우팅은 HTTP 메서드와 경로의 조합으로 요청을 분류합니다.
const express = require('express');
const app = express();
app.get('/users', (req, res) => {
res.json({ users: [] });
});
app.post('/users', (req, res) => {
res.status(201).json({ id: 1 });
});
app.get('/users/:id', (req, res) => {
const { id } = req.params; // ← URL 파라미터 추출
res.json({ id });
});
위 코드에서 :id는 라우트 파라미터입니다. /users/42 요청이 오면 req.params.id에 "42"가 담깁니다.
Router로 모듈화
라우트가 많아지면 파일 하나에 전부 넣을 수 없습니다. express.Router()로 분리합니다.
// routes/users.js
const router = require('express').Router();
router.get('/', (req, res) => { /* 목록 */ });
router.get('/:id', (req, res) => { /* 상세 */ });
router.post('/', (req, res) => { /* 생성 */ });
module.exports = router;
// app.js
const userRouter = require('./routes/users');
app.use('/users', userRouter); // ← /users 접두사 자동 적용
app.use('/users', userRouter)를 등록하면 Router 내부의 /는 실제로 /users/가 됩니다.
미들웨어 — Express의 핵심
미들웨어는 (req, res, next) 세 개의 파라미터를 받는 함수입니다. 요청과 응답 사이에서 원하는 작업을 수행합니다.
// 로깅 미들웨어
function logger(req, res, next) {
console.log(`${req.method} ${req.url}`);
next(); // ← 다음 미들웨어로 제어 전달
}
app.use(logger); // 모든 요청에 적용
위 코드에서 next()를 빠뜨리면 이후의 모든 미들웨어와 라우트 핸들러가 실행되지 않습니다.
미들웨어의 종류
| 종류 | 등록 방법 | 용도 |
|---|---|---|
| 애플리케이션 레벨 | app.use(fn) | 모든 요청에 공통 적용 (로깅, 파싱) |
| ** 라우터 레벨** | router.use(fn) | 특정 라우터에만 적용 |
| ** 라우트 핸들러** | app.get(path, fn) | 특정 경로+메서드에만 적용 |
| ** 에러 핸들링** | app.use((err, req, res, next) => {}) | 에러 처리 (파라미터 4개) |
| ** 내장** | express.json(), express.static() | Express가 제공하는 기본 미들웨어 |
미들웨어 등록 순서가 실행 순서다
이 부분을 놓치면 디버깅할 때 혼란스럽습니다.
// 1번: JSON 바디 파싱
app.use(express.json());
// 2번: 로깅
app.use(logger);
// 3번: 라우트
app.get('/users', handler);
// 4번: 404 처리
app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
// 5번: 에러 핸들러 (반드시 마지막)
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
순서를 바꾸면 동작이 완전히 달라집니다. 예를 들어 express.json()을 라우트 아래에 두면 req.body가 undefined입니다.
req와 res 객체
Express는 Node.js의 기본 http.IncomingMessage와 http.ServerResponse를 확장합니다.
| 객체 | 주요 속성/메서드 | 설명 |
|---|---|---|
req.params | { id: '42' } | URL 파라미터 |
req.query | { page: '1' } | 쿼리스트링 (?page=1) |
req.body | { name: 'Kim' } | 요청 바디 (파싱 미들웨어 필요) |
req.headers | { 'content-type': '...' } | HTTP 헤더 |
res.status(code) | res.status(201) | 상태 코드 설정 (체이닝 가능) |
res.json(data) | res.json({ ok: true }) | JSON 응답 + Content-Type 자동 설정 |
res.send(data) | res.send('Hello') | 데이터 타입에 따라 Content-Type 자동 결정 |
주의할 점
next() 빠뜨림 — 요청이 영원히 대기
가장 흔한 실수입니다. 미들웨어에서 next()도 res.send()도 호출하지 않으면 클라이언트는 타임아웃까지 대기합니다.
app.use((req, res, next) => {
console.log('요청 로그');
// next()를 빠뜨림 — 이후 미들웨어 실행 안 됨
});
res.send() 중복 호출
응답을 두 번 보내면 ERR_HTTP_HEADERS_SENT 에러가 발생합니다.
app.get('/test', (req, res) => {
res.json({ step: 1 });
res.json({ step: 2 }); // ← Error: Cannot set headers after they are sent
});
조건문에서 return을 빠뜨릴 때 자주 발생합니다. return res.json()처럼 return을 붙이는 습관이 좋습니다.
app.use() vs app.get()의 경로 매칭 차이
app.use('/users')는 /users로 ** 시작하는** 모든 경로에 매칭됩니다. 반면 app.get('/users')는 ** 정확히** /users에만 매칭됩니다.
app.use('/users', middleware); // /users, /users/1, /users/1/posts 모두 매칭
app.get('/users', handler); // /users만 매칭
정리
| 항목 | 설명 |
|---|---|
| Express | Node.js 위의 미니멀 웹 프레임워크, 미들웨어 체인 기반 |
| 미들웨어 | (req, res, next) 함수, 등록 순서 = 실행 순서 |
| next() | 다음 미들웨어로 제어 전달, 빠뜨리면 요청 대기 |
| Router | 라우트를 모듈별로 분리, app.use(prefix, router) |
| req 핵심 | params(URL), query(쿼리스트링), body(바디) |
| res 핵심 | res.status().json() 체이닝, 중복 호출 금지 |