미들웨어는 라우트 앞의 접수 직원입니다
지난 장에서는 /products, /products/:id, /search처럼 주소마다 다른 일을 시켰습니다. 이제 서버에 요청이 들어올 때마다 공통으로 해야 하는 일을 생각해 보겠습니다.
예를 들어 작은 예약 접수 서버를 운영한다고 해 봅시다.
누가 어떤 주소로 들어왔는지 기록하기
제출된 예약 내용을 읽을 수 있는 모양으로 바꾸기
로그인한 사람인지 확인하기
없는 주소라면 404 응답 보내기
이런 일을 모든 라우트 안에 반복해서 쓰면 코드가 금방 길어집니다. 그래서 Express에는 미들웨어라는 중간 도우미가 있습니다.
미들웨어는 식당으로 비유하면 주방에 주문서가 들어가기 전에 접수대 앞에서 먼저 확인하는 직원입니다. 손님이 주문서를 내면 접수 직원이 시간을 적고, 주문서 형식을 확인하고, 필요하면 다음 직원에게 넘깁니다. Express의 미들웨어도 비슷합니다. 요청이 라우트에 도착하기 전이나 중간에서 요청을 살펴보고, 필요한 일을 한 뒤 다음 단계로 넘깁니다.
Express 공식 문서는 미들웨어 함수가 req, res, next를 사용할 수 있고, 코드를 실행하거나 요청과 응답을 다루거나, 응답을 끝내거나, 다음 미들웨어로 넘길 수 있다고 설명합니다.1
AI 생성 이미지
app.use는 모든 요청이 지나가는 통로를 만듭니다
라우트는 특정 주소를 담당합니다.
app.get('/products', (req, res) => {
res.send('상품 목록입니다.');
});반면 app.use()는 더 넓은 통로를 만듭니다. 주소를 따로 지정하지 않으면 서버로 들어오는 거의 모든 요청이 이 통로를 지나갑니다.
먼저 요청이 들어올 때마다 기록을 남기는 미들웨어를 만들어 보겠습니다.
무엇을 만들고 있는지부터 정리하겠습니다.
만들 것: 요청 방식과 주소를 터미널에 기록하는 미들웨어
열 파일:
express-practice폴더의app.js실행 명령:
node app.js성공하면 보이는 것: 브라우저 접속 때마다 터미널에
GET /같은 기록이 찍힘
app.js 위쪽, 라우트들보다 먼저 다음 코드를 넣습니다.
app.use((req, res, next) => {
console.log(req.method, req.url);
next();
});한 줄씩 읽어 보겠습니다.
app.use((req, res, next) => {app.use()는 미들웨어를 등록합니다. 안에 들어간 함수는 요청이 들어올 때 실행됩니다. 여기에도 라우트에서 보았던 req와 res가 있습니다. 새로 보이는 next는 다음 담당자에게 요청을 넘기는 신호입니다.
console.log(req.method, req.url);터미널에 요청 방식을 출력합니다. 브라우저 주소창으로 들어오면 보통 GET이 찍힙니다. 이어서 요청 주소도 출력합니다. /, /products, /favicon.ico 같은 값이 보일 수 있습니다.
next();기록을 마쳤으니 다음 미들웨어나 라우트로 요청을 넘깁니다. 이 줄이 없고 응답도 보내지 않으면 브라우저는 기다리기만 할 수 있습니다. 접수 직원이 주문서를 들고 다음 직원에게 전달하지 않은 상태와 비슷합니다.
이제 서버를 실행합니다.
node app.js브라우저에서 다음 주소로 들어가 봅니다.
http://localhost:3000/터미널에 다음과 비슷한 기록이 보이면 성공입니다.
GET /
GET /favicon.ico/favicon.ico가 함께 보여도 이상한 일이 아닙니다. 브라우저가 탭에 표시할 작은 아이콘을 찾으려고 추가 요청을 보낸 것입니다.
next()는 다음 담당자에게 넘기는 말입니다
미들웨어를 이해할 때 가장 중요한 단어는 next()입니다.
서버 요청 흐름을 말로 읽으면 이렇게 됩니다.
요청이 서버에 들어옵니다.
첫 번째 미들웨어가 요청을 봅니다.
next()를 호출하면 다음 미들웨어나 라우트로 넘어갑니다.라우트에서
res.send()나res.json()으로 응답을 보냅니다.한 요청의 처리가 끝납니다.
다음 예시를 보겠습니다.
app.use((req, res, next) => {
console.log('첫 번째 미들웨어');
next();
});
app.use((req, res, next) => {
console.log('두 번째 미들웨어');
next();
});
app.get('/', (req, res) => {
res.send('홈페이지입니다.');
});브라우저에서 /로 접속하면 터미널에는 이런 순서로 보입니다.
첫 번째 미들웨어
두 번째 미들웨어그 뒤 브라우저에는 홈페이지입니다.가 보입니다. 미들웨어가 앞에서 일을 하고, 마지막에 라우트가 응답을 보낸 것입니다.
반대로 첫 번째 미들웨어에서 next()를 지우면 흐름이 멈춥니다.
app.use((req, res, next) => {
console.log('여기에서 멈춥니다.');
});이 코드는 응답도 보내지 않고 다음으로 넘기지도 않습니다. 그러면 브라우저가 계속 기다릴 수 있습니다. 이런 상황은 학습 중 자주 만날 수 있습니다. 서버가 멈춘 것처럼 보이면, 미들웨어 안에서 next()를 호출했는지 또는 res.send() 같은 응답을 보냈는지 확인하면 됩니다.
다만 모든 미들웨어가 반드시 next()를 호출해야 하는 것은 아닙니다. 그 자리에서 res.send(), res.json(), res.status(...).send(...)처럼 응답을 끝냈다면 다음으로 넘기지 않아도 됩니다. 핵심은 한 요청에 대해 응답을 보내거나, 다음 단계로 넘기거나, 둘 중 하나는 해야 한다는 점입니다.
요청 내용을 읽게 해 주는 express.json과 express.urlencoded
GET 요청은 주소에 정보가 드러나는 경우가 많습니다.
/search?q=coffee하지만 POST 요청은 보통 제출 내용을 주소가 아니라 요청의 몸통, 즉 body에 담아 보냅니다. 편지로 비유하면 주소는 봉투 겉면이고, body는 봉투 안의 편지 내용입니다.
문제는 서버가 이 편지 내용을 처음부터 바로 읽기 좋은 모양으로 받지는 않는다는 점입니다. 그래서 Express에게 "POST 요청의 몸통을 읽을 수 있게 해석해 주세요"라고 미리 알려 주어야 합니다.
이때 사용하는 기본 미들웨어가 다음 두 가지입니다.
app.use(express.json());
app.use(express.urlencoded({ extended: true }));express.json()은 JSON 형식으로 들어온 요청 본문을 읽게 해 줍니다. 예를 들어 앱이나 프런트엔드 코드가 다음과 같은 데이터를 보낼 때 사용합니다.
{
"name": "민지",
"date": "2026-07-01"
}express.urlencoded({ extended: true })는 HTML 폼에서 자주 쓰는 형식의 데이터를 읽게 해 줍니다. 예를 들어 나중에 문의 폼에서 name=민지&message=문의합니다 같은 모양으로 데이터가 들어올 수 있습니다.
이렇게 해석된 값은 라우트 안에서 req.body로 꺼내 볼 수 있습니다. 요청 본문을 라우트보다 먼저 해석하면 그 결과가 req.body에 담긴다는 점은 Express의 본문 해석 미들웨어에서 중요한 개념입니다.2
작은 예약 접수 라우트에 적용해 보기
이제 실제로 req.body를 확인해 보겠습니다. app.js를 다음 흐름으로 정리합니다. 이미 있는 코드가 있다면 같은 역할을 하는 부분은 중복해서 쓰지 않아도 됩니다.
먼저 Express를 불러오고 앱을 만듭니다.
const express = require('express');
const app = express();그 아래에 요청 기록 미들웨어를 둡니다.
app.use((req, res, next) => {
const time = new Date().toLocaleString();
console.log(`[${time}] ${req.method} ${req.url}`);
next();
});이번에는 시간도 함께 남겼습니다. 서버를 켜 둔 상태에서 여러 주소로 접속하면 요청이 언제 들어왔는지 눈으로 확인할 수 있습니다.
그다음에 본문을 해석하는 미들웨어를 등록합니다.
app.use(express.json());
app.use(express.urlencoded({ extended: true }));그리고 예약을 받는 POST 라우트를 만듭니다.
app.post('/reservations', (req, res) => {
console.log('받은 예약 내용:', req.body);
res.json({
message: '예약 요청을 받았습니다.',
data: req.body
});
});이 라우트는 /reservations로 들어온 POST 요청을 받습니다. 받은 내용은 터미널에 한 번 출력하고, 브라우저나 요청을 보낸 도구에는 JSON으로 다시 응답합니다.
마지막에는 서버를 켭니다.
app.listen(3000, () => {
console.log('서버가 http://localhost:3000 에서 실행 중입니다.');
});여기까지 합치면 app.js의 큰 모습은 다음과 같습니다.
const express = require('express');
const app = express();
app.use((req, res, next) => {
const time = new Date().toLocaleString();
console.log(`[${time}] ${req.method} ${req.url}`);
next();
});
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/reservations', (req, res) => {
console.log('받은 예약 내용:', req.body);
res.json({
message: '예약 요청을 받았습니다.',
data: req.body
});
});
app.listen(3000, () => {
console.log('서버가 http://localhost:3000 에서 실행 중입니다.');
});서버를 실행합니다.
node app.js브라우저 주소창은 POST 요청을 보내기에 적합하지 않습니다. 이번에는 터미널을 하나 더 열어 작은 요청을 직접 보내 보겠습니다.
macOS, Linux, Git Bash에서는 다음 명령을 입력합니다.
curl -X POST http://localhost:3000/reservations \
-H "Content-Type: application/json" \
-d '{"name":"민지","date":"2026-07-01"}'Windows PowerShell에서는 다음 명령을 입력합니다.
Invoke-RestMethod -Method Post `
-Uri http://localhost:3000/reservations `
-ContentType "application/json" `
-Body '{"name":"민지","date":"2026-07-01"}'성공하면 요청을 보낸 터미널에는 다음과 비슷한 응답이 보입니다.
{
"message": "예약 요청을 받았습니다.",
"data": {
"name": "민지",
"date": "2026-07-01"
}
}서버를 실행 중인 터미널에는 다음과 비슷한 기록이 남습니다.
[2026. 7. 1. 오후 2:10:30] POST /reservations
받은 예약 내용: { name: '민지', date: '2026-07-01' }여기서 중요한 부분은 req.body에 값이 들어왔다는 점입니다. express.json()이 POST 요청의 몸통을 먼저 해석해 주었기 때문에 라우트에서 사용할 수 있었습니다.
순서가 바뀌면 미들웨어가 일을 못 할 수 있습니다
Express 코드는 위에서 아래로 읽히며 등록됩니다. 이 순서는 미들웨어에서 특히 중요합니다.
다음처럼 라우트보다 아래에 express.json()을 쓰면 문제가 생길 수 있습니다.
app.post('/reservations', (req, res) => {
console.log(req.body);
res.json(req.body);
});
app.use(express.json());요청은 위에서부터 내려오다가 /reservations 라우트를 먼저 만납니다. 아직 express.json()을 지나지 않았기 때문에 req.body가 비어 있거나 기대한 값이 아닐 수 있습니다.
그래서 기본 순서는 이렇게 기억하면 좋습니다.
1. Express 앱 만들기
2. 공통 미들웨어 등록하기
3. 라우트 등록하기
4. 404 처리 등록하기
5. app.listen으로 서버 켜기특히 다음 세 가지는 위치를 자주 확인해야 합니다.
요청 기록 미들웨어는 라우트보다 위에 둡니다.
express.json()과express.urlencoded()도 POST 라우트보다 위에 둡니다.없는 주소를 처리하는 404 미들웨어는 보통 맨 아래에 둡니다.
404 미들웨어 예시는 다음과 같습니다.
app.use((req, res) => {
res.status(404).send('페이지를 찾을 수 없습니다.');
});이 코드는 앞의 라우트들이 처리하지 못한 요청을 마지막에 받습니다. 그래서 맨 위에 두면 정상 주소까지 404로 응답할 수 있습니다.
앞에서 만든 예약 접수 코드에 404 미들웨어를 추가한다면, /reservations 라우트 아래와 app.listen() 위에 두면 됩니다.
app.post('/reservations', (req, res) => {
console.log('받은 예약 내용:', req.body);
res.json({
message: '예약 요청을 받았습니다.',
data: req.body
});
});
app.use((req, res) => {
res.status(404).send('페이지를 찾을 수 없습니다.');
});
app.listen(3000, () => {
console.log('서버가 http://localhost:3000 에서 실행 중입니다.');
});이렇게 두면 /reservations로 들어온 POST 요청은 예약 라우트가 처리하고, 그 밖에 아직 만들지 않은 주소는 마지막 404 미들웨어가 처리합니다.
이번 장에서 기억할 것
미들웨어는 요청이 라우트에 도착하기 전이나 중간에서 공통 작업을 처리하는 중간 도우미입니다. app.use()로 등록하고, 필요한 일을 한 뒤 next()로 다음 단계에 넘깁니다.
요청 기록 미들웨어를 만들면 서버가 실제로 어떤 요청을 받는지 터미널에서 확인할 수 있습니다. 서버 개발이 막막하게 느껴질 때는 이런 기록이 길잡이가 됩니다.
POST 요청의 본문을 읽으려면 express.json()이나 express.urlencoded() 같은 미들웨어를 라우트보다 먼저 등록해야 합니다. 그래야 라우트 안에서 req.body를 사용할 수 있습니다.
다음 장에서는 서버가 문자열만 보내는 것을 넘어, HTML, CSS, 이미지 같은 파일을 브라우저에 보여 주는 방법을 배웁니다. 그때도 미들웨어가 다시 등장합니다. 정적 파일을 제공하는 일 역시 Express에서는 미들웨어로 처리합니다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.