Post

Express 미들웨어 파이프라인 해부

Express 요청 흐름, 미들웨어 역할 분리, next와 에러 핸들러 위치를 정리

Express 미들웨어 파이프라인 해부

결론 요약

  • Express는 라우팅 라이브러리가 아니라 요청(Request)을 처리하는 파이프라인 관리자다.
  • 핵심은 req → middleware chain → res 흐름이며, 순서(order)가 동작을 결정한다.
  • 미들웨어는 전처리·라우팅·에러 처리의 역할 분리가 생명이다.
  • next()는 “다음으로 넘긴다”가 아니라 제어권을 명시적으로 전달하는 계약이다.
  • 404와 에러 핸들러는 위치가 틀리면 절대 동작하지 않는다.

Express.js의 본질: 요청 파이프라인과 미들웨어

1️⃣ 정의: Express는 무엇을 관리하는가

Express는 HTTP 서버 위에 얹힌 요청 처리 파이프라인 관리자다. 각 요청은 등록된 미들웨어를 위에서 아래로 순차 통과하며, 응답이 전송되는 순간 파이프라인은 종료된다.

핵심 관점 전환 ❌ “URL → 함수 매핑” ✅ “요청이 흐르는 파이프에 어떤 필터를 어떤 순서로 끼울 것인가”


2️⃣ 왜 필요한가: if/else 서버의 붕괴를 막기 위해

Node의 http.createServer()로도 서버는 만들 수 있다. 문제는 요청 조건이 늘어날수록 제어 흐름이 붕괴한다는 점이다.

  • 인증 여부
  • 바디 파싱
  • 로깅
  • 권한 체크
  • 에러 처리

Express는 이들을 “순서 있는 단계”로 분리하게 강제한다. 이 강제가 없으면, 코드 규모가 커질수록 버그는 구조적으로 늘어난다.


3️⃣ 어떻게 동작하는가: req → middleware chain → res

요청 처리 흐름(단계별)

  1. 클라이언트 요청 수신
  2. app.use / app.get / app.post 등으로 등록된 미들웨어를 등록 순서대로 실행
  3. 각 미들웨어는 세 가지 중 하나를 선택

    • res.send / res.json / res.end → 응답 종료
    • next() → 다음 미들웨어로 전달
    • next(err) → 에러 핸들러로 점프
  4. 응답이 전송되면 파이프라인 종료

이 구조 때문에 “순서”는 로직 그 자체다.


4️⃣ 미들웨어의 3가지 역할 (섞이면 바로 망가진다)

① 전처리 미들웨어 (Pre-processing)

요청을 “사용 가능한 상태”로 만든다.

  • 바디 파싱 (express.json)
  • 인증/권한
  • 로깅
  • request id 부여
1
2
app.use(express.json());
app.use(authMiddleware);

특징

  • 대부분 app.use
  • 요청을 변경(req 확장)하지만, 응답은 보내지 않는다

② 라우팅 미들웨어 (Routing)

요청을 의미 있는 작업 단위로 분기한다.

1
app.get('/products/:id', productController.getProduct);

특징

  • URL + HTTP method 기준
  • 비즈니스 로직의 “입구” 역할

③ 에러 처리 미들웨어 (Error handling)

정상 흐름과 완전히 다른 파이프라인이다.

1
2
3
app.use((err, req, res, next) => {
  res.status(500).json({ message: err.message });
});

특징

  • 인자가 4개 (err, req, res, next)
  • 반드시 모든 라우트 뒤에 위치해야 한다

5️⃣ next()의 의미와 흔한 버그

next()의 정확한 의미

  • “다음 미들웨어를 실행하라”
  • 호출하지 않으면 요청은 거기서 멈춘다

흔한 버그 3종

❌ 1. next() 누락 → 무한 대기

1
2
3
4
5
6
app.use((req, res, next) => {
  if (!req.user) {
    res.status(401).end();
  }
  // next() 없음 → 인증된 요청도 멈춤
});

❌ 2. 응답 후 next() 호출 → 응답 중복

1
2
res.json(data);
next(); // 이미 응답했는데 다음으로 전달

❌ 3. 에러를 던지고 next(err) 안 씀

1
throw new Error('fail'); // async context에서 누락 가능

해결 패턴

  • 응답을 보냈으면 return
  • 에러는 next(err)로 위임
  • 미들웨어 하나 = 책임 하나

6️⃣ 라우터 분리: Express Router 설계 원칙

결론

라우터는 “파일 분리”가 아니라 도메인 경계 선언이다.

기본 원칙

  • URL prefix = 도메인
  • Router 내부에서는 상대 경로만 사용
  • 컨트롤러는 라우터 밖
1
2
3
4
5
// app.js
app.use('/products', productRoutes);

// routes/products.js
router.get('/:id', controller.getProduct);

라우터가 책임지면 안 되는 것

  • DB 접근
  • 검증 로직
  • 인증 판단

→ 라우터는 파이프 연결자, 로직은 미들웨어/컨트롤러로 이동


7️⃣ 404 처리와 에러 핸들러의 위치 규칙

404는 “모든 라우트를 통과한 뒤”에만 의미가 있다

1
2
3
app.use((req, res) => {
  res.status(404).send('Not Found');
});

에러 핸들러 위치 규칙

1
2
3
4
[전처리]
[라우트]
[404]
[에러 핸들러] ← 반드시 맨 마지막

이유

  • Express는 위에서 아래로만 탐색
  • 에러 핸들러는 “최후의 안전망”

8️⃣ 나쁜 예 vs 개선 예

❌ 나쁜 예: 모든 것이 라우트에 섞임

1
2
3
4
5
app.post('/order', (req, res) => {
  if (!req.body.user) return res.status(400).end();
  db.save(req.body);
  res.json({ ok: true });
});

문제:

  • 검증, 비즈니스, 응답이 한 덩어리
  • 테스트/재사용 불가

✅ 개선 예: 미들웨어 분리

1
2
3
4
5
router.post(
  '/',
  validateOrder,
  orderController.createOrder
);
  • 검증/로직/응답 분리
  • 파이프라인이 읽힘

Express 구조 템플릿 (권장)

1
2
3
4
5
6
7
8
9
10
11
src/
 ├─ app.js
 ├─ routes/
 │   ├─ products.js
 │   └─ orders.js
 ├─ controllers/
 ├─ middleware/
 │   ├─ auth.js
 │   ├─ validate.js
 │   └─ error.js
 └─ services/

실무 체크리스트 (재학습용)

  • Express를 “요청 파이프라인”으로 설명할 수 있다
  • 미들웨어 3종(전처리/라우팅/에러)을 구분해 쓸 수 있다
  • next() 누락/중복 호출의 위험을 알고 있다
  • 라우터는 도메인 경계만 책임진다
  • 404는 모든 라우트 뒤에 있다
  • 에러 핸들러는 파일·순서가 보장된다
  • 응답 후에는 반드시 return 한다
  • 미들웨어 하나에 책임 하나만 있다

This post is licensed under CC BY 4.0 by the author.