HTTP 요청이 Express 서버에 들어오면, 응답이 나가기까지 내부에서 어떤 일이 벌어질까요?

Express를 쓰면서 app.get()app.use()를 매일 작성하지만, 요청이 미들웨어를 거쳐 응답으로 나가는 전체 흐름을 한 번에 그려보면 동작이 훨씬 명확해집니다.

개념 정의

Express 는 Node.js 위에서 동작하는 미니멀한 웹 프레임워크입니다. 핵심은 미들웨어 함수의 체인 으로, HTTP 요청이 들어오면 등록된 미들웨어를 순서대로 통과시키며 최종 응답을 만듭니다.

요청-응답 사이클

Express의 모든 처리는 하나의 흐름으로 설명됩니다.

PLAINTEXT
HTTP 요청 → [미들웨어 1] → [미들웨어 2] → [라우트 핸들러] → HTTP 응답
               │               │               │
               └── next() ─────└── next() ──────└── res.send()

이 흐름에서 핵심은 next() 함수입니다. 미들웨어가 next()를 호출하면 다음 미들웨어로 넘어가고, res.send()를 호출하면 응답을 보내고 체인이 끝납니다. 어느 쪽도 하지 않으면 요청이 영원히 대기합니다.

라우팅 — 요청을 올바른 핸들러로 연결

라우팅은 HTTP 메서드와 경로의 조합으로 요청을 분류합니다.

JS
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()로 분리합니다.

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

router.get('/', (req, res) => { /* 목록 */ });
router.get('/:id', (req, res) => { /* 상세 */ });
router.post('/', (req, res) => { /* 생성 */ });

module.exports = router;
JS
// app.js
const userRouter = require('./routes/users');
app.use('/users', userRouter); // ← /users 접두사 자동 적용

app.use('/users', userRouter)를 등록하면 Router 내부의 /는 실제로 /users/가 됩니다.

미들웨어 — Express의 핵심

미들웨어는 (req, res, next) 세 개의 파라미터를 받는 함수입니다. 요청과 응답 사이에서 원하는 작업을 수행합니다.

JS
// 로깅 미들웨어
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가 제공하는 기본 미들웨어

미들웨어 등록 순서가 실행 순서다

이 부분을 놓치면 디버깅할 때 혼란스럽습니다.

JS
// 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.bodyundefined입니다.

req와 res 객체

Express는 Node.js의 기본 http.IncomingMessagehttp.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()도 호출하지 않으면 클라이언트는 타임아웃까지 대기합니다.

JS
app.use((req, res, next) => {
  console.log('요청 로그');
  // next()를 빠뜨림 — 이후 미들웨어 실행 안 됨
});

res.send() 중복 호출

응답을 두 번 보내면 ERR_HTTP_HEADERS_SENT 에러가 발생합니다.

JS
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에만 매칭됩니다.

JS
app.use('/users', middleware);  // /users, /users/1, /users/1/posts 모두 매칭
app.get('/users', handler);    // /users만 매칭

정리

항목설명
ExpressNode.js 위의 미니멀 웹 프레임워크, 미들웨어 체인 기반
미들웨어(req, res, next) 함수, 등록 순서 = 실행 순서
next()다음 미들웨어로 제어 전달, 빠뜨리면 요청 대기
Router라우트를 모듈별로 분리, app.use(prefix, router)
req 핵심params(URL), query(쿼리스트링), body(바디)
res 핵심res.status().json() 체이닝, 중복 호출 금지
댓글 로딩 중...