08. 미들웨어를 사용한 이것저것 검증
공식 문서를 살펴보면 라이브러리를 개발한 사람들이 전체 애플리케이션을 어떻게 바라보고 있는지 알 수 있는 문장들이 곳곳에 숨어있다. Express의 경우 "Express 애플리케이션은 기본적으로 일련의 미들웨어 함수 호출"이라는 표현이 개발자들의 시각을 잘 표현해주고 있다고 생각한다. Express는 일련의 미들웨어 함수가 순차적으로 호출되면서 동작하는 만큼, 미들웨어를 이해하는 것은 Express의 동작을 더 깊게 이해하는 데 큰 도움이 된다.
나는 Express의 미들웨어를 호출 되는 위치에 따라 크게 세 가지로 분류한다. 하나는 '라우터가 호출되기 전에 동작하는 미들웨어'인데, 이는 결국 모든 요청에 대해 동작해야하는 미들웨어를 의미한다. 클라이언트의 요청을 json parse 해주는 express.json() 미들웨어나 요청의 cookie를 이해하기 쉬운 구조로 parse 해주는 cookieParser 라이브러리 등이 여기 속한다.
그 다음은 '라우터와 컨트롤러 사이에 동작하는 미들웨어'가 있는데, 이것은 특정 경로에 대한 요청에 대해 동작해야하는 미들웨어를 의미한다. 해당 경로가 특정 인가를 필요로 할 때 사용할 수 있는 인가 확인 미들웨어나 요청 바디에 딸려온 값을 검증하는 데 사용하는 미들웨어 등이 여기 속한다.
마지막으로 '컨트롤러 호출 후에 동작하는 미들웨어'는 모든 요청이 종료된 후에 동작해야하는 미들웨어를 의미한다. 내 경우에는 prisma 관련 에러 및 컨트롤러에서 던진 예외를 받아주는 미들웨어를 만들어 사용하고 있다.
requestChecker
특정 요청에 필요한 값들이 존재하는지 검증할 때, 이전까지는 assert 함수를 '컨트롤러 내부'에서 직접 호출하여 각각의 값을 검증했다. 이렇게 하다보니 검증해야 하는 값들이 많아질 수록 컨트롤러가 덩달아 복잡해지게 되었다. 이를 해결하기 위해 값 검증을 대신해주는 미들웨어를 만들어 처리하기로 했다.
import { NextFunction, Request, Response } from 'express';
import { Struct, assert } from 'superstruct';
export const requestChecker =
<P extends Struct<any, any>, T extends keyof Request>(type: T, props: P) =>
(req: Request, res: Response, next: NextFunction) => {
try {
assert(req[type], props);
next();
} catch (err: any) {
return res.status(400).send({ message: err.message });
}
};
requestChecker를 살펴보면 requestChecker 그 자체가 미들웨어라고 볼 수는 없다는 사실을 알 수 있다. requestChecker는 미들웨어가 아니라, 두 개의 파라미터를 받아서 '미들웨어를 리턴하는' 함수이다. 그리고 requestChecker가 리턴하는 미들웨어는 값을 검증하고, 문제가 없다면 다음 미들웨어를 호출하고 있다.
loginChecker
특정 경로는 로그인된 사용자만 요청할 수 있어야 한다. 이 경우에도 미들웨어를 사용해 처리할 수 있다. 실제 로직은 보안? 상의 문제로 가려두었다. 각자의 인증 로직에 따라 처리하면 되겠다.
export const loginChecker = (
req: Request,
res: Response,
next: NextFunction,
) => {
if (*************) {
next();
} else {
return res.status(401).send({ message: '로그인이 필요한 서비스입니다' });
}
};
이 두 개의 미들웨어를 컨트롤러 앞에 배치하면, 컨트롤러가 실행되기 전에 미리 인증 여부와 값 검증을 진행할 수 있다. 이렇게 하면 컨트롤러의 부담을 줄이고 각각의 영역을 좀 더 추상화하여 사용할 수 있다.
commentRoutes.delete(
'/:id',
loginChecker,
requestChecker('params', Uuid),
async (req: Request, res: Response) => {
const { id } = req.params;
await commentService.deleteComment(id);
res.sendStatus(204);
},
);
블로그의 정보
Ayden's journal
Beard Weard Ayden