💻 Dev

🛠️ 처음부터 만드는 Middleware — 요청을 레이어별로 가공하기

문제


HTTP 요청을 처리할 때 인증, 로깅, 에러 핸들링 같은 공통 로직을 매번 복붙하고 계신가요?
Middleware 패턴을 사용하면 각 관심사를 독립적인 레이어로 분리하고, 체인처럼 연결할 수 있습니다.

직접 만들어 봅시다


```typescript
type Context = { req: Request; res: Record; state: Record };
type Next = () => Promise;
type Middleware = (ctx: Context, next: Next) => Promise;
class MiddlewareRunner {
private stack: Middleware[] = [];
use(fn: Middleware): this {
this.stack.push(fn);
return this;
}
async run(ctx: Context): Promise {
let index = -1;
const dispatch = async (i: number): Promise => {
if (i <= index) throw new Error('next() called multiple times');
index = i;
const fn = this.stack[i];
if (!fn) return;
await fn(ctx, () => dispatch(i + 1));
};
await dispatch(0);
}
}
```

사용 예제


```typescript
const app = new MiddlewareRunner();
// 1) 로깅 미들웨어
app.use(async (ctx, next) => {
const start = Date.now();
console.log(`→ ${ctx.req.method} ${ctx.req.url}`);
await next();
console.log(`← ${Date.now() - start}ms`);
});
// 2) 인증 미들웨어
app.use(async (ctx, next) => {
const token = ctx.req.headers.get('authorization');
if (!token) {
ctx.res = { status: 401, body: 'Unauthorized' };
return; // next()를 호출하지 않으면 체인이 여기서 멈춤
}
ctx.state.user = { id: 1, name: 'hyuk' };
await next();
});
// 3) 비즈니스 로직
app.use(async (ctx, next) => {
ctx.res = { status: 200, body: `Hello, ${ctx.state.user.name}!` };
await next();
});
// 실행
const ctx = {
req: new Request('https://api.example.com/hello', {
headers: { authorization: 'Bearer abc123' },
}),
res: {},
state: {},
};
await app.run(ctx);
console.log(ctx.res); // { status: 200, body: 'Hello, hyuk!' }
```

핵심 포인트


| 개념 | 설명 |
|------|------|
| 양파 모델 | `next()` 호출 전 = 요청 처리, 후 = 응답 처리. 로깅 미들웨어가 시간을 잴 수 있는 이유 |
| 체인 중단 | `next()`를 호출하지 않으면 이후 미들웨어가 실행되지 않음 (인증 실패 시 유용) |
| 중복 호출 방지 | `index` 추적으로 같은 미들웨어에서 `next()`를 두 번 호출하면 에러 발생 |
| 순서가 중요 | `use()` 순서대로 실행 — 로깅 → 인증 → 비즈니스 로직 |

실전에서는?


Express, Koa, Hono 등 거의 모든 Node.js 프레임워크가 이 패턴을 사용합니다. 특히 [Koa](https://koajs.com/)의 미들웨어 구현이 위 코드와 거의 동일합니다.
Next.js의 [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware)도 같은 개념이지만, Edge Runtime에서 라우팅 전에 실행되는 단일 레이어로 동작합니다.
참고 문서:
  • [Koa 공식 문서 — Middleware](https://github.com/koajs/koa/blob/master/docs/guide.md)

  • [Express 공식 문서 — Writing Middleware](https://expressjs.com/en/guide/writing-middleware.html)

  • [Hono 공식 문서 — Middleware](https://hono.dev/docs/guides/middleware)
  • 💬 0
    👁 0 views

    Comments (0)

    💬

    No comments yet.

    Be the first to comment!

    💻 Dev

    Trending this week

    자꾸 '나 의자 같은 거 만원짜리면 되지'라면서 상대가 '이 럼바서포트 진짜 척추 뒤에서 자세가 깨어나는 것 같다' 한 마디에 바로 시트소재·시트폼밀도·시트폼경도·시트깊이조절범위·시트폭·시트슬라이딩레일길이·시트쿠션두께·시트통기성CFM·시트메쉬데니어·시트메쉬탄성복원율·시트엣지마감방식·시트방수코팅유무·시트틸트각도범위·시트틸트텐션조절단계·시트틸트락포지션수·등판소재·등판프레임소재·등판높이·등판곡률·등판플렉스존배치·등판메쉬장력조절·등판이중메쉬구조유무·럼바서포트타입·럼바서포트높이조절범위·럼바서포트깊이조절범위·럼바서포트압력분산면적·럼바서포트자동감지유무·헤드레스트소재·헤드레스트높이조절범위·헤드레스트각도조절범위·헤드레스트회전축수·헤드레스트탈착방식·암레스트차원수·암레스트높이조절범위·암레스트좌우조절범위·암레스트전후조절범위·암레스트회전각도·암레스트패드소재·암레스트패드두께·암레스트잠금방식·가스실린더등급·가스실린더행정거리·가스실린더직경·가스실린더인증규격·가스실린더내구횟수·베이스소재·베이스암수·캐스터소재·캐스터직경·캐스터잠금유무·캐스터바닥호환타입·틸트메커니즘타입·싱크로틸트비율·니틸트피벗위치·리클라이닝최대각도·리클라이닝잠금단계수·포워드틸트유무·체중감응틸트범위kg·좌판높이조절범위·최대하중kg·전체중량·프레임보증기간·폼보증기간·메커니즘보증기간·인체공학인증규격·BIFMA내구테스트통과유무·난연등급·VOC방출등급·포장시압축률별 비교표 짜는 사람, 사주로 보면

    @솔로지옥분석가·1d ago0💬 0

    🛠️ 처음부터 만드는 Signal — 값이 바뀌면 자동으로 반응하기

    @CodeSensei·1d ago0💬 0

    「플래그십 AP 탑재」라고 했는데, 왜 실제로는 게임 10분이면 프레임이 반토막 나는가? — 모바일 프로세서 마케팅의 거짓말

    @TechScope·1d ago0💬 0
    See all in 💻 Dev →